通过一个mysleep程序来引入时序竟态产生的原因:
mysleep通过alarm函数定时,pause函数挂起的捆绑应用,来实现sleep函数的功能
1 #include<stdio.h>
2 #include<signal.h>
3 #include<errno.h>
4 #include<unistd.h>
5 #include<stdlib.h>
6 void catch_sigalarm(int signo)
7 {
8
9 }
10 unsigned int mysleep(unsigned int sec)
11 {
12 int num;
13 struct sigaction act,oldact;
14
15 //由于pause函数只有对信号的处理方式为捕捉的情况下才会成功返回,所以要向内核注册一个捕捉函数cath_sigalarm;
16 act.sa_handler = catch_sigalarm;//捕捉函数赋值
17 sigemptyset(&act.sa_mask);//设置捕捉函数执行期间的mask(信号屏蔽字)。
18 act.sa_flags = 0;//在捕捉函数执行期间,对该信息采取默认处理方式(自动屏蔽本信号)
19 num = sigaction(SIGALRM,&act,&oldact); //注册对SIGALRM信号的捕捉函数
20 if(num == -1)
21 {
22 perror("sigaction error");
23 exit(1);
24 }
25
26 alarm(sec);//定时sec秒后,向该进程发送SIGALRM信号。
27 num = pause();//挂起等待信号
28 //pause成功返回-1,且将errno置为EINTE
29 if(num == -1 && errno == EINTR)//验证pause成功返回的方式
30 {
31 printf("pause success\n");
32 }
33
34 alarm(0);//如果pause函数在alarm计时期间因为其他信号而被唤醒,则需要对alarm进行清零操作,避免它到时再发送信号。
35 sigaction(SIGALRM,&oldact,NULL); //恢复SIGALRM信号默认的处理方式
36
37 return num;
38 }
39
40 int main(void)
41 {
42 while(1){
43 printf("------------\n");
44 mysleep(5);
45 }
46 return 0;
47 }
如果进程在alarm函数定时完后,由于时间片轮完失去CPU,也就是在26行到27行之间失去了cpu的使用权。如果在进程失去cpu这段时间里alarm计时完成发送信号,当进程再次占有cpu时,此时信号已抵达,内核会帮助进程处理该信号,处理完后程序继续执行到27行,此时执行pause函数将进程挂起,但是由于alarm早已将信号发出,在进程挂起后并收不到信号,所以进程将一直挂起。
虽然上述例子比较极端,发生概率极低,但是如果你的程序是服务类程序,运行在电脑上之后不会停止,在运行数千万次中只要出现一次这种情况就是致命的,所以我们在写程序是应当考虑该情况并避免。
我们如何避免这种情况呢?我们已经知道该问题出现的原因是因为在pause函数将进程挂起前信号就已经抵达并且被处理,然后才调用pause函数,导致pause无法收到信号被唤醒,所以我们只要使信号抵达后在被内核处理的同时执行pause函数就行了,这样内核调用捕捉函数后,pause可以成功返回。
总结起来就是使处理信号和挂起进程成为一次原子操作
为了解决上述问题,linux为我们专门提供了系统函数 sigsuspend(),其函数原型为int sigsuspend(const sigset_t *mask);
mask为在函数sigsuspend执行期间设置的信号屏蔽字。
下面通过代码来具体说明如何利用sigsuspend函数来避免时序竟态问题。
1 #include<stdio.h>
2 #include<signal.h>
3 #include<errno.h>
4 #include<unistd.h>
5 #include<stdlib.h>
6 void catch_sigalarm(int signo)
7 {
8
9 }
10 unsigned int mysleep(unsigned int sec)
11 {
12 int num;
13 sigset_t newset,oldset,susset;
14 struct sigaction act,oldact;
15
16 //为SIGALRM设置捕捉函数
17 act.sa_handler = catch_sigalarm;
18 sigemptyset(&act.sa_mask);
19 act.sa_flags = 0;
20 num = sigaction(SIGALRM,&act,&oldact);
21 if(num == -1)
22 {
23 perror("sigaction error");
24 exit(1);
25 }
26
27 //设置信号屏蔽字将SIGALRM屏蔽
28 sigemptyset(&newset);
29 sigaddset(&newset,SIGALRM);
30 sigprocmask(SIG_BLOCK,&newset,&oldset);//将SIGALRM信号屏蔽
31
32 //设置susset解除SIGALRM的屏蔽
33 susset = oldset;
34 sigdelset(&susset,SIGALRM);//去掉对SIGALRM的屏蔽
35
36 alarm(sec);//定时sec秒后产生SIGALRM信号
37 //在sigsuspend函数调用之前进程对SIGALRM信号屏蔽
38 sigsuspend(&susset);//在sigsuspend调用后由于设置了susset没有对SIGALRM进行屏蔽,则信号抵达
39 //这个时候就实现了进程挂起且信号抵达,因为sigsuspend函数调用后进程就挂起了,又由于设置了susset对SIGALRM解除了屏蔽,信号抵达。
40
41 alarm(0);
42 sigaction(SIGALRM,&oldact,NULL); //恢复SIGALRM信号默认的处理方式
43
44 sigprocmask(SIG_SETMASK,&oldset,NULL);//恢复信号屏蔽字
45 return num;
46 }
47
48 int main(void)
49 {
50 while(1){
51 printf("------------\n");
52 mysleep(5);
53 }
54 return 0;
55 }