所有系统调用都是以原子操作方式执行的。之所以这么说,是指内核保证了某系统调用中的所有步骤会作为独立操作而一次性加以执行,其间不会为其他进程或线程所中断。原子性是某些操作得以圆满成功的关键所在。特别是它规避了竞争状态(race conditions)(有时也称为竞争冒险)。
竞争状态是这样一种情形:操作共享资源的两个进程(或线程),其结果取决于一个无法预期的顺序,即这些进程1 获得 CPU 使用权的先后相对顺序。接下来,将讨论涉及文件 I/O 的两种竞争状态,并展示了如何使用 open()的标志位,来保证相关文件操作的原子性,从而消除这些竞争状态。
以独占方式创建一个文件
当同时指定 O_EXCL 与 O_CREAT 作为 open()的标志位时,如果要打开的文件已然存在,则 open()将返回一个错误。这提供了一种机制,保证进程是打开文件的创建者。对文件是否存在的检查和创建文件属于同一原子操作。要理解这一点的重要性,以下代码,并未使用 O_EXCL 标志。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
void errExit(const char* msg) {
perror(msg);
exit(EXIT_FAILURE);
}
int main(int argc, char *argv[]) {
int fd;
if (argc < 2) {
fprintf(stderr, "Usage: %s <file>\n", argv[0]);
exit(EXIT_FAILURE);
}
fd = open(argv[1], O_WRONLY); // Try to open the file
if (fd != -1) { // Open succeeded
printf("[%ld] File \"%s\" already exists\n", (long)getpid(), argv[1]);
close(fd);
} else {
if (errno != ENOENT) { // Failed for unexpected reason
errExit("open");
} else { // File does not exist, try to create it
fd = open(argv[1], O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1) {
errExit("open");
}
printf("[%ld] Created file \"%s\" exclusively\n", (long)getpid(), argv[1]);
close(fd);
}
}
return EXIT_SUCCESS;
}
程序尝试以只写模式打开文件,如果文件不存在(errno 设置为 ENOENT),则尝试创建文件。然而,这里存在一个竞态条件(race condition)的问题。
问题在于,程序在检查文件是否存在(通过尝试打开它)和实际创建文件之间有一个时间窗口,在这个时间窗口内,其他进程可能会创建该文件。这意味着即使第一次调用 open 失败,表明文件当时不存在,第二次调用 open 时文件可能已经被另一个进程创建了,这违反了独占性的要求。
内核调度器判断出分配给 A 进程的时间片已经耗尽,并将 CPU 使用权交给 B 进程,就可能会发生这种问题。再比如两个进程在一个多CPU 系统上同时运行时,也会出现这种情况。
下图 展示了两个进程同时以上执行程序代码的情形。在这一场景下,进程 A 将得出错误的结论:目标文件是由自己创建的。因为无论目标文件存在与否,进程 A 对 open()的第二次调用都会成功。虽然进程将自己误认为文件创建者的可能性相对较小,但毕竟是存在的,这已然将此段代码置于不可靠的境地。操作的结果将依赖于对两个进程的调度顺序,这一事实也就意味着出现了竞争状态。
为了说明这段代码的确存在问题,对上述代码进一步改造一下更能说明问题,在检查文件是否存在与创建文件这两个动作之间人为制造一个长时间的等待.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
void errExit(const char* msg) {
perror(msg);
exit(EXIT_FAILURE);
}
int main(int argc, char *argv[]) {
int fd;
if (argc < 2) {
fprintf(stderr, "Usage: %s <file>\n", argv[0]);
exit(EXIT_FAILURE);
}
fd = open(argv[1], O_WRONLY); // Try to open the file
if (fd != -1) { // Open succeeded
printf("[%ld] File \"%s\" already exists\n", (long)getpid(), argv[1]);
close(fd);
} else {
if (errno != ENOENT) { // Failed for unexpected reason
errExit("open");
} else { // File does not exist
printf("[%ld] File \"%s\" doesn't exist yet\n", (long)getpid(), argv[1]);
if (argc > 2) {
sleep(5); // Suspend execution for 5 seconds
printf("[%ld] Done sleeping\n", (long)getpid());
}
// Attempt to create the file
fd = open(argv[1], O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1) {
errExit("open");
}
printf("[%ld] Created file \"%s\" exclusively\n", (long)getpid(), argv[1]);
close(fd);
}
}
return EXIT_SUCCESS;
}
两个进程都会声称自己以独占方式创建了文件。由于第一个进程在检查文件是否存在和创建文件之间发生了中断,造成两个进程都声称自己是文件的创建者。结合 O_CREAT 和 O_EXCL 标志来一次性地调用 open()可以防止这种情况,因为这确保了检查文件和创建文件的步骤属于一个单一的原子(即不可中断的)操作。
一种可以正确的做法是:
fd = open(argv[1], O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR);
if (fd == -1) {
if (errno == EEXIST) {
printf("[%ld] File \"%s\" already exists\n", (long)getpid(), argv[1]);
} else {
// Handle other errors
errExit("open");
}
} else {
printf("[%ld] Created file \"%s\" exclusively\n", (long)getpid(), argv[1]);
}