就整体而言,这些不同的标准之间配合得相当好。因为 SUS 基本说明和 POSIX.1 是同一个东西,所以我们不对它们进行特别的说明,我们主要关注ISO C标准和POSIX.1之间的差别。它们之间的冲突并非有意,但如果出现冲突,POSIX.1服从ISO C标准。然而它们之间还是存在着一些差别的。
ISO C定义了clock函数,它返回进程使用的CPU时间,返回值是clock_t类型值,但ISO C 标准没有规定它的单位。为了将此值变换成以秒为单位,需要将其除以在<time.h>头文件中定义的CLOCKS_PER_SEC。POSIX.1定义了times函数,它返回其调用者及其所有终止子进程的CPU 时间以及时钟时间,所有这些值都是clock_t 类型值。sysconf 函数用来获得每秒滴答数,用于表示times函数的返回值。ISO C和POSIX.1用同一种数据类型(clock_t)来保存对时间的测量,但定义了不同的单位。这种差别可以在Solaris中看到,其中clock返回微秒数(CLOCK_PER_SEC是100万),而sysconf为每秒滴答数返回的值是100。因此,我们在使用clock_t类型变量的时候,必须十分小心以免混淆不同的时间单位。
另一个可能产生冲突的地方是:在ISO C标准说明函数时,可能没有像POSIX.1那样严。在POSIX环境下,有些函数可能要求有一个与C环境下不同的实现,因为POSIX环境中有多个进程,而ISO C环境则很少考虑宿主操作系统。尽管如此,很多符合POSIX的系统为了兼容性也会实现ISO C函数。signal函数就是一个例子。如果在不了解的情况下使用了Solaris提供的signal函数(希望编写可在ISO C环境和较早UNIX系统中运行的可兼容程序),那么它提供了与POSIX.1 sigaction函数不同的语义。第10章将对signal函数做更多说明。
在过去25年多的时间里,UNIX编程环境的标准化已经取得了很大进展。本章对3个主要标准——ISO C、POSIX和Single UNIX Specification进行了说明,也分析了这些标准对本书主要关注的4个实现,即FreeBSD、Linux、Mac OS X和Solaris所产生的影响。这些标准都试图定义一些可能随实现而更改的参数,但是我们已经看到这些限制并不完美。本书将涉及很多这些限制和幻常量。
在本书最后的参考书目中,说明了如何获得这些标准的方法。
2.1 在2.8节中提到一些基本系统数据类型可以在多个头文件中定义。例如,在FreeBSD 8.0中, size_t在29个不同的头文件中都有定义。由于一个程序可能包含这29个不同的头文件,但是ISO C却不允许对同一个名字进行多次typedef,那么如何编写这些头文件呢?
2.2 检查系统的头文件,列出实现基本系统数据类型所用到的实际数据类型。
2.3 改写图2-17中的程序,使其在sysconf为OPEN_MAX限制返回LONG_MAX时,避免进行不必要的处理。
本章开始讨论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位整型,则文件最大长度是2 31 -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(2 31 -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更新文件中任一部分的数据?请编写一段程序验证。
上一章我们说明了执行I/O操作的基本函数,其中的讨论是围绕普通文件I/O进行的—打开文件、读文件或写文件。本章将描述文件系统的其他特征和文件的性质。我们将从stat函数开始,逐个说明stat结构的每一个成员以了解文件的所有属性。在此过程中,我们将说明修改这些属性的各个函数(更改所有者、更改权限等),还将更详细地说明UNIX文件系统的结构以及符号链接。本章最后介绍对目录进行操作的各个函数,并且开发了一个以降序遍历目录层次结构的函数。
4.2 函数stat、fstat、fstatat和lstat
本章主要讨论4个stat函数以及它们的返回信息。
#include <sys/stat.h>
int stat(const char *restrict pathname, struct stat *restrict buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *restrict pathname, struct stat *restrict buf);
int fstatat(int fd, const char *restrict pathname, struct stat *restrict buf, int flag);
所有4个函数的返回值:若成功;返回0;若出错,返回-1
一旦给出pathname,stat函数将返回与此命名文件有关的信息结构。fstat函数获得已在描述符fd上打开文件的有关信息。lstat函数类似于stat,但是当命名的文件是一个符号链接时,lstat返回该符号链接的有关信息,而不是由该符号链接引用的文件的信息。(在4.22节中,当以降序遍历目录层次结构时,需要用到lstat。4.17节将更详细地说明符号链接。)
fstatat函数为一个相对于当前打开目录(由fd参数指向)的路径名返回文件统计信息。flag参数控制着是否跟随着一个符号链接。当AT_SYMLINK_NOFOLLOW标志被设置时,fstatat不会跟随符号链接,而是返回符号链接本身的信息。否则,在默认情况下,返回的是符号链接所指向的实际文件的信息。如果fd参数的值是AT_FDCWD,并且pathname参数是一个相对路径名, fstatat会计算相对于当前目录的pathname参数。如果pathname是一个绝对路径,fd参数就会被忽略。这两种情况下,根据flag的取值,fstatat的作用就跟stat或lstat一样
第2个参数buf是一个指针,它指向一个我们必须提供的结构。函数来填充由buf指向的结构。结构的实际定义可能随具体实现有所不同,但其基本形式是:
struct stat {
mode_t st_mode; /* file type & mode (permissions) */
ino_t st_ino; /* i-node number (serial number) */
dev_t st_dev; /* device number (file system) */
dev_t st_rdev; /* device number for special files */
nlink_t st_nlink; /* number of links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
off_t st_size; /* size in bytes, for regular files */
struct timespec st_atime; /* time of last access */
struct timespec st_mtime; /* time of last modification */
struct timespec st_ctime; /* time of last file status change */
blksize_t st_blksize; /* best I/O block size */
blkcnt_t st_blocks; /* number of disk blocks allocated */
};
POSIX.1未要求st_rdev、st_blksize和st_blocks字段。Single UNIX Specification XSI扩展定义了这些字段。
timespec结构类型按照秒和纳秒定义了时间,至少包括下面两个字段:
time_t tv_sec;
long tv_nsec;
在2008年版以前的标准中,时间字段定义成st_atime、st_mtime以及st_ctime,它们都是time_t类型的(以秒来表示)。timespec结构提供了更高精度的时间戳。为了保持兼容性,旧的名字可以定义成tv_sec成员。例如,st_atime可以定义成st_atim.tv_sec。
注意,stat结构中的大多数成员都是基本系统数据类型(见2.8节)。我们将说明此结构的每个成员以了解文件属性。
使用 stat 函数最多的地方可能就是 ls -l 命令,用其可以获得有关一个文件的所有信息。
至此我们已经介绍了两种不同的文件类型:普通文件和目录。UNIX 系统的大多数文件是普通文件或目录,但是也有另外一些文件类型。文件类型包括如下几种。
(1)普通文件(regular file)。这是最常用的文件类型,这种文件包含了某种形式的数据。至于这种数据是文本还是二进制数据,对于UNIX内核而言并无区别。对普通文件内容的解释由处理该文件的应用程序进行。
一个值得注意的例外是二进制可执行文件。为了执行程序,内核必须理解其格式。所有二进制可执行文件都遵循一种标准化的格式,这种格式使内核能够确定程序文本和数据的加载位置。
(2)目录文件(directory file)。这种文件包含了其他文件的名字以及指向与这些文件有关信息的指针。对一个目录文件具有读权限的任一进程都可以读该目录的内容,但只有内核可以直接写目录文件。进程必须使用本章介绍的函数才能更改目录。
(3)块特殊文件(block special file)。这种类型的文件提供对设备(如磁盘)带缓冲的访问,每次访问以固定长度为单位进行。
注意,FreeBSD不再支持块特殊文件。对设备的所有访问需要通过字符特殊文件进行。
(4)字符特殊文件(character special file)。这种类型的文件提供对设备不带缓冲的访问,每次访问长度可变。系统中的所有设备要么是字符特殊文件,要么是块特殊文件。
(5)FIFO。这种类型的文件用于进程间通信,有时也称为命名管道(named pipe)。15.5节将对其进行说明。
(6)套接字(socket)。这种类型的文件用于进程间的网络通信。套接字也可用于在一台宿主机上进程之间的非网络通信。第16章将用套接字进行进程间的通信。
(7)符号链接(symbolic link)。这种类型的文件指向另一个文件。4.17节将更多地描述符号链接。
文件类型信息包含在stat结构的st_mode成员中。可以用图4-1中的宏确定文件类型。这些宏的参数都是stat结构中的st_mode成员。
图4-1 在<sys/stat.h>中的文件类型宏
POSIX.1允许实现将进程间通信(IPC)对象(如消息队列和信号量等)说明为文件。图4-2 中的宏可用来从 stat 结构中确定 IPC 对象的类型。这些宏与图 4-1 中的不同,它们的参数并非st_mode,而是指向stat结构的指针。
图4-2 在<sys/stat.h>中的IPC类型宏
消息队列、信号量以及共享存储对象等将在第 15 章中讨论。但是,本书讨论的 4 种 UNIX系统都不将这些对象表示为文件。
实例
图4-3程序取其命令行参数,然后针对每一个命令行参数打印其文件类型。
图4-3 对每个命令行参数打印文件类型
图4-3程序的示例输出是:
$ ./a.out /etc/passwd /etc /dev/log /dev/tty \
> /var/lib/oprofile/opd_pipe /dev/sr0 /dev/cdrom
/etc/passwd: regular
/etc: directory
/dev/log: socket
/dev/tty: character special
/var/lib/oprofile/opd_pipe: fifo
/dev/sr0: block special
/dev/cdrom: symbolic link
(其中,在第一个命令行末端我们键入了一个反斜杠,通知shell要在下一行继续键入命令,然后, shell在下一行上用其辅助提示符>提示我们。)我们特地使用了lstat函数而不是stat函数以便检测符号链接。如若使用stat函数,则不会观察到符号链接。
早期的UNIX版本并不提供S_ISxxx宏,于是就需要将st_mode与屏蔽字S_IFMT进行逻辑“与”运算,然后与名为S_IFxxx的常量相比较。大多数系统在文件<sys/stat.h>中定义了此屏蔽字和相关的常量。如若查看此文件,则可找到S_ISDIR宏定义为:
#define S_ISDIR (mode) (((mode) & S_IFMT) == S_IFDIR)
我们说过,普通文件是最主要的文件类型,但是观察一下在一个给定的系统中各种文件的比例是很有意思的。图4-4显示了在一个单用户工作站Linux系统中的统计值和百分比。这些数据是由4.22节中的程序得到的。
图4-4 不同类型文件的统计值和百分比
与一个进程相关联的ID有6个或更多,如图4-5所示。
图4-5 与每个进程相关联的用户ID和组ID
•实际用户ID和实际组ID 标识我们究竟是谁。这两个字段在登录时取自口令文件中的登录项。通常,在一个登录会话期间这些值并不改变,但是超级用户进程有方法改变它们, 8.11节将说明这些方法。
•有效用户ID、有效组ID以及附属组ID决定了我们的文件访问权限,下一节将对此进行说明(我们已在1.8节中说明了附属组ID)。
•保存的设置用户ID和保存的设置组ID在执行一个程序时包含了有效用户ID和有效组ID的副本,在8.11节中说明setuid函数时,将说明这两个保存值的作用。
在POSIX.1 2001年版中,要求这些保存的ID。在早期POSIX版本中,它们是可选的。一个应用程序在编译时可测试常量_POSIX_SAVED_IDS,或在运行时以参数_SC_SAVED_IDS调用函数sysconf,以判断此实现是否支持这一功能。
通常,有效用户ID等于实际用户ID,有效组ID等于实际组ID。
每个文件有一个所有者和组所有者,所有者由stat结构中的st_uid指定,组所有者则由st_gid指定。
当执行一个程序文件时,进程的有效用户ID通常就是实际用户ID,有效组ID通常是实际组ID。但是可以在文件模式字(st_mode)中设置一个特殊标志,其含义是“当执行此文件时,将进程的有效用户ID设置为文件所有者的用户ID(st_uid)”。与此相类似,在文件模式字中可以设置另一位,它将执行此文件的进程的有效组ID设置为文件的组所有者ID(st_gid)。在文件模式字中的这两位被称为设置用户ID(set-user-ID)位和设置组ID(set-group-ID)位。
例如,若文件所有者是超级用户,而且设置了该文件的设置用户 ID 位,那么当该程序文件由一个进程执行时,该进程具有超级用户权限。不管执行此文件的进程的实际用户 ID 是什么,都会是这样。例如,UNIX 系统程序passwd(1)允许任一用户改变其口令,该程序是一个设置用户 ID 程序。因为该程序应能将用户的新口令写入口令文件中(一般是/etc/passwd 或/etc/shadow),而只有超级用户才具有对该文件的写权限,所以需要使用设置用户 ID 功能。因为运行设置用户ID 程序的进程通常会得到额外的权限,所以编写这种程序时要特别谨慎。第 8 章将更详细地讨论这种类型的程序。
再回到stat函数,设置用户ID位及设置组ID位都包含在文件的st_mode值中。这两位可分别用常量S_ISUID和S_ISGID测试。
st_mode值也包含了对文件的访问权限位。当提及文件时,指的是前面所提到的任何类型的文件。所有文件类型(目录、字符特别文件等)都有访问权限(access permission)。很多人认为只有普通文件有访问权限,这是一种误解。
每个文件有9个访问权限位,可将它们分成3类,见图4-6。
图4-6 9个访问权限位,取自<sys/stat.h>
在图4-6前3行中,术语用户指的是文件所有者(owner)。chmod(1)命令用于修改这9个权限位。该命令允许我们用u表示用户(所有者),用g表示组,用o表示其他。有些书把这3种用户类型分别称为所有者、组和世界。这会造成混乱,因为chmod 命令用o表示其他,而不是所有者。我们将使用术语用户、组和其他,以便与chmod命令保持一致。
图4-6中的3类访问权限(即读、写及执行)以各种方式由不同的函数使用。我们将这些不同的使用方式汇总在下面。当说明相关函数时,再进一步讨论。
•第一个规则是,我们用名字打开任一类型的文件时,对该名字中包含的每一个目录,包括它可能隐含的当前工作目录都应具有执行权限。这就是为什么对于目录其执行权限位常被称为搜索位的原因。
例如,为了打开文件/usr/include/stdio.h,需要对目录/、/usr和/usr/include具有执行权限。然后,需要具有对文件本身的适当权限,这取决于以何种模式打开它(只读、读-写等)。
如果当前目录是/usr/include,那么为了打开文件stdio.h,需要对当前目录有执行权限。这是隐含当前目录的一个示例。打开stdio.h文件与打开./stdio.h作用相同。注意,对于目录的读权限和执行权限的意义是不相同的。读权限允许我们读目录,获得在该目录中所有文件名的列表。当一个目录是我们要访问文件的路径名的一个组成部分时,对该目录的执行权限使我们可通过该目录(也就是搜索该目录,寻找一个特定的文件名)。引用隐含目录的另一个例子是,如果PATH环境变量(8.10节将对其进行说明)指定了一个我们不具有执行权限的目录,那么shell绝不会在该目录下找到可执行文件。
•对于一个文件的读权限决定了我们是否能够打开现有文件进行读操作。这与open函数的O_RDONLY和O_RDWR标志相关。
•对于一个文件的写权限决定了我们是否能够打开现有文件进行写操作。这与open函数的O_WRONLY和O_RDWR标志相关。
•为了在open函数中对一个文件指定O_TRUNC标志,必须对该文件具有写权限。
•为了在一个目录中创建一个新文件,必须对该目录具有写权限和执行权限。
•为了删除一个现有文件,必须对包含该文件的目录具有写权限和执行权限。对该文件本身则不需要有读、写权限。
•如果用7个exec函数(见8.10节)中的任何一个执行某个文件,都必须对该文件具有执行权限。该文件还必须是一个普通文件。
进程每次打开、创建或删除一个文件时,内核就进行文件访问权限测试,而这种测试可能涉及文件的所有者(st_uid和st_gid)、进程的有效ID(有效用户ID和有效组ID)以及进程的附属组ID(若支持的话)。两个所有者ID是文件的性质,而两个有效ID和附属组ID则是进程的性质。内核进行的测试具体如下。
(1)若进程的有效用户ID是0(超级用户),则允许访问。这给予了超级用户对整个文件系统进行处理的最充分的自由。
(2)若进程的有效用户ID等于文件的所有者ID(也就是进程拥有此文件),那么如果所有者适当的访问权限位被设置,则允许访问;否则拒绝访问。适当的访问权限位指的是,若进程为读而打开该文件,则用户读位应为1;若进程为写而打开该文件,则用户写位应为1;若进程将执行该文件,则用户执行位应为1。
(3)若进程的有效组ID或进程的附属组ID之一等于文件的组ID,那么如果组适当的访问权限位被设置,则允许访问;否则拒绝访问。
(4)若其他用户适当的访问权限位被设置,则允许访问;否则拒绝访问。
按顺序执行这 4 步。注意,如果进程拥有此文件(第 2 步),则按用户访问权限批准或拒绝该进程对文件的访问—不查看组访问权限。类似地,若进程并不拥有该文件。但进程属于某个适当的组,则按组访问权限批准或拒绝该进程对文件的访问—不查看其他用户的访问权限。
在第3章中讲述用open或creat创建新文件时,我们并没有说明赋予新文件的用户ID和组ID是什么。4.21节将说明mkdir函数,此时就会了解如何创建一个新目录。关于新目录的所有权规则与本节将说明的新文件所有权规则相同。
新文件的用户ID设置为进程的有效用户ID。关于组ID,POSIX.1允许实现选择下列之一作为新文件的组ID。
(1)新文件的组ID可以是进程的有效组ID。
(2)新文件的组ID可以是它所在目录的组ID。
FreeBSD 8.0和Mac OS X 10.6.8总是使用目录的组ID作为新文件的组ID。有些Linux文件系统使用 mount(1)命令选项允许在 POSIX.1 提出的两种选项中进行选择。对于 Linux 3.2.0 和Solaris 10,默认情况下,新文件的组ID取决于它所在的目录的设置组ID位是否被设置。如果该目录的这一位已经被设置,则新文件的组ID设置为目录的组ID;否则新文件的组ID设置为进程的有效组ID。
使用POSIX.1所允许的第二个选项(继承目录的组ID)使得在某个目录下创建的文件和目录都具有该目录的组ID。于是文件和目录的组所有权从该点向下传递。例如,在Linux的/var/mail目录中就使用了这种方法。
正如前面提到的,这种设置组所有权的方法是FreeBSD 8.0和Mac OS X 10.6.8系统默认的,但对于Linux和Solaris则是可选的。在Linux 3.2.0和Solaris 10之下,必须使设置组ID位起作用。更进一步,为使这种方法能够正常工作,mkdir函数要自动地传递一个目录的设置组ID位(4.21节将说明mkdir就是这样做的)。
正如前面所说,当用 open 函数打开一个文件时,内核以进程的有效用户 ID 和有效组 ID为基础执行其访问权限测试。有时,进程也希望按其实际用户ID和实际组ID来测试其访问能力。例如,当一个进程使用设置用户ID或设置组ID功能作为另一个用户(或组)运行时,就可能会有这种需要。即使一个进程可能已经通过设置用户ID以超级用户权限运行,它仍可能想验证其实际用户能否访问一个给定的文件。access和faccessat函数是按实际用户ID和实际组ID进行访问权限测试的。(该测试也分成4步,这与4.5节中所述的一样,但将有效改为实际。)
#include <unistd.h>
int access(const char *pathname, int mode);
int faccessat(int fd, const char *pathname, int mode, int flag);
两个函数的返回值:若成功,返回0;若出错,返回-1
其中,如果测试文件是否已经存在,mode就为F_OK;否则mode是图4-7中所列常量的按位或。
图4-7 access函数的mode标志,取自<unistd.h>
faccessat函数与access函数在下面两种情况下是相同的:一种是pathname参数为绝对路径,另一种是fd参数取值为AT_FDCWD而pathname参数为相对路径。否则,faccessat计算相对于打开目录(由fd参数指向)的pathname。
flag参数可以用于改变faccessat的行为,如果flag设置为AT_EACCESS,访问检查用的是调用进程的有效用户ID和有效组ID,而不是实际用户ID和实际组ID。
实例
图4-8显示了access函数的使用方法。
下面是该程序的示例会话:
$ ls -l a.out
-rwxrwxr-x 1 sar 15945 Nov 30 12:10 a.out
$ ./a.out a.out
read access OK
open for reading OK
$ ls -l /etc/shadow
-r-------- 1 root 1315 Jul 17 2002 /etc/shadow
图4-8 access函数实例
$ ./a.out /etc/shadow
access error for /etc/shadow: Permission denied
open error for /etc/shadow: Permission denied
$ su 成为超级用户
Password: 输入超级用户口令
# chown root a.out 将文件用户ID改为 root
# chmod u+s a.out 并打开设置用户ID位
# ls -l a.out 检查所有者和SUID位
-rwsrwxr-x 1 root 15945 Nov 30 12:10 a.out
# exit 恢复为正常用户
$ ./a.out /etc/shadow
access error for /etc/shadow: Permission denied
open for reading OK
在本例中,尽管open函数能打开文件,但通过设置用户ID程序可以确定实际用户不能正常读指定的文件。
在上例及第8章中,我们有时要成为超级用户,以便演示某些功能是如何工作的。如果你使用多用户系统,但无超级用户权限,那么你就不能完整地重复这些实例。
至此我们已说明了与每个文件相关联的9个访问权限位,在此基础上我们可以说明与每个进程相关联的文件模式创建屏蔽字。
umask 函数为进程设置文件模式创建屏蔽字,并返回之前的值。(这是少数几个没有出错返回函数中的一个。)
#include <sys/stat.h>
mode_t umask(mode_t cmask);
返回值:之前的文件模式创建屏蔽字
其中,参数cmask是由图4-6中列出的9个常量(S_IRUSR、S_IWUSR等)中的若干个按位“或”构成的。
在进程创建一个新文件或新目录时,就一定会使用文件模式创建屏蔽字(回忆 3.3 节和 3.4节,在那里我们说明了open和creat函数。这两个函数都有一个参数mode,它指定了新文件的访问权限位)。我们将在4.21节说明如何创建一个新目录。在文件模式创建屏蔽字中为1的位,在文件mode中的相应位一定被关闭。
实例
图4-9程序创建了两个文件,创建第一个时,umask值为0,创建第二个时,umask值禁止所有组和其他用户的访问权限。
图4-9 umask函数实例
若运行此程序可得如下结果,从中可见访问权限位是如何设置的。
$ umask 先打印当前文件模式创建屏蔽字
-rw------- 1 sar 0 Dec 7 21:20 bar
-rw-rw-rw- 1 sar 0 Dec 7 21:20 foo
002
$ ./a.out
$ ls -l foo bar
$ umask 观察文件模式创建屏蔽字是否更改
002
UNIX系统的大多数用户从不处理他们的umask值。通常在登录时,由shell的启动文件设置一次,然后,再不改变。尽管如此,当编写创建新文件的程序时,如果我们想确保指定的访问权限位已经激活,那么必须在进程运行时修改 umask 值。例如,如果我们想确保任何用户都能读文件,则应将umask设置为0。否则,当我们的进程运行时,有效的umask值可能关闭该权限位。
在前面的示例中,我们用shell的umask命令在运行程序的前、后打印文件模式创建屏蔽字。从中可见,更改进程的文件模式创建屏蔽字并不影响其父进程(常常是shell)的屏蔽字。所有shell都有内置umask命令,我们可以用该命令设置或打印当前文件模式创建屏蔽字。
用户可以设置umask值以控制他们所创建文件的默认权限。该值表示成八进制数,一位代表一种要屏蔽的权限,这示于图4-10中。设置了相应位后,它所对应的权限就会被拒绝。常用的几种 umask 值是 002、022 和 027。002 阻止其他用户写入你的文件,022 阻止同组成员和其他用户写入你的文件,027阻止同组成员写你的文件以及其他用户读、写或执行你的文件。
图4-10 umask文件访问权限位
Single UNIX Specification要求shell应该支持符号形式的umask命令。与八进制格式不同,符号格式指定许可的权限(即在文件创建屏蔽字中为0的位)而非拒绝的权限(即在文件创建屏蔽字中为1的位)。下面显示了两种格式的命令。
$ umask 先打印当前文件模式创建屏蔽字
$ umask -S 打印符号格式
$ umask 027 更改文件模式创建屏蔽字
$ umask -S 打印符号格式
002
u=rwx,g=rwx,o=rx
u=rwx,g=rx,o=
chmod、fchmod和fchmodat这3个函数使我们可以更改现有文件的访问权限。
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
int fchmodat(int fd, const char *pathname, mode_t mode, int flag);
3个函数返回值:若成功,返回0;若出错,返回−1
chmod 函数在指定的文件上进行操作,而 fchmod 函数则对已打开的文件进行操作。fchmodat函数与chmod函数在下面两种情况下是相同的:一种是pathname参数为绝对路径,另一种是fd参数取值为AT_FDCWD而pathname参数为相对路径。否则,fchmodat计算相对于打开目录(由fd参数指向)的pathname。flag参数可以用于改变fchmodat的行为,当设置了AT_SYMLINK_NOFOLLOW标志时,fchmodat并不会跟随符号链接。
为了改变一个文件的权限位,进程的有效用户ID必须等于文件的所有者ID,或者该进程必须具有超级用户权限。
参数mode是图4-11中所示常量的按位或。
图4-11 chmod函数的mode常量,取自<sys/stat.h>
注意,在图4-11中,有9项是取自图4-6中的9个文件访问权限位。我们另外加了6个,它们是两个设置ID常量(S_ISUID和S_ISGID)、保存正文常量(S_ISVTX)以及3个组合常量(S_IRWXU、S_IRWXG和S_IRWXO)。
保存正文位(S_ISVTX)不是POSIX.1的一部分。在Single UNIX Specification中,它被定义在XSI扩展中。我们在下一节说明其目的。
实例
为了演示umask函数,我们在前面运行了图4-9程序,先让我们回忆文件foo和bar当时的最后状态:
$ ls -l foo bar
-rw------- 1 sar 0 Dec 7 21:20 bar
-rw-rw-rw- 1 sar 0 Dec 7 21:20 foo
图4-12的程序修改了这两个文件的模式。
图4-12 chmod函数实例
在运行图4-12程序后,这两个文件的最后状态是:
$ ls -l foo bar
-rw-r--r-- 1 sar 0 Dec 7 21:20 bar-rw-rwSrw- 1 sar 0 Dec 7 21:20 foo
在本例中,不管文件bar的当前权限位如何,我们都将其权限设置为一个绝对值。对文件foo,我们相对于其当前状态设置权限。为此,先调用stat获得其当前权限,然后修改它。我们显式地打开了设置组ID位、关闭了组执行位。注意,ls命令将组执行权限表示为S,它表示设置组ID位已经设置,同时,组执行位未设置。
在Solaris中,ls命令显示l而非S,这表明对该文件可以加强制性文件或记录锁。这只能用于普通文件,14.3节将更详细地讨论这一点。
最后还要注意,在运行图4-12程序后,ls命令列出的时间和日期并没有改变。在4.19节中,我们会了解到 chmod 函数更新的只是 i 节点最近一次被更改的时间。按系统默认方式,ls -l列出的是最后修改文件内容的时间。
chmod函数在下列条件下自动清除两个权限位。
•Solaris 等系统对用于普通文件的粘着位赋予了特殊含义,在这些系统上如果我们试图设置普通文件的粘着位(S_ISVTX),而且又没有超级用户权限,那么mode中的粘着位自动被关闭(我们将在下一节说明粘着位)。这意味着只有超级用户才能设置普通文件的粘着位。这样做的理由是防止恶意用户设置粘着位,由此影响系统性能。
在FreeBSD 8.0和Solaris 10中,只有超级用户才能对普通文件设置粘着位。Linux 3.2.0和Mac OS X 10.6.8对设置粘着位并无此种限制,其原因是,粘着位对Linux普通文件并无意义。虽然粘着位对FreeBSD的普通文件也无意义,但还是阻止除超级用户以外的任何用户对普通文件设置该位。
•新创建文件的组 ID 可能不是调用进程所属的组。回忆一下 4.6 节,新文件的组 ID可能是父目录的组ID。特别地,如果新文件的组ID不等于进程的有效组ID或者进程附属组 ID 中的一个,而且进程没有超级用户权限,那么设置组 ID 位会被自动被关闭。这就防止了用户创建一个设置组ID文件,而该文件是由并非该用户所属的组拥有的。
这种情况下,FreeBSD 8.0对试图设置组ID的操作肯定会返回失败,而其他的系统则无声息地关闭该位,但不会对试图改变文件访问权限的操作直接做失败处理。
FreeBSD 8.0、Linux 3.2.0、Mac OS X 10.6.8和Solaris 10增加了另一个安全性功能以试图阻止误用某些保护位。如果没有超级用户权限的进程写一个文件,则设置用户 ID 位和设置组ID位会被自动清除。如果恶意用户找到一个他们可以写的设置组ID和设置用户ID文件,即使可以修改此文件,他们也没有对该文件的特殊权限。
S_ISVTX位有一段有趣的历史。在UNIX尚未使用请求分页式技术的早期版本中,S_ISVTX位被称为粘着位(sticky bit)。如果一个可执行程序文件的这一位被设置了,那么当该程序第一次被执行,在其终止时,程序正文部分的一个副本仍被保存在交换区(程序的正文部分是机器指令)。这使得下次执行该程序时能较快地将其装载入内存。其原因是:通常的UNIX文件系统中,文件的各数据块很可能是随机存放的,相比较而言,交换区是被作为一个连续文件来处理的。对于通用的应用程序,如文本编辑程序和C语言编译器,我们常常设置它们所在文件的粘着位。自然地,对于在交换区中可以同时存放的设置了粘着位的文件数是有限制的,以免过多占用交换区空间,但无论如何这是一个有用的技术。因为在系统再次自举前,文件的正文部分总是在交换区中,这正是名字中“粘着”的由来。后来的UNIX版本称它为保存正文位(saved-text bit),因此也就有了常量S_ISVTX。现今较新的UNIX系统大多数都配置了虚拟存储系统以及快速文件系统,所以不再需要使用这种技术。
现今的系统扩展了粘着位的使用范围,Single UNIX Specification允许针对目录设置粘着位。如果对一个目录设置了粘着位,只有对该目录具有写权限的用户并且满足下列条件之一,才能删除或重命名该目录下的文件:
•拥有此文件;
•拥有此目录;
•是超级用户。
目录/tmp 和/var/tmp 是设置粘着位的典型候选者—任何用户都可在这两个目录中创建文件。任一用户(用户、组和其他)对这两个目录的权限通常都是读、写和执行。但是用户不应能删除或重命名属于其他人的文件,为此在这两个目录的文件模式中都设置了粘着位。
POSIX.1没有定义保存正文位,Single UNIX Specification将它定义在XSI扩展部分。FreeBSD 8.0、Linux 3.2.0、Mac OS X 10.6.8和Solaris 10则支持这种功能。
在Solaris 10中,如果对普通文件设置了粘着位,那么它就具有特殊含义。在这种情况下,如果任何执行位都没有设置,那么操作系统就不会缓存文件内容。
4.11 函数chown、fchown、fchownat和lchown
下面几个chown函数可用于更改文件的用户ID和组ID。如果两个参数owner或group中的任意一个是-1,则对应的ID不变。
#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int fchownat(int fd, const char *pathname, uid_t owner, gid_t group, int flag);
int lchown(const char *pathname, uid_t owner, gid_t group);
4个函数的返回值:若成功,返回0;若出错,返回-1
除了所引用的文件是符号链接以外,这 4 个函数的操作类似。在符号链接情况下,lchown和fchownat(设置了AT_SYMLINK_NOFOLLOW标志)更改符号链接本身的所有者,而不是该符号链接所指向的文件的所有者。
fchown函数改变fd参数指向的打开文件的所有者,既然它在一个已打开的文件上操作,就不能用于改变符号链接的所有者。
fchownat函数与chown或者lchown函数在下面两种情况下是相同的:一种是pathname参数为绝对路径,另一种是fd参数取值为AT_FDCWD而pathname参数为相对路径。在这两种情况下,如果flag参数中设置了AT_SYMLINK_NOFOLLOW标志,fchownat与lchown行为相同,如果flag参数中清除了AT_SYMLINK_NOFOLLOW标志,则fchownat与chown行为相同。如果fd参数设置为打开目录的文件描述符,并且pathname参数是一个相对路径名,fchownat函数计算相对于打开目录的pathname。
基于BSD的系统一直规定只有超级用户才能更改一个文件的所有者。这样做的原因是防止用户改变其文件的所有者从而摆脱磁盘空间限额对他们的限制。System V则允许任一用户更改他们所拥有的文件的所有者。
按照_POSIX_CHOWN_RESTRICTED的值,POSIX.1允许在这两种形式的操作中选用一种。
对于Solaris 10,此功能是个配置选项,其默认值是施加限制。而FreeBSD 8.0、Linux 3.2.0和Mac OS X 10.6.8则总对chown施加限制。
回忆2.6节,_POSIX_CHOWN_RESTRICTED常量可选地定义在头文件<unistd.h>中,而且总是可以用pathconf或fpathconf函数进行查询。此选项还与所引用的文件有关—可在每个文件系统基础上,使该选项起作用或不起作用。在下文中,如提及“若_POSIX_CHOWN_RESTRICTED生效”,则表示“这适用于我们正在谈及的文件”,而不管该实际常量是否在头文件中定义。
若_POSIX_CHOWN_RESTRICTED对指定的文件生效,则
(1)只有超级用户进程能更改该文件的用户ID;
(2)如果进程拥有此文件(其有效用户ID等于该文件的用户ID),参数owner等于-1或文件的用户ID,并且参数group等于进程的有效组ID或进程的附属组ID之一,那么一个非超级用户进程可以更改该文件的组ID。
这意味着,当_POSIX_CHOWN_RESTRICTED有效时,不能更改其他用户文件的用户ID。你可以更改你所拥用的文件的组ID,但只能改到你所属的组。
如果这些函数由非超级用户进程调用,则在成功返回时,该文件的设置用户 ID 位和设置组ID位都被清除。
stat结构成员st_size表示以字节为单位的文件的长度。此字段只对普通文件、目录文件和符号链接有意义。
FreeBSD 8.0、Mac OS X 10.6.8和Solaris 10对管道也定义了文件长度,它表示可从该管道中读到的字节数,我们将在15.2中讨论管道。
对于普通文件,其文件长度可以是0,在开始读这种文件时,将得到文件结束(end-of-file)指示。对于目录,文件长度通常是一个数(如16或512)的整倍数,我们将在4.22节中说明读目录操作。
对于符号链接,文件长度是在文件名中的实际字节数。例如,在下面的例子中,文件长度 7就是路径名usr/lib的长度:
lrwxrwxrwx 1 root 7 Sep 25 07:14 lib -> usr/lib
(注意,因为符号链接文件长度总是由st_size指示,所以它并不包含通常C语言用作名字结尾的null字节。)
现今,大多数现代的UNIX系统提供字段st_blksize和st_blocks。其中,第一个是对文件I/O较合适的块长度,第二个是所分配的实际512字节块块数。回忆3.9节,其中提到了当我们将st_blksize用于读操作时,读一个文件所需的时间量最少。为了提高效率,标准I/O库(我们将在第5章中说明)也试图一次读、写st_blksize个字节。
应当了解的是,不同的UNIX版本其st_blocks所用的单位可能不是512字节的块。使用此值并不是可移植的。
文件中的空洞
在3.6 节中,我们提及普通文件可以包含空洞。在图3-2 程序中例示了这一点。空洞是由所设置的偏移量超过文件尾端,并写入了某些数据后造成的。作为一个例子,考虑下列情况:
$ ls -l core
-rw-r--r-- 1 sar 8483248 Nov 18 12:18 core
$ du -s core
272 core
文件core的长度稍稍超过8 MB,可是du命令报告该文件所使用的磁盘空间总量是272个512字节块(即139 264字节)。很明显,此文件中有很多空洞。
在很多BSD类系统上,du命令报告的是1 024字节块的块数,Solaris报告的是512字节块的块数。在Linux上,报告的块数单位取决于是否设置了环境变量POSIXLY_CORRECT。当设置了该环境变量,du 命令报告的是 1 024 字节块的块数;没有设置该环境变量时,du 命令报告的是512字节块的块数。
正如我们在3.6节中提及的,对于没有写过的字节位置,read函数读到的字节是0。如果执行下面的命令,可以看出正常的I/O操作读整个文件长度:
$ wc -c core
8483248 core
带-c选项的wc(1)命令计算文件中的字符数(字节)。
如果使用实用程序(如cat(1))复制这个文件,那么所有这些空洞都会被填满,其中所有实际数据字节皆填写为0。
$ cat core > core.copy
$ ls -l core*
-rw-r--r-- 1 sar 8483248 Nov 18 12:18 core
-rw-rw-r-- 1 sar 8483248 Nov 18 12:27 core.copy
$ du -s core*
272 core
16592 core.copy
从中可见,新文件所用的实际字节数是8 495 104(512×16 592)。此长度与ls命令报告的长度不同,其原因是,文件系统使用了若干块以存放指向实际数据块的各个指针。
有兴趣的读者可以参阅Bach[1986]的4.2节、McKusick 等[1996]的7.2节和7.3节(或McKusick和Neville-Neil[2005]的8.2节和8.3节)、McDougall和Mauro[2007]的15.2节以及Singh[2006]的第12章,以更详细地了解文件的物理结构。
有时我们需要在文件尾端处截去一些数据以缩短文件。将一个文件的长度截断为0是一个特例,在打开文件时使用O_TRUNC 标志可以做到这一点。为了截断文件可以调用函数 truncate和ftruncate。
#include <unistd.h>
int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);
两个函数的返回值:若成功,返回0;若出错,返回-1
这两个函数将一个现有文件长度截断为 length。如果该文件以前的长度大于 length,则超过length 以外的数据就不再能访问。如果以前的长度小于 length,文件长度将增加,在以前的文件尾端和新的文件尾端之间的数据将读作0(也就是可能在文件中创建了一个空洞)。
早于4.4BSD的BSD系统只能用truncate函数截短一个文件,不能用它扩展一个文件。
Solaris对fcntl函数进行了扩展,增加了F_FREESP,它允许释放一个文件中的任何一部分,而不只是文件尾端处的一部分。
图13-6的程序使用了ftruncate函数,以便在获得对一个文件的锁后,清空该文件。
为了说明文件链接的概念,先要介绍UNIX文件系统的基本结构。同时,了解i节点和指向i节点的目录项之间的区别也是很有益的。
目前,正在使用的UNIX文件系统有多种实现。例如,Solaris支持多种不同类型的磁盘文件系统:传统的基于BSD的UNIX文件系统(称为UFS),读、写DOS格式软盘的文件系统(称为PCFS),以及读CD的文件系统(称为HSFS)。在 图2-20中,我们已经看到了不同类型文件系统的一个区别。UFS是以Berkeley快速文件系统为基础的。本节讨论该文件系统。
每一种文件系统类型都有它各自的特征,有些特征可能是混淆不清的。例如,大部分UNIX文件系统支持大小写敏感的文件名。因此,如果创建了一个名为file.txt的文件以及另外一个名为file.TXT的文件,就是创建了两个不同的文件。在Mac OS X上,HFS文件系统是大小写保留的,并且是大小写不敏感比较的。因此,如果创建了一个名为file.txt的文件,当你再创建名为file.TXT的文件时,就会覆盖原来的file.txt文件。但是,保存在文件系统中的是文件创建时的文件名(即file.txt,因为是大小写保留的)。事实上,在“f, i, l, e, ., t, x, t”这个序列中的大写或小写字母的排列都会在搜索这个文件时得到匹配(大小写不敏感比较)。因此,除了file.txt和file.TXT,我们还可以用File.txt、fILE.tXt以及FiLe.TxT等名字来访问该文件。
我们可以把一个磁盘分成一个或多个分区。每个分区可以包含一个文件系统(见图 4-13)。i节点是固定长度的记录项,它包含有关文件的大部分信息。
图4-13 磁盘、分区和文件系统
如果更仔细地观察一个柱面组的i节点和数据块部分,则可以看到图4-14中所示的情况。注意图4-14中的下列各点。
•在图中有两个目录项指向同一个i节点。每个i节点中都有一个链接计数,其值是指向该i节点的目录项数。只有当链接计数减少至0时,才可删除该文件(也就是可以释放该文件占用的数据块)。这就是为什么“解除对一个文件的链接”操作并不总是意味着“释放该文件占用的磁盘块”的原因。这也是为什么删除一个目录项的函数被称之为 unlink而不是delete的原因。在stat结构中,链接计数包含在st_nlink成员中,其基本系统数据类型是nlink_t。这种链接类型称为硬链接。回忆2.5.2节,其中,POSIX.1常量LINK_MAX指定了一个文件链接数的最大值。
图4-14 较详细的柱面组的i节点和数据块
•另外一种链接类型称为符号链接(symbolic link)。符号链接文件的实际内容(在数据块中)包含了该符号链接所指向的文件的名字。在下面的例子中,目录项中的文件名是 3个字符的字符串lib,而在该文件中包含了7个字节的数据usr/lib:
lrwxrwxrwx 1 root 7 Sep 25 07:14 lib -> urs/lib
该i节点中的文件类型是S_IFLNK,于是系统知道这是一个符号链接。
•i节点包含了文件有关的所有信息:文件类型、文件访问权限位、文件长度和指向文件数据块的指针等。stat结构中的大多数信息都取自i节点。只有两项重要数据存放在目录项中:文件名和i节点编号。其他的数据项(如文件名长度和目录记录长度)并不是本书关心的。i节点编号的数据类型是ino_t。
•因为目录项中的i节点编号指向同一文件系统中的相应i节点,一个目录项不能指向另一个文件系统的i节点。这就是为什么ln(1)命令(构造一个指向一个现有文件的新目录项)不能跨越文件系统的原因。我们将在下一节说明link函数。
•当在不更换文件系统的情况下为一个文件重命名时,该文件的实际内容并未移动,只需构造一个指向现有i节点的新目录项,并删除老的目录项。链接计数不会改变。例如,为将文件/usr/lib/foo重命名为/usr/foo,如果目录/usr/lib和/usr在同一文件系统中,则文件foo的内容无需移动。这就是mv(1)命令的通常操作方式。
我们说明了普通文件的链接计数概念,但是对于目录文件的链接计数字段又如何呢?假定我们在工作目录中构造了一个新目录:
$ mkdir testdir
图4-15显示了其结果。注意,该图显式地显示了.和..目录项。
编号为2549的i节点,其类型字段表示它是一个目录,链接计数为2。任何一个叶目录(不包含任何其他目录的目录)的链接计数总是2,数值2来自于命名该目录(testdir)的目录项以及在该目录中的.项。编号为1267的i节点,其类型字段表示它是一个目录,链接计数大于或等于3。它大于或等于3的原因是,至少有3个目录项指向它:一个是命名它的目录项(在图4-15中没有表示出来),第二个是在该目录中的.项,第三个是在其子目录testdir中的..项。注意,在父目录中的每一个子目录都使该父目录的链接计数增加1。
图4-15 创建了目录testdir后的文件系统实例
这种格式与UNIX文件系统的经典格式类似,在Bach[1986]的第4章中对此进行了详细说明。关于伯克利快速文件系统对此所做的更改请参阅 McKusick 等[1996]的第 7 章以及 McKusick 和Neville-Neil[2005]中的第8章。关于UFS(伯克利快速文件系统的Solaris版)的详细情况,请参见McDougall和Mauro[2007]的第15章。关于Mac OS X使用的HFS文件系统格式,请参阅Singh[2006]的第12章。
4.15 函数link、linkat、unlink、unlinkat和remove
如上节所述,任何一个文件可以有多个目录项指向其i节点。创建一个指向现有文件的链接的方法是使用link函数或linkat函数。
#include <unistd.h>
int link(const char *existingpath, const char *newpath);
int linkat(int efd, const char *existingpath, int nfd, const char *newpath, int flag);
两个函数的返回值:若成功,返回0;若出错,返回-1
这两个函数创建一个新目录项newpath,它引用现有文件existingpath。如果newpath已经存在,则返回出错。只创建newpath中的最后一个分量,路径中的其他部分应当已经存在。
对于linkat函数,现有文件是通过efd和existingpath参数指定的,新的路径名是通过nfd和newpath参数指定的。默认情况下,如果两个路径名中的任一个是相对路径,那么它需要通过相对于对应的文件描述符进行计算。如果两个文件描述符中的任一个设置为AT_FDCWD,那么相应的路径名(如果它是相对路径)就通过相对于当前目录进行计算。如果任一路径名是绝对路径,相应的文件描述符参数就会被忽略。
当现有文件是符号链接时,由flag参数来控制linkat函数是创建指向现有符号链接的链接还是创建指向现有符号链接所指向的文件的链接。如果在flag参数中设置了AT_SYMLINK_FOLLOW标志,就创建指向符号链接目标的链接。如果这个标志被清除了,则创建一个指向符号链接本身的链接。
创建新目录项和增加链接计数应当是一个原子操作(请回忆在3.11节中对原子操作的讨论)。
虽然POSIX.1允许实现支持跨越文件系统的链接,但是大多数实现要求现有的和新建的两个路径名在同一个文件系统中。如果实现支持创建指向一个目录的硬链接,那么也仅限于超级用户才可以这样做。其理由是这样做可能在文件系统中形成循环,大多数处理文件系统的实用程序都不能处理这种情况(4.17 节将说明一个由符号链接引入循环的例子)。因此,很多文件系统实现不允许对于目录的硬链接。
为了删除一个现有的目录项,可以调用unlink函数。
#include <unistd.h>
int unlink(const char *pathname);
int unlinkat(int fd, const char *pathname, int flag);
两个函数的返回值:若成功,返回0;若出错,返回-1
这两个函数删除目录项,并将由pathname所引用文件的链接计数减1。如果对该文件还有其他链接,则仍可通过其他链接访问该文件的数据。如果出错,则不对该文件做任何更改。
我们在前面已经提及,为了解除对文件的链接,必须对包含该目录项的目录具有写和执行权限。正如4.10节所述,如果对该目录设置了粘着位,则对该目录必须具有写权限,并且具备下面三个条件之一:
• 拥有该文件;
•拥有该目录;
•具有超级用户权限。
只有当链接计数达到0时,该文件的内容才可被删除。另一个条件也会阻止删除文件的内容—只要有进程打开了该文件,其内容也不能删除。关闭一个文件时,内核首先检查打开该文件的进程个数;如果这个计数达到0,内核再去检查其链接计数;如果计数也是0,那么就删除该文件的内容。
如果pathname参数是相对路径名,那么unlinkat函数计算相对于由fd文件描述符参数代表的目录的路径名。如果fd参数设置为AT_FDCWD,那么通过相对于调用进程的当前工作目录来计算路径名。如果pathname参数是绝对路径名,那么fd参数被忽略。
flag参数给出了一种方法,使调用进程可以改变unlinkat函数的默认行为。当AT_REMOVEDIR标志被设置时,unlinkat 函数可以类似于 rmdir 一样删除目录。如果这个标志被清除, unlinkat与unlink执行同样的操作。
实例
图4-16的程序打开一个文件,然后解除它的链接。执行该程序的进程然后睡眠15秒,接着就终止。
图4-16 打开一个文件,然后unlink它
运行该程序,其结果是:
$ ls -l tempfile 查看文件大小
-rw-r----- 1 sar 413265408 Jan 21 07:14 tempfile
$ df /home 检查可用磁盘空间
Filesystem 1K-blocks Used Available Use% Mounted on/dev/hda4 11021440 1956332 9065108 18% /home
$ ./a.out & 在后台运行图4-16程序
1364 shell打印其进程ID
$ file unlinked 解除文件链接
ls -l tempfile 观察文件是否仍然存在
ls: tempfile: No such file or directory 目录项已删除
$ df /home 检查可用磁盘空间有无变化
Filesystem 1K-blocks Used Available Use% Mounted on/dev/hda4 11021440 1956332 9065108 18% /home
$ done 程序执行结束,关闭所有打开文件
df /home 现在,应当有更多可用磁盘空间
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/hda4 11021440 1552352 9469088 15% /home
现在,394.1 MB磁盘空间可用
unlink的这种特性经常被程序用来确保即使是在程序崩溃时,它所创建的临时文件也不会遗留下来。进程用open或creat创建一个文件,然后立即调用unlink,因为该文件仍旧是打开的,所以不会将其内容删除。只有当进程关闭该文件或终止时(在这种情况下,内核关闭该进程所打开的全部文件),该文件的内容才被删除。
如果pathname是符号链接,那么unlink删除该符号链接,而不是删除由该链接所引用的文件。给出符号链接名的情况下,没有一个函数能删除由该链接所引用的文件。
如果文件系统支持的话,超级用户可以调用unlink,其参数pathname指定一个目录,但是通常应当使用rmdir函数,而不使用unlink这种方式。我们将在4.21节中说明rmdir函数。
我们也可以用 remove 函数解除对一个文件或目录的链接。对于文件,remove 的功能与unlink相同。对于目录,remove的功能与rmdir相同。
#include <stdio.h>
int remove(const char *pathname);
返回值:若成功,返回0;若出错,返回-1
ISO C指定remove函数删除一个文件,这更改了UNIX历来使用的名字unlink,其原因是实现C标准的大多数非UNIX系统并不支持文件链接。
文件或目录可以用rename函数或者renameat函数进行重命名。
#include <stdio.h>
int rename(const char *oldname, const char *newname);
int renameat(int oldfd, const char *oldname, int newfd, const char *newname);
两个函数的返回值:若成功,返回0;若出错,返回-1
ISO C对文件定义了rename函数(C标准不处理目录)。POSIX.1扩展此定义,使其包含了目录和符号链接。
根据oldname是指文件、目录还是符号链接,有几种情况需要加以说明。我们也必须说明如果newname已经存在时将会发生什么。
(1)如果oldname指的是一个文件而不是目录,那么为该文件或符号链接重命名。在这种情况下,如果newname已存在,则它不能引用一个目录。如果newname已存在,而且不是一个目录,则先将该目录项删除然后将 oldname 重命名为 newname。对包含 oldname 的目录以及包含newname的目录,调用进程必须具有写权限,因为将更改这两个目录。
(2)如若oldname指的是一个目录,那么为该目录重命名。如果newname已存在,则它必须引用一个目录,而且该目录应当是空目录(空目录指的是该目录中只有.和..项)。如果 newname存在(而且是一个空目录),则先将其删除,然后将oldname重命名为newname。另外,当为一个目录重命名时,newname不能包含oldname作为其路径前缀。例如,不能将/usr/foo重命名为/usr/foo/testdir,因为旧名字(/usr/foo)是新名字的路径前缀,因而不能将其删除。
(3)如若oldname或newname引用符号链接,则处理的是符号链接本身,而不是它所引用的文件。
(4)不能对.和..重命名。更确切地说,.和..都不能出现在oldname和newname的最后部分。
(5)作为一个特例,如果oldname和newname引用同一文件,则函数不做任何更改而成功返回。
如若newname已经存在,则调用进程对它需要有写权限(如同删除情况一样)。另外,调用进程将删除oldname目录项,并可能要创建newname目录项,所以它需要对包含oldname及包含newname的目录具有写和执行权限。
除了当oldname或newname指向相对路径名时,其他情况下renameat函数与rename函数功能相同。如果oldname参数指定了相对路径,就相对于oldfd参数引用的目录来计算oldname。类似地,如果newname指定了相对路径,就相对于newfd引用的目录来计算newname。oldfd或newfd参数(或两者)都能设置成AT_FDCWD,此时相对于当前目录来计算相应的路径名。
符号链接是对一个文件的间接指针,它与上一节所述的硬链接有所不同,硬链接直接指向文件的i节点。引入符号链接的原因是为了避开硬链接的一些限制。
•硬链接通常要求链接和文件位于同一文件系统中。
•只有超级用户才能创建指向目录的硬链接(在底层文件系统支持的情况下)。
对符号链接以及它指向何种对象并无任何文件系统限制,任何用户都可以创建指向目录的符号链接。符号链接一般用于将一个文件或整个目录结构移到系统中另一个位置。
当使用以名字引用文件的函数时,应当了解该函数是否处理符号链接。也就是该函数是否跟随符号链接到达它所链接的文件。如若该函数具有处理符号链接的功能,则其路径名参数引用由符号链接指向的文件。否则,一个路径名参数引用链接本身,而不是由该链接指向的文件。图4-17列出了本章中所说明的各个函数是否处理符号链接。在图 4-17 中没有列出 mkdir、mkinfo、mknod和rmdir这些函数,其原因是,当路径名是符号链接时,它们都出错返回。以文件描述符作为参数的一些函数(如fstat、fchmod等)也未在该图中列出,其原因是,对符号链接的处理是由返回文件描述符的函数(通常是open)进行的。chown是否跟随符号链接取决于实现。在所有现代的系统中,chown函数都跟随符号链接。
符号链接由4.2BSD引入,chown最初并不跟随符号链接,但在4.4BSD中情况发生了变化。SVR4中的System V包含了对符号链接的支持,但与原始BSD中的行为已大不相同,也实现了chown函数跟随符号链接。早期Linux版本中(Linux 2.1.81以前的版本),chown并不跟随符号链接。从2.1.81版开始,chown跟随符号链接。FreeBSD 8.0、Mac OS X 10.6.8和Solaris 10中, chown跟随符号链接。所有这些平台都实现了lchown,它改变符号链接自身的所有权。
图4-17 各个函数对符号链接的处理
图4-17的一个例外是,同时用O_CREAT和O_EXCL两者调用open函数。在此情况下,若路径名引用符号链接,open将出错返回,errno设置为EEXIST。这种处理方式的意图是堵塞一个安全性漏洞,以防止具有特权的进程被诱骗写错误的文件。
实例
使用符号链接可能在文件系统中引入循环。大多数查找路径名的函数在这种情况发生时都将出错返回,errno值为ELOOP。考虑下列命令序列:
$ mkdir foo 创建一个新目录
$ touch foo/a 创建一个0长度的文件
$ ln -s ../foo foo/testdir 创建一个符号链接
-rw-r----- 1 sar 0 Jan 22 00:16 a
lrwxrwxrwx 1 sar 6 Jan 22 00:16 testdir -> ../foo
$ ls -l foo
total 0
这创建了一个目录foo,它包含了一个名为a的文件以及一个指向foo的符号链接。在图4-18中显示了这种结果,图中以圆表示目录,以正方形表示一个文件。
图4-18 构成循环的符号链接testdir
如果我们写一段简单的程序,使用Solaris的标准函数ftw(3)以降序遍历文件结构,打印每个遇到的路径名,则其输出是:
foo
foo/a
foo/testdir
foo/testdir/a
foo/testdir/testdir
foo/testdir/testdir/a
foo/testdir/testdir/testdir
foo/testdir/testdir/testdir/a
(更多行,直至ftw出错返回,此时,errno值为ELOOP)
4.22节提供了我们自己的ftw函数版本,它用lstat代替stat以阻止它跟随符号链接。
注意,Linux的ftw和nftw函数记录了所有看到的目录并避免多次重复处理一个目录,因此这两个函数不显示这种程序运行行为。
这样一个循环是很容易消除的。因为 unlink 并不跟随符号链接,所以可以 unlink 文件foo/testdir。但是如果创建了一个构成这种循环的硬链接,那么就很难消除它。这就是为什么link函数不允许构造指向目录的硬链接的原因(除非进程具有超级用户权限)。
实际上,Rich Stevens在写本节的最初版本时,在自己的系统上做了一个这样的实验。结果文件系统变得错误百出。正常的 fsck(1)实用程序不能修复问题。为了修复文件系统,不得不使用了并不推荐使用的工具clri(8)和dcheck(8)。
对目录的硬链接的需求由来已久,但是使用符号链接和mkdir函数,用户就不再需要创建指向目录的硬链接了。
用open打开文件时,如果传递给open函数的路径名指定了一个符号链接,那么open跟随此链接到达所指定的文件。若此符号链接所指向的文件并不存在,则open返回出错,表示它不能打开该文件。这可能会使不熟悉符号链接的用户感到迷惑,例如:
$ ln -s /no/such/file myfile 创建一个符号链接
myfile ls查到该文件
$ cat myfile 试图查看该文件
$ ls myfile
cat: myfile: No such file or directory
$ ls -1 myfile 尝试-l选项
lrwxrwxrwx 1 sar 13 Jan 22 00:26 myfile -> /no/such/file
文件myfile存在,但cat却称没有这一文件。其原因是myfile是个符号链接,由该符号链接所指向的文件并不存在。ls命令的-l选项给我们两个提示:第一个字符是l,它表示这是一个符号链接,而->也表明这是一个符号链接。ls 命令还有另一个选项-F,它会在符号链接的文件名后加一个@符号,在未使用-l选项时,这可以帮助我们识别出符号链接。
可以用symlink或symlinkat函数创建一个符号链接。
#include <unistd.h>
int symlink(const char *actualpath, const char *sympath);
int symlinkat(const char *actualpath, int fd, const char *sympath);
两个函数的返回值:若成功,返回0;若出错,返回-1
函数创建了一个指向actualpath的新目录项sympath。在创建此符号链接时,并不要求actualpath已经存在(在上一节结束部分的例子中我们已经看到了这一点)。并且,actualpath和sympath并不需要位于同一文件系统中。
symlinkat函数与symlink函数类似,但sympath参数根据相对于打开文件描述符引用的目录(由 fd 参数指定)进行计算。如果 sympath 参数指定的是绝对路径或者 fd 参数设置了AT_FDCWD值,那么symlinkat就等同于symlink函数。
因为open函数跟随符号链接,所以需要有一种方法打开该链接本身,并读该链接中的名字。readlink和readlinkat函数提供了这种功能。
#include <unistd.h>
ssize_t readlink(const char *restrict pathname, char *restrict buf,
size_t bufsize);
ssize_t readlinkat(int fd, const char* restrict pathname,
char *restrict buf, size_t bufsize);
两个函数的返回值:若成功,返回读取的字节数;若出错,返回-1
两个函数组合了 open、read 和 close 的所有操作。如果函数成功执行,则返回读入buf的字节数。在buf中返回的符号链接的内容不以null字节终止。
当pathname参数指定的是绝对路径名或者fd参数的值为AT_FDCWD,readlinkat函数的行为与readlink相同。但是,如果fd参数是一个打开目录的有效文件描述符并且pathname参数是相对路径名,则readlinkat计算相对于由fd代表的打开目录的路径名。
在4.2节中,我们讨论了Single UNIX Specification 2008年版如何提高stat结构中时间字段的精度,从原来的秒提高到秒加上纳秒。每个文件属性所保存的实际精度依赖于文件系统的实现。对于把时间戳记录在秒级的文件系统来说,纳秒这个字段就会被填充为 0。对于时间戳的记录精度高于秒级的文件系统来说,不足秒的值被转换成纳秒并记录在纳秒这个字段中。
对每个文件维护3个时间字段,它们的意义示于图4-19中。
图4-19 与每个文件相关的3个时间值
注意,修改时间(st_mtim)和状态更改时间(st_ctim)之间的区别。修改时间是文件内容最后一次被修改的时间。状态更改时间是该文件的i节点最后一次被修改的时间。在本章中我们已说明了很多影响到i节点的操作,如更改文件的访问权限、更改用户ID、更改链接数等,但它们并没有更改文件的实际内容。因为i节点中的所有信息都是与文件的实际内容分开存放的,所以,除了要记录文件数据修改时间以外,还需要记录状态更改时间,也就是更改i节点中信息的时间。
注意,系统并不维护对一个i节点的最后一次访问时间,所以access和stat函数并不更改这3个时间中的任一个。
系统管理员常常使用访问时间来删除在一定时间范围内没有被访问过的文件。典型的例子是删除在过去一周内没有被访问过的名为a.out或core的文件。find(1)命令常被用来进行这种类型的操作。
修改时间和状态更改时间可被用来归档那些内容已经被修改或i节点已经被更改的文件。
ls命令按这3个时间值中的一个排序进行显示。系统默认(用-l或-t选项调用时)是按文件的修改时间的先后排序显示。-u选项使ls命令按访问时间排序,-c选项则使其按状态更改时间排序。
图4-20列出了我们已说明过的各种函数对这3个时间的作用。回忆4.14节中所述,目录是包含目录项(文件名和相关的i节点编号)的文件,增加、删除或修改目录项会影响到它所在目录相关的3个时间。这就是在图4-20中包含两列的原因,其中一列是与该文件(或目录)相关的3个时间,另一列是与所引用的文件(或目录)的父目录相关的3个时间。例如,创建一个新文件影响到包含此新文件的目录,也影响该新文件的i节点。但是,读或写一个文件只影响该文件的i节点,而对目录则无影响。
图4-20 各种函数对访问、修改和状态更改时间的作用
(mkdir和rmdir函数将在4.21节中说明。utimes、utimensat、futimens函数将在下一节中说明。7个exec函数将在8.10节中讨论。第15章将说明mkfifo和pipe函数。)
4.20 函数futimens、utimensat和utimes
一个文件的访问和修改时间可以用以下几个函数更改。futimens和utimensat函数可以指定纳秒级精度的时间戳。用到的数据结构是与stat函数族相同的timespec结构(见4.2节)。
#include <sys/stat.h>
int futimens(int fd, const struct timespec times[2]);
int utimensat(int fd, const char *path, const struct timespec times[2], int flag);
两个函数返回值:若成功,返回0;若出错,返回-1
这两个函数的times数组参数的第一个元素包含访问时间,第二元素包含修改时间。这两个时间值是日历时间,如1.10节所述,这是自特定时间(1970年1月1日00:00:00)以来所经过的秒数。不足秒的部分用纳秒表示。
时间戳可以按下列4种方式之一进行指定。
(1)如果times参数是一个空指针,则访问时间和修改时间两者都设置为当前时间。
(2)如果times参数指向两个timespec结构的数组,任一数组元素的tv_nsec字段的值为UTIME_NOW,相应的时间戳就设置为当前时间,忽略相应的tv_sec字段。
(3)如果times参数指向两个timespec结构的数组,任一数组元素的tv_nsec字段的值为UTIME_OMIT,相应的时间戳保持不变,忽略相应的tv_sec字段。
(4)如果 times 参数指向两个 timespec 结构的数组,且 tv_nsec 字段的值为既不是UTIME_NOW 也不是 UTIME_OMIT,在这种情况下,相应的时间戳设置为相应的 tv_sec 和tv_nsec字段的值。
执行这些函数所要求的优先权取决于times参数的值。
• 如果times是一个空指针,或者任一tv_nsec字段设为UTIME_NOW,则进程的有效用户ID必须等于该文件的所有者ID;进程对该文件必须具有写权限,或者进程是一个超级用户进程。
• 如果 times 是非空指针,并且任一 tv_nsec 字段的值既不是 UTIME_NOW 也不是UTIME_OMIT,则进程的有效用户ID必须等于该文件的所有者ID,或者进程必须是一个超级用户进程。对文件只具有写权限是不够的。
• 如果times是非空指针,并且两个tv_nsec字段的值都为UTIME_OMIT,就不执行任何的权限检查。
futimens 函数需要打开文件来更改它的时间,utimensat 函数提供了一种使用文件名更改文件时间的方法。pathname参数是相对于fd参数进行计算的,fd要么是打开目录的文件描述符,要么设置为特殊值 AT_FDCWD(强制通过相对于调用进程的当前目录计算pathname)。如果pathname指定了绝对路径,那么fd参数被忽略。
utimensat的flag参数可用于进一步修改默认行为。如果设置了AT_SYMLINK_NOFOLLOW标志,则符号链接本身的时间就会被修改(如果路径名指向符号链接)。默认的行为是跟随符号链接,并把文件的时间改成符号链接的时间。
futimens 和utimensat 函数都包含在POSIX.1 中,第3 个函数utimes 包含在Single UNIX Specification的XSI扩展选项中。
#include <sys/time.h>
int utimes(const char *pathname, const struct timeval times[2]);
函数返回值:若成功,返回0;若出错,返回-1
utimes函数对路径名进行操作。times参数是指向包含两个时间戳(访问时间和修改时间)元素的数组的指针,两个时间戳是用秒和微妙表示的。
struct timeval {
time_t tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
注意,我们不能对状态更改时间st_ctim(i节点最近被修改的时间)指定一个值,因为调用utimes函数时,此字段会被自动更新。
在某些UNIX版本中,touch(1)命令使用这些函数中的某一个。另外,标准归档程序tar(1)和cpio(1)可选地调用这些函数,以便将一个文件的时间值设置为将它归档时保存的时间。
实例
图4-21的程序使用带O_TRUNC选项的open函数将文件长度截断为0,但并不更改其访问时间及修改时间。为了做到这一点,首先用stat函数得到这些时间,然后截断文件,最后再用futimens函数重置这两个时间。可以用以下Linux命令演示图4-21中的程序:
图4-21 futimens函数实例
$ ls -l changemod times 查看长度和最后修改时间
-rwxr-xr-x 1 sar 13792 Jan 22 01:26 changemod
-rwxr-xr-x 1 sar 13824 Jan 22 01:26 times
$ ls -lu changemod times 查看最后访问时间
-rwxr-xr-x 1 sar 13792 Jan 22 22:22 changemod
-rwxr-xr-x 1 sar 13824 Jan 22 22:22 times
$ date 打印当天日期Fri Jan 27 20:53:46 EST 2012
$ ./a.out changemod times 运行图4-21的程序
$ ls -l changemod times 检查结果
-rwxr-xr-x 1 sar 0 Jan 22 01:26 changemod
-rwxr-xr-x 1 sar 0 Jan 22 01:26 times
$ ls -lu changemod times 检查最后访问时间
-rwxr-xr-x 1 sar 0 Jan 22 22:22 changemod
-rwxr-xr-x 1 sar 0 Jan 22 22:22 times
$ ls -lc changemod times 检查状态更改时间
-rwxr-xr-x 1 sar 0 Jan 27 20:53 changemod
-rwxr-xr-x 1 sar 0 Jan 27 20:53 times
正如我们所预见的一样,最后修改时间和最后访问时间未变。但是,状态更改时间则更改为程序运行时的时间。
用mkdir和mkdirat函数创建目录,用rmdir函数删除目录。
#include <sys/stat.h>
int mkdir(const char *pathname, mode_t mode);
int mkdirat(int fd, const char *pathname, mode_t mode);
两个函数返回值:若成功,返回0;若出错,返回-1
这两个函数创建一个新的空目录。其中,.和..目录项是自动创建的。所指定的文件访问权限mode由进程的文件模式创建屏蔽字修改。
常见的错误是指定与文件相同的mode(只指定读、写权限)。但是,对于目录通常至少要设置一个执行权限位,以允许访问该目录中的文件名(见习题4.16)。
按照4.6节中讨论的规则来设置新目录的用户ID和组ID。
Solaris 10和Linux 3.2.0也使新目录继承父目录的设置组ID位。这就使得在新目录中创建的文件将继承该目录的组ID。对于 Linux,文件系统的实现决定是否支持此特征。例如,ext2、ext3和ext4文件系统用mount(1)命令的一个选项来控制是否支持此特征。但是,Linux的UFS文件系统实现则是不可选择的,新目录继承父目录的设置组ID位,这仿效了历史上BSD的实现。在BSD系统中,新目录的组ID是从父目录继承的。
基于BSD的系统并不要求在目录间传递设置组ID位,因为不论设置组ID位如何,新创建的文件和目录总是继承父目录的组ID。因为FreeBSD 8.0和Mac OS X 10.6.8是基于4.4BSD的,它们不要求继承设置组 ID 位。在这些平台上,新创建的文件和目录总是继承父目录的组 ID,这与是否设置了设置组ID位无关。
早期的UNIX版本并没有mkdir函数,它是由4.2BSD和SVR3引入的。在早期版本中,进程要调用mknod函数创建一个新目录,但是只有超级用户进程才能使用mknod函数。为了避免这一点,创建目录的命令mkdir(1)必须由根用户拥有,而且对它设置了设置用户ID位。要通过一个进程创建一个目录,必须用system(3)函数调用mkdir(1)命令。
mkdirat函数与mkdir函数类似。当fd参数具有特殊值AT_FDCWD或者pathname参数指定了绝对路径名时,mkdirat与mkdir完全一样。否则,fd参数是一个打开目录,相对路径名根据此打开目录进行计算。
用rmdir函数可以删除一个空目录。空目录是只包含.和..这两项的目录。
#include <unistd.h>
int rmdir(const char *pathname);
返回值:若成功,返回0;若出错,返回-1
如果调用此函数使目录的链接计数成为 0,并且也没有其他进程打开此目录,则释放由此目录占用的空间。如果在链接计数达到0时,有一个或多个进程打开此目录,则在此函数返回前删除最后一个链接及.和..项。另外,在此目录中不能再创建新文件。但是在最后一个进程关闭它之前并不释放此目录。(即使另一些进程打开该目录,它们在此目录下也不能执行其他操作。这样处理的原因是,为了使rmdir函数成功执行,该目录必须是空的。)
对某个目录具有访问权限的任一用户都可以读该目录,但是,为了防止文件系统产生混乱,只有内核才能写目录。回忆 4.5 节,一个目录的写权限位和执行权限位决定了在该目录中能否创建新文件以及删除文件,它们并不表示能否写目录本身。
目录的实际格式依赖于 UNIX 系统实现和文件系统的设计。早期的系统(如 V7)有一个比较简单的结构:每个目录项是16个字节,其中14个字节是文件名,2个字节是i节点编号。而对于4.2BSD,由于它允许更长的文件名,所以每个目录项的长度是可变的。这就意味着读目录的程序与系统相关。为了简化读目录的过程,UNIX 现在包含了一套与目录有关的例程,它们是POSIX.1的一部分。很多实现阻止应用程序使用read函数读取目录的内容,由此进一步将应用程序与目录格式中与实现相关的细节隔离。
#include <dirent.h>
DIR *opendir(const char *pathname);
DIR *fdopendir(int fd);
两个函数返回值:若成功,返回指针;若出错,返回NULL
struct dirent *readdir(DIR *dp);
返回值:若成功,返回指针;若在目录尾或出错,返回NULL
void rewinddir(DIR *dp);
int closedir(DIR *dp);
返回值:若成功,返回0;若出错,返回-1
long telldir(DIR *dp);
返回值:与dp关联的目录中的当前位置
void seekdir(DIR *dp, long loc);
fdopendir函数最早出现在SUSv4(Single UNIX Specification第4版)中,它提供了一种方法,可以把打开文件描述符转换成目录处理函数需要的DIR结构。
telldir 和 seekdir 函数不是基本 POSIX.1 标准的组成部分。它们是 Single UNIX Specification中的XSI扩展,所以可以期望所有符合UNIX系统的实现都会提供这两个函数。
回忆一下,在图1-3程序中(ls命令的基本实现部分)使用了其中几个函数。
定义在头文件<dirent.h>中的dirent结构与实现有关。实现对此结构所做的定义至少包含下列两个成员:
ino_t d_ino; /* i-node number */
char d_name[]; /* null-terminated filename */
POSIX.1并没有定义d_ino项,因为这是一个实现特征,但在POSIX.1的XSI扩展中定义了d_ino。POSIX.1在此结构中只定义了d_name项。
注意,d_name项的大小并没有指定,但必须保证它能包含至少NAME_MAX个字节(不包含终止null字节,回忆图2-15)。因为文件名是以null字节结束的,所以在头文件中如何定义数组d_name并无多大关系,数组大小并不表示文件名的长度。
DIR 结构是一个内部结构,上述 7 个函数用这个内部结构保存当前正在被读的目录的有关信息。其作用类似于FILE结构。FILE结构由标准I/O库维护,我们将在第5章中对它进行说明。
由opendir和fdopendir返回的指向DIR结构的指针由另外5个函数使用。opendir执行初始化操作,使第一个readdir返回目录中的第一个目录项。DIR结构由fdopendir创建时,readdir返回的第一项取决于传给fdopendir函数的文件描述符相关联的文件偏移量。注意,目录中各目录项的顺序与实现有关。它们通常并不按字母顺序排列。
实例
我们将使用这些对目录进行操作的例程编写一个遍历文件层次结构的程序,其目的是得到如图4-4中所示的各种类型的文件计数。图4-22的程序只有一个参数,它说明起点路径名,从该点开始递归降序遍历文件层次结构。Solaris提供了一个遍历此层次结构的函数 ftw(3),对于每一个文件它都调用一个用户定义的函数。ftw 函数的问题是:对于每一个文件,它都调用stat函数,这就使程序跟随符号链接。例如,如果从根目录(root)开始,并且有一个名为/lib的符号链接,它指向/usr/lib,则所有在目录/usr/lib中的文件都会被计数两次。为了纠正这一点,Solaris 提供了另一个函数 nftw(3),它具有一个停止跟随符号链接的选项。尽管可以使用nftw,但是为了说明目录例程的使用方法,我们还是编写了一个简单的文件遍历程序。
在SUSv4中,nftw包含在XSI选项中。FreeBSD 8.0、Linux 3.2.0、Mac OS X 10.6.8以及Solaris 10都包括了该函数的实现。(在SUSv4中,ftw函数已被标记为弃用。)基于BSD的UNIX系统则有另一个函数fts(3),它提供类似的功能。该函数在FreeBSD 8.0、Linux 3.2.0和Mac OS X 10.6.8中是可用的。
图4-22 递归降序遍历目录层次结构,并按文件类型计数
在程序中,我们提供了比所要求的更多的通用性,这样做的目的是为了具体说明ftw和nftw函数的应用。例如,函数myfunc总是返回0,即使调用它的函数准备了处理非0返回也是如此。
关于降序遍历文件系统的更多信息,以及在很多标准UNIX命令(如find、ls、tar等)中使用这种技术的情况,请参阅Fowler、Korn和Vo[1989]。
每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点(不以斜线开始的路径名为相对路径名)。当用户登录到 UNIX 系统时,其当前工作目录通常是口令文件(/etc/passwd)中该用户登录项的第6个字段—用户的起始目录(home directory)。当前工作目录是进程的一个属性,起始目录则是登录名的一个属性。
进程调用chdir或fchdir函数可以更改当前工作目录。
#include <unistd.h>
int chdir(const char *pathname);
int fchdir(int fd);
两个函数的返回值:若成功,返回0;若出错,返回-1
在这两个函数中,分别用pathname或打开文件描述符来指定新的当前工作目录。
实例
因为当前工作目录是进程的一个属性,所以它只影响调用 chdir 的进程本身,而不影响其他进程(我们将在第8章更详细地说明进程之间的关系)。这就意味着图4-23的程序并不会产生我们可能希望得到的结果。
图4-23 chdir函数实例
如果编译图4-23程序,并且调用其可执行目标代码文件mycd,则可以得到下列结果:
$ pwd
/usr/lib
$ mycd
chdir to /tmp succeeded
$ pwd
/usr/lib
从中可以看出,执行mycd命令的shell的当前工作目录并没有改变,这是shell执行程序工作方式的一个副作用。每个程序运行在独立的进程中,shell 的当前工作目录并不会随着程序调用chdir而改变。由此可见,为了改变shell进程自己的工作目录,shell应当直接调用chdir函数,为此,cd命令内建在shell中。
因为内核必须维护当前工作目录的信息,所以我们应能获取其当前值。遗憾的是,内核为每个进程只保存指向该目录 v 节点的指针等目录本身的信息,并不保存该目录的完整路径名。
Linux内核可以确定完整路径名。完整路径名的各个组成部分分布在mount表和dcache表中,然后进行重新组装,比如在读取/proc/self/cwd符号链接时。
我们需要一个函数,它从当前工作目录(.)开始,用..找到其上一级目录,然后读其目录项,直到该目录项中的i节点编号与工作目录i节点编号相同,这样地就找到了其对应的文件名。按照这种方法,逐层上移,直到遇到根,这样就得到了当前工作目录完整的绝对路径名。很幸运,函数getcwd就提供了这种功能。
#include <unistd.h>
char *getcwd(char *buf, s i z e_t size);
返回值:若成功,返回buf;若出错,返回NULL
必须向此函数传递两个参数,一个是缓冲区地址buf,另一个是缓冲区的长度size(以字节为单位)。该缓冲区必须有足够的长度以容纳绝对路径名再加上一个终止 null 字节,否则返回出错(请回忆2.5.5节中有关为最大长度路径名分配空间的讨论)。
某些getcwd的早期实现允许第一个参数buf为NULL。在这种情况下,此函数调用malloc动态地分配size字节数的空间。这不是POSIX.1或Single UNIX Specification的所属部分,应当避免使用。
实例
图 4-24的程序将工作目录更改至一个指定的目录,然后调用 getcwd,最后打印该工作目录。如果运行该程序,则可得
$ ./a.out
cwd = /var/spool/uucppublic
$ ls -l /usr/spool
lrwxrwxrwx 1 root 12 Jan 31 07:57 /usr/spool -> ../var/spool
图4-24 getcwd函数实例
注意,chdir跟随符号链接(正如我们希望的,如图4-17中所示),但是当getcwd沿目录树上溯遇到/var/spool 目录时,它并不了解该目录由符号链接/usr/spool 所指向。这是符号链接的一种特性。
当一个应用程序需要在文件系统中返回到它工作的出发点时,getcwd 函数是有用的。在更换工作目录之前,我们可以调用getcwd函数先将其保存起来。在完成了处理后,就可将所保存的原工作目录路径名作为调用参数传送给chdir,这样就返回到了文件系统中的出发点。
fchdir函数向我们提供了一种完成此任务的便捷方法。在更换到文件系统中的不同位置前,无需调用getcwd函数,而是使用open打开当前工作目录,然后保存其返回的文件描述符。当希望回到原工作目录时,只要简单地将该文件描述符传送给fchdir。
st_dev和st_rdev这两个字段经常引起混淆,在18.9节,我们编写ttyname函数时,需要使用这两个字段。有关规则很简单:
•每个文件系统所在的存储设备都由其主、次设备号表示。设备号所用的数据类型是基本系统数据类型dev_t。主设备号标识设备驱动程序,有时编码为与其通信的外设板;次设备号标识特定的子设备。回忆图4-13,一个磁盘驱动器经常包含若干个文件系统。在同一磁盘驱动器上的各文件系统通常具有相同的主设备号,但是次设备号却不同。
•我们通常可以使用两个宏:major和minor来访问主、次设备号,大多数实现都定义这两个宏。这就意味着我们无需关心这两个数是如何存放在dev_t对象中的。
早期的系统用16位整型存放设备号:8位用于主设备号,8位用于次设备号。FreeBSD 8.0和Mac OS X 10.6.8使用32位整型,其中8位表示主设备号,24位表示次设备号。在32位系统中,Solaris 10用32位整型表示dev_t,其中14位用于主设备号,18位用于次设备号。在64位系统中,Solaris 10用64位整型表示dev_t,主设备号和次设备号各用其中的32位表示。在Linux 3.2.0上,虽然dev_t是64位整型,但其中只有12位用于主设备号,20位用于次设备号。
POSIX.1说明dev_t类型是存在的,但没有定义它包含什么,或如何取得其内容。大多数实现定义了宏major和minor,但在哪一个头文件中定义它们则与实现有关。基于BSD的UNIX系统将它们定义在<sys/types>中。Solaris 在<sys/mkdev.h>中定义了它们的函数原型,因为在<sys/sysmacros.h>中的宏定义都弃用了。Linux 将它们定义在<sys/sysmacros.h>中,而该头文件又包含在<sys/type.h>中。
•系统中与每个文件名关联的 st_dev 值是文件系统的设备号,该文件系统包含了这一文件名以及与其对应的i节点。
•只有字符特殊文件和块特殊文件才有st_rdev值。此值包含实际设备的设备号。
实例
图4-25的程序为每个命令行参数打印设备号,另外,若此参数引用的是字符特殊文件或块特殊文件,则还打印该特殊文件的st_rdev值。
图4-25 打印st_dev和st_rdev值
在Linux上运行此程序得到下面的输出:
$ ./a.out / /home/sar /dev/tty[01]
/: dev = 8/3
/home/sar: dev = 8/4
/dev/tty0: dev = 0/5 (character) rdev = 4/0
/dev/tty1: dev = 0/5 (character) rdev = 4/1
$ mount 哪些目录安装在哪些设备上?
/dev/sda3 on / type ext3 (rw,errors=remount-ro,commit=0)
/dev/sda4 on /home type ext2 (rw,commit=0)
$ ls -l /dev/tty[01] /dev/sda[34]
brw-rw---- 1 root 8, 3 2011-07-01 11:08 /dev/sda3
brw-rw---- 1 root 8, 4 2011-07-01 11:08 /dev/sda4
crw--w---- 1 root 4, 0 2011-07-01 11:08 /dev/tty0
crw------- 1 root 4, 1 2011-07-01 11:08 /dev/tty1
传给该程序的前两个参数是目录(/和/home/sar),后两个参数是设备名/dev/tty[01]。(我们用 shell 正则表达式语言以缩短所需的输入量。shell 将字符串/dev/tty[01]扩展为/dev/tty0 /dev/tty1。)
我们期望设备是字符特殊文件。从程序的输出可见,根目录和/home/sar 目录的设备号不同,这表示它们位于不同的文件系统中。运行mount(1)命令可以证明了这一点。
然后用ls命令查看由mount命令报告的两个磁盘设备和两个终端设备。这两个磁盘设备是块特殊文件,而两个终端设备是字符特殊文件。(通常,只有那些包含随机访问文件系统的设备类型是块特殊文件设备,如硬盘驱动器、软盘驱动器和CD-ROM等。UNIX的早期版本支持磁带存放文件系统,但这从未广泛使用过。)
注意,两个终端设备(st_dev)的文件名和i节点在设备0/5上(devtmpfs伪文件系统,它实现了/dev文件系统),但是它们的实际设备号是4/0和4/1。
我们已经说明了所有文件访问权限位,其中某些位有多种用途。图4-26列出了所有这些权限位,以及它们对普通文件和目录文件的作用。
最后9个常量还可以分成如下3组:
S_IRWXU = S_IRUSR|S_IWUSR|S_IXUSR
S_IRWXG = S_IRGRP|S_IWGRP|S_IXGRP
S_IRWXO = S_IROTH|S_IWOTH|S_IXOTH
图4-26 文件访问权限位小结
本章内容围绕stat函数,详细介绍了stat结构中的每一个成员。这使我们对UNIX文件和目录的各个属性都有所了解。我们讨论了文件和目录在文件系统中是如何设计的以及如何使用文件系统命名空间。对文件和目录的所有属性以及对文件和目录进行操作的所有函数的全面了解,对于UNIX编程是非常重要的。
4.1 用stat函数替换图4-3程序中的lstat函数,如若命令行参数之一是符号链接,会发生什么变化?
4.2 如果文件模式创建屏蔽字是777(八进制),结果会怎样?用shell的umask命令验证该结果。
4.3 关闭一个你所拥有文件的用户读权限,将导致拒绝你访问自己的文件,对此进行验证。
4.4 创建文件foo和bar后,运行图4-9的程序,将发生什么情况?
4.5 4.12节中讲到一个普通文件的大小可以为0,同时我们又知道st_size字段是为目录或符号链接定义的,那么目录和符号链接的长度是否可以为0?
4.6 编写一个类似cp(1)的程序,它复制包含空洞的文件,但不将字节0写到输出文件中去。
4.7 在4.12节ls命令的输出中,core和core.copy的访问权限不同,如果创建两个文件时umask没有变,说明为什么会发生这种差别。
4.8 在运行图 4-16 的程序时,使用了 df(1)命令来检查空闲的磁盘空间。为什么不使用 du(1)命令?
4.9 图4-20中显示unlink函数会修改文件状态更改时间,这是怎样发生的?
4.10 4.22节中,系统对可打开文件数的限制对myftw函数会产生什么影响?
4.11 在4.22节中的myftw从不改变其目录,对这种处理方法进行改动:每次遇到一个目录就用其调用chdir,这样每次调用lstat时就可以使用文件名而非路径名,处理完所有的目录项后执行chdir("..")。比较这种版本的程序和书中程序的运行时间。
4.12 每个进程都有一个根目录用于解析绝对路径名,可以通过 chroot 函数改变根目录。在手册中查阅此函数。说明这个函数什么时候有用。
4.13 如何只设置两个时间值中的一个来使用utimes函数?
4.14 有些版本的finger(1)命令输出“New mail received ...”和“unread since ...”,其中...表示相应的日期和时间。程序是如何决定这些日期和时间的?
4.15 用cpio(1)和tar(1)命令检查档案文件的格式(请参阅《UNIX程序员手册》第5部分中的说明)。3 个可能的时间值中哪几个是为每一个文件保存的?你认为文件复原时,文件的访问时间是什么?为什么?
4.16 UNIX系统对目录树的深度有限制吗?编写一个程序循环,在每次循环中,创建目录,并将该目录更改为工作目录。确保叶节点的绝对路径名的长度大于系统的 PATH_MAX 限制。可以调用getcwd得到目录的路径名吗?标准UNIX系统工具是如何处理长路径名的?对目录可以使用tar或cpio命令归档吗?
4.17 3.16 节中描述了/dev/fd 特征。如果每个用户都可以访问这些文件,则其访问权限必须为rw-rw-rw-。有些程序创建输出文件时,先删除该文件以确保该文件名不存在,忽略返回码。
unlink (path);
if ( (fd = creat(path, FILE_MODE)) < 0)
err_sys(...);
如果path是/dev/fd/1,会出现什么情况?
本章讲述标准I/O库。不仅是UNIX,很多其他操作系统都实现了标准I/O库,所以这个库由ISO C标准说明。Single UNIX Specification对ISO C标准进行了扩充,定义了另外一些接口。
标准I/O库处理很多细节,如缓冲区分配、以优化的块长度执行I/O等。这些处理使用户不必担心如何选择使用正确的块长度(如3.9节中所述)。这使得它便于用户使用,但是如果我们不深入地了解I/O库函数的操作,也会带来一些问题。
标准I/O库是由Dennis Ritchie在1975年左右编写的。它是Mike Lesk编写的可移植I/O库的主要修改版本。令人惊讶的是,35年来,几乎没有对标准I/O库进行修改。
在第3章中,所有I/O函数都是围绕文件描述符的。当打开一个文件时,即返回一个文件描述符,然后该文件描述符就用于后续的I/O操作。而对于标准I/O库,它们的操作是围绕流(stream)进行的(请勿将标准I/O术语流与System V的STREAMS I/O系统相混淆,STREAMS I/O系统是System V的组成部分,Single UNIX Specification则将其标准化为XSI STREAMS选项,但是在SUSv4中已经将其标记为弃用)。当用标准I/O库打开或创建一个文件时,我们已使一个流与一个文件相关联。
对于ASCII字符集,一个字符用一个字节表示。对于国际字符集,一个字符可用多个字节表示。标准I/O文件流可用于单字节或多字节(“宽”)字符集。流的定向(stream's orientation)决定了所读、写的字符是单字节还是多字节的。当一个流最初被创建时,它并没有定向。如若在未定向的流上使用一个多字节 I/O 函数(见<wchar.h>),则将该流的定向设置为宽定向的。若在未定向的流上使用一个单字节I/O函数,则将该流的定向设为字节定向的。只有两个函数可改变流的定向。freopen函数(稍后讨论)清除一个流的定向;fwide函数可用于设置流的定向。
#include <stdio.h>
#include <wchar.h>
int fwide(FILE *fp, int mode);
返回值:若流是宽定向的,返回正值;若流是字节定向的,返回负值;若流是未定向的,返回0
根据mode参数的不同值,fwide函数执行不同的工作。
•如若mode参数值为负,fwide将试图使指定的流是字节定向的。
•如若mode参数值为正,fwide将试图使指定的流是宽定向的。
•如若mode参数值为0,fwide将不试图设置流的定向,但返回标识该流定向的值。
注意,fwide 并不改变已定向流的定向。还应注意的是,fwide 无出错返回。试想,如若流是无效的,那么将发生什么呢?我们唯一可依靠的是,在调用 fwide 前先清除 errno,从fwide返回时检查errno的值。在本书的其余部分,我们只涉及字节定向流。
当打开一个流时,标准I/O函数fopen(参考5.5节)返回一个指向FILE对象的指针。该对象通常是一个结构,它包含了标准I/O库为管理该流需要的所有信息,包括用于实际I/O的文件描述符、指向用于该流缓冲区的指针、缓冲区的长度、当前在缓冲区中的字符数以及出错标志等。
应用程序没有必要检验FILE对象。为了引用一个流,需将FILE指针作为参数传递给每个标准I/O函数。在本书中,我们称指向FILE对象的指针(类型为FILE*)为文件指针。
在本章中,我们在UNIX系统环境中说明标准I/O库。正如前述,此标准库已移植到UNIX之外的很多系统中。但是为了说明该库实现的一些细节,我们将讨论其在UNIX系统上的典型实现。
对一个进程预定义了 3 个流,并且这 3 个流可以自动地被进程使用,它们是:标准输入、标准输出和标准错误。这些流引用的文件与在 3.2 节中提到文件描述符 STDIN_FILENO、STDOUT_FILENO 和STDERR_FILENO所引用的相同。
这3个标准I/O流通过预定义文件指针stdin、stdout和stderr加以引用。这3个文件指针定义在头文件<stdio.h>中。
标准I/O库提供缓冲的目的是尽可能减少使用read和write调用的次数(见图3-6,其中显示了在不同缓冲区长度情况下,执行I/O所需的CPU时间量)。它也对每个I/O流自动地进行缓冲管理,从而避免了应用程序需要考虑这一点所带来的麻烦。遗憾的是,标准I/O库最令人迷惑的也是它的缓冲。
标准I/O提供了以下3种类型的缓冲。
(1)全缓冲。在这种情况下,在填满标准I/O缓冲区后才进行实际I/O操作。对于驻留在磁盘上的文件通常是由标准I/O库实施全缓冲的。在一个流上执行第一次I/O操作时,相关标准I/O函数通常调用malloc(见7.8节)获得需使用的缓冲区。
术语冲洗(flush)说明标准I/O缓冲区的写操作。缓冲区可由标准I/O例程自动地冲洗(例如,当填满一个缓冲区时),或者可以调用函数 fflush 冲洗一个流。值得注意的是,在 UNIX环境中,flush有两种意思。在标准I/O库方面,flush(冲洗)意味着将缓冲区中的内容写到磁盘上(该缓冲区可能只是部分填满的)。在终端驱动程序方面(例如,在第18章中所述的tcflush函数),flush(刷清)表示丢弃已存储在缓冲区中的数据。
(2)行缓冲。在这种情况下,当在输入和输出中遇到换行符时,标准I/O库执行I/O操作。这允许我们一次输出一个字符(用标准I/O函数fputc),但只有在写了一行之后才进行实际I/O操作。当流涉及一个终端时(如标准输入和标准输出),通常使用行缓冲。
对于行缓冲有两个限制。第一,因为标准I/O库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使还没有写一个换行符,也进行I/O操作。第二,任何时候只要通过标准I/O 库要求从(a)一个不带缓冲的流,或者(b)一个行缓冲的流(它从内核请求需要数据)得到输入数据,那么就会冲洗所有行缓冲输出流。在(b)中带了一个在括号中的说明,其理由是,所需的数据可能已在该缓冲区中,它并不要求一定从内核读数据。很明显,从一个不带缓冲的流中输入(即(a)项)需要从内核获得数据。
(3)不带缓冲。标准I/O库不对字符进行缓冲存储。例如,若用标准I/O函数fputs写15个字符到不带缓冲的流中,我们就期望这15个字符能立即输出,很可能使用3.8节的write函数将这些字符写到相关联的打开文件中。
标准错误流stderr通常是不带缓冲的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个换行符。
ISO C要求下列缓冲特征。
•当且仅当标准输入和标准输出并不指向交互式设备时,它们才是全缓冲的。
•标准错误决不会是全缓冲的。
但是,这并没有告诉我们如果标准输入和标准输出指向交互式设备时,它们是不带缓冲的还是行缓冲的;以及标准错误是不带缓冲的还是行缓冲的。很多系统默认使用下列类型的缓冲:
•标准错误是不带缓冲的。
•若是指向终端设备的流,则是行缓冲的;否则是全缓冲的。
本书讨论的4种平台都遵从标准I/O缓冲的这些惯例,标准错误是不带缓冲的,打开至终端设备的流是行缓冲的,其他流是全缓冲的。
我们将在5.12节和图5-1对标准I/O缓冲做更详细的说明。
对任何一个给定的流,如果我们并不喜欢这些系统默认,则可调用下列两个函数中的一个更改缓冲类型。
#include <stdio.h>
void setbuf(FILE *restrict fp, char *restrict buf);
int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);
返回值:若成功,返回0;若出错,返回非0
这些函数一定要在流已被打开后调用(这是十分明显的,因为每个函数都要求一个有效的文件指针作为它们的第一个参数),而且也应在对该流执行任何一个其他操作之前调用。
可以使用setbuf 函数打开或关闭缓冲机制。为了带缓冲进行 I/O,参数buf必须指向一个长度为BUFSIZ的缓冲区(该常量定义在<stdio.h>中)。通常在此之后该流就是全缓冲的,但是如果该流与一个终端设备相关,那么某些系统也可将其设置为行缓冲的。为了关闭缓冲,将buf设置为NULL。
使用setvbuf,我们可以精确地说明所需的缓冲类型。这是用mode参数实现的:
_IOFBF 全缓冲
_IOLBF 行缓冲
_IONBF 不带缓冲
如果指定一个不带缓冲的流,则忽略buf和size参数。如果指定全缓冲或行缓冲,则buf和size可选择地指定一个缓冲区及其长度。如果该流是带缓冲的,而buf是NULL,则标准I/O库将自动地为该流分配适当长度的缓冲区。适当长度指的是由常量BUFSIZ所指定的值。
某些C函数库实现使用stat结构中的成员st_blksize所指定的值(见4.2节)决定最佳I/O缓冲区长度。在本章的后续内容中可以看到,GNU C函数库就使用这种方法。
图5-1列出了这两个函数的动作,以及它们的各个选项。
图5-1 setbuf和setvbuf函数
要了解,如果在一个函数内分配一个自动变量类的标准I/O缓冲区,则从该函数返回之前,必须关闭该流(7.8节将对此做更多讨论)。另外,其些实现将缓冲区的一部分用于存放它自己的管理操作信息,所以可以存放在缓冲区中的实际数据字节数少于 size。一般而言,应由系统选择缓冲区的长度,并自动分配缓冲区。在这种情况下关闭此流时,标准I/O库将自动释放缓冲区。
任何时候,我们都可强制冲洗一个流。
#include<stdio.h>
int fflush(FILE *fp);
返回值:若成功,返回0;若出错,返回EOF
此函数使该流所有未写的数据都被传送至内核。作为一种特殊情形,如若fp是NULL,则此函数将导致所有输出流被冲洗。
下列3个函数打开一个标准I/O流。
#include <stdio.h>
FILE *fopen(const char *restrict pathname, const char *restrict type);
FILE *freopen(const char *restrict pathname, const char *restrict type, FILE *restrict fp);
FILE *fdopen(int fd, const char *type);
3个函数的返回值:若成功,返回文件指针;若出错,返回NULL
这3个函数的区别如下。
(1)fopen函数打开路径名为pathname的一个指定的文件。
(2)freopen 函数在一个指定的流上打开一个指定的文件,如若该流已经打开,则先关闭该流。若该流已经定向,则使用 freopen 清除该定向。此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出或标准错误。
(3)fdopen函数取一个已有的文件描述符(我们可能从open、dup、dup2、fcntl、pipe、socket、socketpair或accept函数得到此文件描述符),并使一个标准的I/O流与该描述符相结合。此函数常用于由创建管道和网络通信通道函数返回的描述符。因为这些特殊类型的文件不能用标准I/O函数fopen打开,所以我们必须先调用设备专用函数以获得一个文件描述符,然后用fdopen使一个标准I/O流与该描述符相结合。
fopen和freopen是ISO C的所属部分。而ISO C并不涉及文件描述符,所以仅有POSIX.1具有fdopen。
type参数指定对该I/O流的读、写方式,ISO C规定type参数可以有15种不同的值,如图5-2所示。
图5-2 打开标准I/O流的type参数
使用字符b作为type的一部分,这使得标准I/O系统可以区分文本文件和二进制文件。因为UNIX内核并不对这两种文件进行区分,所以在UNIX系统环境下指定字符b作为type的一部分实际上并无作用。
对于fdopen,type参数的意义稍有区别。因为该描述符已被打开,所以fdopen为写而打开并不截断该文件。(例如,若该描述符原来是由open函数创建的,而且该文件已经存在,则其O_TRUNC标志将决定是否截断该文件。fdopen函数不能截断它为写而打开的任一文件。)另外,标准I/O追加写方式也不能用于创建该文件(因为如果一个描述符引用一个文件,则该文件一定已经存在)。
当用追加写类型打开一个文件后,每次写都将数据写到文件的当前尾端处。如果有多个进程用标准I/O追加写方式打开同一文件,那么来自每个进程的数据都将正确地写到文件中。
4.4BSD 以前的伯克利版本以及 Kernighan 和 Ritchie[1988]第 177 页上所示的简单版本的 fopen 函数并不能正确地处理追加写方式。这些版本在打开流时,调用lseek定位到文件尾端。在涉及多个进程时,为了正确地支持追加写方式,该文件必须用O_APPEND标志打开,我们已在3.3节中对此进行了讨论。在每次写前,做一次lseek操作同样也不能正确工作(如同在3.11节中讨论的一样)。
当以读和写类型打开一个文件时(type中+号),具有下列限制。
•如果中间没有fflush、fseek、fsetpos或rewind,则在输出的后面不能直接跟随输入。
•如果中间没有fseek、fsetpos或rewind,或者一个输入操作没有到达文件尾端,则在输入操作之后不能直接跟随输出。
对应于图5-2,图5-3中列出了打开一个流的6种不同的方式。
图5-3 打开一个标准I/O流的6种不同方式
注意,在指定w或a类型创建一个新文件时,我们无法说明该文件的访问权限位(第3章中所述的open函数和creat函数则能做到这一点)。POSIX.1要求实现使用如下的权限位集来创建文件:
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH
回忆4.8节,我们可以通过调整umask值来限制这些权限。
除非流引用终端设备,否则按系统默认,流被打开时是全缓冲的。若流引用终端设备,则该流是行缓冲的。一旦打开了流,那么在对该流执行任何操作之前,如果希望,则可使用前节所述的setbuf和setvbuf改变缓冲的类型。
调用fclose关闭一个打开的流。
#include <stdio.h>
int fclose(FILE *fp);
返回值:若成功,返回0;若出错,返回EOF
在该文件被关闭之前,冲洗缓冲中的输出数据。缓冲区中的任何输入数据被丢弃。如果标准I/O库已经为该流自动分配了一个缓冲区,则释放此缓冲区。
当一个进程正常终止时(直接调用exit函数,或从main函数返回),则所有带未写缓冲数据的标准I/O流都被冲洗,所有打开的标准I/O流都被关闭。
一旦打开了流,则可在3种不同类型的非格式化I/O中进行选择,对其进行读、写操作。
(1)每次一个字符的I/O。一次读或写一个字符,如果流是带缓冲的,则标准I/O函数处理所有缓冲。
(2)每次一行的I/O。如果想要一次读或写一行,则使用fgets和fputs。每行都以一个换行符终止。当调用fgets时,应说明能处理的最大行长。5.7节将说明这两个函数。
(3)直接 I/O。fread和fwrite函数支持这种类型的I/O。每次 I/O操作读或写某种数量的对象,而每个对象具有指定的长度。这两个函数常用于从二进制文件中每次读或写一个结构。5.9节将说明这两个函数。
直接I/O(direct I/O)这个术语来自ISO C标准,有时也被称为:二进制I/O、一次一个对象I/O、面向记录的I/O或面向结构的I/O。不要把这个特性和FreeBSD和Linux支持的open函数的O_DIRECT标志混淆,它们之间是没有关系的。
(5.11节说明了格式化I/O函数,如printf和scanf。)
1.输入函数
以下3个函数可用于一次读一个字符。
#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
3个函数的返回值:若成功,返回下一个字符;若已到达文件尾端或出错,返回EOF
函数getchar等同于getc(stdin)。前两个函数的区别是,getc可被实现为宏,而fgetc不能实现为宏。这意味着以下几点。
(1)getc的参数不应当是具有副作用的表达式,因为它可能会被计算多次。
(2)因为fgetc一定是个函数,所以可以得到其地址。这就允许将fgetc的地址作为一个参数传送给另一个函数。
(3)调用fgetc所需时间很可能比调用getc要长,因为调用函数所需的时间通常长于调用宏。
这3个函数在返回下一个字符时,将其unsigned char类型转换为int类型。说明为无符号的理由是,如果最高位为1也不会使返回值为负。要求整型返回值的理由是,这样就可以返回所有可能的字符值再加上一个已出错或已到达文件尾端的指示值。在<stdio.h>中的常量EOF被要求是一个负值,其值经常是−1。这就意味着不能将这3个函数的返回值存放在一个字符变量中,以后还要将这些函数的返回值与常量EOF比较。
注意,不管是出错还是到达文件尾端,这3个函数都返回同样的值。为了区分这两种不同的情况,必须调用ferror或feof。
#include <stdio.h>
int ferror(FILE *fp);
int feof(FILE *fp);
两个函数返回值:若条件为真,返回非0(真);否则,返回0(假)
void clearerr(FILE *fp);
在大多数实现中,为每个流在FILE对象中维护了两个标志:
•出错标志;
•文件结束标志。
调用clearerr可以清除这两个标志。
从流中读取数据以后,可以调用ungetc将字符再压送回流中。
#include <stdio.h>
int ungetc(int c, FILE *fp);
返回值:若成功,返回c;若出错,返回EOF
压送回到流中的字符以后又可从流中读出,但读出字符的顺序与压送回的顺序相反。应当了解,虽然ISO C允许实现支持任何次数的回送,但是它要求实现提供一次只回送一个字符。我们不能期望一次能回送多个字符。
回送的字符,不一定必须是上一次读到的字符。不能回送EOF。但是当已经到达文件尾端时,仍可以回送一个字符。下次读将返回该字符,再读则返回EOF。之所以能这样做的原因是,一次成功的ungetc调用会清除该流的文件结束标志。
当正在读一个输入流,并进行某种形式的切词或记号切分操作时,会经常用到回送字符操作。有时需要先看一看下一个字符,以决定如何处理当前字符。然后就需要方便地将刚查看的字符回送,以便下一次调用getc时返回该字符。如果标准I/O库不提供回送能力,就需将该字符存放到一个我们自己的变量中,并设置一个标志以便判别在下一次需要一个字符时是调用 getc,还是从我们自己的变量中取用这个字符。
用ungetc压送回字符时,并没有将它们写到底层文件中或设备上,只是将它们写回标准I/O库的流缓冲区中。
2.输出函数
对应于上面所述的每个输入函数都有一个输出函数。
#include <stdio.h>
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);
3个函数返回值:若成功,返回c;若出错,返回EOF
与输入函数一样,putchar(c)等同于putc(c, stdout),putc可被实现为宏,而fputc不能实现为宏。
下面两个函数提供每次输入一行的功能。
#include <stdio.h>
char *fgets(char *restrict buf, int n,FILE *restrict fp);
char *gets(char *buf);
两个函数返回值:若成功,返回buf;若已到达文件尾端或出错,返回NULL
这两个函数都指定了缓冲区的地址,读入的行将送入其中。gets从标准输入读,而fgets则从指定的流读。
对于fgets,必须指定缓冲的长度n。此函数一直读到下一个换行符为止,但是不超过n − 1个字符,读入的字符被送入缓冲区。该缓冲区以null字节结尾。如若该行包括最后一个换行符的字符数超过n − 1,则fgets只返回一个不完整的行,但是,缓冲区总是以null字节结尾。对fgets的下一次调用会继续读该行。
gets 是一个不推荐使用的函数。其问题是调用者在使用 gets 时不能指定缓冲区的长度。这样就可能造成缓冲区溢出(如若该行长于缓冲区长度),写到缓冲区之后的存储空间中,从而产生不可预料的后果。这种缺陷曾被利用,造成1988年的因特网蠕虫事件。有关说明请见1989年6月的Communications of the ACM(vol.32,no.6)。gets与fgets的另一个区别是,gets并不将换行符存入缓冲区中。
这两个函数对换行符处理方式的差别与UNIX的进展有关。在V7的手册(1979)中就说明:“为了向后兼容,gets删除换行符,而fgets则保留换行符。”
虽然ISO C要求提供gets,但请使用fgets,而不要使用gets。事实上,在SUSv4中, gets被标记为弃用的接口,而且在ISO C标准的最新版本(ISO/IEC 9899:2011)中已被忽略。
fputs和puts提供每次输出一行的功能。
#include <stdio.h>
int fputs(const char *restrict str, FILE *restrict fp);
int puts(const char *str);
两个函数返回值:若成功,返回非负值;若出错,返回EOF
函数fputs将一个以null字节终止的字符串写到指定的流,尾端的终止符null不写出。注意,这并不一定是每次输出一行,因为字符串不需要换行符作为最后一个非null字节。通常,在null字节之前是一个换行符,但并不要求总是如此。
puts将一个以null字节终止的字符串写到标准输出,终止符不写出。但是,puts随后又将一个换行符写到标准输出。
puts 并不像它所对应的 gets 那样不安全。但是我们还是应避免使用它,以免需要记住它在最后是否添加了一个换行符。如果总是使用 fgets 和 fputs, 那么就会熟知在每行终止处我们必须自己处理换行符。
使用前面所述的函数,我们能对标准I/O系统的效率有所了解。图5-4程序类似于图3-4程序,它使用getc和putc将标准输入复制到标准输出。这两个例程可以实现为宏。
图5-4 用getc和putc将标准输入复制到标准输出
可以用fgetc和fputc改写该程序,这两个一定是函数,而不是宏(我们没有给出对源代码更改的细节)。
最后,我们还编写了一个读、写行的版本,见图5-5。
图5-5 用fgets和fputs将标准输入复制到标准输出
注意,在图5-4程序和图5-5程序中,没有显式地关闭标准I/O流。我们知道exit函数将会冲洗任何未写的数据,然后关闭所有打开的流(我们将在8.5节讨论这一点)。将这3个程序的时间与图3-6中的时间进行比较是很有趣的。图5-6中显示了对同一文件(98.5 MB,300万行)进行操作所得的数据。
图5-6 使用标准I/O例程得到的时间结果
对于这3个标准I/O版本的每一个,其用户CPU时间都大于图3-6中的最佳read版本,因为在每次读一个字符的标准I/O版本中有一个要执行1亿次的循环,而在每次读一行的版本中有一个要执行3 144 984次的循环。在read版本中,其循环只需执行25 224次(对于缓冲区长度为4 096字节)。因为系统CPU时间几乎相同,所以用户CPU时间的差别以及等待I/O结束所消耗时间的差别造成了时钟时间的差别。
系统CPU时间几乎相同,原因是因为所有这些程序对内核提出的读、写请求数基本相同。注意,使用标准I/O例程的一个优点是无需考虑缓冲及最佳I/O长度的选择。在使用fgets时需要考虑最大行长,但是与选择最佳I/O长度比较,这要方便得多。
图5-6的最后一列是每个main函数的文本空间字节数(由C编译器产生的机器指令)。从中可见,使用getc和putc的版本与使用fgetc和fputc的版本在文本空间长度方面大体相同。通常,getc和putc实现为宏,但在GNU C库实现中,宏简单地扩充为函数调用。
使用每次一行I/O版本的速度大约是每次一个字符版本速度的两倍。如果fgets和fputs函数是用getc和putc实现的(参见Kernighan和Ritchie[1988]的7.7节),那么,可以预期fgets版本的时间会与getc 版本接近。实际上,每次一行的版本会更慢一些,因为除了现已存在的 6百万次函数调用外还需另外增加 2 亿次函数调用。而在本测试中所用的每次一行函数是用memccpy(3)实现的。通常,为了提高效率,memccpy函数用汇编语言而非C语言编写。正因为如此,每次一行版本才会有较高的速度。
这些时间数字的最后一个有趣之处在于:fgetc版本较图3-6中BUFFSIZE=1的版本要快得多。两者都使用了约2亿次的函数调用,在用户CPU时间方面,fgetc版本的速度大约是后者的16倍,而在时钟时间方面几乎是39倍。造成这种差别的原因是:使用read的版本执行了2亿次函数调用,这也就引起2亿次系统调用。而对于fgetc版本,它也执行2亿次函数调用,但是这只引起25 224次系统调用。系统调用与普通的函数调用相比需要花费更多的时间。
需要声明的是,这些时间结果只在某些系统上才有效。这种时间结果依赖于很多实现的特征,而这种特征对于不同的UNIX系统可能是不同的。尽管如此,有这样一组数据,并对各种版本的差别做出解释,这有助于我们更好地了解系统。在本节及 3.9 节中我们了解到的基本事实是,标准I/O库与直接调用read和write函数相比并不慢很多。对于大多数比较复杂的应用程序,最主要的用户CPU时间是由应用本身的各种处理消耗的,而不是由标准I/O例程消耗的。