3.11 原子操作
- 追加到一个文件
考虑一个进程,他要将数据追加到一个文件尾端。早期的UNIX系统版本并不支持open的O_APPEND选项,所以程序被编写程下列形式:
if (lseek(fd, OL, 2)) /* position to EOF */
err_sys("lseek error");
if (write(fd, buf, 100) != 100) /* and write */
err_sys("write error");
对单个进程而言,这段程序能正常工作,但若有多个进程同时使用这种方法将数据追加写到同一文件,则会产生问题(例如,若此程序由多个进程同时执行,各自将消息追加到一个日志文件中,就会产生这种情况)。
假定有两个独立的进程A和B都对同一文件进行追加写操作。每个进程都已打开了该文件,但未使用O_APPEND标志。此时,各数据结构之间的关系如下图所示:
每个进程都有它自己的文件表项,但是共享一个v节点表项。假定进程A调用了lseek,它将进程A的该文件当前偏移量设置为1500(当前文件尾端处)。然后内核切换进程,进程B运行。进程B执行lseek,也将其对该文件的当前偏移量设置为1500字节(当前文件尾端处)。然后B调用write,它将B的该文件当前文件偏移量增加值1600。因为该文件的长度已经增加了,所以内核将v节点中的当前文件长度我更新为1600.然后,内核有进行进程切换,使进程A恢复运行。当A调用write时,就从其当前文件偏移量(1500)处开始将数据写入到文件。这样也就覆盖了B刚才写入到该文件中的数据。
问题出在逻辑操作“先定位到文件尾端,然后写”,它使用了两个分开的函数调用。解决问题的方法是使这两个操作对于其他进程而言成为一个原子操作。任何要求多于一个函数调用的操作都不是原子操作,因为在两个函数调用之间,内核有可能会临时挂起进程(正如我前面所假定的)。
UNIX系统为这样的操作提供了一种原子操作方法,即在打开文件时设置O_APPEND标志。这样做使得内核在每次写操作之前,都将进程的当前偏移量设置到该文件的尾端处,于是在每次写之前不再需要调用lseek。
- 函数pread和pwrite
#include
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
ssize_t pwrite(int fd, const void * buf, size_t nbytes, off_t offset);
调用pread相当于调用lseek后调用read,但是pread又与这种顺序调用有下列重要区别。
- 调用pread时,无法中断其定位和读操作
- 不更新当前文件偏移量
调用pwrite相当于调用lseek后调用write,但也与它们有类似的区别。
- 创建一个文件
对open函数的O_CREAT和O_EXCL选项进行说明时,当同时指定这两个选项,而该文件又已经存在时,open将失败。检查文件是否存在和创建未见这两个操作作为一个原子操作执行。如果没有原子操作,可能会编写下列程序段:
if((fd = open(pathname, O_WRINLY)) < 0)
{
if(errno == ENOENT)
{
if((fd = creat(path, mode)) < 0)
err_sys("creat error");
}
else
{
err_sys("open error");
}
}
如果在open和creat之间,另一个进程创建了该文件,并且写入了一些数据,然后,原先进程执行这段程序中的creat,这是,刚由另一进程写入的数据就会被擦去。若这两者合并在一个原子操作中,这就问题就不会出现。
一般而言,原子操作指的是由多部组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。