一、pause函数
#include <unistd.h>
int pause(void);
/*
返回值(成功):-1, 并设置errno为EINTR
1、如果信号的默认处理动作是终止进程,则进程终止,pause函数没有机会返回。
2、如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回。
3、如果信号的处理动作是捕捉,则调用完信号处理函数之后,pause返回-1。
4、pause收到的信号不能被屏蔽,如果被屏蔽,那么pause就不能被唤醒。
*/
调用该函数可以造成进程主动挂起,等待信号唤醒。调用该系统调用的进程将处于阻塞状态(主动放弃CPU)直到有信号送给大家将其唤醒。
二、什么是时序竟态?
利用alarm+pause实现sleep功能:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <errno.h>
void catch_sigalrm(int signo)
{
}
unsigned int my_sleep(unsigned int seconds)
{
int ret;
struct sigaction act, oldact;
act.sa_handler = catch_sigalrm;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
// 注册SIGALRM处理函数
ret = sigaction(SIGALRM, &act, &oldact);
if (ret == -1)
{
perror("sigaction error");
exit(EXIT_FAILURE);
}
// 使用alarm+pause实现sleep功能
alarm(seconds);
// -----------这里可能会失去CPU,导致SIGALRM已经发出,但下面的pause还没有调用
ret = pause();
if (ret == -1 && errno == EINTR)
{
printf("pause sucess\n");
}
// 如果在等待期间有其它信号被捕捉,则关闭闹钟,保证程序的健壮性。
ret = alarm(0);
// 恢复SIGALRM默认处理函数
sigaction(SIGALRM, &oldact, NULL);
// 返回闹钟等待的时长
return(seconds-ret);
}
int main()
{
while(1) printf("---------sleep %u seconds-------\n", my_sleep(1));
return 0;
}
上面的代码中存在时序竟态的问题。设想如下场景:
想睡觉,定闹钟10分钟,希望10分钟后闹钟将自己唤醒。
正常:定时,睡觉,10分钟后被闹钟唤醒。
异常:闹钟定好后,睡了5分钟被人喊醒外出劳动(失去CPU),20分钟后劳动结束。回来继续睡觉计划,但劳动期间闹钟已经响过(alarm函数已经发送过了信号),不会再将我唤醒。
三、时序问题分析
同理,回顾借助pause和alarm实现的my_sleep函数。设想如下时序:
- 注册
SIGALRM
信号处理函数(sigaction...
)- 调用
alarm(1)
函数设定闹钟为1秒。- 函数调用刚结束,开始倒计时为1秒。当前进程失去CPU,内核调度优先级高的进程(有多个)取代当前进程。当前进程无法获得CPU,进入就绪态等待CPU。
- 1秒后,闹钟超时,内核向当前进程发送
SIGALRM
信号(自然定时法,与进程状态无关),高优先级进程尚未执行完,当前进程仍处于就绪态,信号无法处理(未决)。- 优先级高的进程执行完,当前进程获得CPU资源,内核调度回当前进程执行。
SIGALRM
信号递达,信号设置捕捉,执行处理函数。- 信号处理函数执行结束,返回当前进程主控流程,
pause()
被调用挂起等待。(欲等待alarm
函数发送的SIGALRM
信号将自己唤醒)- 可
SIGALRM
信号已经处理完毕,pause
不会等到想要的信号,这样就会一起等下去了。
四、解决时序问题:sigsuspend函数
屏蔽SIGALRM // 这样信号就能阻塞住
alarm(seconds);
// ------------------失去CPU
解除屏蔽
// ------------------这里也可能失去CPU
ret = pause(); //主动挂起,等信号
可以通过设置屏蔽
SIGALRM
的方法来控制程序执行逻辑,但无论如何设置,程序都有可能在解除信号屏蔽
与挂起等待信号
这两个操作间隙失去CPU资源。除非将这两个步骤合并成一个原子操作
。sigsuspend
函数具备这个功能。在对时序要求严格的场合下都应用使用sigsuspend
替换pause
。
#include <signal.h>
int sigsuspend(const sigset_t *mask); // 挂起等待信号。
/*
RETURN VALUE
sigsuspend() always returns -1,
with errno set to inndicate the error (normally, EINTR).
*/
sigsuspend函数调用期间,进程信号屏蔽字由其参数mask指定。
可将某个信号(如
SIGALRM
)从临时信号屏蔽字mask
中删除,这样在调用sigsuspend
时将解除对信号的屏蔽,然后挂起等待。 当sigsuspend
返回时,进程的信号屏蔽字恢复为原来的值。如果原来对该信号是屏蔽态,sigsuspend
函数返回后仍然屏蔽该信号。
改进版my_sleep
:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
void catch_sigalrm(int signo)
{
}
unsigned int my_sleep(unsigned int seconds)
{
int ret;
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspendmask;
unsigned int unslept;
// 1、为SIGALRM设置捕捉函数
newact.sa_handler = catch_sigalrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
ret = sigaction(SIGALRM, &newact, &oldact);
if (ret == -1)
{
perror("sigaction error");
exit(EXIT_FAILURE);
}
// 2、设置阻塞信号集,阻塞SIGALRM信号
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
// 3、定时n秒,到时后可以生产SIGALRM信号
alarm(seconds);
// 4、构造一个调用sigsuspend临时有效的阻塞信号集,解除对SIGALRM的阻塞
suspendmask = oldmask;
sigdelset(&suspendmask, SIGALRM); // 原子操作
// 5、sigsuspend调用期间,采用临时阻塞信号集suspendmask替换原有的阻塞信号集
// 这个信号集中不包含SIGALRMM信号,同时挂起等待,
// 这样,即使在此之前失去CPU,闹钟已经发送了SIGALRM信号,信号会被阻塞住。
// sigsuspend调用时解除了对SIGALRM的阻塞,会捕捉到信号,恢复程序运行。
sigsuspend(&suspendmask);
// 获得上次闹钟剩余时间
unslept = alarm(0);
// 6、恢复SIGALRM原有的处理动作,呼应前面注释1
// 当sigsuspend被信号唤醒返回时,恢复原有的阻塞信号集。
sigaction(SIGALRM, &oldact, NULL);
// 7、解除对SIGALRM的阻塞,呼应前面注释2
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return(seconds-unslept);
}
int main()
{
while(1) printf("---------sleep %u seconds-------\n", my_sleep(1));
return 0;
}
五、总结
竟态条件,跟系统负载有很紧密的关系,体现出信号的不可靠性。系统负载越严重,信号不可靠性越强。
不可靠由其实现原理所致。信号是通过软件方式实现(跟内核调度高度依赖,延时性强),每次系统调用结束后,或中断处理处理结束后,需通过扫描PCB中的未决信号集,来判断是否处理某个信号,当系统负载过重时,会出现时序混乱。
这种意外情况只能在编写程序过程中,提早预见,主动规避,而无法通过GDB程序高度等其他手段弥补。且由于该错误不具规律性,后期捕捉和重现十分困难。