当多个进程都企图对共享数据进行某种处理, 而最后的结果又取决于进程运行的顺序时, 我们认为发生了竞争条件. 如果 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
只是其中的一个小函数存在这种功能, 还有许许多多的函数需要我们去了解. 我们目前位置先掌握竞态条件什么, 我们再写程序的时候尽量避免竞
态条件的生成, 如果没有办法避免就要查找相关函数, 尽量保证关键操作的原子性.
来源: http://blog.csdn.net/dawn_sf/article/details/79105706