pause函数
调用该函数可以造成进程主动挂起,等待信号唤醒。调用该系统调用的进程将处于阻塞状态(主动放弃cpu) 直到有信号递达将其唤醒。
int pause(void);
返回值:-1 并设置errno为EINTR
返回值:
① 如果信号的默认处理动作是终止进程,则进程终止,pause函数没有机会返回。
② 如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回。
③ 如果信号的处理动作是捕捉,则调用完信号处理函数之后,pause返回-1,errno设置为EINTR,表示“被信号中断”。
④ pause收到的信号不能被屏蔽,如果被屏蔽,那么pause就不能被唤醒。
例:使用pause和alarm来实现sleep函数。
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
void sig_deal(int signo)
{
}
unsigned int mysleep(unsigned int n)
{
struct sigaction newact, oldact;
unsigned int ret;
newact.sa_handler = &sig_deal;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);//SIGALRM信号回调函数
alarm(n);
int pause_ret=pause();
if(pause_ret==-1&&errno==EINTR)
printf("pause success\n");
ret = alarm(0);//取消定时器,返回旧闹钟余下秒数。
sigaction(SIGALRM, &oldact, NULL);//恢复SIGALRM信号旧有的处理方式
return ret;
}
int main(void)
{
while(1){
mysleep(2);
printf("2s passed\n");
}
return 0;
}
时序问题分析
回顾,借助pause和alarm实现的mysleep函数。设想如下时序:
1.注册SIGALRM信号处理函数(sigaction…)
2.调用alarm(1) 函数设定闹钟1秒。
3.函数调用刚结束,开始倒计时1秒。当前进程失去cpu,内核调度优先级更高的进程(有多个)取代当前进程。当前进程无法获得cpu,进入就绪态等待cpu。
4.1秒后,闹钟超时,内核向当前进程发送SIGALRM信号(自然定时法,与进程状态无关),高优先级进程尚未执行完,当前进程仍处于就绪态,信号无法处理(未决)
5.优先级高的进程执行完,当前进程获得cpu资源,内核调度回当前进程执行。SIGALRM信号递达,信号设置捕捉,执行处理函数sig_deal。
6.信号处理函数执行结束,返回当前进程主控流程,pause()被调用挂起等待。(欲等待alarm函数发送的SIGALRM信号将自己唤醒)
7.SIGALRM信号已经处理完毕,pause永远不会等到。
解决时序问题
可以通过设置屏蔽SIGALRM的方法来控制程序执行逻辑,但无论如何设置,程序都有可能在“解除信号屏蔽”与“挂起等待信号”这个两个操作间隙失去cpu资源。除非将这两步骤合并成一个“原子操作”。sigsuspend函数具备这个功能。在对时序要求严格的场合下都应该使用sigsuspend替换pause。
int sigsuspend(const sigset_t *mask);
sigsuspend函数调用期间,进程信号屏蔽字由其参数mask指定。
可将某个信号(如SIGALRM)从临时信号屏蔽字mask中删除,这样在调用sigsuspend时将解除对该信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值。
例:改进后的mysleep。
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
void sig_alrm(int signo)
{
/* nothing to do */
}
unsigned int mysleep(unsigned int n)
{
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;
unsigned int unslept;
//1.为SIGALRM设置捕捉函数,一个空函数
newact.sa_handler = sig_alrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);
//2.设置阻塞信号集,阻塞SIGALRM信号
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask); //信号屏蔽字,阻塞SIGALRM信号
//3.定时n秒,到时后可以产生SIGALRM信号
alarm(n);
/*4.构造一个调用在sigsuspend函数执行过程中临时有效的阻塞信号集suspmask,
* 在临时阻塞信号集里解除SIGALRM的阻塞*/
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM);
/*5.sigsuspend调用期间,采用临时阻塞信号集suspmask替换原有阻塞信号集
* 这个信号集中不包含SIGALRM信号,同时挂起等待,
* 当sigsuspend被信号唤醒返回时,恢复原有的阻塞信号集*/
sigsuspend(&suspmask); //原子操作
unslept = alarm(0);
//6.恢复SIGALRM原有的处理动作
sigaction(SIGALRM, &oldact, NULL);
//7.解除对SIGALRM的阻塞
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return unslept;
}
int main(void)
{
while(1)
{
mysleep(2);
printf("2s passed\n");
}
return 0;
}
整理一下,其实解决该问题的核心思路就是先把SIGALRM信号屏蔽,然后利用sigsuspend函数(系统调用是原子操作)把解除SIGALRM信号屏蔽和执行该信号的回调函数这两步一气呵成。
但是我认为,这个函数可以解决时序竞态,但是定时的时间肯定就不准了(如果信号发出的时候,当前进程没有拿到cpu,那么虽然该信号被阻塞了,但是当前进程还是要等待cpu,接下来等到当前进程拿到cpu,然后sigsuspend也顺利完成了任务,但是总时间变长了)。
总结
竞态条件,跟系统负载有很紧密的关系,体现出信号的不可靠性。系统负载越严重,信号不可靠性越强。
不可靠由其实现原理所致。信号是通过软件方式实现(跟内核调度高度依赖,延时性强),每次系统调用结束后,或中断处理处理结束后,需通过扫描PCB中的未决信号集,来判断是否应处理某个信号。当系统负载过重时,会出现时序混乱。
这种意外情况只能在编写程序过程中,提早预见,主动规避,而无法通过gdb程序调试等其他手段弥补。且由于该错误不具规律性,后期捕捉和重现十分困难。