3.11 原子操作
1、追加到一个文件
考虑一个进程,它要将数据追加到一个文件尾端。早期的UNIX系统版本并不支持open的O_APPEND选项,所以程序被编写成下列形式:
if (lseek(fd, OL, 2) < 0)
err_sys("lseek error");
if (write(fd, buf, 100) != 100)
err_sys("write error");
对单个进程而言,这段程序能正常工作,但若有多个进程同时使用这种方法将数据追加写到同一文件,则会产生问题(例如,若此程序由多个进程同时执行,各自将消息追加到一个日志文件中,就会产生这种情况)。
假定有两个独立的进程A和B都对同一文件进行追加写操作。每个进程都已打开了该文件。但未使用O_APPEND标志。此时,各数据结构之间的关系如图3-8所示。每个进程都有它自己的文件表项,但是共享一个v节点表项。假定进程A调用了lseek,它将进程A的该文件当前偏移量设置为1500字节(当前文件尾端处)。然后内核切换进程,进程B运行。进程B执行lseek,也将其对该文件的当前偏移量增加至1600。因为该文件的长度已经增加了,所以内核将v节点中的当前文件长度更新至1600。然后,内核又进行进程切换,使进程A恢复运行。当A调用write时,就从其当前文件偏移量(1500)处开始将数据写入到文件。这样也就覆盖了进程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时,无法中断其定位和读操作。
- 不更新当前文件偏移量。
3、创建一个文件
对open函数的O_CREAT和O_EXCL选项进行说明时,我们已看到另一个有关原子操作的例子。当同时指定这两个选项,而该文件又已经存在时,open将失败。我们曾提及检查文件是否存在和创建文件这两个操作是作为一个原子操作执行的。如果没有这样一个原子操作,那么可能会编写下列程序段:
if ( (fd = open(pathname, O_WONLY)) < 0) {
if (errno == ENOENT) {
if ((fd = creat(path, mode)) < 0)
err_sys("creat error");
} else {
err_sys("open error");
}
}
如果在open和create之间,另一个进程创建了该文件,就会出现问题。若在这两个函数调用之间,另一个进程创建了该文件,并且写入了一些数据,然后,原先进程执行这段程序中的creat,这时,刚由另一进程写入的数据就会被擦去。如若将这两者合并在一个原子操作中,这种问题也就不会出现。
一般而言,原子操作指的是由多步组成的一个操作。如果该操作原子性地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。在4.15节描述link函数以及在14.3节中说明记录锁时,还将讨论原子操作。