1 引言
本章首先讨论Unix系统中大多数文件I/O最常用的5个系统函数:open、read、write、lseek以及close。
本章所说明的函数又被成为不带缓冲的I/O,不带缓冲是指每个read和write都调用内核中的一个系统调用。
接着讨论原子操作的概念,只要涉及到多个进程之间共享资源,原子操作相当重要。
最后,本章将进一步讨论在多个进程之间共享文件以及所涉及的内核数据结构,之后将说明dup、fcntl、sync、fsync和ioctl函数。
2 文件描述符
对于内核而言,所有的打开的文件都由文件描述符引用。文件描述符是一个非负整数。Unix系统会按照惯例把下列描述符和系统输入相关联:
0 | 标准输入 | STDIN_FILENO |
1 | 标准输出 | STDOUT_FILENO |
2 | 标准错误输出 | STDERR_FILENO |
3 open、create、close函数
#include <fcntl.h> int open(const char *pathname, int oflag, ... /*mode_t mode*/); /*返回:成功返回文件描述符,出错返回-1*/
调用open函数可以打开或者创建一个文件。对于open函数而言,仅当创建新文件时才使用最后一个参数,oflag来说明文件选项:
(1)打开方式(必选)
O_RDONLY | 以只读方式打开 |
O_WRONLY | 以只写方式打开 |
O_RDWR | 以读写方式打开 |
(2)可选项
O_APPEND | 每次写时都追加到文件的尾端 |
O_CREAT | 若文件不存在则创建,若文件存在则返回错误。使用此选项时,需要第三个参数mode来指定新文件的访问权限位 |
O_EXCEL | 测试文件是否存在,一般与O_CREAT搭配使用 |
O_TRUNC | 若文件存在,且为只写或读写打开,则将其长度截短为0 |
O_NONBLOCK | 如果pathname指的是一个FIFO、一个块特殊文件或者是一个字符特殊文件,则此选项为文件的本次打开和后续的I/O操作设置为非阻塞模式 |
…… |
由open返回的文件描述符一定是最小的未用描述符数值。
也可调用一个creat创建一个新文件。
#include <fcntl.h> int creat(const char *pathname, mode_t mode); /*返回:成功返回只写打开的文件描述符,出错返回-1*/
此函数等同于open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);
调用close函数关闭一个打开的文件。关闭一个文件还会释放该进程加在该文件上的所有记录锁。
#include <unistd.h> int close(int fd) /*返回:成功返回0,失败返回-1*/
4 lseek函数
每个打开的文件都有一个与其相关联的文件偏移量offset。它通常是一个非负整数,用以度量从文件开始处计算的字节数。通常读写操作从当前文件偏移量开始,并使偏移量增加读写的字节数。当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被默认设置为0。
可以调用lseek显式地为一个打开的文件设置文件偏移量。
#include <unistd.h> off_t lseek(int fd, off_t offset, int whence); /*返回:成功返回新的偏移量,出错返回-1*/
对参数offset的解释与参数whence的值有关,若whence的值是:
SEEK_SET | 将该文件偏移量设置为距文件开始处offset个字节 |
SEEK_CUR | 当前偏移量加offset,offset可正可负 |
SEEK_END | 文件长度加offset,offset可正可负 |
例如,获取当前偏移量:cur_pos = lseek(fd, 0, SEEK_CUR);
若文件描述符引用的是一个管道、FIFO或网络套接字,则lseek返回-1,并将errno设置为ESPIPE。
文件偏移量可以是负值,但对于普通文件其偏移量必须是非负子,因此在比较lseek的返回值时应谨慎,不要测试它是否小于0,而是测试它是否等于-1.
文件偏移量可以大于当前文件长度,只是这样做会在文件中形成一个空洞,位于文件中但没有写过的字节都被读为0,文件的空洞并不占用磁盘存储空间。
lseek只修改文件表项的当前文件偏移量,不进行任何I/O操作。
5 read、write函数
#include <unistd.h> ssize_t read(int fd, void *buf, size_t bytes); ssize_t write(int fd, const void *buf, size_t bytes); /*以上函数返回:成功返回读(写)到的字节数,若已到文件尾返回0,出错返回-1*/
有多种情况使得实际读到的字节数小于要求的字节数:
- 读普通文件时,在读到要求的字节数之前就到达了文件尾端
- 从终端设备读取,通常只能一次读一行
- 从网络设备、FIFO或管道读取,设备的缓冲小于要求的字节数
- 当某一设备中断,而已经读了部分数据量时
- ……
write函数的返回值通常与参数nbytes的值相同,否则表示出错。对于普通文件,写操作从文件的当前偏移量开始。
6 文件共享
Unix系统支持在不同的进程之间共享打开的文件。内核使用三种数据结构表示打开的文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响,如下图所示:
(1)每个进程在进程表中都有一个记录项,记录项中包含有一张打开的文件描述符表。与每个文件描述符相关联的是:文件描述符标志(close_on_exec)和指向一个文件表项的指针。
(2)内核为所有打开文件维护一张文件表。每个文件表包含:文件状态标志(读、写、添写、同步、非阻塞等)、当前文件偏移量和指向该文件的v节点表项指针。
(3)每个打开的文件(或设备)都有一v节点结构。v节点包含了文件类型和对文件进行各种操作的函数指针。对于大多数文件,v节点还包含了该文件的i节点。
给出了数据结构后,对前面描述的操作进行进一步的说明:
- 在完成每个write之后,在文件表项中的当前文件偏移量即增加的字节数。如果使当前文件偏移量超过了当前文件长度,那么i节点表项的当前文件长度被设置为当前文件偏移量(文件被加长了)
- 如果用O_APPEND选项打开了一个文件,则相应的标志也被设置到文件表项的文件状态标志中。此时每次进行写操作时,文件表项中的当前文件偏移量首先被设置为i节点里的当前文件长度。这就使得每次写的数据都能添加到文件的尾部。
- 如果一个文件用lseek定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为i节点里的当前文件长度(但是与O_APPEND选项打开文件时不同的)。
- lseek函数只修改文件表项的当前文件偏移量,不进行任何I/O操作
两个独立的进程打开同一个文件:
注意,文件描述符标志和文件状态标志在作用域方面的区别,前者只用于一个进程的一个描述符,而后者则适用于指向该给定文件表项的任何进程中的所有描述符。
7 原子操作
之前描述的方法对多个进程读取同一个文件都能正确工作,因为每个进程都有自己的文件表项和当前文件偏移量。但是多个进程写一个文件时,可能产生意想不到的后果。为了避免这种情况,需要理解原子操作的概念。
原子操作是指不会被线程调度打断的操作,因此任何一个需要多个函数调用的操作都不可能是原子操作。
#include <unistd.h> ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset); ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);
调用pread和pwrite相当于顺序调用lseek、read或write,但是他们是原子操作:
- 调用时无法中断其定位和读写操作
- 不更新文件指针
8 dup、dup2函数
#include <unistd.h> int dup(int fd); int dup2(int fd, int fd2); /*返回:成功返回新的描述符,失败返回-1*/
dup和dup2函数用来复制一个现有的文件描述符。
dup返回值是当前可用的文件描述符的最小值,而dup2可以用fd2指定新的文件描述符。若fd2已经打开,则将其关闭,若fd2==fd,则dup2返回fd2。
正如之前描述的那样,dup和dup2函数返回的新的文件描述符和参数fd共享同一个文件表项。
复制一个文件描述符的另一个方法是使用fcntl函数:
dup(fd); 等效于 fcntl(fd, F_DUPFD, 0)
dup2(fd, fd2); 等效于 close(fd2); fcntl(fd, F_DUPFD, fd2);
它们之间的区别是:
- dup2是一个原子操作
- dup2和fcntl有某些不同的errno
9 sync、fsync、fdatasync函数
#include <unistd.h> int sync(void); int fsync(int fd); int fdatasync(int fd); /*以上函数返回:成功返回0,出错返回-1*/
Unix内核中一般设有缓存区,大多数磁盘I/O都通过缓冲进行,这样能减少磁盘读写次数。
sync只是将所有修改过的缓冲写入队列,然后就返回,不等待实际磁盘操作结束。通常称为update的系统守护进程会定期的调用sync函数。
fsync只对文件描述符fd指向的单一文件起作用,并等待磁盘操作结束后返回。
fdatasync函数类似于fsync,但它只影响文件数据部分,还会同步更新文件属性。
10 fcntl函数
fcntl函数可以改变以打开文件的性质。
#include <fcntl.h> int fcntl(int fd, int cmd, .../*int arg*/); /*返回:成功返回依赖于cmd,失败返回-1*/
fcntl函数有5种功能,取决于cmd参数:
F_DUPFD | 复制一个现有的描述符 |
F_GETFD/F_SETFD | 获得/设置文件描述符标记 |
F_GETFL/F_SETFL | 获得/设置文件状态标志 |
F_GETOWN/F_SETOWN | 获得/设置异步I/O所有权 |
F_GETLK、F_SETLK或F_SETLKW | 获得/设置文件记录锁 |