信号的捕捉在Linux下机制:如图
由此可知此机制下发生了四次的模式切换:用户态--->内核态、内核态--->用户态、用户态--->内核态、内核态--->用户态,从中也可以看出进程处理信号的时机是从内核态切回到用户态时候,若这时有信号可以递达则去执行其自定义处理动作,具体执行完成以后则在返回到用户态main函数继续上下文执行,若处理动作为退出则就直接退出。
注意:自定义捕捉函数和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
信号捕捉函数:
#include <signal.h>
int sigaction(int signo,const struct sigaction* act,struct sigaction* oact); //读取或修改与指定信号相关联的处理动作
参数:
signo:信号编号
act:若act指针非空,则根据act修改该信号的处理动作
oact:若oact指针非空,则通过oact传出该信号原来的处理动作
act和oact指向sigaction结构体:
struct sigaction
{
void (*sa_handler)(int); //函数指针指向自定义捕捉函数或赋值与默认或忽略动作
sigset_t sa_mask; //通过此信号集参数可屏蔽当前进程中别的信号
sa_flags; //默认为0
void (*sa_sigaction)(int,siginfo_t*,void*); //实时信号函数指针
};
返回值:
调用成功则返回0,出错则返回- 1;
注意:
将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号;
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止;
如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字.
pause函数:
#include <unistd.h>
int pause(void);
说明:使当前进程挂起直至收到一个自定义捕捉信号出错返回;
若收到信号的处理动作是终止进程,则进程终止,pause函数没有机会返回;如果信号的处理动作是忽略,则进程继续于挂起状态,pause不返回;如果信号的处理动作是捕捉,则调用了信号处理函数之后pause返回-1,errno设置为EINTR, 所以pause只有出错的返回值。
运用以上函数与以前知识模拟实现sleep函数:
代码如下:
mysleep.c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void myhandler(int sig)
{
}
int mysleep(int seconds)
{
struct sigaction act;
struct sigaction oact;
act.sa_handler=myhandler;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGALRM,&act,&oact); //用此函数自定义信号SIGALRM捕捉函数
alarm(seconds); //设定闹钟函数
pause(); //挂起操作直至收到自定义捕捉函数的信号
sigaction(SIGALRM,&oact,&act); //恢复SIGALRM信号以前动作
int ret=alarm(0); //取消闹钟
return ret; //返回以前设定闹钟的剩余秒数
}
int main()
{
while(1)
{
int ret=mysleep(3); //自定义实现sleep()函数
printf("mysleep done......%d\n",ret);
}
}
运行结果如下:
在以上实现中虽然实现了sleep功能,但其有一个问题:
1.当在代码中调用alarm(seconds)时,这时并没有执行下一条指令pause()使进程挂起;
2.这时若有内核调度优先级更高的进程取代当前进程执行,并且优先级更高的进程有很多个,每个都要执行很长时间;
3.当seconds秒之后闹钟超时了,内核发送SIGALRM信号给被取代切出的进程,其信号处于未决状态;
4.当优先级更高的进程执行完了,内核调度会使被取代切出进程执行,这时SIGALRM信号递达,执行其自定义捕捉函数之后再次进入内核;
5.最后返回这个进程的主控制流程,alarm(seconds)返回,再调用pause()挂起等待;
但这时由于别的进程优先级高影响了此进程调度时间,使alarm()函数seconds秒发送的SIGALRM信号已经处理完成,所以pause使进程挂起后将不会在收到自定义捕捉的信号。
竞态条件:以上alarm()紧接着的下一行就是pause(),但是无法保证pause()一定会在调用alarm()之后的seconds秒之内被调用,是由于异步事件在任何时候都有可能发生(这里异步事件指出现更高优先级的进程),从而可能由于时序问题而导致错误,这叫做竞态条件.
解决以上问题:可以使在调用pause()之前一直使SIGALRM信号阻塞(即使当pause()调用alarm()之后的seconds秒之内没有被调用,但由于信号阻塞它也不会递达),直至在调用pause时取消其阻塞并使进程挂起(合成原子操作)
用以下函数实现:
int sigsuspend(const sigset_t *sigmask);//解除对某信号屏蔽并使当前进程挂起
参数:
sigmask:进程的信号屏蔽字由sigmask参数指定,可以通过指定sigmask来临时解除当前进程中对某个信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值(即原来对该信号是屏蔽的,从sigsuspend返回后仍然是屏蔽的).
返回值:
与pause相同,sigsuspend没有成功返回值,只有执行了一个信号处理函数之后sigsuspend才返回,返回值为-1,errno设置为EINTR。
依此来实现sleep函数解除竞态条件问题的优化:
SafeMysleep.c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
//实现解决时序问题引起的竞态条件问题的sleep()函数
void myhandler(int sig)
{
}
int mysleep(int seconds)
{
struct sigaction act;
struct sigaction oact;
act.sa_handler=myhandler;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGALRM,&act,&oact); //定义信号SIGALRM的自定义捕捉函数
sigset_t newset,oldset,sigmask;
sigemptyset(&newset);
sigemptyset(&oldset);
sigaddset(&newset,SIGALRM);
sigprocmask(SIG_BLOCK,&newset,&oldset); //阻塞信号SIGALRM
alarm(seconds); //设定闹钟
sigmask=oldset;
sigdelset(&sigmask,SIGALRM);
sigsuspend(&sigmask); //将信号SIGALRM取消阻塞并挂起当前进程
//此函数调用完后SIGALRM恢复阻塞
sigaction(SIGALRM,&oact,&act); //恢复SIGALRM信号以前动作
sigprocmask(SIG_SETMASK,&oldset,&newset); //恢复当前系统旧的阻塞集
int ret=alarm(0); //取消闹钟
return ret;
}
int main()
{
while(1)
{
int ret=mysleep(3);
printf("mysleep done......%d\n",ret);
}
}
结果如下: