本章开始讨论UNIX系统,先说明可用的文件I/O函数——打开文件、读文件、写文件等。UNIX系统中的大多数文件I/O只需用到5个函数:open、read、write、lseek以及close。然后说明不同缓冲长度对read和write函数的影响。
本章描述的函数经常被称为不带缓冲的I/O(unbuffered I/O,与将在第5章中说明的标准I/O函数相对照)。术语不带缓冲指的是每个read和write都调用内核中的一个系统调用。这些不带缓冲的I/O函数不是ISO C的组成部分,但是,它们是POSIX.1和Single UNIX Specification的组成部分。
只要涉及在多个进程间共享资源,原子操作的概念就变得非常重要。我们将通过文件I/O和open函数的参数来讨论此概念。然后,本章将进一步讨论在多个进程间如何共享文件,以及所涉及的内核有关数据结构。在描述了这些特征后,将说明dup、fcntl、sync、fsync和ioctl函数。
对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,使用open或creat返回的文件描述符标识该文件,将其作为参数传送给read或write。
按照惯例,UNIX系统shell把文件描述符0与进程的标准输入关联,文件描述符1与标准输出关联,文件描述符2与标准错误关联。这是各种 shell以及很多应用程序使用的惯例,与UNIX内核无关。尽管如此,如果不遵循这种惯例,很多UNIX系统应用程序就不能正常工作。
在符合POSIX.1的应用程序中,幻数0、1、2虽然已被标准化,但应当把它们替换成符号常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO以提高可读性。这些常量都在头文件<unistd.h>中定义。
文件描述符的变化范围是0~OPEN_MAX-1(见图2-11)。早期的UNIX系统实现采用的上限值是19(允许每个进程最多打开20个文件),但现在很多系统将其上限值增加至63。
对于FreeBSD 8.0、Linux 3.2.0、Mac OS X 10.6.8以及Solaris 10,文件描述符的变化范围几乎是无限的,它只受到系统配置的存储器总量、整型的字长以及系统管理员所配置的软限制和硬限制的约束。
调用open或openat函数可以打开或创建一个文件。
#include <fcntl.h>
int open(const char *path, int oflag,... /* mode_t mode */);
int openat(int f d, const char *path, int oflag, ... /* mode_t mode */ );
两函数的返回值:若成功,返回文件描述符;若出错,返回−1
我们将最后一个参数写为...,ISO C用这种方法表明余下的参数的数量及其类型是可变的。对于open函数而言,仅当创建新文件时才使用最后这个参数(稍后将对此进行说明)。在函数原型中将此参数放置在注释中。
path参数是要打开或创建文件的名字。oflag参数可用来说明此函数的多个选项。用下列一个或多个常量进行“或”运算构成oflag参数(这些常量在头文件<fcntl.h>中定义)。
O_RDONLY 只读打开。
O_WRONLY 只写打开。
O_RDWR 读、写打开。
大多数实现将O_RDONLY定义为0,O_WRONLY定义为1,O_RDWR定义为2,以与早期的程序兼容。
O_EXEC 只执行打开。
O_SEARCH 只搜索打开(应用于目录)。
O_SEARCH常量的目的在于在目录打开时验证它的搜索权限。对目录的文件描述符的后续操作就不需要再次检查对该目录的搜索权限。本书中涉及的操作系统目前都没有支持O_SEARCH。
在这5个常量中必须指定一个且只能指定一个。下列常量则是可选的。
O_APPEND 每次写时都追加到文件的尾端。3.11节将详细说明此选项。
O_CLOEXEC 把FD_CLOEXEC常量设置为文件描述符标志。3.14节中将说明文件描述符标志。
O_CREAT 若此文件不存在则创建它。使用此选项时,open函数需同时说明第3个参数mode(openat函数需说明第4个参数mode),用mode指定该新文件的访问权限位(4.5 节将说明文件的权限位,那时就能了解如何指定mode,以及如何用进程的umask值修改它)。
O_DIRECTORY 如果path引用的不是目录,则出错。
O_EXCL 如果同时指定了 O_CREAT,而文件已经存在,则出错。用此可以测试一个文件是否存在,如果不存在,则创建此文件,这使测试和创建两者成为一个原子操作。3.11节将更详细地说明原子操作。
O_NOCTTY 如果path引用的是终端设备,则不将该设备分配作为此进程的控制终端。9.6节将说明控制终端。
O_NOFOLLOW 如果path引用的是一个符号链接,则出错。4.17节将说明符号链接。
O_NONBLOCK 如果path引用的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置非阻塞方式。14.2节将说明此工作模式。
较早的System V引入了O_NDELAY(不延迟)标志,它与O_NONBLOCK(不阻塞)选项类似,但它的读操作返回值具有二义性。如果不能从管道、FIFO或设备读得数据,则不延迟选项使read返回0,这与表示已读到文件尾端的返回值0冲突。基于SVR4的系统仍支持这种语义的不延迟选项,但是新的应用程序应当使用不阻塞选项代替之。
O_SYNC 使每次write等待物理I/O操作完成,包括由该write操作引起的文件属性更新所需的I/O。3.14节将使用此选项。
O_TRUNC 如果此文件存在,而且为只写或读-写成功打开,则将其长度截断为0。
O_TTY_INIT 如果打开一个还未打开的终端设备,设置非标准 termios 参数值,使其符合Single UNIX Specification。第18章将讨论终端I/O的termios结构。
下面两个标志也是可选的。它们是Single UNIX Specification(以及POSIX.1)中同步输入和输出选项的一部分。
O_DSYNC 使每次write要等待物理I/O操作完成,但是如果该写操作并不影响读取刚写入的数据,则不需等待文件属性被更新。
O_DSYNC 和 O_SYNC 标志有微妙的区别。仅当文件属性需要更新以反映文件数据变化(例如,更新文件大小以反映文件中包含了更多的数据)时,O_DSYNC标志才影响文件属性。而设置O_SYNC标志后,数据和属性总是同步更新。当文件用O_DSYN标志打开,在重写其现有的部分内容时,文件时间属性不会同步更新。与此相反,如果文件是用O_SYNC标志打开,那么对该文件的每一次write都将在write返回前更新文件时间,这与是否改写现有字节或追加写文件无关。
O_RSYNC 使每一个以文件描述符作为参数进行的read操作等待,直至所有对文件同一部分挂起的写操作都完成。
Solaris 10 支持所有这 3 个标志。FreeBSD(和 Mac OS X)设置了另外一个标志(O_FSYNC),它与标志O_SYNC的作用相同。因为这两个标志是等效的,它们定义的标志具有相同的值。FreeBSD 8.0不支持O_DSYNC或O_RSYNC标志。Mac OS X并不支持O_RSYNC,但却定义了O_DSYNC,处理O_DSYNC与处理O_SYNC相同。Linux 3.2.0定义了O_DSYNC,但处理O_RSYNC与处理O_SYNC相同。
由open和openat函数返回的文件描述符一定是最小的未用描述符数值。这一点被某些应用程序用来在标准输入、标准输出或标准错误上打开新的文件。例如,一个应用程序可以先关闭标准输出(通常是文件描述符1),然后打开另一个文件,执行打开操作前就能了解到该文件一定会在文件描述符1上打开。在3.12节说明dup2函数时,可以了解到有更好的方法来保证在一个给定的描述符上打开一个文件。
fd参数把open和openat函数区分开,共有3种可能性。
(1)path参数指定的是绝对路径名,在这种情况下,fd参数被忽略,openat函数就相当于open函数。
(2)path参数指定的是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址。fd参数是通过打开相对路径名所在的目录来获取。
(3)path参数指定了相对路径名,fd参数具有特殊值AT_FDCWD。在这种情况下,路径名在当前工作目录中获取,openat函数在操作上与open函数类似。
openat函数是POSIX.1最新版本中新增的一类函数之一,希望解决两个问题。第一,让线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录。在第 11 章我们会看到,同一进程中的所有线程共享相同的当前工作目录,因此很难让同一进程的多个不同线程在同一时间工作在不同的目录中。第二,可以避免time-of-check-to-time-of-use(TOCTTOU)错误。
TOCTTOU错误的基本思想是:如果有两个基于文件的函数调用,其中第二个调用依赖于第一个调用的结果,那么程序是脆弱的。因为两个调用并不是原子操作,在两个函数调用之间文件可能改变了,这样也就造成了第一个调用的结果就不再有效,使得程序最终的结果是错误的。文件系统命名空间中的TOCTTOU错误通常处理的就是那些颠覆文件系统权限的小把戏,这些小把戏通过骗取特权程序降低特权文件的权限控制或者让特权文件打开一个安全漏洞等方式进行。Wei和Pu[2005]在UNIX文件系统接口中讨论了TOCTTOU的缺陷。
文件名和路径名截断
如果NAME_MAX是14,而我们却试图在当前目录中创建一个文件名包含15个字符的新文件,此时会发生什么呢?按照传统,早期的System V版本(如SVR2)允许这种使用方法,但总是将文件名截断为 14 个字符,而且不给出任何信息,而 BSD 类的系统则返回出错状态,并将 errno 设置为ENAMETOOLONG。无声无息地截断文件名会引起问题,而且它不仅仅影响到创建新文件。如果NAME_MAX是14,而存在一个文件名恰好就是14个字符的文件,那么以路径名作为其参数的任一函数(open、stat等)都无法确定该文件的原始名是什么。其原因是这些函数无法判断该文件名是否被截断过。
在POSIX.1中,常量_POSIX_NO_TRUNC决定是要截断过长的文件名或路径名,还是返回一个出错。正如我们在第 2 章中已经见过的,根据文件系统的类型,此值可以变化。我们可以用fpathconf或pathconf来查询目录具体支持何种行为,到底是截断过长的文件名还是返回出错。
是否返回一个出错值在很大程度上是历史形成的。例如。基于SVR4的系统对传统的System V文件系统(S5)并不出错,但是它对BSD风格的文件系统(UFS)则出错。作为另一个例子(参见图2-20),Solaris对UFS返回出错,对与DOS兼容的文件系统PCFS则不返回出错,其原因是DOS会无声无息地截断不匹配8.3格式的文件名。BSD类系统和Linux总是会返回出错。
若_POSIX_NO_TRUNC有效,则在整个路径名超过PATH_MAX,或路径名中的任一文件名超过NAME_MAX时,出错返回,并将errno设置为ENAMETOOLONG。
大多数的现代文件系统支持文件名的最大长度可以为255。因为文件名通常比这个限制要短,因此对大多数应用程序来说这个限制还未出现什么问题。
也可调用creat函数创建一个新文件。
#include <fcntl.h>
int creat(const char *path, mode_t mode);
返回值:若成功,返回为只写打开的文件描述符;若出错,返回−1
注意,此函数等效于:
open(path, O_WRONLY|O_CREAT|O_TRUNC, mode);
在早期的UNIX系统版本中,open的第二个参数只能是0、1或2。无法打开一个尚未存在的文件,因此需要另一个系统调用creat以创建新文件。现在,open函数提供了选项O_CREAT和O_TRUNC,于是也就不再需要单独的creat函数。
在4.5节中,我们将详细说明文件访问权限,并说明如何指定mode。
creat的一个不足之处是它以只写方式打开所创建的文件。在提供open的新版本之前,如果要创建一个临时文件,并要先写该文件,然后又读该文件,则必须先调用creat、close,然后再调用open。现在则可用下列方式调用open实现:
open(path, O_RDWR|O_CREAT|O_TRUNC, mode);
可调用close函数关闭一个打开文件。
#include <unistd.h>
int close (int fd);
返回值:若成功,返回0;若出错,返回−1
关闭一个文件时还会释放该进程加在该文件上的所有记录锁。14.3节将讨论这一点。
当一个进程终止时,内核自动关闭它所有的打开文件。很多程序都利用了这一功能而不显式地用close关闭打开文件。实例见图1-4程序。
每个打开文件都有一个与其相关联的“当前文件偏移量”(current file offset)。它通常是一个非负整数,用以度量从文件开始处计算的字节数(本节稍后将对“非负”这一修饰词的某些例外进行说明)。通常,读、写操作都从当前文件偏移量处开始,并使偏移量增加所读写的字节数。按系统默认的情况,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0。
可以调用lseek显式地为一个打开文件设置偏移量。
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
返回值:若成功,返回新的文件偏移量;若出错,返回为−1
对参数offset的解释与参数whence的值有关。
•若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节。
•若whence是SEEK_CUR,则将该文件的偏移量设置为其当前值加offset,offset可为正或负。
•若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可正可负。
若lseek成功执行,则返回新的文件偏移量,为此可以用下列方式确定打开文件的当前偏移量:
off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);
这种方法也可用来确定所涉及的文件是否可以设置偏移量。如果文件描述符指向的是一个管道、FIFO或网络套接字,则lseek返回−1,并将errno设置为ESPIPE。
3个符号常量SEEK_SET、SEEK_CUR和SEEK_END是在System V中引入的。在System V之前,whence被指定为 0(绝对偏移量)、1(相对于当前位置的偏移量)或 2(相对文件尾端的偏移量)。很多软件仍然把这些数字直接写在代码里。
在lseek中的字符l表示长整型。在引入off_t数据类型之前,offset参数和返回值是长整型的。lseek是在UNIX V7中引入的,当时C语言中增加了长整型(在UNIX V6中,用函数seek和tell提供类似功能)。
实例
图3-1所示的程序用于测试对其标准输入能否设置偏移量。
图3-1 测试标准输入能否被设置偏移量
如果用交互方式调用此程序,则可得
$ ./a.out < /etc/passwd
seek OK
$ cat < /etc/passwd| ./a.out
cannot seek
$ ./a.out < /var/spool/cron/FIFO
cannot seek
通常,文件的当前偏移量应当是一个非负整数,但是,某些设备也可能允许负的偏移量。但对于普通文件,其偏移量必须是非负值。因为偏移量可能是负值,所以在比较 lseek 的返回值时应当谨慎,不要测试它是否小于0,而要测试它是否等于−1。
在Intel x86处理器上运行的FreeBSD的设备/dev/kmem支持负的偏移量。
因为偏移量(off_t)是带符号数据类型(见图2-21),所以文件的最大长度会减少一半。例如,若off_t是32位整型,则文件最大长度是231 -1个字节。
lseek仅将当前的文件偏移量记录在内核中,它并不引起任何I/O操作。然后,该偏移量用于下一个读或写操作。
文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为0。
文件中的空洞并不要求在磁盘上占用存储区。具体处理方式与文件系统的实现有关,当定位到超出文件尾端之后写时,对于新写的数据需要分配磁盘块,但是对于原文件尾端和新开始写位置之间的部分则不需要分配磁盘块。
实例
图3-2所示的程序用于创建一个具有空洞的文件。
运行该程序得到:
$ ./a.out
$ ls -l file.hole 检查其大小
-rw-r--r-- 1 sar 16394 Nov 25 01:01 file.hole
$ od -c file.hole 观察实际内容
图3-2 创建一个具有空洞的文件
0000000 a b c d e f g h i j \0 \0 \0 \0 \0 \0
0000020 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
*
0040000 A B C D E F G H I J
0040012
使用od(1)命令观察该文件的实际内容。命令行中的-c标志表示以字符方式打印文件内容。从中可以看到,文件中间的30个未写入字节都被读成0。每一行开始的一个7位数是以八进制形式表示的字节偏移量。
为了证明在该文件中确实有一个空洞,将刚创建的文件与同样长度但无空洞的文件进行比较:
$ ls -ls file.hole file.nohole 比较长度
8 -rw-r--r-- 1 sar 16394 Nov 25 01:01 file.hole
20 -rw-r--r-- 1 sar 16394 Nov 25 01:03 file.nohole
虽然两个文件的长度相同,但无空洞的文件占用了20个磁盘块,而具有空洞的文件只占用8个磁盘块。
在此实例中调用了将在3.8节中说明的write函数。4.12节将对具有空洞的文件进行更多说明。
因为lseek使用的偏移量是用off_t类型表示的,所以允许具体实现根据各自特定的平台自行选择大小合适的数据类型。现今大多数平台提供两组接口以处理文件偏移量。一组使用 32位文件偏移量,另一组则使用64位文件偏移量。
Single UNIX Specification向应用程序提供了一种方法,使其通过sysconf函数确定支持何种环境(见2.5.4节)。图3-3总结了定义的sysconf常量。
图3-3 sysconf的数据大小选项和name参数
c99 编译器要求使用 getconf(1)命令将所期望的数据大小模型映射为编译和链接程序所需的标志。根据每个平台支持环境的不同,可能需要不同的标志和库。
遗憾的是,在这方面,实现还未跟上标准的步伐。如果你的系统没有匹配标准的最新版本,那么系统还可能支持Single UNIX Specification前一版本中的选项名:_POSIX_V6_ILP32_OFF32、_POSIX_V6_ILP32_OFFBIG、_POSIX_V6_LP64_OFF64和_POSIX_V6_LP64_OFFBIG。
为了避开这一点,应用程序可以将符号常量_FILE_OFFSET_BITS设置为64,以支持64位偏移量。这样就将off_t定义更改为64位带符号整型。将_FILE_OFFSET_BITS符号常量设置为32以支持32位偏移量。但是,应当注意的是,虽然本书讨论的4种平台都支持32位和64位文件偏移量,但是通过设置_FILE_OFFSET_BITS符号常量的值这种方法并不能保证应用程序是可移植的,也有可能达不到预期的效果。
图3-4总结了在本书涉及的4种平台上,当应用程序没有定义_FILE_OFFSET_BITS时,off_t数据类型的字节数以及_FILE_OFFSET_BITS被定义成32或64时,off_t数据类型的字节数。
图3-4 不同平台上off_t的字节数
注意:尽管可以实现64位文件偏移量,但是能否创建一个大于2 GB(231 -1字节)的文件则依赖于底层文件系统的类型。
调用read函数从打开文件中读数据。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
返回值:读到的字节数,若已到文件尾,返回0;若出错,返回−1
如read成功,则返回读到的字节数。如已到达文件的尾端,则返回0。
有多种情况可使实际读到的字节数少于要求读的字节数:
•读普通文件时,在读到要求字节数之前已到达了文件尾端。例如,若在到达文件尾端之前有30个字节,而要求读100个字节,则read返回30。下一次再调用read时,它将返回0(文件尾端)。
•当从终端设备读时,通常一次最多读一行(第18章将介绍如何改变这一点)。
•当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数。
•当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。
•当从某些面向记录的设备(如磁带)读时,一次最多返回一个记录。
•当一信号造成中断,而已经读了部分数据量时。我们将在10.5节进一步讨论此种情况。读操作从文件的当前偏移量处开始,在成功返回之前,该偏移量将增加实际读到的字节数。POSIX.1从几个方面对read函数的原型做了更改。经典的原型定义是:
int read(int fd, char *buf, unsigned nbytes);
•首先,为了与ISO C一致,第2个参数由char *改为void *。在ISO C中,类型void *用于表示通用指针。
•其次,返回值必须是一个带符号整型(ssize_t),以保证能够返回正整数字节数、0(表示文件尾端)或−1(出错)。
•最后,第3个参数在历史上是一个无符号整型,这允许一个16位的实现一次读或写的数据可以多达 65 534 个字节。在 1990 POSIX.1 标准中,引入了新的基本系统数据类型ssize_t以提供带符号的返回值,不带符号的size_t则用于第3个参数(见2.5.2节中的SSIZE_MAX常量)。
调用write函数向打开文件写数据。
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
返回值:若成功,返回已写的字节数;若出错,返回−1
其返回值通常与参数nbytes的值相同,否则表示出错。write出错的一个常见原因是磁盘已写满,或者超过了一个给定进程的文件长度限制(见7.11节及习题10.11)。
对于普通文件,写操作从文件的当前偏移量处开始。如果在打开该文件时,指定了O_APPEND选项,则在每次写操作之前,将文件偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。
图3-5程序只使用read和write函数复制一个文件。
图3-5 将标准输入复制到标准输出
关于该程序应注意以下几点。
•它从标准输入读,写至标准输出,这就假定在执行本程序之前,这些标准输入、输出已由shell安排好。确实,所有常用的UNIX系统shell都提供一种方法,它在标准输入上打开一个文件用于读,在标准输出上创建(或重写)一个文件。这使得程序不必打开输入和输出文件,并允许用户利用shell的I/O重定向功能。
•考虑到进程终止时,UNIX系统内核会关闭进程的所有打开的文件描述符,所以此程序并不关闭输入和输出文件。
•对 UNIX 系统内核而言,文本文件和二进制代码文件并无区别,所以本程序对这两种文件都有效。
我们还没有回答的一个问题是如何选取BUFFSIZE值。在回答此问题之前,让我们先用各种不同的BUFFSIZE值来运行此程序。图3-6显示了用20种不同的缓冲区长度,读516 581 760字节的文件所得到的结果。
用图 3-5 的程序读文件,其标准输出被重新定向到/dev/null 上。此测试所用的文件系统是Linux ext4文件系统,其磁盘块长度为4 096字节(磁盘块长度由st_blksize表示,在4.12节中说明其值为 4 096)。这也证明了图 3-6 中系统 CPU 时间的几个最小值差不多出现在BUFFSIZE为4 096及以后的位置,继续增加缓冲区长度对此时间几乎没有影响。
图3-6 Linux上用不同缓冲长度进行读操作的时间结果
大多数文件系统为改善性能都采用某种预读(read ahead)技术。当检测到正进行顺序读取时,系统就试图读入比应用所要求的更多数据,并假想应用很快就会读这些数据。预读的效果可以从图3-6中看出,缓冲区长度小至32字节时的时钟时间与拥有较大缓冲区长度时的时钟时间几乎一样。
我们以后还将回到这一实例上。3.14 节将用此说明同步写的效果,5.8 节将比较不带缓冲的I/O时间与标准I/O库所用的时间。
应当了解,在什么时间对实施文件读、写操作的程序进行性能度量。操作系统试图用高速缓存技术将相关文件放置在主存中,所以如若重复度量程序性能,那么后续运行该程序所得到的计时很可能好于第一次。其原因是,第一次运行使得文件进入系统高速缓存,后续各次运行一般从系统高速缓存访问文件,无需读、写磁盘。(incore这个词的意思是在主存中,早期计算机的主存是用铁氧体磁心(ferrite core)做的,这也是“core dump”这个词的由来:程序的主存镜像存放在磁盘的一个文件中以便测试诊断)。
在图 3-6 所示的测试数据中,不同缓冲区长度的各次运行使用不同的文件副本,所以后一次运行不会在前一次运行的高速缓存中找到它需要的数据。这些文件都足够大,不可能全部保留在高速缓存中(测试系统配置了6 GB RAM)。
UNIX系统支持在不同进程间共享打开文件。在介绍dup函数之前,先要说明这种共享。为此先介绍内核用于所有I/O的数据结构。
下面的说明是概念性的,与特定实现可能匹配,也可能不匹配。请参阅Bach[1986]对System V中相关数据结构的讨论。McKusick 等[1996]说明 4.4BSD 中的相关数据结构。McKusick 和Neville-Nell[2005]对 FreeBSD 5.2 进行了介绍。对 Solaris 的类似讨论请参见 McDougall 和Marno[2007]。Linux 2.6内核体系结构介绍请参见Bovet和Cesati[2006]。
内核使用3种数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
(1)每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
a.文件描述符标志(close_on_exec,参见图3-7和3.14节);
b.指向一个文件表项的指针。
(2)内核为所有打开文件维持一张文件表。每个文件表项包含:
a.文件状态标志(读、写、添写、同步和非阻塞等,关于这些标志的更多信息参见3.14节);
b.当前文件偏移量;
c.指向该文件v节点表项的指针。
(3)每个打开文件(或设备)都有一个 v 节点(v-node)结构。v 节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。这些信息是在打开文件时从磁盘上读入内存的,所以,文件的所有相关信息都是随时可用的。例如,i 节点包含了文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等(4.14节较详细地说明了典型UNIX系统文件系统,并将更多地介绍i节点)。
Linux没有使用v节点,而是使用了通用i节点结构。虽然两种实现有所不同,但在概念上, v节点与i节点是一样的。两者都指向文件系统特有的i节点结构。
我们忽略了那些不影响讨论的实现细节。例如,打开文件描述符表可存放在用户空间(作为一个独立的对应于每个进程的结构,可以换出),而非进程表中。这些表也可以用多种方式实现,不必一定是数组,例如,可将它们实现为结构的链表。如果不考虑实现细节的话,通用概念是相同的。
图3-7显示了一个进程对应的3张表之间的关系。该进程有两个不同的打开文件:一个文件从标准输入打开(文件描述符0),另一个从标准输出打开(文件描述符为1)。
图3-7 打开文件的内核数据结构
从UNIX系统的早期版本[Thompson 1978]以来,这3张表之间的关系一直保持至今。这种关系对于在不同进程之间共享文件的方式非常重要。在以后的章节中涉及其他文件共享方式时还会回到这张图上来。
创建 v 节点结构的目的是对在一个计算机系统上的多文件系统类型提供支持。这一工作是Peter Weinberger(贝尔实验室)和Bill Joy(Sun公司)分别独立完成的。Sun把这种文件系统称为虚拟文件系统(Virtual File System),把与文件系统无关的i节点部分称为v节点[Kleiman 1986]。
当各个制造商的实现增加了对Sun的网络文件系统(NFS)的支持时,它们都广泛采用了v节点结构。在BSD系列中首先提供v节点的是增加了NFS的4.3BSD Reno。
在SVR4中,v节点替代了SVR3中与文件系统无关的i节点结构。Solaris是从SVR4发展而来的,因此它也使用v节点。
Linux没有将相关数据结构分为i节点和v节点,而是采用了一个与文件系统相关的i节点和一个与文件系统无关的i节点。
如果两个独立进程各自打开了同一文件,则有图3-8中所示的关系。
图3-8 两个独立进程各自打开同一个文件
我们假定第一个进程在文件描述符3上打开该文件,而另一个进程在文件描述符4上打开该文件。打开该文件的每个进程都获得各自的一个文件表项,但对一个给定的文件只有一个v节点表项。之所以每个进程都获得自己的文件表项,是因为这可以使每个进程都有它自己的对该文件的当前偏移量。
给出了这些数据结构后,现在对前面所述的操作进一步说明。
•在完成每个write后,在文件表项中的当前文件偏移量即增加所写入的字节数。如果这导致当前文件偏移量超出了当前文件长度,则将i节点表项中的当前文件长度设置为当前文件偏移量(也就是该文件加长了)。
•如果用O_APPEND标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先会被设置为i节点表项中的文件长度。这就使得每次写入的数据都追加到文件的当前尾端处。
•若一个文件用lseek定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为i节点表项中的当前文件长度(注意,这与用O_APPEND标志打开文件是不同的,详见3.11节)。
•lseek函数只修改文件表项中的当前文件偏移量,不进行任何I/O操作。
可能有多个文件描述符项指向同一文件表项。在3.12 节中讨论dup 函数时,我们就能看到这一点。在fork后也发生同样的情况,此时父进程、子进程各自的每一个打开文件描述符共享同一个文件表项(见8.3节)。
注意,文件描述符标志和文件状态标志在作用范围方面的区别,前者只用于一个进程的一个描述符,而后者则应用于指向该给定文件表项的任何进程中的所有描述符。在3.14节说明fcntl函数时,我们将会了解如何获取和修改文件描述符标志和文件状态标志。
本节前面所述的一切对于多个进程读取同一文件都能正确工作。每个进程都有它自己的文件表项,其中也有它自己的当前文件偏移量。但是,当多个进程写同一文件时,则可能产生预想不到的结果。为了说明如何避免这种情况,需要理解原子操作的概念。
1.追加到一个文件
考虑一个进程,它要将数据追加到一个文件尾端。早期的UNIX系统版本并不支持open的O_APPEND选项,所以程序被编写成下列形式:
if (lseek(fd,OL, 2) < 0) /*position to EOF*/
if (write(fd, buf, 100) != 100) /*and write*/
err_sys("lseek error");
err_sys("write error");
对单个进程而言,这段程序能正常工作,但若有多个进程同时使用这种方法将数据追加写到同一文件,则会产生问题(例如,若此程序由多个进程同时执行,各自将消息追加到一个日志文件中,就会产生这种情况)。
假定有两个独立的进程A和B都对同一文件进行追加写操作。每个进程都已打开了该文件,但未使用O_APPEND标志。此时,各数据结构之间的关系如图3-8中所示。每个进程都有它自己的文件表项,但是共享一个v节点表项。假定进程A调用了lseek,它将进程A的该文件当前偏移量设置为1 500字节(当前文件尾端处)。然后内核切换进程,进程B运行。进程B执行lseek,也将其对该文件的当前偏移量设置为1 500字节(当前文件尾端处)。然后B调用write,它将 B的该文件当前文件偏移量增加至1 600。因为该文件的长度已经增加了,所以内核将v节点中的当前文件长度更新为1 600。然后,内核又进行进程切换,使进程A恢复运行。当A调用write时,就从其当前文件偏移量(1 500)处开始将数据写入到文件。这样也就覆盖了进程B刚才写入到该文件中的数据。
问题出在逻辑操作“先定位到文件尾端,然后写”,它使用了两个分开的函数调用。解决问题的方法是使这两个操作对于其他进程而言成为一个原子操作。任何要求多于一个函数调用的操作都不是原子操作,因为在两个函数调用之间,内核有可能会临时挂起进程(正如我们前面所假定的)。
UNIX系统为这样的操作提供了一种原子操作方法,即在打开文件时设置O_APPEND标志。正如前一节中所述,这样做使得内核在每次写操作之前,都将进程的当前偏移量设置到该文件的尾端处,于是在每次写之前就不再需要调用lseek。
2.函数pread和pwrite
Single UNIX Specification包括了XSI扩展,该扩展允许原子性地定位并执行I/O。pread和pwrite就是这种扩展。
#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,但也与它们有类似的区别。
3u创建一个文件
对open函数的O_CREAT和O_EXCL选项进行说明时,我们已见到另一个有关原子操作的例子。当同时指定这两个选项,而该文件又已经存在时,open 将失败。我们曾提及检查文件是否存在和创建文件这两个操作是作为一个原子操作执行的。如果没有这样一个原子操作,那么可能会编写下列程序段:
if ((fd = open(pathname, O_WRONLY)) <0){
if (errno == ENOENT) {
if ((fd = creat(path, mode)) < 0)
err_sys("creat error");
} else{
err_sys("open error");
}
}
如果在open和creat之间,另一个进程创建了该文件,就会出现问题。若在这两个函数调用之间,另一个进程创建了该文件,并且写入了一些数据,然后,原先进程执行这段程序中的creat,这时,刚由另一进程写入的数据就会被擦去。如若将这两者合并在一个原子操作中,这种问题也就不会出现。
一般而言,原子操作(atomic operation)指的是由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。在4.15节描述link函数以及在14.3节中说明记录锁时,还将讨论原子操作。
下面两个函数都可用来复制一个现有的文件描述符。
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
两函数的返回值:若成功,返回新的文件描述符;若出错,返回−1
由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。对于 dup2,可以用fd2参数指定新描述符的值。如果fd2已经打开,则先将其关闭。如若fd等于fd2,则dup2返回fd2,而不关闭它。否则,fd2的FD_CLOEXEC文件描述符标志就被清除,这样fd2在进程调用exec时是打开状态。
这些函数返回的新文件描述符与参数fd共享同一个文件表项,如图3-9所示。
图3-9 dup(1)后的内核数据结构
在此图中,我们假定进程启动时执行了:
newfd = dup(1);
当此函数开始执行时,假定下一个可用的描述符是3(这是非常可能的,因为0,1和2都由shell打开)。因为两个描述符指向同一文件表项,所以它们共享同一文件状态标志(读、写、追加等)以及同一当前文件偏移量。
每个文件描述符都有它自己的一套文件描述符标志。正如我们将在下一节中说明的那样,新描述符的执行时关闭(close-on-exec)标志总是由dup函数清除。
复制一个描述符的另一种方法是使用 fcntl 函数,3.14 节将对该函数进行说明。实际上,调用
dup(fd);
等效于
fcntl (fd, F_DUPFD, 0);
而调用
dup2(fd, fd2);
等效于
close(fd2);
fcntl(fd, F_DUPFD, fd2);
在后一种情况下,dup2并不完全等同于close加上fcntl。它们之间的区别具体如下。
(1)dup2 是一个原子操作,而 close 和 fcntl 包括两个函数调用。有可能在 close 和fcntl之间调用了信号捕获函数,它可能修改文件描述符(第10章将说明信号)。如果不同的线程改变了文件描述符的话也会出现相同的问题(第11章将说明线程)。
(2)dup2和fcntl有一些不同的errno。
dup2系统调用起源于V7,然后传播至所有BSD版本。而复制文件描述符的fcntl方法则首先由系统III使用,然后由System V继续采用。SVR3.2选用了dup2函数,4.2BSD则选用了fcntl函数及F_DUPFD功能。POSIX.1要求兼有dup2及fcntl的F_DUPFD两种功能。
传统的UNIX系统实现在内核中设有缓冲区高速缓存或页高速缓存,大多数磁盘I/O都通过缓冲区进行。当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式被称为延迟写(delayed write)(Bach[1986]的第3章详细讨论了缓冲区高速缓存)。
通常,当内核需要重用缓冲区来存放其他磁盘块数据时,它会把所有延迟写数据块写入磁盘。为了保证磁盘上实际文件系统与缓冲区中内容的一致性,UNIX 系统提供了 sync、fsync 和fdatasync三个函数。
#include<unistd.h>
int fsync(int fd);
int fdatasync(int fd);
返回值:若成功,返回0;若出错,返回−1
void sync(void);
sync只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。
通常,称为update的系统守护进程周期性地调用(一般每隔30秒)sync函数。这就保证了定期冲洗(flush)内核的块缓冲区。命令sync(1)也调用sync函数。
fsync函数只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回。fsync可用于数据库这样的应用程序,这种应用程序需要确保修改过的块立即写到磁盘上。
fdatasync函数类似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。
本书说明的所有4种平台都支持sync和fsync函数。但是,FreeBSD 8.0不支持fdatasync。
fcntl函数可以改变已经打开文件的属性。
#include<fcntl.h>
int fcntl(int fd, int cmd, ... /* int arg */);
返回值:若成功,则依赖于cmd(见下);若出错,返回−1
在本节的各实例中,第3个参数总是一个整数,与上面所示函数原型中的注释部分对应。但是在14.3节说明记录锁时,第3个参数则是指向一个结构的指针。
fcntl函数有以下5种功能。
(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_SETLKW)。
我们先说明这11种cmd中的前8种(14.3节说明后3种,它们都与记录锁有关)。参照图3-7,我们将讨论与进程表项中各文件描述符相关联的文件描述符标志以及每个文件表项中的文件状态标志。
F_DUPFD 复制文件描述符fd。新文件描述符作为函数值返回。它是尚未打开的各描述符中大于或等于第3个参数值(取为整型值)中各值的最小值。新描述符与 fd共享同一文件表项(见图 3-9)。但是,新描述符有它自己的一套文件描述符标志,其 FD_CLOEXEC 文件描述符标志被清除(这表示该描述符在exec时仍保持有效,我们将在第8章对此进行讨论)。
F_DUPFD_CLOEXEC 复制文件描述符,设置与新描述符关联的FD_CLOEXEC文件描述符标志的值,返回新文件描述符。
F_GETFD 对应于fd的文件描述符标志作为函数值返回。当前只定义了一个文件描述符标志FD_CLOEXEC。
F_SETFD 对于fd设置文件描述符标志。新标志值按第3个参数(取为整型值)设置。
要知道,很多现有的与文件描述符标志有关的程序并不使用常量FD_CLOEXEC,而是将此标志设置为0(系统默认,在exec时不关闭)或1(在exec时关闭)。
F_GETFL 对应于fd的文件状态标志作为函数值返回。我们在说明open函数时,已描述了文件状态标志。它们列在图3-10中。
图3-10 对于fcntl的文件状态标志
遗憾的是,5个访问方式标志(O_RDONLY、O_WRONLY、O_RDWR、O_EXEC以及O_SEARCH)并不各占1位(如前所述,由于历史原因,前3个标志的值分别是0、1和2。这5个值互斥,一个文件的访问方式只能取这5个值之一)。因此首先必须用屏蔽字O_ACCMODE取得访问方式位,然后将结果与这5个值中的每一个相比较。
F_SETFL 将文件状态标志设置为第3个参数的值(取为整型值)。可以更改的几个标志是:O_APPEND、O_NONBLOCK、O_SYNC、O_DSYNC、O_RSYNC、O_FSYNC和O_ASYNC。
F_GETOWN 获取当前接收SIGIO和SIGURG信号的进程ID或进程组ID。14.5.2节将论述这两种异步I/O信号。
F_SETOWN 设置接收SIGIO和SIGURG信号的进程ID或进程组ID。正的arg指定一个进程ID,负的arg表示等于arg绝对值的一个进程组ID。
fcntl的返回值与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。下列4个命令有特定返回值:F_DUPFD、F_GETFD、F_GETFL以及F_GETOWN。第1个命令返回新的文件描述符,第2个和第3个命令返回相应的标志,最后一个命令返回一个正的进程ID或负的进程组ID。
实例
图3-11中所示程序的第1个参数指定文件描述符,并对于该描述符打印其所选择的文件标志说明。
图3-11 对于指定的描述符打印文件标志
注意,我们使用了功能测试宏_POSIX_C_SOURCE,并且条件编译了POSIX.1中没有定义的文件访问标志。下面显示了从bash(Bourne-again shell)调用该程序时的几种情况。当使用不同shell时,结果会有些不同。
$./a.out 0 < /dev/tty
read only
$./a.out 1 > temp.foo
$ cat temp.foo
write only
$./a.out 2 2>>temp.foo
write only, append
$./a.out 5 5<>temp.foo
read write
子句5<>temp.foo表示在文件描述符5上打开文件temp.foo以供读、写。
实例
在修改文件描述符标志或文件状态标志时必须谨慎,先要获得现在的标志值,然后按照期望修改它,最后设置新标志值。不能只是执行F_SETFD或F_SETFL命令,这样会关闭以前设置的标志位。
图3-12是对于一个文件描述符设置一个或多个文件状态标志的函数。
图3-12 对一个文件描述符开启一个或多个文件状态标志
如果将中间的一条语句改为:
val &= ~flags; /* turn flags off */
就构成另一个函数,我们称为 clr_fl,并将在后面某些例子中用到它。此语句使当前文件状态标志值val与flags的反码进行逻辑“与”运算。
如果在图3-5程序的开始处加上下面一行以调用set_fl,则开启了同步写标志。
set_fl(STDOUT_FILENO, O_SYNC);
这就使每次write都要等待,直至数据已写到磁盘上再返回。在UNIX系统中,通常write只是将数据排入队列,而实际的写磁盘操作则可能在以后的某个时刻进行。而数据库系统则需要使用 O_SYNC,这样一来,当它从 write 返回时就知道数据已确实写到了磁盘上,以免在系统异常时产生数据丢失。
程序运行时,设置O_SYNC标志会增加系统时间和时钟时间。为了测试这一点,先运行图3-5程序,它从一个磁盘文件中将492.6 MB的数据复制到另一个文件。然后,对比设置了O_SYNC标志的程序,使其完成同样的工作。在使用ext4文件系统的Linux上执行上述操作,得到的结果如图3-13所示。
图3-13 在Linux ext4中采用各种同步机制后的计时结果
图3-13中的6行都是在BUFFSIZE为4 096字节时测量的。图3-6中的结果所测量的情况是读一个磁盘文件,然后写到/dev/null,所以没有磁盘输出。图3-13中的第2行对应于读一个磁盘文件,然后写到另一个磁盘文件中。这就是为什么图3-13中第1行与第2行有差别的原因。在写磁盘文件时,系统时间增加了,其原因是内核需要从进程中复制数据,并将数据排入队列以便由磁盘驱动器将其写到磁盘上。当写至磁盘文件时,我们期望时钟时间也会增加。
当支持同步写时,系统时间和时钟时间应当会显著增加。但从第3行可见,同步写所用的系统时间并不比延迟写所用的时间增加很多。这意味着要么Linux操作系统对延迟写和同步写操作的工作量相同(这其实是不太可能的),要么 O_SYNC 标志并没有起到期望的作用。在这种情况下,Linux操作系统并不允许我们用fcntl设置O_SYNC标志,而是显示失败但没有返回出错(但如果在文件打开时能指定该标志,我们还是应该遵重这个标志的)。
最后 3 行中的时钟时间反映了所有写操作写入磁盘时需要的附加等待时间。同步写入文件之后,我们希望对 fsync 的调用并不会产生效果。这种情况理应在图 3-13 中的最后一行中呈现,但既然 O_SYNC 标志并没有起到预期的作用,所以最后一行和第 5 行的表现几乎相同。
图3-14显示了在采用HFS文件系统的Mac OS X 10.6.8上运行同样的测试得到的计时结果。该计时结果与我们的期望相符:同步写比延迟写所消耗的时间增加了很多,而且在同步写后再调用函数fsync并不产生测量结果上的显著差别。还要注意的是,在延迟写后增加一个fsync函数调用,测量结果的差别也不大。其可能原因是,在向某个文件写入新数据时,操作系统已经将以前写入的数据都冲洗到了磁盘上,所以在调用函数fsync时只需要做很少的工作。
图3-14 在Mac OS X HFS中采用各种同步机制后的计时结果
比较fsync和fdatasync,两者都更新文件内容,用了O_SYNC标志,每次写入文件时都更新文件内容。每一种调用的性能依赖很多因素,包括底层的操作系统实现、磁盘驱动器的速度以及文件系统的类型。
在本例中,我们看到了fcntl的必要性。我们的程序在一个描述符(标准输出)上进行操作,但是根本不知道由shell打开的相应文件的文件名。因为这是shell打开的,因此不能在打开时按我们的要求设置O_SYNC标志。使用fcntl,我们只需要知道打开文件的描述符,就可以修改描述符的属性。在讲解非阻塞管道时(15.2节)还会用到fcntl,因为对于管道,我们所知的只有其描述符。
ioctl函数一直是I/O操作的杂物箱。不能用本章中其他函数表示的I/O操作通常都能用ioctl表示。终端I/O是使用ioctl最多的地方(在第18章中将看到,POSIX.1已经用一些单独的函数代替了终端I/O操作)。
#include <unistd.h> /* System V */
#include <sys/ioctl.h> /* BSD and Linux */
int ioctl(int fd, int request, ...);
返回值:若出错,返回−1;若成功,返回其他值
ioctl函数是Single UNIX Specification标准的一个扩展部分,以便处理STREAMS设备[Rago 1993],但是,在SUSv4中已被移至弃用状态。UNIX系统实现用它进行很多杂项设备操作。有些实现甚至将它扩展到用于普通文件。
我们所示的函数原型对应于POSIX.1,FreeBSD 8.0和Mac OS X 10.6.8将第2个参数声明为unsigned long。因为第2个参数总是头文件中一个#defined的名字,所以这种细节并没有什么影响。
对于ISO C原型,它用省略号表示其余参数。但是,通常只有另外一个参数,它常常是指向一个变量或结构的指针。
在此原型中,我们表示的只是ioctl函数本身所要求的头文件。通常,还要求另外的设备专用头文件。例如,除POSIX.1所说明的基本操作之外,终端I/O的ioctl命令都需要头文件<termios.h>。
每个设备驱动程序可以定义它自己专用的一组 ioctl 命令,系统则为不同种类的设备提供通用的ioctl命令。图3-15中总结了FreeBSD支持的通用ioctl命令的一些类别。
图3-15 FreeBSD中通用的ioctl操作
磁带操作使我们可以在磁带上写一个文件结束标志、倒带、越过指定个数的文件或记录等,用本章中的其他函数(read、write、lseek 等)都难于表示这些操作,所以,对这些设备进行操作最容易的方法就是使用ioctl。
在18.12节中将说明使用ioctl函数获取和设置终端窗口大小,19.7节中使用ioctl函数访问伪终端的高级功能。
较新的系统都提供名为/dev/fd 的目录,其目录项是名为 0、1、2 等的文件。打开文件/dev/fd/n等效于复制描述符n(假定描述符n是打开的)。
/dev/fd这一功能是由Tom Duff开发的,它首先出现在Research UNIX系统的第8版中,本书说明的所有4种系统(FreeBSD 8.0、Linux 3.2.0、Mac OS X 10.6.8和Solaris 10)都支持这一功能。它不是POSIX.1的组成部分。
在下列函数调用中:
fd = open("/dev/fd/0", mode);
大多数系统忽略它所指定的 mode,而另外一些系统则要求 mode 必须是所引用的文件(在这里是标准输入)初始打开时所使用的打开模式的一个子集。因为上面的打开等效于
fd = dup(0);
所以描述符0和fd共享同一文件表项(见图3-9)。例如,若描述符0先前被打开为只读,那么我们也只能对fd进行读操作。即使系统忽略打开模式,而且下列调用是成功的:
fd = open("/dev/fd/0", O_RDWR);
我们仍然不能对fd进行写操作。
Linux实现中的/dev/fd是个例外。它把文件描述符映射成指向底层物理文件的符号链接。例如,当打开/dev/fd/0时,事实上正在打开与标准输入关联的文件,因此返回的新文件描述符的模式与/dev/fd文件描述符的模式其实并不相关。
我们也可以用/dev/fd作为路径名参数调用creat,这与调用open时用O_CREAT作为第2个参数作用相同。例如,若一个程序调用creat,并且路径名参数是/dev/fd/1,那么该程序仍能工作。
注意,在Linux上这么做必须非常小心。因为Linux实现使用指向实际文件的符号链接,在/dev/fd文件上使用creat会导致底层文件被截断。
某些系统提供路径名/dev/stdin、/dev/stdout 和/dev/stderr,这些等效于/dev/fd/0、/dev/fd/1和/dev/fd/2。
/dev/fd文件主要由shell使用,它允许使用路径名作为调用参数的程序,能用处理其他路径名的相同方式处理标准输入和输出。例如,cat(1)命令对其命令行参数采取了一种特殊处理,它将单独的一个字符“-”解释为标准输入。例如:
filter file2 | cat file1 - file3 | lpr
首先cat读file1,接着读其标准输入(也就是filter file2命令的输出),然后读file3,如果支持/dev/fd,则可以删除cat对“-”的特殊处理,于是我们就可键入下列命令行:
filter file2 | cat file1 /dev/fd/0 file3 | lpr
作为命令行参数的“-”特指标准输入或标准输出,这已由很多程序采用。但是这会带来一些问题,例如,如果用“-”指定第一个文件,那么看来就像指定了命令行的一个选项。/dev/fd则提高了文件名参数的一致性,也更加清晰。
本章说明了UNIX系统提供的基本 I/O函数。因为read和write都在内核执行,所以称这些函数为不带缓冲的I/O函数。在只使用read和write情况下,我们观察了不同的I/O长度对读文件所需时间的影响。我们也观察了许多将已写入的数据冲洗到磁盘上的方法,以及它们对应用程序性能的影响。
在说明多个进程对同一文件进行追加写操作以及多个进程创建同一文件时,本章介绍了原子操作。也介绍了内核用来共享打开文件信息的数据结构。在本书的稍后还将涉及这些数据结构。
我们还介绍了ioctl和fcntl函数,本书后续部分还会涉及这两个函数。第14章还将fcntl用于记录锁,第18章和第19章将ioctl用于终端设备。
3.1 当读/写磁盘文件时,本章中描述的函数确实是不带缓冲机制的吗?请说明原因。
3.2 编写一个与3.12节中dup2功能相同的函数,要求不调用fcntl函数,并且要有正确的出错处理。
3.3 假设一个进程执行下面3个函数调用:
fd1 = open(path, oflags);
fd2 = dup(fd1);
fd3 = open(path, oflags);
画出类似于图3-9的结果图。对fcntl作用于fd1来说,F_SETFD命令会影响哪一个文件描述符?F_SETFL呢?
3.4 许多程序中都包含下面一段代码:
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
if (fd > 2)
close(fd);
为了说明if语句的必要性,假设fd是1,画出每次调用dup2时3个描述符项及相应的文件表项的变化情况。然后再画出fd为3的情况。
3.5 在Bourne shell、Bourne-again shell和Korn shell中,digit1>&digit2表示要将描述符digit1重定向至描述符digit2的同一文件。请说明下面两条命令的区别。
./a.out > outfile 2>&1
./a.out 2>&1 > outfile
(提示:shell从左到右处理命令行。)
3.6 如果使用追加标志打开一个文件以便读、写,能否仍用lseek在任一位置开始读?能否用lseek更新文件中任一部分的数据?请编写一段程序验证。