可以通过设置屏蔽SIGALRM的方法来控制程序执行逻辑,但无论如何设置,程序都有可能在“解除信号屏蔽”与“挂起等待信号”这个两个操作间隙失去cpu资源。除非将这两步骤合并成一个“原子操作”。sigsuspend函数具备这个功能。在对时序要求严格的场合下都应该使用sigsuspend替换pause。信号被解除屏蔽和进程挂起等待(sigsuspend函数执行)是同时进行的,为原子操作,不可分割。
int sigsuspend(const sigset_t *mask); //其作用与返回值都与pause函数一样(-1,EINTR)。
sigsuspend函数调用期间,进程信号屏蔽字由其参数mask指定,该mask代替了PCB中的信号屏蔽字,当函数执行完(即返回-1)后,信号屏蔽字才会恢复作用。
可将某个信号(如SIGALRM)从临时信号屏蔽字mask中删除,这样在调用sigsuspend时将解除对该信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值。如果原来对该信号是屏蔽态,sigsuspend函数返回后仍然屏蔽该信号。
在单核处理器中,一条指令能完成的操作是原子的(read、write、printf对应N多条汇编指令自然不是原子操作),因为中断是发生指令之间的。多条指令之间基本都是通过lock来实现原子操作,将这多条指令绑定为一个整体,不可分割,即原语。原语就是由若干条指令组成的,用于完成一定功能的一个过程,原语在执行过程中不允许被中断(类似一条指令)。原语在系统态下执行,常驻内存。在内核中有许多原语,如进程创建原语creat、挂起原语suspend、激活原语active、阻塞原语block和唤醒原语wakeup等。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 nsecs)
{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); //信号屏蔽字mask//3.定时n秒,到时后可以产生SIGALRM信号alarm(nsecs);/*4.构造一个调用sigsuspend临时有效的阻塞信号集,* 在临时阻塞信号集里解除SIGALRM的阻塞*/suspmask = oldmask;sigdelset(&suspmask, SIGALRM);/*5.sigsuspend调用期间,采用临时阻塞信号集suspmask替换原有阻塞信号集* 这个信号集中不包含SIGALRM信号,同时挂起等待,* 当sigsuspend被信号唤醒返回时,恢复原有的阻塞信号集*/sigsuspend(&suspmask);unslept = alarm(0);//6.恢复SIGALRM原有的处理动作,呼应前面注释1(思想)sigaction(SIGALRM, &oldact, NULL);//7.解除对SIGALRM的阻塞,呼应前面注释2(思想)sigprocmask(SIG_SETMASK, &oldmask, NULL);return(unslept);
}int main(void)
{while(1){mysleep(2);printf("Two seconds passed\n");}return 0;
}
时序竞态总结
竞态条件,跟系统负载有很紧密的关系,体现出信号的不可靠性。系统负载越严重,信号不可靠性越强。不可靠由其实现原理所致。信号是通过软件方式实现(跟内核调度高度依赖,延时性强),每次系统调用结束后,或中断处理处理结束后,需通过扫描PCB中的未决信号集,来判断是否应处理某个信号。当系统负载过重时,会出现时序混乱。
其本质原因就是进程随时都有可能会失去CPU,失去CPU时间太长导致信号的处理与进程中主控程序的执行顺序发生了改变,从而产生了不符合预期的结果,出现错误。如上面的pause函数本应该在信号处理之前进行,但是延时太长,导致pause函数执行时,信号提前被处理了。这都是因为信号的处理与主控程序的执行时序不一样产生了不一样的结果。这种错误只有程序员提前遇见,主动规避。
这种意外情况只能在编写程序过程中,提早预见,主动规避,而无法通过gdb程序调试等其他手段弥补。且由于该错误不具规律性,后期捕捉和重现十分困难。