====================
== Hi, I'm Vimiix ==
====================
Get hands dirty.

TLPI笔记—通用文件I/O模型

tlpi Linux note I/O

文件描述符

所有执行 I/O 操作的系统调用都是以文件描述符,一个非负整数来指代打开的文件。文件描述符用以表示所有类型的已打开的文件,包括管道(pipe)、FIFO、socket、终端、设备和普通文件。每个进程都各自独立维护着一张文件描述符表。

标准文件描述符

文件描述符用途POSIX 名称stdio 流
0标准输入STDIN_FILENOstdin
1标准输出STDOUT_FILENOstdout
2标准错误STDERR_FILENOstderr

通用 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截断已有文件,使其长度为 0v3
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 可正可负)

附一张书中图:

whence

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——