- 将某一系统调用所要完成的各个动作作为不可中断的操作,一次性加以执行
- 原子操作是很多系统调用得以正确执行的必要条件。
所有的系统调用都是以原子操作方式执行的。之所以这么说,是指内核保证了某系统调用的所有步骤会作为独立操作一次性加以执行,其间不会为其他进程或者线程所中断。
原子性是某些操作得以圆满成功的关键所在。特别是它规避了竞争状态(race conditions)。竞争状态是这样一种情形:操作共享资源的两个进程(或线程),其结果取决于一个无法预期的顺序,即这些进程获得 CPU 使用权的先后相对顺序。
接下来,将讨论涉及文件 I/O 的两种竞争状态,并展示了如何使用 open()的标志位,来保证相关文件操作的原子性,从而消除这些竞争状态
以独占的方式创建一个文件
我们知道,当同时指定以O_EXCL与O_CREAT作为open()的标志位时,如果文件已经存在,则open()返回一个错误。这提供了一种机制,保证进程是打开文件的创建者。对文件是否存在检查和创建文件属于同一原子操作。要理解这一点的重要性,请思考下面代码,该段代码中并未使用 O_EXCL 标志。
// 试图以独占方式打开文件的错误代码
#include <iostream>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <zconf.h>
int main(int argc, char *argv[])
{
int fd;
if (argc < 2 || strcmp(argv[1], "--help") == 0){
printf("%s file\n", argv[0]);
exit(EXIT_FAILURE);
}
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 */
perror("open");
exit(EXIT_FAILURE);
} 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) {
perror("open");
exit(EXIT_FAILURE);
}
/*FIXME: should use %zd here, and remove (long) cast */
printf("[PID %ld] Created file \"%s\" exclusively\n",
(long) getpid(), argv[1]); /* MAY NOT BE TRUE! */
}
}
exit(EXIT_SUCCESS);
}
假设如下情况:当第一次调用 open()时,希望打开的文件还不存在,而当第二次调用 open()时,其他进程已经创建了该文件。如下图所示,若内核调度器判断出分配给 A 进程的时间片已经耗尽,并将 CPU 使用权交给 B 进程,就可能会发生这种问题。再比如两个进程在一个多CPU 系统上同时运行时,也会出现这种情况。在这一场景下,进程 A 将得出错误的结论:目标文件是由自己创建的。因为无论目标文件存在与否,进程 A 对 open()的第二次调用都会成功
如果同时运行该程序的两个实例,两个进程都会声称自己以独占方式创建了文件
由于第一个进程在检查文件是否存在和创建文件之间发生了中断,造成两个进程都声称自己是文件的创建者。结合 O_CREAT 和 O_EXCL 标志来一次性地调用 open()可以防止这种情况,因
为这确保了检查文件和创建文件的步骤属于一个单一的原子(即不可中断的)操作。
向文件尾部追加数据
用以说明原子操作必要性的第二个例子是:多个进程同时向同一个文件(例如,全局日志文件)尾部添加数据。为了达到这一目的,也许可以考虑在每个写进程中使用如下代码。
if(lseek(fd, 0, SEEK_END) == -1){
perror("lseek");
exit(EXIT_FAILURE);
}
if(write(fd, buf, len) != len){
perror("partial/failed write");
exit(EXIT_FAILURE);
}
但是,这段代码存在的缺陷与前一个例子如出一辙。如果第一个进程执行到 lseek()和 write()之间,被执行相同代码的第二个进程所中断,那么这两个进程会在写入数据前,将文件偏移量设为相同位置,而当第一个进程再次获得调度时,会覆盖第二个进程已写入的数据。此时再次出现了竞争状态,因为执行的结果依赖于内核对两个进程的调度顺序
要规避这一问题,需要将文件偏移量的移动与数据写操作纳入同一原子操作。在打开文件时加入 O_APPEND 标志就可以保证这一点。
在打开文件时设置O_APPEND
标志:每次内核在写之前,都会将进程的当前文件偏移量设置到文件的末尾