竞争条件
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,我们认为发生了竞争条件. 如果fork之后的某
种逻辑显示
或
者隐式的依赖于在fork之后是父进程先运行还是子进程先运行,那么fork函数就会使竞争条件活跃的滋生地.通常 我们不
能预料到哪一个进程先运
行
,即使我们知道那一个进程先运行,在该进程开始运行后所发生的事情也依赖于系统负载以及内核的调度算
法.
我们都知道如果一个进程fork了一个子进程,但不要它等待子进程终止,也不希望子进程处于僵死状态知道父进程终止,实现这个
要求
的诀窍就是
调
用fork两次. 相关代码我放到下面:
/*************************************************************************
> File Name: fork.c
> Author: ma6174
> Mail: ma6174@163.com
> Created Time: Thu 18 Jan 2018 09:54:34 PM PST
************************************************************************/
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<errno.h>
int main()
{
pid_t pid;
if((pid = fork()) < 0)
perror("fork error");
else if(pid == 0)
{
if((pid = fork()) < 0)
perror("fork error");
else if(pid > 0)
exit(0);
sleep(2);
printf("second child , parent pid = %d\n",(int)getppid());
exit(0);
}
if(waitpid(pid,NULL,0) != pid)
perror("waitpid error\n");
exit(0);
}
最后来看一下我们的结果是什么:
当第二个子进程打印其父进程ID时,我们看到了一个潜在的竞争条件. 如果第二个子进程在第一个子进程之前运行,则其父进程将会是
第一个子进
程. 但是,如果第一个子进程先运行,并有足够的时间到达并执行exit.,则第二个子进程的父进程就是init. 即使在程
序中调用sleep,也不能保
证什么, 如果系统负载很重,那么在slee返回之后,第一个子进程得到机会运行之前,第二个子进程可能恢复
运行. 这种形式的问题很难调试,因
为在大部分时间,这种问题并不出现.
如果一个进程希望等待一个子进程终止,则它必须调用wait函数中的一个. 如果一个进程要等待其父进程终止,则可使用下列形式的循环:
while(getppid() != 1)
sleep(1);
这种形式的循环称为轮询,他的问题是浪费了cpu时间,因为调用者每隔一秒都被唤醒,然后进行条件测试.
为了避免竞争条件和轮询,在多个进程之间需要有某种形式的信号发送和接受方法. 在unix中可以使用信号机制。接下来我们就开始介
绍到!
优化版本的mysleep
我们再上一个博客信号捕捉当中实现了一个我们自己认为很不错的一个mysleep函数,并且可以正常运行! 但是其实我们实现的sleep
就
会有
竞争的情况出现,比如我们出现了下面的时序步骤:
1.注册SIGALRM信号的处理函数.
2.调用alarm(nsecs)设置闹钟.
3.内核调度优先级更高的进程取代了当前进程的执行,并且优先级更高的进程有好多个,每个都要执行很长时间.
4.nsecs秒钟之后闹钟超时了,内核发送SIGALRM信号给这个进程,处于未决状态.
5.优先级更高的进程执行完了,内核要调度回这个进程执行,SIGALRM信号递达,执行处理函数sig_alrm之后再次进入内核.
6.返回这个进程的主控制流程,alarm返回,调用pause()挂起等待.
7.可是SIGALRM信号已经处理完了,还等待什么呢?
这个就是一个很标准的我们的竞态条件,出现这个问题的根本原因就是系统运行的时序并不像我们写程序时所设想的那些. 虽然alarm
紧接着的下一
行就是pause(),但是无法保证pause()一定会调用alarm(nsecs)之后的nsecs秒之内被调用. 由于异步事件在任何时候
都有可能发送(优先级级更高)
的进程,如果我们写程序时考虑不够周密,就有可能出现时序问题,而导致错误. 那么我们的sleep应该
如何解决的这个问题呢???
这时候有人提出来了既然你是因为在pause之前处理掉了SIGALRM信号,那么我可不可以让进程在调用pause函数之前,将SIGALRM信号
阻塞,当使用
pause函数的时候再取消阻塞,这样想没有错! 但是!优先级更高的进程替换你是随时随刻的,比如刚刚取消信号的
阻塞,又被切换走了,还是有
可能发生竞态条件. 如果能够让对信号取消阻塞和调用pause同时运行该有多好! 也就是一个原子操作
多好啊! 还别说! 真的有这种操作:
sigsuspend函数
#include <signal.h>
int sigsuspend(const sigset_t *mask);
和pause一样,sigsuspend没有成功返回值,只有执行了一个信号处理函数之后sigsuspend才返回,返回值为-1,errno设置为EINTR.
调用sigsuspend时,进程的信号屏蔽字由sigmask参数指定,可以通过指定sigmask来临时解除对某个信号的阻塞,然后挂起等待,
当sigsuspend返
回时,进程的信号屏蔽字恢复为原来的值,如果原来对信号是阻塞的,从sigsuspend返回后仍然是阻塞的.
接下来,我们就使用这个函数对我们的sleep重新编写,让它更加强大!
/*************************************************************************
> File Name: mysleep2.c
> Author: ma6174
> Mail: ma6174@163.com
> Created Time: Thu 18 Jan 2018 11:29:18 PM PST
************************************************************************/
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void sig_alrm(int sig)
{
//DO SOMETHING
}
unsigned int mysleep(unsigned int nsecs)
{
struct sigaction new,old;
sigset_t newmask,oldmask,suspendmask;
unsigned int unslept = 0;
new.sa_handler = sig_alrm;
sigemptyset(&new.sa_mask);
new.sa_flags = 0;
sigaction(SIGALRM,&new,&old);
sigemptyset(&newmask);
sigaddset(&newmask,SIGALRM);
sigprocmask(SIG_BLOCK,&newmask,&oldmask);
alarm(nsecs);
suspendmask = oldmask;
sigdelset(&suspendmask,SIGALRM);
sigsuspend(&suspendmask);
unslept = alarm(0);
sigaction(SIGALRM,&old,NULL);
sigprocmask(SIG_BLOCK,&oldmask,NULL);
return unslept;
}
int main()
{
while(1)
{
mysleep(1);
printf("1 seconds passed\n");
}
}
这就是一种常见的解决竞争条件的方法,当然我们看到了最有效的方法就是让好几个操作变成一个原子性的集中操作. 我们所
学习到的sigsuspend
只是其中的一个小函数存在这种功能,还有许许多多的函数需要我们去了解. 我们目前位置先掌握竞态
条件什么,我们再写程序的时候尽量避免竞
态条件的生成,如果没有办法避免就要查找相关函数,尽量保证关键操作的原子性.