APUE学习笔记之文件IO(三)
本文记录《UNIX环境高级编程》第3版中第3章文件IO的一些知识点。
文件描述符
对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,使用open
或creat
返回的文件描述符标识该文件,将其作为参数传送给read
或write
。
按照惯例,UNIX系统shell把文件描述符0与进程的标准输入关联,文件描述符1与标准输出关联,文件描述符2与标准错误关联。这是各种shell以及很多应用程序使用的惯例,与UNIX内核无关。
在符合POSIX.1的应用程序中,幻数0、1、2虽然已被标准化,但应当把它们替换成符号常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO以提高可读性。这些常量都在头文件<unistd.h>
中定义。
函数open和openat
调用open
或openat
函数可以打开或创建一个文件。
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
只搜索打开(应用于目录)。
在这5个常量中必须指定一个且只能指定一个。下列常量则是可选的。
O_APPEND
每次写时都追加到文件的尾端。O_CLOEXEC
把FD_CLOEXEC
常量设置为文件描述符标志。O_CREAT
若此文件不存在则创建它。使用此选项时,open
函数需同时说明第3个参数mode(openat函数需说明第4个参数mode),用mode指定该新文件的访问权限位。O_DIRECTORY
如果path引用的不是目录,则出错。O_EXCL
如果同时指定了O_CREAT
,而文件已经存在,则出错。用此可以测试一个文件是否存在,如果不存在,则创建此文件,这使测试和创建两者成为一个原子操作。O_NOCTTY
如果path引用的是终端设备,则不该将设备分配作为此进程的控制终端。O_NOFOLLOW
如果path引用的是一个符号链接,则出错。O_NONBLOCK
如果path引用的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置非阻塞方式。O_SYNC
使每次write等待物理I/O操作完成,包括由该write操作引起的文件属性更新所需的I/O。O_TRUNC
如果此文件存在,而且为只写或读-写成功打开,则将其长度截断为0。
由open
和openat
函数返回的文件描述符一定是最小的未用描述符数值。例如,一个应用程序可以先关闭标准输出(通常是文件描述符1),然后打开另一个文件,执行打开操作前就能了解到该文件一定会在文件描述符1上打开。
函数close
可调用close
函数关闭一个打开文件。
1 |
|
关闭一个文件时还会释放该进程加在该文件上的所有记录锁。当一个进程终止时,内核自动关闭它所有的打开文件。很多程序都利用了这一功能而不显式地用close
关闭打开文件。
函数lseek
每个打开文件都有一个与其相关联的“当前文件偏移量”(current file
offset)。它通常是一个非负整数,用以度量从文件开始处计算的字节数(本节稍后将对“非负”这一修饰词的某些例外进行说明)。通常,读、写操作都从当前文件偏移量处开始,并使偏移量增加所读写的字节数。按系统默认的情况,当打开一个文件时,除非指定O_APPEND
选项,否则该偏移量被设置为0。
可以调用lseek
显式地为一个打开文件设置偏移量。
1 |
|
对参数offset的解释与参数whence的值有关。
- 若whence是
SEEK_SET
,则将该文件的偏移量设置为距文件开始处offset个字节。 - 若whence是
SEEK_CUR
,则将该文件的偏移量设置为其当前值加offset,offset可为正或负。 - 若whence是
SEEK_END
,则将该文件的偏移量设置为文件长度加offset,offset可正可负。
若lseek
成功执行,则返回新的文件偏移量,为此可以用下列方式确定打开文件的当前偏移量:
1 | off_t currpos; |
这种方法也可用来确定所涉及的文件是否可以设置偏移量。如果文件描述符指向的是一个管道、FIFO或网络套接字,则lseek
返回−1,并将errno设置为ESPIPE
。
通常,文件的当前偏移量应当是一个非负整数,但是,某些设备也可能允许负的偏移量。但对于普通文件,其偏移量必须是非负值。因为偏移量可能是负值,所以在比较lseek
的返回值时应当谨慎,不要测试它是否小于0,而要测试它是否等于−1。
因为偏移量(off_t
)是带符号数据类型,所以文件的最大长度会减少一半。例如,若off_t
是32位整型,则文件最大长度是\(2^{31}-1\)个字节。
lseek
仅将当前的文件偏移量记录在内核中,它并不引起任何I/O操作。然后,该偏移量用于下一个读或写操作。
文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为0。
文件中的空洞并不要求在磁盘上占用存储区。具体处理方式与文件系统的实现有关,当定位到超出文件尾端之后写时,对于新写的数据需要分配磁盘块,但是对于原文件尾端和新开始写位置之间的部分则不需要分配磁盘块。
函数read
调用read
函数从打开文件中读数据。
1 |
|
有多种情况可使实际读到的字节数少于要求读的字节数:
- 读普通文件时,在读到要求字节数之前已到达了文件尾端。例如,若在到达文件尾端之前有30个字节,而要求读100个字节,则read返回30。下一次再调用read时,它将返回0(文件尾端)。
- 当从终端设备读时,通常一次最多读一行。
- 当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数。
- 当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。
- 当从某些面向记录的设备(如磁带)读时,一次最多返回一个记录。
- 当一信号造成中断,而已经读了部分数据量时。
函数write
调用write
函数向打开文件写数据。
1 |
|
其返回值通常与参数nbytes的值相同,否则表示出错。write出错的一个常见原因是磁盘已写满,或者超过了一个给定进程的文件长度限制。
对于普通文件,写操作从文件的当前偏移量处开始。如果在打开该文件时,指定了O_APPEND
选项,则在每次写操作之前,将文件偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。
I/O的效率
下面的程序使用read
和write
函数复制一个文件。
1 |
|
如何选取BUFFSIZE
值?在回答此问题之前,先用各种不同的BUFFSIZE
值来运行此程序。下图显示了用20种不同的缓冲区长度,读516
581 760字节的文件所得到的结果。
用上面的程序读文件,其标准输出被重新定向到/dev/null
上。此测试所用的文件系统是Linux
ext4文件系统,其磁盘块长度为4096字节(磁盘块长度由st_blksize
表示)。这也证明了图中系统CPU时间的几个最小值差不多出现在BUFFSIZE
为4096及以后的位置,继续增加缓冲区长度对此时间几乎没有影响。
大多数文件系统为改善性能都采用某种预读(read ahead)技术。当检测到正进行顺序读取时,系统就试图读入比应用所要求的更多数据,并假想应用很快就会读这些数据。预读的效果可以从图中看出,缓冲区长度小至32字节时的时钟时间与拥有较大缓冲区长度时的时钟时间几乎一样。
文件共享
UNIX系统支持在不同进程间共享打开文件。在介绍dup
函数之前,先要说明这种共享。为此先介绍内核用于所有I/O的数据结构。
内核使用3种数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
(1)每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
a.文件描述符标志(close_on_exec)
b.指向一个文件表项的指针。
(2)内核为所有打开文件维持一张文件表。每个文件表项包含:
a.文件状态标志(读、写、添写、同步和非阻塞等);
b.当前文件偏移量;
c.指向该文件v节点表项的指针。
(3)每个打开文件(或设备)都有一个 v 节点(v-node)结构。v节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。这些信息是在打开文件时从磁盘上读入内存的,所以,文件的所有相关信息都是随时可用的。例如,i节点包含了文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等。
Linux没有使用v节点,而是使用了通用i节点结构。虽然两种实现有所不同,但在概念上, v节点与i节点是一样的。两者都指向文件系统特有的i节点结构。
下图显示了一个进程对应的3张表之间的关系。该进程有两个不同的打开文件:一个文件从标准输入打开(文件描述符0),另一个从标准输出打开(文件描述符为1)。
如果两个独立进程各自打开了同一文件,则有下图中所示的关系。
我们假定第一个进程在文件描述符3上打开该文件,而另一个进程在文件描述符4上打开该文件。打开该文件的每个进程都获得各自的一个文件表项,但对一个给定的文件只有一个v节点表项。之所以每个进程都获得自己的文件表项,是因为这可以使每个进程都有它自己的对该文件的当前偏移量。
给出了这些数据结构后,现在对前面所述的操作进一步说明。
在完成每个
write
后,在文件表项中的当前文件偏移量即增加所写入的字节数。如果这导致当前文件偏移量超出了当前文件长度,则将i节点表项中的当前文件长度设置为当前文件偏移量(也就是该文件加长了)。如果用
O_APPEND
标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先会被设置为i节点表项中的文件长度。这就使得每次写入的数据都追加到文件的当前尾端处。若一个文件用
lseek
定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为i节点表项中的当前文件长度(注意,这与用O_APPEND
标志打开文件是不同的)。lseek
函数只修改文件表项中的当前文件偏移量,不进行任何I/O操作。
注意,文件描述符标志和文件状态标志在作用范围方面的区别,前者只用于一个进程的一个描述符,而后者则应用于指向该给定文件表项的任何进程中的所有描述符。
原子操作
函数pread
和pwrite
Single UNIX
Specification包括了XSI扩展,该扩展允许原子性地定位并执行I/O。pread
和pwrite
就是这种扩展。
1 |
|
调用pread
相当于调用lseek
后调用read
,但是pread
又与这种顺序调用有下列重要区别。
调用
pread
时,无法中断其定位和读操作。不更新当前文件偏移量。
调用pwrite
相当于调用lseek
后调用write
,但也与它们有类似的区别。
一般而言,原子操作(atomic operation)指的是由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
函数dup和dup2
下面两个函数都可用来复制一个现有的文件描述符。
1 |
|
由dup
返回的新文件描述符一定是当前可用文件描述符中的最小数值。对于
dup2
,可以用fd2参数指定新描述符的值。如果fd2已经打开,则先将其关闭。如若fd等于fd2,则dup2返回fd2,而不关闭它。
这些函数返回的新文件描述符与参数fd共享同一个文件表项,如图所示。
在此图中,假定进程启动时执行了:newfd = dup(1);
当此函数开始执行时,假定下一个可用的描述符是3(这是非常可能的,因为0,1和2都由shell打开)。因为两个描述符指向同一文件表项,所以它们共享同一文件状态标志(读、写、追加等)以及同一当前文件偏移量。
复制一个描述符的另一种方法是使用fcntl
函数。实际上,调用dup(fd);
等效于fcntl (fd, F_DUPFD, 0);
而调用dup2(fd, fd2);
等效于close(fd2); fcntl(fd, F_DUPFD, fd2);
函数sync、fsync和fdatasync
传统的UNIX系统实现在内核中设有缓冲区高速缓存或页高速缓存,大多数磁盘I/O都通过缓冲区进行。当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式被称为延迟写(delayed write)。
通常,当内核需要重用缓冲区来存放其他磁盘块数据时,它会把所有延迟写数据块写入磁盘。为了保证磁盘上实际文件系统与缓冲区中内容的一致性,UNIX系统提供了sync
、fsync
和fdatasync
三个函数。
1 |
|
sync
只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。
通常,称为update的系统守护进程周期性地调用(一般每隔30秒)sync
函数。这就保证了定期冲洗(flush)内核的块缓冲区。
fsync
函数只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回。fsync
可用于数据库这样的应用程序,这种应用程序需要确保修改过的块立即写到磁盘上。
fdatasync
函数类似于fsync
,但它只影响文件的数据部分。而除数据外,fsync
还会同步更新文件的属性。
函数fcntl
fcntl
函数可以改变已经打开文件的属性。
1 |
|
在本节的各实例中,第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种。我们将讨论与进程表项中各文件描述符相关联的文件描述符标志以及每个文件表项中的文件状态标志。
F_DUPFD
复制文件描述符fd。新文件描述符作为函数值返回。它是尚未打开的各描述符中大于或等于第3个参数值(取为整型值)中各值的最小值。新描述符与 fd共享同一文件表项。但是,新描述符有它自己的一套文件描述符标志,其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
函数时,已描述了文件状态标志。它们列在图中。
F_SETFL
将文件状态标志设置为第3个参数的值(取为整型值)。可以更改的几个标志是:O_APPEND
、O_NONBLOCK
、O_SYNC
、O_DSYNC
、O_RSYNC
、O_FSYNC
和O_ASYNC
。F_GETOWN
获取当前接收SIGIO
和SIGURG
信号的进程ID或进程组ID。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。
函数ioctl
ioctl
函数一直是I/O操作的杂物箱。不能用本章中其他函数表示的I/O操作通常都能用ioctl
表示。
1 |
|
对于ISO C原型,它用省略号表示其余参数。但是,通常只有另外一个参数,它常常是指向一个变量或结构的指针。
在此原型中,我们表示的只是ioctl
函数本身所要求的头文件。通常,还要求另外的设备专用头文件。例如,除POSIX.1所说明的基本操作之外,终端I/O的ioctl
命令都需要头文件<termios.h>
。
磁带操作使我们可以在磁带上写一个文件结束标志、倒带、越过指定个数的文件或记录等,用本章中的其他函数(read
、write
、lseek
等)都难于表示这些操作,所以,对这些设备进行操作最容易的方法就是使用ioctl
。