[APUE] 第三章 文件 I/O

作者 Shilei Tian 日期 2016-04-29
[APUE] 第三章 文件 I/O

大概四年前陆陆续续读过 APUE,那个时候还是第二版,并且那个时候对于很多概念没有什么清楚的认识,读起来很快就给忘了。
时光荏苒,四年很快过去了,我已经从当时刚转专业的大二学生,变成了正在读 CS 的研一学生。最近突然发现,原来我对 Unix 系统以及网络的协议栈感兴趣。趁着现在还在上学,搞来了一本第三版打算系统学起来。经过这几年的沉淀,对于书中讲述的很多问题不再是比较陌生,或者是读完了没什么感觉。感触最深的就是多线程和多进程部分,当时对于这些知识在生活中或者在现成的技术中是如何的应用还没什么概念,所以看的时候可能也没有用心。现在对于它们有了全新的理解,所以重新看一遍觉得确实受益匪浅。

不过,看书不能从中间取一段看,知识都是环环相扣的,特别是 APUE 这种经典,只能从头开始看。于是将我觉得重要的内容记录下来,留作以后查阅。

  1. 术语不带缓冲指的是每个 readwrite 都调用内核中的一个系统调用,不带缓冲的 I/O 函数不是 ISO C 的组成部分。
  2. 对于内核而言,所有打开的文件都通过文件描述符(非负整数)引用。
  3. 按照惯例,Unix 系统 shell 把文件描述符 0 与进程的标准输入关联(STDIN_FILENO),文件描述符 1 与标准输出关联(STDOUT_FILENO),文件描述符 2 与标准错误关联(STDERR_FILENO)。
  4. 文件描述符的变化范围是 0~OPEN_MAX - 1
  5. openoepnat 函数返回的文件描述符一定是最小的未用的描述符数值。
  6. openat 函数希望解决两个问题:第一,让线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录。第二,可以避免 time-of-chech-to-time-of-use (TOCTTOU) 错误。
  7. TOCTTOU 错误的基本思想:如果有两个基于文件的函数调用,其中第二个调用依赖于第一个调用的结果,那么程序是脆弱的。因为两个调用并不是原子操作,在两个函数调用之间文件可能改变了,这样也就造成了第一个调用的结果就不再有效。
  8. 关闭一个文件时还会释放该进程加在该文件上的所有记录锁。
  9. 关闭一个文件时,内核自动关闭它所有的打开文件。很多程序都利用了这一功能而不显示地用 close 关闭打开文件。
  10. 按系统默认的情况,当打开一个文件时,除非指定 O_APPEND 选项,否则该偏移量被设置为 0
  11. 通常,文件的当前偏移量应当是一个非负整数,但是,某些设备也可能允许负的偏移量。但对于普通文件,其偏移量必须是非负值。因为偏移量可能是负值,所以在比较 lseek 的返回值时应当谨慎,不要测试它是否小于 0,而要测试它是否等于 -1
  12. 文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为 0文件中的空洞并不要求在磁盘上占用存储区。
  13. 尽管可以实现 64 位文件偏移量,但是能否创建一个大于 2 GB 的文件则依赖于底层文件系统的类型。
  14. write 出错的一个常见原因是磁带已满,或者超过了一个给定进程的文件长度限制。
  15. 创建 v 节点结构的目的是对在一个计算机系统上的多文件系统类型提供支持。
  16. Unix 系统为追加到文件尾端这样的操作提供了一种原子操作方法,即在打开文件时设置 O_APPEND 标志,这样做使得内核在每次写操作之前,都将进程的当前偏移量设置到该文件的尾端,于是在每次写之前就不需要调用 lseek
  17. open 函数的 O_CREATEO_EXCL 选项同时指定后,如果该文件已经存在时,open 将失败。
  18. 新描述符的 close-on-exec 标志总是由 dup 函数清除。
  19. 复制一个文件描述符的另一种方法是使用 fcntl 函数。
  20. sync 函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写词盘操作结束。通常,称为 update 的系统守护进程周期性地调用(一般每隔 30 秒)sync 函数。
  21. fsync 函数只对由文件描述符 fd 指定的一个文件起作用,并且等待写词盘操作结束才返回。
  22. fcntl 的返回值与命令有关,如果出错,所有命令都返回 -1,如果成功则返回某个其他值。
  23. 在修改文件描述符标志位时必须谨慎,首先获得现在的标志值,然后按照期望修改它。不能只是执行 F_SETFDF_SETFL 命令,这样会关闭以前设置的标志位。
  24. Unix 系统中,通常 write 只是将数据排入队列,而实际的写词盘操作则可能在以后的某个时刻进行。
  25. 终端 I/O 是使用 ioctl 最多的地方。

函数 openoflag 参数

Flag 备注
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读、写打开
O_EXEC 只执行打开
O_EXEC 只搜索打开(应用于目录)

以上 5 个常量中必须制定一个且只能指定一个。还有一些常量是可选的,这里不赘述。

函数 lseek 对参数 offset 的解释

#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
// 返回值:若成功,返回新的文件偏移量;若出错,返回 -1

对参数 offset 的解释与参数 whence 的值有关

  • whenceSEEK_SET,则将该文件的偏移量设置为距文件开始处 offset 个子节。
  • whenceSEEK_CUR,则将该文件的偏移量设置为其当前值加 offsetoffset 可为正或负。
  • whenceSEEK_END,则将该文件的偏移量设置为文件长度加 offsetoffset 可为正或负。

lseek 成功执行,则返回新的文件偏移量,为此可以用下列方式确定打开文件的当前偏移量:

off_t currpos = lseek(fd, 0, SEEK_CUR);

这种方法也可涌来确定所涉及的文件是否可以设置偏移量。如果文件描述符指向的是一个管道、FIFO 或网络套接字,则 lseek 返回 -1,并将 errno 设置为 ESPIPE

函数 read

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
// 返回值:读到的字节数,若已到文件尾,返回 0;若出错,返回 -1

由多种情况可使实际读到的字节数少于要求读到的字节数:

  • 读普通文件时,在读到要求字节数之前已到达了文件尾端。
  • 当从终端设备读时,通常一次最多读一行。
  • 当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数。
  • 当从管道或 FIFO 读时,如若管道包含的字节少于所需的数量,那么 read 将只返回实际可用的字节数。
  • 当从某些面向记录的设备(如磁带)读时,一次最多返回一个记录。
  • 当一信号造成中断,而已经读了部分数据量时。

函数 preadpwrite

#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
// 返回值:读到的字节数,若已到文件尾,返回 0;若出错,返回 -1
ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);
// 返回值:若成功,返回已写的字节数;若出错,返回 -1

调用 pread 相当于调用 lseek 后调用 read,但是 pread 又与这种顺序调用有下列重要区别:

  • 调用 pread 时,无法中断其定位和操作。
  • 不更新当前文件偏移。
    调用 pwrite 相当于调用 lseek 后调用 write,但也与它们有类似的区别。

函数 dupdup2

#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
// 返回值:若成功,返回新的文件描述符;若出错,返回 -1

dup 返回的新文件描述符一定是当前可用文件描述符中的最小数值。对于 dup2,可以用 fd2 参数指定新描述符的值。如果 fd2 已经打开,则先将其关闭。如若 fd 等于 fd2,则 dup2 返回 fd2,而不关闭它。
这些函数返回的新文件描述符与参数 fd 共享同一个文件表项。