8.9 竞态条件

8.9 竞态条件

从本书的目的出发,当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞态条件( race condition。如果在fork之后的某种逻辑显式或隐式地依赖于在fork之后是父进程先运行还是子进程先运行,那么fork函数就会是竞态条件活跃的滋生地。通常,我们不能预料哪一个进程先运行。即使知道哪一个进程先运行,那么在该进程开始运行后,所发生的事情也依赖于系统负载以及内核的调度算法。

在程序8 – 5中,当第二个子进程打印其父进程ID时,我们看到了一个潜在的竞态条件。如果第二个子进程在第一个子进程之前运行,则其父进程将会是第一个子进程。但是,如果第一个子进程先运行,并有足够的时间到达并执行exit,则第二个子进程的父进程就是init。即使在程序中调用sleep,这也不保证什么。如果系统负担很重,那么在第二个子进程从sleep返回时,可能第一个子进程还没有得到机会运行。这种形式的问题很难排除,因为在大部分时间,这种问题并不出现。

如果一个进程希望等待一个子进程终止,则它必须调用wait函数。如果一个进程要等待其父进程终止(如程序8 – 5中一样),则可使用下列形式的循环:

while(getppid() !=1)

sleep ( 1 ) ;

这种形式的循环(称为轮询polling))的问题是它浪费了CPU时间,因为调用者每隔1秒都被唤醒,然后进行条件测试。

为了避免竞态条件和轮询,在多个进程之间需要有某种形式的信号机制。在UNIX中可以使用信号机制,在1 0 . 1 6节将说明它的一种用法。各种形式的进程间通信( IPC )也可使用,在第151 7章将对此进行讨论。

在父、子进程的关系中,常常出现下述情况。在fork之后,父、子进程都有一些事情要做。例如,父进程可能以子进程ID更新日志文件中的一个记录,而子进程则可能要为父进程创建一个文件。在本例中,要求每个进程在执行完它的一套初始化操作后要通知对方,并且在继续运行之前,要等待另一方完成其初始化操作。这种情况可以描述如下:

#include  "***"

TELL_WAIT(); /* set things up for TELL_xxx & WAIT_xxx */

if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) { /* child */

/* child does whatever is necessary ... */

TELL_PARENT(getppid()); /* tell parent we're done */
WAIT_PARENT(); /* and wait for parent */

/* and the child continues on its way ... */

exit(0);
}

/* parent does whatever is necessary ... */

TELL_CHILD(pid); /* tell child we're done */
WAIT_CHILD(); /* and wait for child */

/* and the parent continues on its way ... */

exit(0);

程序8 – 6输出两个字符串:一个由子进程输出,一个由父进程输出。因为输出依赖于内核使进程运行的顺序及每个进程运行的时间长度,所以该程序包含了一个竞态条件。程序具有竞争条件的程序。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

static void charactatime(char *str)
{
char *ptr;
int c;
setbuf(stdout, NULL); //set unbuffered
for (ptr = str; (c = *ptr++) != 0;)
putc(c, stdout);
}

int main(void)
{
pid_t pid;

if ((pid = fork()) < 0)
perror("fork error");
else if (pid == 0) //child
charactatime("output from childn");
else
charactatime("output from parentn");

exit(0);
}

由于setbuf(stdout, NULL);     //set unbuffered所以每个字符输出都需要调用一次write函数。这样就会导致输出是不可控的。

如果加入我们刚开始讨论的子进程要等父进程操作完成后或者相反的代码,就能得到预期的输出,两个进程的输出不再交叉混合。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

static void charactatime(char *str)
{
char *ptr;
int c;
setbuf(stdout, NULL); //set unbuffered,so every character will call write
for (ptr = str; (c = *ptr++) != 0;)
putc(c, stdout);
}

int main(void)
{
pid_t pid;

TELL_WAIT();

if ((pid = fork()) < 0)
perror("fork error");
else if (pid == 0) //child
{
WAIT_PARENT();
charactatime("output from childn");

} else {
charactatime("output from parentn");
TELL_CHILD();
}
exit(0);
}