(1) 添写至一个文件
考虑一个进程,它将数据添加到一个文件的尾端。早期的UNIX系统并不支持open的O_APPEND选项,所以程序被编写成下列形式:
if (lseek(fd, 0L, 2) < 0) err_sys("lseek error"); if (write(fd, buf, 100) != 100) err_sys("write error");
对单个进程而言,这段程序能正常工作,但若有多个进程同时使用这种方法将数据添加到同一个文件,则会产生问题。
问题出在逻辑操作“定位到文件结尾处,然后写”上,它使用了两个分开的函数调用(定位后可能被切换到另一个进程,再切换回来写入数据时,可能就不是末端了)。解决问题的方法是使这两个操作对于其他进程而言成为一个原子操作。任何一个需要多个函数调用的操作都不可能是原子操作,因为在两个函数调用之间,内核可能会临时挂起该进程。
UNIX系统提供了一种方法使这种操作成为原子操作,该方法是在打开文件时设置O_APPEND标志。这就使内核每次对这种文件进行写之前,都将进程的当前偏移量设置到该文件的尾端处。
(2) pread和pwrite函数
这两个函数允许原子性地定位搜索和执行I/O。
#include <unistd.h> ssize_t pread(int fildes, void *buf, size_t nbyte, off_t offset); ssize_t pwrite(int fildes, const void *buf, size_t nbyte, off_t offset);
无法中断其定位和读(写)操作。
不更新文件指针。
(3) 创建一个文件
对open函数同时指定O_CREAT和O_EXCL选项,而该文件又存在时,open将失败。检查该文件是否存在以及创建该文件这两个操作是作为一个原子操作执行的。如果没有这样一个原子操作,那么可能会编写下列程序段:
if ((fd = open(pathname, O_WRONLY)) < 0) { if (errno == ENOENT) { if ((fd = creat(pathname, mode)) < 0) err_sys("creat error"); } else { err_sys("open error"); } }
如果在open和creat之间,另一个进程创建了该文件,那么会引起问题。
一般而言,原子操作指的是由多步组成的操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
下面两个函数都可用来复制一个现存的文件描述符。
#include <unistd.h> int dup(int fildes); int dup2(int fildes, int fildes2);
由dup返回的新文件描述符一定是当前可用文件描述符中的最小值。
用dup2则可以用fildes2参数指定新描述符的数值。如果fildes2已经打开,则先将其关闭。如若fildes等于fildes2,则dup2返回fildes2,而不关闭它。
这些函数返回的新文件描述符都与fildes共享同一个文件表项。
图1 指定dup(1)后的内核数据结构
在此图中,我们假定进程执行了:
newfd = dup(1);
当此函数开始执行时,我们假定下一个可用的描述符示3。因为两个描述符指向同一个文件表,所以它们共享同一文件状态标志(读、写、添写等)以及同一当前文件偏移量。
每个文件描述符都有它自己的一套文件描述符标志。新描述符的执行关闭(close-on-exec)标志总是由dup函数清除。
复制一个描述符的另一种方法时使用fcntl函数。
实际上,调用
dup(filedes);
等效于
fcntl(filedes, F_DUPFD, 0);
而调用
dup2(filedes, filedes2)
等效于
close(filedes2) fcntl(filedes, F_DUPFD, filedes2);
在后一种情况下,dup2并不完全等同于close加上fcntl。它们之间的区别是:
(1)dup2是一个原子操作,而close及fcntl则包括两个函数调用。有可能在close和fcntl之间插入执行信号捕获函数,它可能修改文件描述符。
(2)dup2和fcntl有某些不同的errno。
传统的UNIX实现在内核中设有缓冲区高速缓存或页面高速缓存,大多数磁盘I/O都通过缓冲进行。当将数据写入文件时,内核通常先将该数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则并不将其排入输出队列,而是等待其写满或者当内核需要重用缓冲区以便存放其他磁盘块时,再将该缓冲排入输出队列,然后待其到达队首时,才进行实际的I/O操作。这种方式被称为延迟写。
延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度,使得欲写到文件中的数据在一段时间内并没有写到磁盘上。当系统发生故障时,这种延迟可能造成文件更新内容的丢失。为了保证磁盘上实际文件系统与缓冲区高速缓存中内容一致性,UNIX系统提供了sync、fsync和fdatasync三个函数。
#include <unistd.h> void sync(void); int fsync(int fildes); int fdatasync(int fildes);
sync函数只是将所有修改过的块缓冲区排入写队列,然后返回,它并不等待实际写磁盘操作结束。通常称为update的系统守护进程会周期性地调用sync函数。
fsync函数只对文件描述符filedes指定的单一文件其作用,并且等待写磁盘操作结束,然后返回。
fdatasync函数类似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。
尝试一下使用dup函数。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char *argv[]) { int newfd; char str1[100] = "write to newfd\n"; char str2[100] = "write to STDOUT_FILENO\n"; newfd = dup(STDOUT_FILENO); // 复制标准输出文件描述符 printf("newfd = %d\n", newfd); write(newfd, str1, sizeof(str1)); // 向新文件描述符写入数据 printf("STDOUT_FILENO = %d\n", STDOUT_FILENO); write(STDOUT_FILENO, str2, sizeof(str2)); // 向标准输出文件描述符写入数据 exit(0); }
编译后执行程序执行程序
$ ./05 newfd = 3 write to newfd STDOUT_FILENO = 1 write to STDOUT_FILENO
向newfd写入数据,该数据从屏幕输出,这与向标准输出(STDOUT_FILENO)写入数据的效果是一样的。执行该程序时文件的内核数据结构与图1中的情况相同。
再试一试dup2,只需要改动一行代码。将
newfd = dup(STDOUT_FILENO); // 复制标准输出文件描述符
换成
newfd = dup2(STDOUT_FILENO, 5); // 复制标准输出文件描述符
编译后执行执行程序
$ ./06 newfd = 5 write to newfd STDOUT_FILENO = 1 write to STDOUT_FILENO
newfd不再是当前可用文件描述符中的最小值(3),而是指定的数值(5)。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <string.h> #define FILE_MODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH) int main(int argc, char *argv[]) { int fd; int newfd; char str[16] = "hello world\n"; /* * O_WRONLY 只写 * O_CREAT 不存在则创建 * O_TRUNC 存在则截断为0 */ if ((fd = open("file.test", O_WRONLY | O_CREAT | O_TRUNC, FILE_MODE)) == -1) { fprintf(stderr, "open error:%s\n", strerror(errno)); exit(-1); } // 复制文件描述符 if ((newfd = dup(fd)) == -1) { fprintf(stderr, "dup error:%s\n", strerror(errno)); exit(-1); } // 输出写入数据前的偏移量 printf("write(fd) before:\n"); printf("lseek(fd) = %ld\n", lseek(fd, 0, SEEK_CUR)); printf("lseek(newfd) = %ld\n", lseek(newfd, 0, SEEK_CUR)); // 向fd中写入数据 if (write(fd, str, sizeof(str)) != sizeof(str)) { fprintf(stderr, "write error:%s\n", strerror(errno)); exit(-1); } // 输出写入数据后的偏移量 printf("wirte(fd) after:\n"); printf("lseek(fd) = %ld\n", lseek(fd, 0, SEEK_CUR)); printf("lseek(newfd) = %ld\n", lseek(newfd, 0, SEEK_CUR)); exit(0); }
编译后运行程序:
$ ./07 write(fd) before: lseek(fd) = 0 lseek(newfd) = 0 wirte(fd) after: lseek(fd) = 16 lseek(newfd) = 16
该结果证明,偏移量被共享了。