“本章说明了UNIX系统提供的基本I/O函数。因为read和write都在内核执行,所以称这些函数为不带缓冲的I/O函数。在只使用read和write情况下,我们观察了不同的I/O长度对读文件所需时间的影响。我们也观察了许多将已写入的数据冲洗到磁盘上的方法,以及它们对应用程序性能的影响。
在说明多个进程对同一文件进行追加写操作以及多个进程创建同一文件时,本章介绍了原子操作。也介绍了内核用来共享打开文件信息的数据结构。”
本章介绍的打开文件的内核数据结构可以说是操作系统中最重要的部分之一。
3.2 文件描述符
3.3 函数open和openat
今后将碰到很多和函数xxx类似的、名为xxxat的函数,它们的作用是:1) 让线程可以使用相对路径名打开目录中的文件;2) 可以避免time-of-check-to-time-of-use错误。
time-of-check-to-time-of-use(TOCTTOU)错误说明了原子操作的必要性。
3.4 函数create
3.5 函数 close
当一个进程终止时,内核自动关闭它所有打开的文件。很多程序都利用了这一功能而不显式地用close关闭打开的文件。
3.6 函数 lseek
函数名字中的l表示长整形。在C语言引入长整形之前,用函数seek和tell提供类似功能。
3.7 函数 read
3.8 函数 write
3.9 I/O的效率
3.10 文件共享
3.11 原子操作
原子性的定位并执行I/O的函数:pread, pwrite。这两个函数定位并操作后,并不更新当前文件偏移量。
同样,“一个文件,如果存在就打开,如果不存在就创建”这组操作也必须原子性的执行,方法就是为open函数同时指定O_CREATE和O_EXCL选项。
3.12 函数dup和dup2
这两个函数的作用是复制一个现有的文件描述符。
dup返回的新文件描述符一定是当前可用文件描述符中的最小值;对于dup2,可以用fd2参数指定新描述符的值。
这里讲到了文件描述符的执行时关闭(FD_CLOEXEC)标志。但此处对它的意义及作用并没有解释,算是一个疑点吧。
3.13 函数sync、fsync和fdatasync
这三个函数的作用是保证磁盘上实际文件系统与缓冲区中内容的一致性。
sync只是将所有修改过的块缓冲区排入写队列,然后返回,并不等待实际写磁盘操作的结束。
fsync函数只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回。
fdatasync函数类似于fsync,但它只影响文件的数据部分(除数据外,fsync还会同步更新文件的属性)。
3.14 函数fcntl
fcntl函数可以改变已经打开的文件的属性。fcntl究竟是什么的简写?
它的功能很多,用处很大:
1) 复制一个已有的描述符(cmd=F_DUPFD或F_DUPFD_CLOEXEC);
2) 获取/设置文件描述符标志(cmd=F_GETFD或F_SETFD);
3) 获取/设置文件状态标志(cmd=F_GETFL或F_SETFL);
4) 获取/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN);
5) 获取/设置纪录锁(cmd=F_GETLK、F_SETLK或F_SELKW)。
3.15 函数 ioctl
ioctl函数一直是I/O操作的杂物箱,不能用本章中其他函数表示的I/O操作通常都能用ioctl表示。
ioctl是Single UNIX Specification标准的一个扩展部分,但是在SUSv4中已被移至弃用状态。POSIX.1已经用一些单独的函数代替了ioctl的部分功能。
3.16 /dev/fd
较新的系统都提供名为/dev/fd的目录,其目录项是名为0、1、2等的文件。打开文件/dev/fd/n等效于复制描述符n(假定描述符n是打开的)。
此功能不是POSIX.1的组成部分。
习题 3.1
这题比较重要,照抄附录中的答案吧。
所有磁盘I/O都要经过内核的块缓存区(也称为内核的缓冲区高速缓存)。唯一例外的是对原始磁盘设备的I/O,但是我们不考虑这种情况(Bach[1986]的第3章讲述了这种缓存区高速缓存的操作)。既然read或write的数据都要被内核缓冲,那么术语“不带缓冲的I/O”指的就是在用户的进程中对这两个函数不会自动缓冲,每次read或write就要进行一次系统调用。
习题 3.2
不能用fcntl,那么就只能用dup了。而dup返回的描述符编号是当前可用最小值,不能任意指定,程序就只能用不断dup的方式取到想要的编号了。
下面是实现代码(用事务保护取到的描述符的有效性、保存大量描述符、关闭大量描述符的代码并未实现,只是做了标记):
#include <fcntl.h>
#include <unistd.h>
#include "apue.h"
int my_dup2(int fd, int fd2);
int main()
{
int fd;
fd = open("test", O_RDWR | O_APPEND | O_CREAT, 0664);
if (fd < 0) err_sys("open error");
if (my_dup2(fd, 1) < 0) err_sys("my_dup2 error");
printf("my_dup2(fd, 1) success\n");
if (my_dup2(fd, 100) < 0) err_sys("my_dup2 error");
printf("my_dup2(fd, 100) success\n");
exit(0);
}
int my_dup2(int fd, int fd2)
{
int new_fd = -1;
if (fd < 0 || fd2 < 0) err_msg("parameters error");
/* make sure fd is open */
if (fd == fd2) return fd2;
/* begin transaction */
close(fd2);
while (new_fd != fd2)
{
if ((new_fd = dup(fd)) == -1) err_sys("dup error");
}
/* close all wrong new_fd */
/* end transaction */
return fd2;
}
习题 3.5
对于操作符>&,题目的解释方法很难让人明白。其实,m>&n等效于执行dup2(n, m),也就是复制n的描述符,并指定新的描述符编号为m。
于是,第一行首先把标准输出重定向到outfile,然后复制outfile的描述符到标准错误上,即标准错误也重定向到outfile;
第二行首先把标准错误重定向到标准输出,然后把标准输出重定向到outfile。
即,第一行把标准输出和标准错误都重定向到outfile,而第二行只把标准输出重定向到outfile。