一、信号的基本概念
1、基本概念
信号是进程在运行过程中,由自身产生或由进程外部发过来的消息(事件)。信号是硬件中断的软件模拟(软中断)。每个信号用一个整型常量宏表示,以SIG开头,比如SIGCHLD、SIGINT等,它们在系统头文件<signal.h>中定义.
软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。
收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。第二种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。
在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号,当有信号发送给进程时,对应位置位。由此可以看出,进程对不同的信号可以同时保留,但对于同一个信号,进程并不知道在处理之前来过多少个。
2、信号的类型
发出信号的原因很多,这里按发出信号的原因简单分类,以了解各种信号:
(1)与进程终止相关的信号。当进程退出,或者子进程终止时,发出这类信号。
(2)与进程例外事件相关的信号。如进程越界,或企图写一个只读的内存区域(如程序正文区),或执行一个特权指令及其他各种硬件错误。
(3)与在系统调用期间遇到不可恢复条件相关的信号。如执行系统调用exec时,原有资源已经释放,而目前系统资源又已经耗尽。
(4)与执行系统调用时遇到非预测错误条件相关的信号。如执行一个并不存在的系统调用。
(5)在用户态下的进程发出的信号。如进程调用系统调用kill向其他进程发送信号。
(6)与终端交互相关的信号。如用户关闭一个终端,或按下break键等情况。
(7)跟踪进程执行的信号。
信号来源
(1)程序错误,如非法访问内存
(2)外部信号,如按下了CTRL+C
(3)通过kill或sigqueue向另外一个进程发送信号
可靠信号和不可靠信号
信号代码从SIGRTMIN到SIGRTMAX之间的信号是可靠信号。可靠信号不存在丢失,由sigqueue发送,可靠信号支持排队。
可靠信号注册机制:
内核每收到一个可靠信号都会去注册这个信号,在信号的未决信号链中分配sigqueue结构,因此,不会存在信号丢失的问题。
不可靠信号的注册机制:
而对于不可靠的信号,如果内核已经注册了这个信号,那么便不会再去注册,对于进程来说,便不会知道本次信号的发生。
可靠信号与不可靠信号与发送函数没有关系,取决于信号代码,前面的32种信号就是不可靠信号,而后面的32种信号就是可靠信号。
二、信 号 机 制
上一节中介绍了信号的基本概念,在这一节中,我们将介绍内核如何实现信号机制。即内核如何向一个进程发送信号、进程如何接收一个信号、进程怎样控制自己对信号的反应、内核在什么时机处理和怎样处理进程收到的信号。还要介绍一下setjmp和longjmp在信号中起到的作用。
1、内核对信号的基本处理方法
内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。这里要补充的是,如果信号发送给一个正在睡眠的进程,那么要看该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。这一点比较重要,因为进程检查是否收到信号的时机是:一个进程在即将从内核态返回到用户态时;或者,在一个进程要进入或离开一个适当的低调度优先级睡眠状态时。
内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。
内核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。前面介绍概念的时候讲过,处理信号有三种类型:进程接收到信号后退出;进程忽略该信号;进程收到信号后执行用户设定用系统调用signal的函数。当进程接收到一个它忽略的信号时,进程丢弃该信号,就象没有收到该信号似的继续运行。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。
在信号的处理方法中有几点特别要引起注意。第一,在一些系统中,当一个进程处理完中断信号返回用户态之前,内核清除用户区中设定的对该信号的处理例程的地址,即下一次进程对该信号的处理方法又改为默认值,除非在下一次信号到来之前再次使用signal系统调用。这可能会使得进程在调用signal之前又得到该信号而导致退出。在BSD中,内核不再清除该地址。但不清除该地址可能使得进程因为过多过快的得到某个信号而导致堆栈溢出。为了避免出现上述情况。在BSD系统中,内核模拟了对硬件中断的处理方法,即在处理某个中断时,阻止接收新的该类中断。
第二个要引起注意的是,如果要捕捉的信号发生于进程正在一个系统调用中时,并且该进程睡眠在可中断的优先级上,这时该信号引起进程作一次longjmp,跳出睡眠状态,返回用户态并执行信号处理例程。当从信号处理例程返回时,进程就象从系统调用返回一样,但返回了一个错误代码,指出该次系统调用曾经被中断。这要注意的是,BSD系统中内核可以自动地重新开始系统调用。
第三个要注意的地方:若进程睡眠在可中断的优先级上,则当它收到一个要忽略的信号时,该进程被唤醒,但不做longjmp,一般是继续睡眠。但用户感觉不到进程曾经被唤醒,而是象没有发生过该信号一样。
第四个要注意的地方:内核对子进程终止(SIGCLD)信号的处理方法与其他信号有所区别。当进程检查出收到了一个子进程终止的信号时,缺省情况下,该进程就象没有收到该信号似的,如果父进程执行了系统调用wait,进程将从系统调用wait中醒来并返回wait调用,执行一系列wait调用的后续操作(找出僵死的子进程,释放子进程的进程表项),然后从wait中返回。SIGCLD信号的作用是唤醒一个睡眠在可被中断优先级上的进程。如果该进程捕捉了这个信号,就象普通信号处理一样转到处理例程。如果进程忽略该信号,那么系统调用wait的动作就有所不同,因为SIGCLD的作用仅仅是唤醒一个睡眠在可被中断优先级上的进程,那么执行wait调用的父进程被唤醒继续执行wait调用的后续操作,然后等待其他的子进程。
如果一个进程调用signal系统调用,并设置了SIGCLD的处理方法,并且该进程有子进程处于僵死状态,则内核将向该进程发一个SIGCLD信号。
2、setjmp和longjmp的作用
前面在介绍信号处理机制时,多次提到了setjmp和longjmp,但没有仔细说明它们的作用和实现方法。这里就此作一个简单的介绍。
在介绍信号的时候,我们看到多个地方要求进程在检查收到信号后,从原来的系统调用中直接返回,而不是等到该调用完成。这种进程突然改变其上下文的情况,就是使用setjmp和longjmp的结果。setjmp将保存的上下文存入用户区,并继续在旧的上下文中执行。这就是说,进程执行一个系统调用,当因为资源或其他原因要去睡眠时,内核为进程作了一次setjmp,如果在睡眠中被信号唤醒,进程不能再进入睡眠时,内核为进程调用longjmp,该操作是内核为进程将原先setjmp调用保存在进程用户区的上下文恢复成现在的上下文,这样就使得进程可以恢复等待资源前的状态,而且内核为setjmp返回1,使得进程知道该次系统调用失败。这就是它们的作用。
三、信号的生命周期与处理过程分析
1.信号的生命周期
信号产生->信号注册->信号在进程中注销->信号处理函数执行完毕
1)信号的产生是指触发信号的事件的发生
(2)信号注册
指的是在目标进程中注册,该目标进程中有未决信号的信息:
structsigpending pending:
structsigpending{
structsigqueue *head, **tail;
sigset_tsignal;
};
structsigqueue{
structsigqueue *next;
siginfo_tinfo;
}
其中sigqueue结构组成的链称之为未决信号链,sigset_t称之为未决信号集。
*head,**tail分别指向未决信号链的头部与尾部。
siginfo_tinfo是信号所携带的信息。
信号注册的过程就是将信号值加入到未决信号集siginfo_t中,将信号所携带的信息加入到未决信号链的某一个sigqueue中去。
因此,对于可靠的信号,可能存在多个未决信号的sigqueue结构,对于每次信号到来都会注册。而不可靠信号只注册一次,只有一个sigqueue结构。
只要信号在进程的未决信号集中,表明进程已经知道这些信号了,还没来得及处理,或者是这些信号被阻塞。
(3)信号在目标进程中注销
在进程的执行过程中,每次从系统调用或中断返回用户空间的时候,都会检查是否有信号没有被处理。如果这些信号没有被阻塞,那么就调用相应的信号处理函数来处理这些信号。则调用信号处理函数之前,进程会把信号在未决信号链中的sigqueue结构卸掉。是否从未决信号集中把信号删除掉,对于实时信号与非实时信号是不相同的。
非实时信号:由于非实时信号在未决信号链中只有一个sigqueue结构,因此将它删除的同时将信号从未决信号集中删除。
实时信号:由于实时信号在未决信号链中可能有多个sigqueue结构,如果只有一个,也将信号从未决信号集中删除掉。如果有多个那么不从未决信号集中删除信号,注销完毕。
(4)信号处理函数执行完毕
执行处理函数,本次信号在进程中响应完毕。
在第4步,只简单的描述了信号处理函数执行完毕,就完成了本次信号的响应,但这个信号处理函数空间是怎么处理的呢?内核栈与用户栈是怎么工作的呢?这就涉及到了信号处理函数的过程。
信号处理函数的过程:
(1)注册信号处理函数
信号的处理是由内核来代理的,首先程序通过sigal或sigaction函数为每个信号注册处理函数,而内核中维护一张信号向量表,对应信号处理机制。这样,在信号在进程中注销完毕之后,会调用相应的处理函数进行处理。
(2)信号的检测与响应时机
在系统调用或中断返回用户态的前夕,内核会检查未决信号集,进行相应的信号处理。
(3)处理过程:
程序运行在用户态时->进程由于系统调用或中断进入内核->转向用户态执行信号处理函数->信号处理函数完毕后进入内核->返回用户态继续执行程序
首先程序执行在用户态,在进程陷入内核并从内核返回的前夕,会去检查有没有信号没有被处理,如果有且没有被阻塞就会调用相应的信号处理程序去处理。首先,内核在用户栈上创建一个层,该层中将返回地址设置成信号处理函数的地址,这样,从内核返回用户态时,就会执行这个信号处理函数。当信号处理函数执行完,会再次进入内核,主要是检测有没有信号没有处理,以及恢复原先程序中断执行点,恢复内核栈等工作,这样,当从内核返回后便返回到原先程序执行的地方了。
四、常见信号处理函数
1、signal函数:
将信号与处理函数绑定在一起。
使用signal函数时只需将处理的信号和处理的函数列出即可。
下面使用代码展示如何使用signal函数:
#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
staticvoid sig_usr(int signo);
intmain(void)
{
if(signal(SIGUSR1,sig_usr) ==SIG_ERR)
printf("Can'tcatch SIGUSR1");
if(signal(SIGUSR2,sig_usr) ==SIG_ERR)
printf("Can'tcatch SIGUSR2");
for(;;)
pause();
}
staticvoid sig_usr(int signo)
{
if(signo== SIGUSR1)
printf("receviedSIGUSR1\n");
elseif (signo == SIGUSR2)
printf("receivedSIGUSR2\n");
else
printf("receivedsignal %d\n",signo);
}
2、高级信号处理函数
signal是比较简单的注册函数,不能实现很多比较复杂的控制,而sigaction函数可以实现。但sigaction函数有点难,很多东西还不是很理解。
intsigaction(int signum,const struct sigaction*act,struct sigaction *oact);
参数signum为需要捕捉的信号;
参数act是一个结构体,里面包含信号处理函数地址、处理方式等信息。
参数oldact是一个传出参数,sigaction函数调用成功后,oldact里面包含以前对signum的处理方式的信息。
结构体 structsigaction(注意名称与函数sigaction相同)的原型为:
structsigaction {
void(*sa_handler)(int); //老类型的信号处理函数指针
void(*sa_sigaction)(int, siginfo_t *, void *);//新类型的信号处理函数指针
sigset_tsa_mask; //将要被阻塞的信号集合
intsa_flags; //信号处理方式掩码
void(*sa_restorer)(void); //保留,不要使用。
}
该结构体的各字段含义及使用方式:
1、字段sa_handler是一个函数指针,用于指向原型为voidhandler(int)的信号处理函数地址, 即老类型 的信号处理函数;
2、字段sa_sigaction也是一个函数指针,用于指向原型为:
voidhandler(int iSignNum,siginfo_t *pSignInfo,void *pReserved);
的信号处理函数,即新类型的信号处理函数。
该函数的三个参数含义为:
iSignNum:传入的信号
pSignInfo:与该信号相关的一些信息,它是个结构体
pReserved:保留,现没用
3、字段sa_handler和sa_sigaction只应该有一个生效,如果想采用老的信号处理机制,就应该让sa_handler指向正确的信号处理函数;否则应该让sa_sigaction指向正确的信号处理函数,并且让字段sa_flags包含SA_SIGINFO选项。
4、字段sa_mask是一个包含信号集合的结构体,该结构体内的信号表示在进行信号处理时,将要被阻塞的信号。
5、 字段sa_flags是一组掩码的合成值,指示信号处理时所应该采取的一些行为,各掩码的含义为:
SA_RESETHAND
处理完毕要捕捉的信号后,将自动撤消信号处理函数的注册,即必须再重新注册信号处理函数,才能继续处理接下来产生的信号。该选项不符合一般的信号处理流程,现已经被废弃。
SA_NODEFER
在处理信号时,如果又发生了其它的信号,则立即进入其它信号的处理,等其它信号处理完毕后,再继续处理当前的信号,即递规地处理。如果sa_flags包含了该掩码,则结构体sigaction的sa_mask将无效!
SA_RESTART
如果在发生信号时,程序正阻塞在某个系统调用,例如调用read()函数,则在处理完毕信号后,接着从阻塞的系统返回。该掩码符合普通的程序处理流程,所以一般来说,应该设置该掩码,否则信号处理完后,阻塞的系统调用将会返回失败!
SA_SIGINFO
指示结构体的信号处理函数指针是哪个有效,如果sa_flags包含该掩码,则sa_sigactiion指针有效,否则是sa_handler指针有效。
#include<unistd.h>
#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
//自定义信号处理函数
voidmy_func(int signum)
{
printf("Ifyou want to quit,please try SIGQUIT\n");
}
intmain()
{
structsigaction action1,action2;
sigemptyset(&action1.sa_mask);
action1.sa_handler=my_func;
action1.sa_flags=SA_SIGINFO;
sigaction(SIGINT,&action1,$action2);
}
3、信号集操作
信号集是一个能表示多个信号的数据类型,
sigset_tset;set即一个信号集。
既然是一个集合,就需要对集合进行添加/删除等操作。
intsigemptyset(sigset_t *set);将set集合置空
intsigfillset(sigset_t *set); 将所有信号加入set集合
intsigaddset(sigset_t *set,int signo);将signo信号加入到set集合
intsigdelset(sigset_t *set,intsigno);从set集合中移除signo信号
intsigismember(const sigset_t *set,int signo); signo判断信号是否存在于set集合中
4、信号屏蔽
所谓屏蔽,并不是禁止递送信号,而是暂时阻塞信号的递送,
解除屏蔽后,信号将被递送,不会丢失
#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
#include<error.h>
#include<string.h>
voidsig_handler(int signum)
{
printf("catchSIGINT\n");
}
intmain(int argc, char **argv)
{
sigset_tblock;
structsigaction action, old_action;
/*安装信号*/
action.sa_handler= sig_handler;
sigemptyset(&action.sa_mask);
action.sa_flags= 0;
sigaction(SIGINT,NULL, &old_action);
if(old_action.sa_handler != SIG_IGN) {
sigaction(SIGINT,&action, NULL);
}
/*屏蔽信号*/
sigemptyset(&block);
sigaddset(&block,SIGINT);
printf("blockSIGINT\n");
sigprocmask(SIG_BLOCK,&block, NULL);
printf("-->send SIGINT -->\n");
kill(getpid(),SIGINT);
printf("-->send SIGINT -->\n");
kill(getpid(),SIGINT);
sleep(1);
/*解除信号后,之前触发的信号将被递送,
*但SIGINT是非可靠信号,只会递送一次
*/
printf("unblockSIGINT\n");
sigprocmask(SIG_UNBLOCK,&block, NULL);
sleep(2);
return0;
}
5、未觉信号
intsigpending(sigset_t *set))
获得当前已递送到进程,却被阻塞的所有信号,在set指向的信号集中返回结果。
6、等待信号
intpause(void);
pause()会使当前进程挂起,直到捕捉到一个信号,对指定为忽略的信号,pause()不会返回。只有执行了一个信号处理函数,并从其返回,puase()才返回-1,并将errno设为EINTR。
7。高级等待函数
intsigsuspend(const sigset_t *mask));
sigsuspend()能让解除信号阻塞和等待信号成为一个原子操作,这样就避免了上述的问题。它会把当前进程的信号屏蔽字设定为sigmask指定的值,所以在等待信号期间,sigmask中的信号会被暂时阻塞,而sigmask之外的信号都会被暂时解除阻塞。然后sigsuspend()挂起当前进程,等待,直到捕捉到一个信号或发生了一个会终止该进程的信号。如果是捕捉到一个信号并从出来程序中返回,则sigsuspend()返回-1,把进程信号屏蔽字设回调用sigsuspend()之前的值,并将errno设为EINTR。注意,指定为忽略的信号,并不会导致sissuspend()返回。
8、信号的发送
①raise
向自身发送信号
②kill
③sigqueue`
sigqueue也可以发送信号,并且能传递附加的信息。
原型为:
#include<signal.h>
intsigqueue(pid_t pid, int sig, const union sigval value);
参数pid为接收信号的进程;
参数sig为要发送的信号;
参数value为一整型与指针类型的联合体:
unionsigval {
int sival_int;
void*sival_ptr;
};
由sigqueue函数发送的信号的第3个参数value的值,可以被进程的信号处理函数的第2个参数info->si_ptr接收到。
9、时钟处理
睡眠函数
Linux下有两个睡眠函数,原型为:
#include<unistd.h>
unsignedint sleep(unsigned int seconds);
voidusleep(unsigned long usec);
函数sleep让进程睡眠seconds秒,函数usleep让进程睡眠usec毫秒。
sleep睡眠函数内部是用信号机制进行处理的,用到的函数有:
#include<unistd.h>
unsignedint alarm(unsigned int seconds); //告知自身进程,要进程在seconds秒后自动产生一个//SIGALRM的信号,
intpause(void); //将自身进程挂起,直到有信号发生时才从pause返回
注意:因为sleep在内部是用alarm实现的,所以在程序中最好不要sleep与alarm混用,以免造成混乱。
时钟处理
Linux为每个进程维护3个计时器,分别是真实计时器、虚拟计时器和实用计时器。
l真实计时器计算的是程序运行的实际时间;
l虚拟计时器计算的是程序运行在用户态时所消耗的时间(可认为是实际时间减掉(系统调用和程序睡眠所消耗)的时间);
l实用计时器计算的是程序处于用户态和处于内核态所消耗的时间之和。
例如:有一程序运行,在用户态运行了5秒,在内核态运行了6秒,还睡眠了7秒,则真实计算器计算的结果是18秒,虚拟计时器计算的是5秒,实用计时器计算的是11秒。
用指定的初始间隔和重复间隔时间为进程设定好一个计时器后,该计时器就会定时地向进程发送时钟信号。3个计时器发送的时钟信号分别为:SIGALRM,SIGVTALRM和SIGPROF。
用到的函数与数据结构:
#include<sys/time.h>
//获取计时器的设置
//which指定哪个计时器,可选项为ITIMER_REAL(真实计时器)、ITIMER_VITUAL(虚拟计时器、ITIMER_PROF(实用计时器))
//value为一结构体的传出参数,用于传出该计时器的初始间隔时间和重复间隔时间
//如果成功,返回0,否则-1
intgetitimer(int which, struct itimerval *value);
//设置计时器
//which指定哪个计时器,可选项为ITIMER_REAL(真实计时器)、ITIMER_VITUAL(虚拟计时器、ITIMER_PROF(实用计时器))
//value为一结构体的传入参数,指定该计时器的初始间隔时间和重复间隔时间
//ovalue为一结构体传出参数,用于传出以前的计时器时间设置。
//如果成功,返回0,否则-1
intsetitimer(int which, const struct itimerval *value, struct itimer val*ovalue);
structitimerval {
structtimeval it_interval; /* next value */ //重复间隔
structtimeval it_value; /* current value */ //初始间隔
};
structtimeval {
longtv_sec; /* seconds */ //时间的秒数部分
longtv_usec; /* microseconds */ //时间的微秒部分
};
10、子进程结束
子进程在结束时会向父进程发送SIGCLD消息。
11、一些其他的高深知识
①sigsetjmp和siglongjmp
在介绍信号的时候,我们看到多个地方要求进程在检查收到信号后,从原来的系统调用中直接返回,而不是等到该调用完成。这种进程突然改变其上下文的情况,就是使用setjmp和longjmp的结果。setjmp将保存的上下文存入用户区,并继续在旧的上下文中执行。这就是说,进程执行一个系统调用,当因为资源或其他原因要去睡眠时,内核为进程作了一次setjmp,如果在睡眠中被信号唤醒,进程不能再进入睡眠时,内核为进程调用longjmp,该操作是内核为进程将原先setjmp调用保存在进程用户区的上下文恢复成现在的上下文,这样就使得进程可以恢复等待资源前的状态,而且内核为setjmp返回1,使得进程知道该次系统调用失败。这就是它们的作用。
intsetjmp(jmp_buf env);
intsigsetjmp(sigjmp_buf env, int savesigs);
voidlongjmp(jmp_buf env, int val);
voidsiglongjmp(sigjmp_buf env, int val);
--------------------------------------------------------
setjmp()会保存目前堆栈环境,然后将目前的地址作一个记号,
而在程序其他地方调用longjmp时便会直接跳到这个记号位置,
然后还原堆栈,继续程序好执行。
setjmp和sigsetjmp 的唯一区别是:setjmp不一定会恢复信号集合,
而sigsetjmp可以保证恢复信号集合
②可重入不可重入函数
在信号处理函数中应使用可重入函数。
信号处理程序中应当使用可重入函数
(注:所谓可重入函数是指一个可以被多个任务调用的过程,
任务在调用时不必担心数据是否会出错)。因为进程在收到信号
后,就将跳转到信号处理函数去接着执行。如果信号处理函数中
使用了不可重入函数,那么信号处理函数可能会修改原来进程中
不应该被修改的数据,这样进程从信号处理函数中返回接着执行时,
可能会出现不可预料的后果。不可再入函数在信号处理函数中被视为
不安全函数。满足下列条件的函数多数是不可再入的:
③当使用fork()函数时,子进程会继承父进程完全相同的信号语义,这也是有道理的,因为父子进程共享一个地址空间,所以父进程的信号处理程序也存在于子进程中。
④信号出处理函数应该尽量简单