5.1 原子操作和竞争条件
所有系统调用都是以原子操作方式执行的。之所以这么说,是指内核保证了某系统调用中的所有步骤会作为独立操作而一次性加以执行,其间不会为其他进程或线程所中断。
原子性是某些操作得以圆满成功的关键所在。特别是它规避了竞争状态(race conditions)(有时也称为竞争冒险)。竞争状态是这样一种情形:操作共享资源的两个进程(或线程),其结果取决于一个无法预期的顺序,即这些进程或线程获得 CPU 使用权的先后相对顺序。
接下来两个小节,将讨论涉及文件 I/O 的两种竞争状态,并展示了如何使用 open()的标志位,来保证相关文件操作的原子性,从而消除这些竞争状态。
5.1.1 以独占方式创建一个文件
首先引入一个程序示例:
fd = open(argv[1], O_WRONLY); /* Open 1: check if file exists */
if (fd != -1)
{ /* Open succeeded */
printf("[PID %ld] File \"%s\" already exists\n",
(long) getpid(), argv[1]);
close(fd);
} else
{
if (errno != ENOENT)
errExit("open"); /* Failed for unexpected reason */
else
{
fd = open(argv[1], O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1)
errExit("open");
printf("[PID %ld] Created file \"%s\" exclusively\n",
(long) getpid(), argv[1]); /* MAY NOT BE TRUE! */
}
}
对于上述程序,假设如下情况:当第一次调用 open()时,希望打开的文件还不存在,而当第二次调用 open()时,其他进程已经创建了该文件。如下图所示,若内核调度器判断出分配给 A 进程的时间片已经耗尽,并将 CPU 使用权交给 B 进程,就可能会发生这种问题。再比如两个进程在一个多CPU 系统上同时运行时,也会出现这种情况。下图展示了两个进程同时执行程序的情形。在这一场景下,进程 A 将得出错误的结论:目标文件是由自己创建的。因为无论目标文件存在与否,进程 A 对 open()的第二次调用都会成功。
虽然进程将自己误认为文件创建者的可能性相对较小,但毕竟是存在的,这已然将此段代码置于不可靠的境地。操作的结果将依赖于对两个进程的调度顺序,这一事实也就意味着出现了竞争状态。
为了直观地说明这段代码的确存在问题,将上述程序稍加修改,在检查文件是否存在与创建文件这两个动作之间人为制造一个长时间的等待。
/***************************bad_exclusive_open.c***************************/
#include <sys/stat.h>
#include <fcntl.h>
#include "tlpi_hdr.h"
int
main(int argc, char *argv[])
{
int fd;
if (argc < 2 || strcmp(argv[1], "--help") == 0)
usageErr("%s file\n", argv[0]);
fd = open(argv[1], O_WRONLY); /* Open 1: check if file exists */
if (fd != -1) { /* Open succeeded */
printf("[PID %ld] File \"%s\" already exists\n",
(long) getpid(), argv[1]);
close(fd);
} else {
if (errno != ENOENT) { /* Failed for unexpected reason */
errExit("open");
} else {
printf("[PID %ld] File \"%s\" doesn't exist yet\n",
(long) getpid(), argv[1]);
if (argc > 2) { /* Delay between check and create */
sleep(5); /* Suspend execution for 5 seconds */
printf("[PID %ld] Done sleeping\n", (long) getpid());
}
fd = open(argv[1], O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1)
errExit("open");
printf("[PID %ld] Created file \"%s\" exclusively\n",
(long) getpid(), argv[1]); /* MAY NOT BE TRUE! */
}
}
exit(EXIT_SUCCESS);
}
同时运行两个实例,由于第一个进程在检查文件是否存在和创建文件之间发生了中断,造成两个进程都声称自己是文件的创建者。
vainx@DESKTOP-0DN0PNJ:~/wsl-code/tlpi-book/fileio$ ./bad_exclusive_open mfile sleep &
[1] 27900
[PID 27900] File "mfile" doesn't exist yet
vainx@DESKTOP-0DN0PNJ:~/wsl-code/tlpi-book/fileio$ ./bad_exclusive_open mfile
[PID 27934] File "mfile" doesn't exist yet
[PID 27934] Created file "mfile" exclusively
vainx@DESKTOP-0DN0PNJ:~/wsl-code/tlpi-book/fileio$ [PID 27900] Done sleeping
[PID 27900] Created file "mfile" exclusively # 显然是不对的
当同时指定 O_EXCL 与 O_CREAT 作为 open()的标志位时,如果要打开的文件已然存在,则 open()将返回一个错误。这提供了一种机制,保证进程是打开文件的创建者。结合O_CREAT 和O_EXCL 标志来一次性地调用open()可以防止这种情况,因为这确保了检查文件和创建文件的步骤属于一个单一的原子(即不可中断的)操作。
5.1.2 向文件尾部追加数据
用以说明原子操作必要性的第二个例子是:多个进程同时向同一个文件(例如,全局日志文件)尾部添加数据。为了达到这一目的,也许可以考虑在每个写进程中使用如下代码。
if(lseek(fd, O, SEEK_END) == -1)
errExit("lseek");
if(write(fd, buf, len) != len)
fatal("Partial/failed write");
但是,这段代码存在的缺陷与前一个例子如出一辙。如果第一个进程执行到 lseek()和 write()之间,被执行相同代码的第二个进程所中断,那么这两个进程会在写入数据前,将文件偏移量设为相同位置,而当第一个进程再次获得调度时,会覆盖第二个进程已写入的数据。此时再次出现了竞争状态,因为执行的结果依赖于内核对两个进程的调度顺序。
要规避这一问题,需要将文件偏移量的移动与数据写操作纳入同一原子操作。在打开文件时加入 O_APPEND 标志就可以保证这一点。
5.2 文件控制操作:fcntl()
int fcntl(int fd, int cmd, ...);
系统调用对一个打开的文件描述符执行一系列控制操作。
cmd 参数所支持的操作范围很广。fcntl()的第三个参数以省略号来表示,这意味着可以将其设置为不同的类型,或者加以省略。内核会依据 cmd 参数(如果有的话)的值来确定该参数的数据类型。
5.2.1 打开文件的状态标志
fcntl()的用途之一是针对一个打开的文件,获取或修改其访问模式和状态标志(这些值是通过指定 open()调用的 flag 参数来设置的)。要获取这些设置,应将 fcntl()的 cmd 参数设置为F_GETFL。
int flags, accessMode;
flags = fcntl(fd, F_GETFL);
if(flags == -1)
errExit("fcntl");
/* 之后就可以测试文件是以何种方式打开的 */
if(flags & O_SYNC)
printf("writes are sunchronized\n");
判定文件的访问模式有一点复杂,这是因为O_RDONLY(0)、O_WRONLY(1)和O_RDWR(2)这 3 个常量并不与打开文件状态标志中的单个比特位对应。因此,要判定访问模式,需使用掩码 O_ACCMODE 与 flag 相与,将结果与 3 个常量进行比对,示例代码如下:
accessMode = flags & )ACCMODE;
if(accessMode == O_WRONLY || accessMode == O_RDWR)
printf("file is writable\n");
可以使用 fcntl()的 F_SETFL 命令来修改打开文件的某些状态标志。允许更改的标志有O_APPEND、O_NONBLOCK、O_NOATIME、O_ASYNC 和 O_DIRECT。系统将忽略对其他标志的修改操作。使用 fcntl()修改文件状态标志,尤其适用于如下场景:
- 文件不是由调用程序打开的,所以程序也无法使用 open()调用来控制文件的状态标志(例如,文件是 3 个标准输入输出描述符中的一员,这些描述符在程序启动之前就被打开)。
- 文件描述符的获取是通过 open()之外的系统调用。比如 pipe()调用,该调用创建一个管道,并返回两个文件描述符分别对应管道的两端。再比如 socket()调用,该调用创建一个套接字并返回指向该套接字的文件描述符。
为了修改打开文件的状态标志,可以使用 fcntl()的 F_GETFL 命令来获取当前标志的副本,然后修改需要变更的比特位,最后再次调用 fcntl()函数的 F_SETFL 命令来更新此状态标志。因此,为了添加 O_APPEND 标志,可以编写如下代码:
int flags;
flags = fcntl(fd, F_GETFL);
if(flags == -1)
errExit("fcntl");
flags |= O_APPEND;
if(fcntl(fd, F_SETFL, flags) == -1)
errExit("fcntl");
5.3 文件描述符和打开文件之间的关系
到目前为止,文件描述符和打开的文件之间似乎呈现出一一对应的关系。然而,实际并非如此。多个文件描述符指向同一打开文件,这既有可能,也属必要。**这些文件描述符可在相同或不同的进程中打开。**要理解具体情况如何,需要查看由内核维护的 3 个数据结构。
- 进程级的文件描述符表(open file descriptor)。该表的每一条目都记录了单个文件描述符的相关信息,如下所示。
- 控制文件描述符操作的一组标志。(目前,此类标志仅定义了一个,即 close-on-exec 标志。)
- 对打开文件句柄的引用。
- 系统级的打开文件表(open file description table),表中各条目称为打开文件句柄(open file handle),一个打开文件句柄存储了与一个打开文件相关的全部信息,如下所示。
- 当前文件偏移量(调用 read()和 write()时更新,或使用 lseek()直接修改)。
- 打开文件时所使用的状态标志(即,open()的 flags 参数)。
- 文件访问模式(如调用 open()时所设置的只读模式、只写模式或读写模式)。
- 与信号驱动 I/O 相关的设置(见 63.3 节)。
- 对该文件 i-node 对象的引用。
- 文件系统的 i-node 表。每个文件系统都会为驻留其上的所有文件建立一个 i-node 表。每个文件的 i-node 信息,具体如下。
- 文件类型(例如,常规文件、套接字或 FIFO)和访问权限。
- 一个指针,指向该文件所持有的锁的列表。
- 文件的各种属性,包括文件大小以及与不同类型操作相关的时间戳。
【注】此处将忽略 i-node 在磁盘和内存中的表示差异。磁盘上的 i-node 记录了文件的固有属性,诸如:文件类型、访问权限和时间戳。访问一个文件时,会在内存中为 i-node 创建一个副本,其中记录了引用该 i-node 的打开文件句柄数量以及该 i-node 所在设备的主、从设备号,还包括一些打开文件时与文件相关的临时属性,例如:文件锁。
上图展示了文件描述符、打开的文件句柄以及 i-node 之间的关系。在下图中,两个进程拥有诸多打开的文件描述符。
在进程 A 中,文件描述符 1 和 20 都指向同一个打开的文件句柄(标号为 23)。这可能是通过调用 dup()、dup2()或 fcntl()而形成的。
进程A的文件描述符2和进程B的文件描述符2都指向同一个打开的文件句柄(标号为73)。这种情形可能在调用 fork()后出现(即,进程 A 与进程 B 之间是父子关系),或者当某进程通过UNIX 域套接字将一个打开的文件描述符传递给另一进程时,也会发生。
此外,进程 A 的描述符 0 和进程 B 的描述符 3 分别指向不同的打开文件句柄,但这些句柄均指向 i-node 表中的相同条目(1976),换言之,指向同一文件。发生这种情况是因为每个进程各自对同一文件发起了 open()调用。同一个进程两次打开同一文件,也会发生类似情况。
述讨论揭示出如下要点。
- **两个不同的文件描述符,若指向同一打开文件句柄,将共享同一文件偏移量。**因此,如果通过其中一个文件描述符来修改文件偏移量(由调用 read()、write()或 lseek()所致),那么从另一文件描述符中也会观察到这一变化。无论这两个文件描述符分属于不同进程,还是同属于一个进程,情况都是如此。
- 要获取和修改打开的文件标志(例如,O_APPEND、O_NONBLOCK 和 O_ASYNC),可执行 fcntl()的 F_GETFL 和 F_SETFL 操作,其对作用域的约束与上一条颇为类似。
- 相形之下,文件描述符标志(亦即,close-on-exec 标志)为进程和文件描述符所私有。对这一标志的修改将不会影响同一进程或不同进程中的其他文件描述符。
【注】文件描述符标志仅仅是一个标志,当进程fork一个子进程的时候,在子进程中调用了exec函数时就用到了这个标志。意义是执行exec前是否要关闭这个文件描述符。
5.4 复制文件描述符
Bourne shell 的 I/O 重定向语法 2>&1,意在通知 shell 把标准错误(文件描述符 2)重定向到标准输出(文件描述符 1)。因此,下列命令将把(因为 shell 按从左至右的顺序处理 I/O 重定向语句)标准输出和标准错误写入 result.log 文件:
./myscript > result.log 2>&1
shell 通过复制文件描述符 2实现了标准错误的重定向操作,因此文件描述符 2 与文件描述符 1 指向同一个打开文件句柄。可以通过调用 dup()和 dup2()来实现此功能。
int dup(int oldfd)
调用复制一个打开的文件描述符 oldfd,并返回一个新描述符,二者都指向同一打开的文件句柄。系统会保证新描述符一定是编号值最低的未用文件描述符
int dup2(int oldfd, int newfd)
系统调用会为 oldfd 参数所指定的文件描述符创建副本,其编号由 newfd 参数指定。如果由 newfd 参数所指定编号的文件描述符之前已经打开,那么 dup2()会首先将其关闭。(dup2()调用会默然忽略 newfd 关闭期间出现的任何错误。故此,编码时更为安全的做法是:在调用dup2()之前,若 newfd 已经打开,则应显式调用 close()将其关闭。)若调用 dup2()成功,则将返回副本的文件描述符编号(即 newfd 参数指定的值)。如果 oldfd 并非有效的文件描述符,那么 dup2()调用将失败并返回错误 EBADF,且不关闭 newfd。如果 oldfd 有效,且与 newfd 值相等,那么 dup2()将什么也不做,不关闭 newfd,并将其作为调用结果返回。
fcntl()的 F_DUPFD 操作是复制文件描述符的另一接口,更具灵活性。
newdf = fcntl(oldfd, F_DUPFD, startfd);
该调用为 oldfd 创建一个副本,且将使用大于等于 startfd 的最小未用值作为描述符编号。该调用还能保证新描述符(newfd)编号落在特定的区间范围内。总是能将 dup()和 dup2()调用改写为对 close()和 fcntl()的调用,虽然前者更为简洁。
【注】dup2()和 fcntl()二者返回的 errno 错误码存在一些差别。
int dup3(int oldfd, int newfd, int flags)
系统调用完成的工作与 dup2()相同,只是新增了一个附加参数 flag,这是一个可以修改系统调用行为的位掩码。目前,dup3()只支持一个标志 O_CLOEXEC,这将促使内核为新文件描述符设置 close-on-exec 标志(FD_CLOEXEC)。
5.5 在文件特定偏移量处的 I/O:pread()和 pwrite()
系统调用 pread()和 pwrite()完成与 read()和 write()相类似的工作,只是前两者会在 offset 参数所指定的位置进行文件 I/O 操作,而非始于文件的当前偏移量处,且它们不会改变文件的当前偏移量。其实就是相当于把lseek()设定偏移量、read()或write()之后再lseek()设定为原偏移量这几个调用纳入同一原子操作。
ssize_t pread(int fd, void *buf, size_t count, off_t offset); /* 返回读到的字节数,EOF返回0,错误返回-1 */
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset); /* 返回写入的字节数,错误返回-1 */
进程下辖的所有线程将共享同一文件描述符表。这也意味着每个已打开文件的文件偏移量为所有线程所共享。当调用pread()或 pwrite()时,多个线程可同时对同一文件描述符执行 I/O 操作,且不会因其他线程修改文件偏移量而受到影响。如果还试图使用 lseek()和 read()(或 write())来代替 pread()(或pwrite()),那么将引发竞争状态(当多个进程的文件描述符指向相同的打开文件句柄时,使用 pread()和 pwrite()系统调用同样能够避免进程间出现竞争状态)。而且执行单个 pread()(或 pwrite())系统调用的成本要低于执行 lseek()和 read()(或 write())两个系统调用。
5.6 分散输入和集中输出(Scatter-Gather I/O):readv()和 writev()
系统调用readv()和 writev()并非只对单个缓冲区进行读写操作,而是一次即可传输多个缓冲区的数据。
ssize_t readv(int fd, const struct iovec * iov, int iovent); /* 返回读到的字节数,EOF返回0,错误返回-1 */
ssize_t writev(int fd, const struct iovec * iov, int iovent); /* 返回写入的字节数,错误返回-1 */
数组 iov 定义了一组用来传输数据的缓冲区。整型数 iovcnt 则指定了 iov 的成员个数。iov 中的每个成员都是如下形式的数据结构。
struct iovec
{
void *iov_base; /* buffer 的起始地址 */
size_t iov_len; /* 传出或者传入buffer的字节数 */
}
下图是一个关于 iov、iovcnt 以及 iov 指向缓冲区之间关系的示例。
5.6.1 分散输入
readv()系统调用实现了分散输入的功能:从文件描述符 fd 所指代的文件中读取一片连续的字节,然后将其散置(“分散放置”)于 iov 指定的缓冲区中。这一散置动作从 iov[0]开始,依次填满每个缓冲区。
原子性是 readv()的重要属性。换言之,从调用进程的角度来看,当调用 readv()时,内核在 fd 所指代的文件与用户内存之间一次性地完成了数据转移。这意味着,假设即使有另一进程(或线程)与其共享同一文件偏移量,且在调用 readv()的同时企图修改文件偏移量,readv()所读取的数据仍将是连续的。
下面程序展示了 readv()的用法。
#include <sys/stat.h>
#include <sys/uio.h>
#include <fcntl.h>
#include "tlpi_hdr.h"
int
main(int argc, char *argv[])
{
int fd;
struct iovec iov[3];
struct stat myStruct; /* First buffer */
int x; /* Second buffer */
#define STR_SIZE 100
char str[STR_SIZE]; /* Third buffer */
ssize_t numRead, totRequired;
if (argc != 2 || strcmp(argv[1], "--help") == 0)
usageErr("%s file\n", argv[0]);
fd = open(argv[1], O_RDONLY);
if (fd == -1)
errExit("open");
totRequired = 0;
/* buffer[0]的起始地址为myStruct的地址,长度为myStruct结构体的大小,即将读到的数据写入myStruct结构体中 */
iov[0].iov_base = &myStruct;
iov[0].iov_len = sizeof(struct stat);
totRequired += iov[0].iov_len;
/* buffer[1]的起始地址为变量x的地址,长度为变量x的大小,即将读到的数据赋值为x */
iov[1].iov_base = &x;
iov[1].iov_len = sizeof(x);
totRequired += iov[1].iov_len;
/* buffer[2]的起始地址为数组str的首地址,长度为数组str的大小,即将读到的数据赋值给数组str */
iov[2].iov_base = str;
iov[2].iov_len = STR_SIZE;
totRequired += iov[2].iov_len;
numRead = readv(fd, iov, 3);
if (numRead == -1)
errExit("readv");
if (numRead < totRequired)
printf("Read fewer bytes than requested\n");
printf("total bytes requested: %ld; bytes read: %ld\n",
(long) totRequired, (long) numRead);
exit(EXIT_SUCCESS);
}
【补充】上述程序中的struct stat 是一个结构体,用于存储文件或文件系统的状态信息。其定义在 sys/stat.h 中,具体的字段可能会因操作系统和文件系统类型而异,但通常会包含以下一些字段:
- dev_t st_dev: 文件所在设备的标识符。
- ino_t st_ino: 文件的 i-node 号。
- mode_t st_mode: 文件类型和权限。
- nlink_t st_nlink: 文件的硬链接数。
- uid_t st_uid: 文件所有者的用户 ID。
- gid_t st_gid: 文件所有者的组 ID。
- dev_t st_rdev: 如果该文件是一个特殊设备文件,则此字段表示设备类型。
- off_t st_size: 文件的大小(以字节为单位)。
- blksize_t st_blksize: 文件系统的最佳 I/O 块大小。
- blkcnt_t st_blocks: 文件占用的磁盘块数。
- time_t st_atime: 文件的最后访问时间。
- time_t st_mtime: 文件的最后修改时间。
- time_t st_ctime: 文件的最后状态改变时间(例如,权限或所有权改变)。
5.6.2 集中输出
writev()系统调用实现了集中输出:将 iov 所指定的所有缓冲区中的数据拼接(“集中”)起来,然后以连续的字节序列写入文件描述符 fd 指代的文件中。对缓冲区中数据的“集中”始于iov[0]所指定的缓冲区,并按数组顺序展开。
像 readv()调用一样,writev()调用也属于原子操作,即所有数据将一次性地从用户内存传输到 fd 指代的文件中。因此,在向普通文件写入数据时,writev()调用会把所有的请求数据连续写入文件,而不会在其他进程(或线程)写操作的影响下分散地写入文件 。
5.6.3 在指定的文件偏移量处执行分散输入/集中输出
Linux 2.6.30 版本新增了两个系统调用:preadv()、pwritev(),将分散输入/集中输出和于指定文件偏移量处的 I/O 二者集于一身。它们并非标准的系统调用,但获得了现代 BSD 的支持。
ssize_t preadv(int fd, const struct iovec * iov, int iovent, off_t offset); /* 返回读到的字节数,EOF返回0,错误返回-1 */
ssize_t pwritev(int fd, const struct iovec * iov, int iovent, off_t offset); /* 返回写入的字节数,错误返回-1 */
preadv()和 pwritev()系统调用所执行的任务与 readv()和 writev()相同,但执行 I/O 的位置将由 offset 参数指定(类似于 pread()和 pwrite()系统调用) 。
5.7 截断文件:truncate()和 ftruncate()系统调用
truncate()和 ftruncate()系统调用将文件大小设置为 length 参数指定的值。若文件当前长度大于参数 length,调用将丢弃超出部分,若小于参数 length,调用将在文件尾部添加一系列空字节或是一个文件空洞。
int truncate(const char *pathname, off_t length); /* 成功返回0,错误返回-1 */
int ftruncate(int fd, off_t length); /* 成功返回0,错误返回-1 */
truncate()以路径名字符串来指定文件,并要求可访问该文件 ,且对文件拥有写权限。若文件名为符号链接,那么调用将对其进行解引用。而调用 ftruncate()之前,需以可写方式打开操作文件,获取其文件描述符以指代该文件,该系统调用不会修改文件偏移量。
5.8 非阻塞 I/O
在打开文件时指定 O_NONBLOCK 标志,目的有二。
- 若 open()调用未能立即打开文件,则返回错误,而非陷入阻塞。有一种情况属于例外,调用 open()操作 FIFO 可能会陷入阻塞。
- 调用 open()成功后,后续的I/O 操作也是非阻塞的。若I/O 系统调用未能立即完成,则可能会只传输部分数据,或者系统调用失败,并返回 EAGAIN 或 EWOULDBLOCK 错误。具体返回何种错误将依赖于系统调用。Linux 系统与许多 UNIX 实现一样,将两个错误常量视为同义。
管道、FIFO、套接字、设备(比如终端、伪终端)都支持非阻塞模式。(因为无法通过 open()来获取管道和套接字的文件描述符,所以要启用非阻塞标志,就必须使用 fcntl()的F_SETFL 命令。)由于内核缓冲区保证了普通文件 I/O 不会陷入阻塞,故而打开普通文件时一般会忽略 O_NONBLOCK 标志。然而,当使用强制文件锁时(55.4 节),O_NONBLOCK标志对普通文件也是起作用的。
5.9 /dev/fd 目录
对于每个进程,内核都提供有一个特殊的虚拟目录/dev/fd。该目录中包含“/dev/fd/n”形式的文件名,其中 n 是与进程中的打开文件描述符相对应的编号。因此,例如,/dev/fd/0 就对应于进程的标准输入。打开/dev/fd 目录中的一个文件等同于复制相应的文件描述符,所以下列两行代码是等价的:
fd = open("/dev/fd/1", O_WRONLY);
fd = dup(1);
【注】/dev/fd 实际上是一个符号链接,链接到 Linux 所专有的/proc/self/fd 目录。后者又是 Linux特有的/proc/PID/fd 目录族的特例之一,此目录族中的每一目录都包含有符号链接,与一进程所打开的所有文件相对应。
程序中很少会使用/dev/fd 目录中的文件。其主要用途在 shell 中。许多用户级 shell 命令将文件名作为参数,有时需要将命令输出至管道,并将某个参数替换为标准输入或标准输出。出于这一目的,有些命令(例如,diff、ed、tar 和 comm)提供了一个解决方法,使用“-”符号作为命令的参数之一,用以表示标准输入或输出(视情况而定)。所以,要比较 ls 命令输出的文件名列表与之前生成的文件名列表,命令就可以写成:
$ ls | diff - oldfilelist # diff file1 file2
即用“-”代替 ls 命令输出的文件名列表,标准输入或标准输出由系统视情况而定,但是这种方法有不少问题。首先,该方法要求每个程序都对“-”符号做专门处理,但是许多程序并未实现这样的功能,这些命令只能处理文件,不支持将标准输入或输出作为参数。其次,有些程序还将单个“-”符解释为表征命令行选项结束的分隔符。
使用/dev/fd 目录,上述问题将迎刃而解,可以把标准输入、标准输出和标准错误作为文件名参数传递给任何需要它们的程序。所以,可以将前一个 shell 命令改写成如下形式:
$ ls | diff /dev/fd/0 oldfilelist
【注】方便起见,系统还提供了 3 个符号链接:/dev/stdin、/dev/stdout 和/dev/stderr,分别链接到/dev/fd/0、/dev/fd/1 和/dev/fd/2。
5.10 创建临时文件
有些程序需要创建一些临时文件,仅供其在运行期间使用,程序终止后即行删除。基于调用者提供的模板,mkstemp()函数生成一个唯一文件名并打开该文件,返回一个可用于 I/O 调用的文件描述符。
int mkstemp(char *template); /* 成功返回文件描述符,错误返回-1 */
模板参数采用路径名形式,其中最后 6 个字符必须为 XXXXXX。这 6 个字符将被替换,以保证文件名的唯一性,且修改后的字符串将通过template参数传回。因为会对传入的template参数进行修改,所以必须将其指定为字符数组,而非字符串常量。
文件拥有者对 mkstemp()函数建立的文件拥有读写权限(其他用户则没有任何操作权限),且打开文件时使用了 O_EXCL 标志,以保证调用者以独占方式访问文件。
通常,打开临时文件不久,程序就会使用unlink 系统调用(参见18.3 节)将其删除。故而,mkstemp()函数的示例代码如下所示:
int fd;
char template[] = "/tmp/somestringXXXXXX";
fd = mkstemp(template);
if(fd == -1)
errExit("mkstemp");
printf("Generated filename was: %s\n", template);
ulink(template);
if(close(fd) == -1)
errExit("close");
tmpfile()函数会创建一个名称唯一的临时文件,并以读写方式将其打开。(打开该文件时使用了 O_EXCL 标志,以防一个可能性极小的冲突,即另一个进程已经创建了一个同名文件。)
FILE *tmpfile(void); /* 成功返回文件指针,错误返回-1 */
tmpfile()函数执行成功,将返回一个文件流供 stdio 库函数使用。文件流关闭后将自动删除临时文件。为达到这一目的,tmpfile()函数会在打开文件后,从内部立即调用 unlink()来删除该文件名。