当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程进行的顺序时,则我们认为这发生了竞争条件。一个例子是在fork出子进程之后,父、子进程的执行顺序是不确定的,这种不确定决定了后面程序的结果,那么这便产生了一个竞争条件。
如果一个进程希望等待一个子进程终止,则它必须调用wait或waitpid函数。如果一个进程要等待其父进程终止,则可使用如下的轮询方式:
while (getppid() != 1)
sleep(1);
这样的轮询需要休眠、唤醒等工作,很显然浪费了CPU资源。
如果要消除竞争条件而又不想使用轮询,则有如下两种方法:
- 信号机制
- 进程间通信(IPC)
这两种机制以后再讲,先来看看一个包含竞争条件的程序:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
static void charatatime(char *str)
{
char *ptr;
int c;
setbuf(stdout, NULL); /* 标准输出设置为不带缓冲 */
for (ptr = str; (c = *ptr++) != 0; )
putc(c, stdout);
}
int main(void)
{
pid_t pid;
pid = fork();
if (pid == 0)
charatatime("output from child\n");
else
charatatime("output from parent\n");
return 0;
}
上述程序由fork产生了一个竞争条件。把标准输出的缓冲区大小设为了0,每输入一个字符就调用一次write系统调用,增大了进程运行时间,为的是让两个进程尽可能的切换。
运行结果:
可以看到,父、子进程交替输出结果。
解决办法1:使用信号
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
static volatile sig_atomic_t sigflag;
static sigset_t newmask, oldmask, zeromask; /* 定义三个信号集 */
static void sig_usr(int signo)
{
sigflag = 1;
}
void TELL_WAIT(void)
{
if (signal(SIGUSR1, sig_usr) == SIG_ERR)
exit(-1);
if (signal(SIGUSR2, sig_usr) == SIG_ERR)
exit(-1);
sigemptyset(&zeromask); /* 初始化信号集 */
sigemptyset(&newmask); /* 初始化信号集 */
sigaddset(&newmask, SIGUSR1); /* 向信号集中添加信号 */
sigaddset(&newmask, SIGUSR2); /* 向信号集中添加信号 */
/* 更改信号屏蔽字,阻塞SIGUSR1和SIGUSR2两个信号
* SIG_BLOCK表示新的信号屏蔽字是当前信号屏蔽字和第二个参数newmask的并集
* oldmask保存当前的信号屏蔽字,以便稍后恢复用
*/
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
exit(-1);
}
void TELL_PARENT(pid_t pid)
{
kill(pid, SIGUSR2);
}
void WAIT_PARENT(void)
{
while (sigflag == 0)
sigsuspend(&zeromask); /* 将信号屏蔽字设置为zeromask并在捕捉到信号之前挂起进程 */
sigflag = 0;
/* 恢复信号屏蔽字
* SIG_SETMASK表示新的信号屏蔽字为由oldmask参数提供
*/
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
exit(-1);
}
void TELL_CHILD(pid_t pid)
{
kill(pid, SIGUSR1);
}
void WAIT_CHILD(void)
{
while (sigflag == 0)
sigsuspend(&zeromask);
sigflag = 0;
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
exit(-1);
}
static void charatatime(char *str)
{
char *ptr;
int c;
setbuf(stdout, NULL); /* 标准输出设置为不带缓冲 */
for (ptr = str; (c = *ptr++) != 0; )
putc(c, stdout);
}
int main(void)
{
pid_t pid;
TELL_WAIT();
pid = fork();
if (pid == 0)
{
WAIT_PARENT();
charatatime("output from child\n");
}
else
{
charatatime("output from parent\n");
TELL_CHILD(pid);
waitpid(pid, NULL, 0);
}
return 0;
}
这里使用到了信号集的概念,把若干个信号放入一个以sigset_t类型表示的信号集中,然后调用相关接口实现了父、子进程的同步。运行结果如下:
解决办法2:使用管道
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
static int pfd1[2], pfd2[2];
void TELL_WAIT(void)
{
pipe(pfd1); /* 建立管道1 */
pipe(pfd2); /* 建立管道2 */
}
void TELL_PARENT(pid_t pid)
{
if (write(pfd2[1], "c", 1) != 1) /* 写输出描述符 */
exit(-1);
}
void WAIT_PARENT(void)
{
char c;
if (read(pfd1[0], &c, 1) != 1) /* 读输入描述符 */
exit(-1);
if (c != 'p')
exit(-1);
}
void TELL_CHILD(pid_t pid)
{
if (write(pfd1[1], "p", 1) != 1) /* 写输出描述符 */
exit(-1);
}
void WAIT_CHILD(void)
{
char c;
if (read(pfd2[0], &c, 1) != 1) /* 读输入描述符 */
exit(-1);
if (c != 'c')
exit(-1);
}
static void charatatime(char *str)
{
char *ptr;
int c;
setbuf(stdout, NULL); /* 标准输出设置为不带缓冲 */
for (ptr = str; (c = *ptr++) != 0; )
putc(c, stdout);
}
int main(void)
{
pid_t pid;
TELL_WAIT();
pid = fork();
if (pid == 0)
{
WAIT_PARENT();
charatatime("output from child\n");
}
else
{
charatatime("output from parent\n");
TELL_CHILD(pid);
waitpid(pid, NULL, 0);
}
return 0;
}
上述程序会建立如下进程关系:
处于等待的子进程调用read函数并阻塞,等待父进程发送字符。运行结果如下:
参考:
《unix环境高级编程》 P185-P188、P256-P273、P398-P403.