5.1 原子性和竞争条件
原子性是一个在system call里经常能碰到的一个概念,所有的system call都以原子的方式执行。原子性可以类比在微观物理的概念上,很早以前人们大概认为原子不可以再分,所以原子化的程序也是一个不可被打断,需要作为一个整体执行的程序。对于每一个system call的所有步骤都可以被视为一个整体而不可被别的进程或者线程中断。
原子性之所以重要,是因为它可以帮助我们避免很多竞争条件race conditions。之所以会有竞争条件,是因为有的时候共享同一资源的两个进程或者线程进入CPU调用的顺序不明确从而导致可能的混乱。
在下面的内容当中,本书将讨论两种包含文件I/O的竞争条件,然后再进一步讨论如何使用open()消除并保证相关文件操作的原子性。
创造互斥文件
在之前的文章中 4. 文件I/O:通用I/O模型_猴子头头123的博客-CSDN博客提到说关于O_EXCL的大概使用:
O_EXCL 这个一般和O_CREAT一起使用。当文件已经存在的时候,则文件不可以被打开,并且要返回一个错误(errno EEXIST)。换句话说,O_EXCL|O_CREAT是用来确保该process只在没有该文件的情况下创建文件。这一个过程是以atomically的方式执行(原子操作:线程或进程执行过程中不会被打断,也就意味着不会有别的线程进程在同一时间段内使用同一个resource)。5.1中还会继续展开解释。
至于说为什么这个O_EXCL非常重要,我们可以看一个下面的例子,在例子当中本书展示了在缺失O_EXCL的情况下会发生什么:
#include <sys/stat.h>
#include <fcntl.h>
#include "tlpi_hdr.h"
int
main(int argc, char *argv[])
{
int fd;
if (argc < 2 || strcmp(argv[1], "--help") == 0)
usageErr("%s file\n", argv[0]);
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 */
errExit("open");
} 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)
errExit("open");
printf("[PID %ld] Created file \"%s\" exclusively\n",
(long) getpid(), argv[1]); /* MAY NOT BE TRUE! */
}
}
exit(EXIT_SUCCESS);
}
正常来讲,如果只要一个进程,那么它会先打开上述 Open 1 位置的文件,结果发现不存在,所以之后会进入下面 open的内容部分当中,并新建一个新的文件。
fd = open(argv[1], O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
我们假设上述的进程为A。 当进程A执行完Open 1的任务之后,因为多进程调度的原因,这时候执行同样内容的进程B开始被执行(也许高优先级什么的),那么它会顺序执行Open1,然后在进入新建文件的过程,并且新建成功-->结束。这时候交还CPU给进程A,并且进程A也执行上述单行代码,不过因为只有O_WRONLY|O_CREAT的存在,该打开也会被成功执行,虽然并未实际生成新的文件。
那么这个时候,其实进程A会误认为自己才是那个生成新文件的进程,会输出一个进程A 生成文件xxx的内容。当然这并不是我们所期待的东西,所以这样的情况被我们称之为race condition,它会使得程序不再可靠。可以想象更奇特的例子比如打印机,如果进程A刚进入打印程序,进程B插入并占用打印机打印了内容B,这时候再回到进程A,但是进程A误以为自己已经打印了内容A,结果事实上我们只能得到一张上面写有内容B的A4纸,这大概是没有人想要得到的结果。
下面这部分代码是为了帮助实现上面竞争条件而额外加入的内容。也就是当我们在命令行给入超过两个参数的时候它就会实现5秒的休眠,这个时候可以赶快去执行另一个进程则可以实现上面的race condition。
if (argc > 2) { /* Delay between check and create */
sleep(5); /* Suspend execution for 5 seconds */
printf("[PID %ld] Done sleeping\n", (long) getpid());
}
测试结果如下:
pi@raspberrypi:~/sysprog/learn_tlpi/build $ ./out tfile sleep &
[1] 4330
pi@raspberrypi:~/sysprog/learn_tlpi/build $ [PID 4330] File "tfile" doesn't exist yet
./out tfile
[PID 4331] File "tfile" doesn't exist yet
[PID 4331] Created file "tfile" exclusively
pi@raspberrypi:~/sysprog/learn_tlpi/build $ [PID 4330] Done sleeping
[PID 4330] Created file "tfile" exclusively
[1]+ Done ./out tfile sleep
明显可知,这里的PID号4330即是上述A进程,PID号4331即是B进程。很明显B进程输出