TLPI笔记—通用文件I/O模型
tlpi Linux note I/O文件描述符
所有执行 I/O 操作的系统调用都是以文件描述符,一个非负整数来指代打开的文件。文件描述符用以表示所有类型的已打开的文件,包括管道(pipe)、FIFO、socket、终端、设备和普通文件。每个进程都各自独立维护着一张文件描述符表。
标准文件描述符
文件描述符 | 用途 | POSIX 名称 | stdio 流 |
---|---|---|---|
0 | 标准输入 | STDIN_FILENO | stdin |
1 | 标准输出 | STDOUT_FILENO | stdout |
2 | 标准错误 | STDERR_FILENO | stderr |
通用 I/O
UNIX I/O 模型的限制特点之一是其输入/输出的通用性概念。这意味着使用 4 个同样的系统调用open()
,resd()
,write()
,close()
可以对所有类型的文件执行 I/O 操作,包括终端之类的设备。因此,仅使用这些系统调用编写的程序,将对任何类型的文件都有效。
要实现通用 I/O,就必须确保每一文件系统和设备驱动程序都实现了相同的 I/O 系统调用集。由于文件系统和设备所特有的操作细节在内核中处理,在编程时通常可以忽略设备转悠的因素。一旦应用程序需要访问文件系统或设备的专有功能时,可以选择强大的ioctl()
系统调用,该调用为通用 I/O 模型之外的专有特性提供了访问接口。
打开一个文件:open()
open()
调用既可以打开一个已经存在的文件,也可以创建并打开一个新的文件。如果调用成功返回指代所打开的文件的文件描述符,若发生错误,则返回-1。
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags, .../* mode_t mode*/);
// Return file description on success, or -1 in error
- pathname:要打开的文件,如果 pathname 是一个符号链接,会对其进行解引用。
- flags: 位掩码参数,用户指定文件的访问模式,后面给出常用访问模式列表
- mode:位掩码参数,指定了文件的访问权限(如果访问模式未指定 O_CREATE 标志,则可以省略 mode 参数)
常用 flags(文件访问模式)参数
标志 | 用途 | 统一 UNIX 规范版本 |
---|---|---|
O_RDONLY | 以只读方式打开 | v3 |
O_WRONLY | 以只写方式打开 | v3 |
O_RDWR | 以读写方式打开 | v3 |
O_CLOEXEC | 设置 close-on-exec 标志(Linux 2.6.23+) | v4 |
O_CREATE | 若文件不存在则创建 | v3 |
O_DIRECT | 无缓冲的输入输出 | |
O_DIRECTORY | 如果 pathname 不是目录,则失败 | |
O_EXCL | 结合 O_CREATE 参数使用,确保调用者以独占方式访问文件 | v4 |
O_LARGEFILE | 在 32 位系统中使用此标志打开大文件 | |
O_NOATIME | 调用read() 时,不修改文件最近访问时间(Linux2.6.8+) | |
O_NOCTTY | 不让 pathname 所指向的终端设备成为控制终端 | v3 |
O_NOFOLLOW | 对符号链接不予解引用 | v4 |
O_TRUNC | 截断已有文件,使其长度为 0 | v3 |
O_APPEND | 总在文件尾部追加数据 | v3 |
O_ASYNC | 当 I/O 操作可行时,产生信号通知进程(信号驱动 I/O) | |
O_DSYNC | 提供同步的 I/O 数据完整性(Linux 2.6.33+) | v3 |
O_NONBLOCK | 以非阻塞方式打开 | v3 |
O_SYNC | 以同步方式写入文件 | v3 |
open()函数的错误
若打开文件时发生错误,open()
将返回-1,错误号errno
标识错误原因。以下是一些可能发生的错误:
errno | 描述 |
---|---|
EACCES | 文件权限不允许调用进程以 flags 参数指定的方式开大文件。无法访问文件,其可能原因有目录权限的限制、文件不存在并且无法创建该文件。 |
EISDIR | 所指定的文件属于目录,而调用者企图打开该文件进行写操作。不允许这种用法。(另一方面,在某些场合中,打开目录进行读操作是有必要的。) |
EMFILE | 进程已打开的文件描述符数量达到了进程资源限制所设定的上限(RLIMIT_NOFILE 参数) |
ENFILE | 文件打开数量已经达到系统允许的上限 |
ENOENT | 要么文件不存在且未指定 O_CREATE 标志,要么指定了 O_CREATE 标志,但 pathname 参数所指定路径的目录之一不存在,或者 pathname 参数为符号链接,而该链接指向的文件不存在 |
EROFS | 所指定的文件隶属于只读文件系统,而调用者企图以写方式打开文件 |
ETXTBSY | 所指定的文件为可执行文件,且正在运行中。系统不允许修改正在运行的程序。必须先终止程序运行,然后方可修改可执行文件。 |
读取文件内容:read()
read()
系统调用从文件描述符 fd 所指代的打开文件中读取数据。如果调用成功,将返回实际读取的字节数,如果遇到文件结束(EOF)则返回 0,如果出现错误则返回-1。ssize_t
数据类型属于有符号的整数,size_t
数据类型是无符号的整数类型。
#include <unistd.h>
ssize_t read(int fd, void *buffer, size_t count);
// Return number of bytes read, 0 on EOF, or -1 on error
- fd:已打开的文件描述符
- buffer:用来存放输入数据的内存缓冲区地址
- count:指定最多能读取的字节数,count 应该小于等于 buffer 的大小
一个小细节
因为read()
能够从文件中读取任意序列的字节。有些情况下,输入信息可能是文件数据,但在其他情况下,也有可能是二进制整数或者二进制形式的 C 语言数据结构。read()
无从区分这些数据,故而也无法遵从 C 语言对字符串处理的约定—在字符串尾部追加标识字符串结束的空字符。如果输入缓冲区的结尾处需要一个表示终止的空字符,必须显式追加。
char buffer[MAX_READ + 1];
ssize_t numRead;
numRead = read(STDIN_FILENO, buffer, MAX_READ);
if (numRead==-1)
errExit("read");
buffer[numRead] = '\0';
printf("The input data was: %s\n", buffer);
由于表示字符串终止的空字符需要一个字节的内存,所以缓冲区的大小至少要比预计独缺的最大字符串长度多出 1 个字节。
数据写入文件:write()
write()
系统调用将数据写入一个已打开的文件中。如果调用成功,将返回实际写入文件的字节数,该返回值可能小于count
参数值。这被称为“部分写”。对于磁盘文件来说,造成“部分写”的原因可能是由于磁盘已满,或是因为进程资源对文件大小的限制。
#include <unistd.h>
ssize_t write(int fd, void *buffer, size_t count);
// Return number of bytes written, or -1 on error
对磁盘文件之心 I/O 操作时,write()
调用成功并不能保证数据已经写入磁盘。因为为了减少磁盘活动量和加快write()
系统调用,内核会缓存磁盘的 I/O 操作。
关闭文件:close()
close()
系统调用关闭一个打开的文件描述符,并将其释放回调用进程,供该进程继续使用。当一进程终止时,将自动关闭其已打开的所有文件描述符。
#include <unistd.h>
int close(int fd);
// Return 0 on success, or -1 on error
改变文件偏移量:lseek()
对于每个打开的文件,系统内核会记录其文件偏移量(或者读写偏移量,或者指针)。文件偏移量是指执行下一个read()
或write()
操作的文件起始位置,会以相对于文件头部起始点的文件当前位置来表示。文件第一个字节的偏移量为 0。
文件打开时,会将文件偏移量设置为指向文件开始,以后每次read()
或write()
调用将自动对其进行调整,以指向已读或已写数据后的下一个字节。因此,连续的read()
或write()
调用将按照顺序递进对文件进行操作。
lseek()
系统调用是用来调整这个文件偏移量的。调用成功会返回新的文件偏移量。
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
// Return new file offset if successful, or -1 on error
- fd:已打开的文件描述符
- offset:指定一个以字节为单位的数值(SUSv3 规定
off_t
数据类型为有符号整型) - whence:指定按照哪个基准点来解释
offset
参数
whence 参数可选值
参数 | 描述 |
---|---|
SEEK_SET | 将文件偏移量设置为从文件头部起始点开始的 offset 个字节(offset 非负) |
SEEK_CUR | 相对于当前位置将文件偏移量调整 offset 个字节(offset 可正可负) |
SEEK_END | 起始文件偏移量设置在文件尾部,调整 offset 个字节(offset 可正可负) |
附一张书中图:
lseek()
调用只是调整内核中与文件描述符相关的文件偏移量记录,并没有引起对任何物理设备的访问。
lseek()
并不使用于所有类型的文件。不允许将lseek()
应用与管道、FIFO、socket 或者终端。一旦如此,调用将会失败,并将errno
置为ESPIPE。另一方面,只要合情合理,也可以将lseek()
应用于设备。例如,在磁盘或磁带上查找一处具体位置。
lseek()
调用名中的l
源于这样一个事实:offset 参数和调用返回值的类型期初都是long
型。早期的 UNIX 系统还提供seek()
系统调用,当时这个调用的 offset 和返回值类型为int
型。
文件空洞
如果程序的文件偏移量已然跨越了文件结尾,然后再执行 I/O 操作,read()
调用将返回 0,表示文件结尾。但!write()
函数可以在文件结尾后的任意位置写入数据。
从文件结尾后到新写入数据间的这段空间被称为文件空洞。从编程角度看,文件空洞中是存在字节的,读取空洞将返回以 0(空字节)填充的缓冲区。
然而,文件空洞不占用任何磁盘空间。知道后续某个时间点,在文件空洞中写入了数据,文件系统才会位置分配磁盘块。文件空洞的主要优势在于,于实际需要的空字节分配磁盘快相比,稀疏填充的文件会占用较少的磁盘空间。例如:核心转储(core dump)就是包含空洞文件的常见例子。
——EOF——