本篇索引:
1、引言
2、文件描述符
3、open函数
4、close函数
5、read函数
6、write函数
7、lseek函数
8、i/o效率问题
9、内核用以维护打开文件的相关数据结构
10、O_APPEND标志
11、dup函数(文件描述符重定位函数)
12、有关文件共享的问题
13、fcntl函数
14、ioctl函数
1、引言
1.1、文件io这个词的含义
实现对文件的数据输入(input)和输出(output),所以简称为文件io。
1.2、什么需要文件io
程序的目的是为了处理信息,而信息在计算机中的表现形式就是数据,所以程序就是为了处理数据并输出数据,而所有的数据几乎都与文件有关(linux下一切皆是文件),所以程序就必须实现对文件的读写。
对于有OS的计算机来说,应用程序是无法直接读写文件的(隔离保护作用),文件基本是靠下层的机制,必须经过OS才能访问,所以我们就必须学习linux专门提供给应用程序,让其实现文件io操作的系统调用函数,利用这些专门的文件io接口实现对文件的访问。
1.3、文件io函数
常见的文件io函数有open、read、write、close、lseek这五个,我们经过第二篇的学习,已经知道,相对于标准io来说文件io常被称为不带缓存的io。在前面我们也说过,这几个系统调用不是ANSI C的组成部分,但是这几个系统函数的函数原型确是ANSI C提供的。
1.4、原子操作
当多个进程共享同一资源,就比如当多个进程都想对同一文件操作,那么原子操作的概念是非常重要的。比如,A进程写xxx文件时,如果某个条件未发生,那么B进程无论如何都不能写该文件,直到A进程一直写到该条件发生,才轮到B进程写。B进程写时也是如此,这样就避免了A与B之间互相串改对方写入文件的数据的可能,本片会通过一个O_APPEND标志给大家引入原子操作的概念,,后续课程我们还会再次接触到。
2、文件描述符
2.1、文件指针和文件描述符
我们学习标准io时知道,标准io实现对文件读写操作时,用的是文件指针FILE*fp。在第二篇中我们也说了,如果是在linux OS下,标准io向下继续调用时,实际调用的还是文件io,而文件IO则使用文件描述符来实现对文件的操作,该文件描述符就存在了文件指针fp指向的结构体中。
2.2、什么是文件描述符
每成功打开(打开文件用文件路径)一个文件,内核都会返回一个非负的整数,read、write时就用此整数进行操作,该整数一般都是在调用open或create函数时返回的,这个整数就是文件描述符。
在linux下,一般来说每个进程可以使用的文件描述符都是在0~1023之间,总共1024个可用文件描述符,当然上限值是可以更改的。其中0与标准输入、1与标准输出、2与出错输出结合在了一起,因为系统启动时按顺序打开了标准输入,输出,出错输出三个文件,所以0、1、2也与这三个文件顺序的结合在了一起,之后每个运行的进程都将继承这三个文件描述符,所以之后的每个进程不必再次打开这三个文件,就可以直接使用。这就是为什么scanf、printf函数能够直接被使用的原因了。
这三个文件描述符对应STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO这三个宏,分别定义在了<unistd.h>中,鼓励使用宏而不是直接使用0、1、2数字,目的是为了提高程序的可辨识度和跨平台操作性。
3、open函数
3.1、函数原型和所需头文件
#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);
3.2、函数功能
int open(const char *pathname, int flags);按照flags的要求打开已存在的文件,如果文件不存在则报错。
int open(const char *pathname, int flags, mode_t mode);如果文件已经存在就直接按照flags的要求打开文件。如 果文件事先不存在,则按照第三个参数的权限要求创建一个新的文件,然后再按照flags要求打开文件。
3.3、参数说明
3.3.1、第一个参数:const char *pathname
文件路径。
3.3.2、第二个参数:int flags,打开文件的方式
flags由如下宏选项中的一个或多个,通过|运算组成。
1)、O_RDONLY:只读方式打开文件
2)、O_WRONLY:只写方式打开文件
3)、O_RDWR:可读可写方式打开文件
以上这三个只能指定其中一个,不可组合。
4)、O_APPEND:每次在文件末尾追加信息,此选项很重要,后面会详说。
5)、O_ASYNC:异步标志,后面学习异步通知时将会用到。
6)、O_CLOEXEC:如果该进程exec新程序后,打开的文件将自动关闭,该描述符与该文件的结合无效。
在后面学习进程控制时,我们会对此进行举例说明。
7)、O_CREAT:指定了该标志后,如果文件不存在则创建一个该名字的文件,但这需要用到第三个参数
来指定新创建文件的原始权限。
8)、O_EXCL:指定这个标志时,O_CREAT必须也被指定,这两个标志联合使用可实文件存在则报错 的功能。因为有时我们就是需要创建出一个不与现存任何文件同名的新文件,如果发现该文件是一 个已经存在的文件我们必须报错,然后重新命名创建文件。使用O_EXCL就能实现这样的功能,否
则的话,这个已经存在的文件会被直接打开并使用,这与我们的愿望相违背。
9)、O_TRUNC:打开时将文件内容全部清零空
10)、O_NOCTTY:如果打开的文件是一个终端备文件的话,不要将此终端指定为控制终端,比如以后我 们涉及打开一个串口的时候,就会用到这个标志。
11)、O_NONBLOCK:这个只能用在字符类设备或网络设备文件上,对于磁盘上的普通文件是无效的。打开字符类设备文件时,默认就是以阻塞方式打开的,但是我们可以将其改为非阻塞的。大家知道getchar,scanf等函数实现键盘输入时会导致阻塞,就是因为这个原因。阻塞与非阻塞有时是由文件类型决定的,因为同一个函数操作A文件时是阻塞,但是操作B文件时却是非阻塞的。但是有时又是由函数本身的特性决定的,导致阻塞的原因要视情况而定,后面讲信号时我们将详细讨论此问题。
12)、O_SYNC:同步标志,write系统调用会一直等到,直到物理设备读写完毕后才会返回到应用层。如果不指定的话,write只需要将内容写到内核缓存中后就立即返回,剩下的事情就由内核定时将内核缓存中的数据分批写到物理设备上。
3.4、返回值
函数调用成功返回进程描述符集合中(0~1023)当前最小且未用的描述符,失败返回-1,并设置errno。
3.5、简单用例
3.5.1、打开已有文件
先vi或touch出一个名叫“file”的文件。
int main(void){ int fd = -1; fd = open("file", O_RDWR); if(fd < 0) { perror("open is fail"); exit(-1); } return 0; }
3.5.2、如果文件存在则直接打开,不存在则新建一个该名字的文件,然后再打开
int main(void){ int fd = -1; fd = open("file", O_RDWR|O_CREAT, 0664);//指定文件的权限 if(fd < 0) { perror("open is fail"); exit(-1); } return 0; }
3.5.2、如果存在报错
int main(void) { int fd = -1; fd = open("file", O_RDWR|O_CREATE|O_EXCL, 0664); if(fd < 0) { perror("open is fail"); exit(-1); } return 0; }
我们可以在出错处理中重新创建一个名叫“file1”的文件,如该名字的文件也已经有了,那就再换一个名字,直到找到一个不冲突的名字为止。
理解O_EXEC标志对于我们理解一些其它系统调用的类似的xxx_EXEC标志是很有帮助的,因为基本思想是一致的。
3.6、注意点
打开文件时,打开方式必须符合文件创建时文件的创建权限。换句话说,创建文件时的权限不允许写,你却想要以写方式打开,这会导致函数调用失败,这里说的很笼统,下章会对此做详解。
4、close函数
4.1、函数原型和头文件
#include <unistd.h>
#int close(int fd);
4.2、函数功能说明
关闭打开的文件。
4.3、函数参数
int fd:文件描述符
4.4、返回值
调用成功返回0,失败返回-1,errno被设置
4.5、测试用例:略
4.6、注意点
Close函数可不必显示调用,因为程序正常结束时会隐式的调用该函数,记住这里说的是程序正常结束,后面讲进程控制时会告诉大家什么是程序异常结束,这里只须简单记住,exit和main函数中return都可以正常退出。
5、read函数
5.1、函数原型和头文件
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
5.2、函数功能说明
以字节单位,按块(一块包含很多字节)读取文件中的数据到用户缓存。
5.3、函数参数
5.3.1、第一个参数int fd:文件描述符
5.3.2、第二个参数void *buf:用户缓存
5.3.3、第三个参数size_t count:指定读取一次的块大小,换句话说一块包含count字节
5.4、返回值
调用成功返回read函数实际读取到的字节数,如果失败,返回-1,并且errro被设置
5.5、测试用例:略
5.6、注意点
如果read调用成功,则返回实际读取的到字节数,0=<该字节数<=count,当读到文件末尾时,读取到的字节数很有可能实际小于count的要求,这是很正常的,如果返回0代表已经读取到文件末尾。
linux并不区分文本二进制和纯二进制,read函数都以字节为单位读取,至于拿到这些数据后,如何处理或解释那就是应用程序所要做的事情了,read并不关心读到的是文本二进制还是纯二进制。
6、write函数
6.1、函数原型和头文件
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
6.2、函数功能说明
以字节单位,按块将用户数据写入文件中。
6.3、函数参数
5.3.1、第一个参数int fd:文件描述符
5.3.2、第二个参数void *buf:用户缓存或直接用户数据
5.3.3、第三个参数size_t count:指定写入一次块的大小(以字节为单位计算)
6.4、返回值
调用成功,返回write函数实际成功写入的字节数,如果返回0表示无数据写入文件。如果失败,返回-1并且errro被设置。
6.5、测试用例:略
6.6、注意点:暂无
7、lseek函数
7.1、函数原型和头文件
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
7.2、函数功能说明
定位对文件的操作位置,就像在纸上写字时调动笔尖到某处一样。功能同标准io中的fseek函数,可认为fseek函数就是对lseek函数做的一个封装。其实标准io中的fseek和ftell函数向下调用的都是是lseek,因为这个函数兼具这两个方面的功能,即能调动对文件的操作位置,又能返回文件的当前位移量。
7.3、函数参数
7.3.1、第一个参数:int fd
文件描述符
7.3.2、第二个参数:off_t offset
精确定位,负数代表从现在的位置向前移动offset字节,正数代表从当前位置向后移动offset字节,当对文件的操作位置定位在了文件的起始位置时,那么再向前移动的话没有意义,函数会报错返回。
7.3.3、第三个参数:int whence
粗定位,选项有SEEK_SET:调到文件起始位置,SEEK_CUR:调到文件当前位置,SEEK_END:调到文件末尾的位置。
7.4、返回值
成功,返回文件的当前位移量,失败返回-1,errno被设置。
7.5、测试用例: 略
7.6、注意点
此函数只能对普通文件进行操作,不能对字符设备,管道等其它文件操作,因为这些文件在磁盘上只有属性信息,并没有真实的数据存放,lseek定位毫无意义。
Lseek可以用来实现空洞文件,但是lseek的调动后,必须使用write函数向文件里写点数据,该调动结果才能被记录下来。
一般来说按照正常情况打开一个文件时,文件的“当前位移量”为0(笔尖放在了文件在开始的位置),读写数据时从文件的最开始处进行,但是如果我们打开指定了O_APPEND标志的话,笔尖回调到文件的末尾,当前文件位移量为文件长度,这一点我们需要注意。
7.7、函数一些特殊用法
lseek函数可以用来构建空动文件,空洞文件一种很有用的文件,这可实现多线程并发地同时向文件不同区域写数据。先看lseek如何被用来构建空洞文件的。
int main(void) { int fd = -1; off_t f_len = -1; fd = open("file", O_CREAT|O_RDWR, 0664) ; if(fd < 0) { perror("open is fail"); exit(-1); } /* 返回新打开文件的文件长度,其结果肯定时0 */ f_len = lseek(fd, 0, SEEK_END); printf("f_len = %d ", f_len); /* 文件指针向后移动十个字节 */ f_len = lseek(fd, 10, SEEK_SET); printf("f_len = %d ", f_len); /* lseek只是调动文件操作位置,不会去修改文件,需要人为的调 * 用write函数去修改下文件,否则无法固定下lseek的修改结果 */ write(fd, "a", 1); return 0; }
查看文件大小
[linux@localhost 1402]$ ls -al file
-rw-rw-r--. 1 linux linux 11 Apr 11 15:04 file
查看文件内容
[linux@localhost 1402]$ od file -c
0000000