1、什么是操作系统API
(1)API是一些函数,这些函数是由linux系统提供支持的,由应用层程序来使用。
(2)应用层程序通过调用API来调用操作系统中的各种功能,来干活。
(3)学习一个操作系统,其实就是学习使用这个操作系统的API。
(4今天我们要使用linux系统来读写文件,手段就是学习linux系统API中和文件IO有关的几个。
2、linux常用文件IO接口
(1)open、close、write、read、lseek
open函数:
#include <fcntl.h>
int open(const char *pathname, int oflag, ... );
返回值:成功则返回文件描述符,否则返回 -1
对于 open 函数来说,第三个参数(...)仅当创建新文件时才使用,用于指定文件的访问权限位(access permission bits)。pathname 是待打开/创建文件的路径名(如 C:/cpp/a.cpp);oflag 用于指定文件的打开/创建模式,这个参数可由以下常量(定义于 fcntl.h)通过逻辑或构成。
O_RDONLY 只读模式
O_WRONLY 只写模式
O_RDWR 读写模式
close函数:
close
函数关闭一个已打开的文件:
#include <unsitd.h>
int close(int fd);
返回值:成功返回0,出错返回-1,并设置errno
参数fd
是要关闭的文件描述符。需要说明的是,当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用close
关闭,所以即使用户程序不调用close
,在终止时内核也会自动关闭它打开的所有文件。但是对于一个长年累月运行的程序(比如网络服务器),打开的文件描述符一定要记得关闭,否则随着打开的文件越来越多,会占用大量文件描述符和系统资源。
由open
返回的文件描述符一定是该进程尚未使用的最小描述符。由于程序启动时自动打开文件描述符0、1、2,因此第一次调用open
打开文件通常会返回描述符3,再调用open
就会返回4。可以利用这一点在标准输入、标准输出或标准错误输出上打开一个新文件,实现重定向的功能。例如,首先调用close
关闭文件描述符1,然后调用open
打开一个常规文件,则一定会返回文件描述符1,这时候标准输出就不再是终端,而是一个常规文件了,再调用printf
就不会打印到屏幕上,而是写到这个文件中了。后面要讲的dup2
函数提供了另外一种办法在指定的文件描述符上打开文件。
3、文件操作的一般步骤
(1)在linux系统中要操作一个文件,一般是先open打开一个文件,得到一个文件描述符,然后对文件进行读写操作(或其他操作),最后close关闭文件即可
(2)强调一点:我们对文件进行操作时,一定要先打开文件,打开成功后才能去操作(如果打开本身失败,后面就不用操作了);最后读写完成之后一定要close关闭文件,否则可能会造成文件损坏。
(3)文件平时是存在块设备中的文件系统中的,我们把这种文件叫静态文件。当我们去open打开一个文件时,linux内核做的操作包括:内核在进程中建立了一个打开文件的数据结构,记录下我们打开的这个文件;内核在内存中申请一段内存,并且将静态文件的内容从块设备中读取到内存中特定地址管理存放(叫动态文件)。
(4)打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件的,而并不是针对静态文件的。当我们对动态文件进行读写后,此时内存中的动态文件和块设备中的静态文件就不同步了,当我们close关闭动态文件时,close内部内核将内存中的动态文件的内容去更新(同步)块设备中的静态文件。
(5)常见的一些现象:
第一个:打开一个大文件时比较慢
第二个:我们写了一半的文件,如果没有点保存直接关机/断电,重启后文件内容丢失。
(6)为什么要这么设计?
以为块设备本身有读写限制(回忆NnadFlash、SD等块设备的读写特征),本身对块设备进行操作非常不灵活。而内存可以按字节为单位来操作,而且可以随机操作(内存就叫RAM,random),很灵活。所以内核设计文件操作时就这么设计了。
4、重要概念:文件描述符
(1)文件描述符其实实质是一个数字,这个数字在一个进程中表示一个特定的含义,当我们open打开一个文件时,操作系统在内存中构建了一些数据结构来表示这个动态文件,然后返回给应用程序一个数字作为文件描述符,这个数字就和我们内存中维护这个动态文件的这些数据结构挂钩绑定上了,以后我们应用程序如果要操作这一个动态文件,只需要用这个文件描述符进行区分。
(2)一句话讲清楚文件描述符:文件描述符就是用来区分一个程序打开的多个文件的。
(3)文件描述符的作用域就是当前进程,出了当前进程这个文件描述符就没有意义了
一个简单的文件读写实例.
1、打开文件与关闭文件
(1)linux中的文件描述符fd的合法范围是0或者一个正正数,不可能是一个负数。
(2)open返回的fd程序必须记录好,以后向这个文件的所有操作都要靠这个fd去对应这个文件,最后关闭文件时也需要fd去指定关闭这个文件。如果在我们关闭文件前fd丢掉了那就惨了,这个文件没法关闭了也没法读写了。
2、实时查man手册
(1)当我们写应用程序时,很多API原型都不可能记得,所以要实时查询,用man手册
(2)man 1 xx查linux shell命令,man 2 xxx查API, man 3 xxx查库函数
man 2 open
SYNOPSIS
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int creat(const char *pathname, mode_t mode);
由此我们知道了open函数包含的头文件,以及open函数包含两种形式,pathname表示路径以及文件名,open不仅能打开当前目录下的文件,也可以打开其他目录下的文件;flag是属性,mode与flag有关,
3、读取文件内容
(1)ssize_t read(int fd, void *buf, size_t count);
fd表示要读取哪个文件,fd一般由前面的open返回得到
buf是应用程序自己提供的一段内存缓冲区,用来存储读出的内容
count是我们要读取的字节数
返回值ssize_t类型是linux内核用typedef重定义的一个类型(其实就是int),返回值表示成功读取的字节数。
count是我们想读取的字节数,而read返回值为我们实际读取的字节数。
4、向文件中写入
(1)写入用write系统调用,write的原型和理解方法和read相似
(2)注意const在buf前面的作用,结合C语言高级专题中的输入型参数和输出型参数一节来理解。
(3)注意buf的指针类型为void,结合C语言高级专题中void类型含义的讲解
(4)刚才先写入12字节,然后读出结果读出是0(但是读出成功了),这个问题的答案后面章节会讲,大家先思考一下。
5、例程代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char *argv[])
{
int fd = -1; // fd 就是file descriptor,文件描述符
char buf[100] = {0};
char writebuf[20] = "l love linux";
int ret = -1;
// 第一步:打开文件
fd = open("a.txt", O_RDWR);
if (-1 == fd) // 有时候也写成: (fd < 0)
{
printf("文件打开错误
");
}
else
{
printf("文件打开成功,fd = %d.
", fd);
}
// 第二步:读写文件
// 写文件
ret = write(fd, writebuf, strlen(writebuf));
if (ret < 0)
{
printf("write失败.
");
}
else
{
printf("write成功,写入了%d个字符
", ret);
}
/*
// 读文件
ret = read(fd, buf, 5);
if (ret < 0)
{
printf("read失败
");
}
else
{
printf("实际读取了%d字节.
", ret);
printf("文件内容是:[%s].
", buf);
}
*/
// 第三步:关闭文件
close(fd);
return 0;
}
6、向文件中写入(1)写入用write系统调用
write的原型和理解方法和read相似(2)注意const在buf前面的作用,结合C语言高级专题中的输入型参数和输出型参数一节来理解。(3)注意buf的指针类型为void,结合C语言高级专题中void类型含义的讲解(4)刚才先写入12字节,然后读出结果读出是0(但是读出成功了),这个问题的答案后面章节会讲,大家先思考一下。