TLPI笔记—通用文件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可正可负)

附一张书中图:

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

交流

Scroll to top