信号的捕捉
信号捕捉:如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。(递达在前面已经讲过了)。先画张图来简单的描述一下信号捕捉的过程。
上图中很好的说明了信号的处理是从内核态切换到用户态是处理信号的。
1. 用户程序注册了SIGQUIT信号的处理函数sighandler。
2. 当前正在执行main函数,这时发生中断或异常切换到内核态。
3. 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。
4. 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同
的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
5. sighandler函数返回后自动执⾏行特殊的系统调用sigreturn再次进入内核态。
6. 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
上面的ABCD则是4次模式切换。其中CB是从内核态切换到用户态,而AD则是从用户态切换到内核态的。而C的这一
步则是信号的捕捉(用户自定义捕捉信号)假如3这一步是信号忽略或者是执行默认动作则不会切换到用户态(因为信号的捕捉是用户自定义的执行函数,而用户自定义的函数必须要切回到用户态,而执行默认动作或者忽略是操作系统直接给该进程发送信号),操作系统将信号递达后如果是忽略则直接返回到main函数中断的下一条语句中如果是执行默认动作则一般是终止此进程。
信号捕捉函数sigaction
#include<signal.h>
int sigaction(int signo, const struct sigaction* act, struct sigaction* oact);
下面来解释一下这个函数。
sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功后返回0,出错则返回-1,
signo则是捕捉信号的编号。若act非空,则根据act修改信号的处理动作。若oact非空,则通过oact保存该信号原来的处理动作。
struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
void (*sa_sigaction)(int, siginfo_t, void*);
};
参数1、sa_handler:将sa_handler赋值给常数SIG_IGN传给sigaction表示忽略此信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用户自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int的参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多个信号。
当我们在捕捉一个信号时也就是信号处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。
参数2、sa_handler:如果调用信号处理函数时,除了需要当前信号被自动屏蔽之外,还需要屏蔽另外一些信号,则用sa_handler字段说明这些需要额外屏蔽的信号,当前信号处理函数返回时自动恢复原来的信号屏蔽字。
参数3、sa_flags,一般设置为0和参数4、就不在这里多讲了(其我也懂的不多,有兴趣大家可以上网查查)。
sigaction测试:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int sig)//
{
/*这里的\n一定要加不加话在发送SIGINT信号时因为往显示器打印是行缓冲如果不加\n就
会只显示Ctrl C而不显示这条语句,而加了\n每次一发送信号就会直接刷新出去*/
printf("hello world, mysigal is: %d\n", sig);
}
int main()
{
struct sigaction act, oatc;
act.sa_handler = handler;//设置用户自定义捕捉函数
sigemptyset(&act.sa_mask);//将sa_mask信号集清空
act.sa_flags = 0;
sigaction(SIGINT, &act, &oatc);//调用sigaction函数捕捉SIGINT信号
while(1);
sigaction(SIGINT, &oatc, NULL);//恢复oact对SIGINT信号的处理
return 0;
}
从上图我们可以看出当我们ctrl c 时就会打印hello world, mysiganl is:2这句话说明我们成功的捕捉了2号信号。
pasue函数:
#include <unistd.h>
int puase(void);
pause函数使调用进程挂起直到有信号递达。如果信号的处理动作是终止进程,则进程终止,pause函数没有机会返回;如果信号的处理动作是忽略,则进程继续处于挂起状态,pause不返回;如果信号的处理动作是捕捉,则调用了信号处理函数之后pause返回-1,errno设置为EINTR, 所以pause只有出错的返回值(想以前还学过什么函数只有出错返回值?)。错误码EINTR表示“被信号中断”。
pause函数是挂起进程,直到有信号递达:
1、该信号递达方式为忽略,则进程一直挂起,不返回。
2、该信号递达方式为默认,则进程直接终止,不会返回。
3、该信号递达方式为捕捉,则调用了信号处理函数之后puase返回-1。
所以pasue只会出错返回。
下面我们用pasue函数写一个mysleep函数:
但是pasue只是挂起等待,而不能自动退出,如果不让他退出是无法完成我们想要实现的mysleep函数的,所以在这介绍一个闹钟函数alarm,顾名思义闹钟函数就是设置一个闹钟然后过一段时间叫醒它。
alarm:
#include<unsitd.h>
unsigned int alarm(unsigned int seconds);
下面就是一个alarm的测试:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int count = 14;
alarm(1);//设置1秒的闹钟
for(; 1 ; count++)
{
printf("count = %d ", count);
}
return 0;
}
由于cpu运算速度非常的快所以1秒可以打印很多的count值在这我只截一部分给大家,
下面我在这里测试一个让闹钟提前退出的测试
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int count = 14;
alarm(1);
for(; 1 ; count++)
{
printf("count = %d ", count);
int rec = alarm(0);//提前取消闹钟这是返回的是原来闹钟还有多少秒
printf("rec = %d\n", rec);
break;
}
return 0;
}
在这里可以看出其实还是有1秒的闹钟的因为我在这里已进入for循环就直接提前取消了闹钟所以还有1秒的时间。
有了alarm函数我们可以在pasue函数挂起之前设置一个闹钟,然后在pasue函数执行的时候然后让闹钟发送一个信号取消挂起动作。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{}//这个函数是个句柄函数,什么功能都不用做
unsigned int mysleep(unsigned int timeout)
{
//创建sigaction结构体
struct sigaction act, oact;
//给act赋值为⼀一个函数指针表⽰用自定义函数捕捉信号
act.sa_handler = handler;
//清空act额外要屏蔽的信号
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
//捕捉SIGALRM信号并根据act修改信号的处理动作,而且将原信号的处理动作保存在oact中
sigaction(SIGALRM, &act, &oact);
//设置闹钟
alarm(timeout);
//挂起等待
pause();
//清空闹钟
int rec = alarm(0);
//还原原信号的处理动作
sigaction(SIGALRM, &oact, NULL);
return rec;
}
int main()
{
while(1)
{
mysleep(3);
printf("hello world\n");
}
return 0;
}
测试的时候没过3秒就会打印一句hello world 因为我们对闹钟信号进行了捕捉,所以在闹钟信号来临只会让pasue终止挂起然后执行handler函数但是我们在这个函数中什么都没做,所以就返回main函数中打印hello world。
下面我就按照前面的那个信号捕捉的图给大家解释一下这个信号的捕捉过程:
1、main函数中调用了mysleep函数,进入mysleep函数之后,我们首先对SIGALRM信号进行设置捕捉,等待SIGALRM信号的来临,然后设置了闹钟函数alarm。
2、pasue挂起等待,此时内核切换到别进程。
3、tiamout秒后闹钟时间到,内核发送SIGALRM信号给这个进程。
4、从内核返回用户态之前处理递达信号,发现有SIGALRM信号,其处理函数为handler则由从内核态切回到用户态执行用户自定义捕捉函数handler。
5、切回到用户态进入handler函数,SIGALRM信号被自动屏蔽,从handler函数返回时则结束对SIGALRM信号的屏蔽。然后自动执行系统调用sigrenturn再次进入内核,在返回用户态执行进程的主控流程。
但是这里的mysleep函数还有一点缺陷:如果在调用alarm函数设置闹钟之后来了一个优先级更高的进程取代了当前进程,并且优先级高的进程有很多,而且每个进程执行时间很多,假如此时闹钟的时间到了,内核发送SIGALRM信号,优先级更高的进程执行完了,内核要调度回这个进程执行,SIGALRM信号递达,执行处理函数handler,再次进入内核返回主控制流,alarm函数返回,调用pasue函数挂起等待,可是SIGALRM信号已经处理完了,pasue则一直挂起了。
出现这个问题的根本原因是系统运行的时序(Timing)并不像我们写程序时所设想的那样。虽然alarm(nsecs)紧接着的下一行就是pause(),但是无法保证pause()一定会在调用alarm(nsecs)之后的tiamout秒之内被调用。由于异步事件在任何时候都有可能发⽣生(这里的异步事件指出现更高优先级的进程),如果我们写程序时考虑不周密,就可能由于时序问题而导致错误,这叫做竞态条件(Race Condition)。
如果“解除信号屏蔽”和“挂起等待信号”这两步能合并成一个原子操作就好了,这正是sigsuspend函数的功能。sigsuspend包含了pause的挂起等待功能,同时解决了竞态条件的问题,在对时序要求严格的场合下都应该调用sigsuspend而不是pause。
sigsuspend:
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
调用sigsuspend时,进程的信号屏蔽字由sigmask参数指定,可以通过指定sigmask来临时解除对某个信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值,如果原来对该信号是屏蔽的,从sigsuspend返回后仍然是屏蔽的。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig)
{}
unsigned int mysleep(unsigned int timeout)
{
struct sigaction act, oact;
sigset_t newmask, oldmask, suspmask;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
//捕捉SIGALRM信号并根据act修改信号的处理动作,而且将原信号的处理动作保存在oact中
sigaction(SIGALRM, &act, &oact);
//清空newmask
sigemptyset(&newmask);
//将SIGALRM信号添加到newmask信号集中
sigaddset(&newmask, SIGALRM);
//阻塞SIGALRM信号
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
//设置闹钟
alarm(timeout);
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM);
//sigsuspend函数接受一个信号集指针,将信号屏蔽字设置为信号集中的值,在进程接受到一个信号之前,进程会挂起,当捕捉一个信
//号,首先执行信号处理程序,然后从sigsuspend返回,最后将信号屏蔽字恢复为调用sigsuspend之前的值
/*调用sigsuspend函数取消对SIGALRM信号的阻塞并挂起等待,改函数返回后则还原原来的状态对SIGALRM信号阻塞
而此时将信号屏蔽字设置为suspmask信号集中的值,而suspmask没有对SIGALRM信号屏蔽所以就可以收到SIGALRM的信号*/
sigsuspend(&suspmask);
//清空闹钟
int rec = alarm(0);
//还原原信号的处理动作
sigaction(SIGALRM, &oact, NULL);
//对原信号SIGALRM解除阻塞
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return rec;
}
int main()
{
while(1)
{
mysleep(3);
printf("hello world\n");
}
return 0;
}