Linux 信号signal处理机制

信号是Linux编程中非常重要的部分,本文将详细介绍信号机制的基本概念、Linux对信号机制的大致实现方法、如何使用信号,以及有关信号的几个系统调用。 

信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称作软中断。从它的命名可以看出,它的实质和使用很象中断。所以,信号可以说是进程控制的一部分。 

一、信号的基本概念 

本节先介绍信号的一些基本概念,然后给出一些基本的信号类型和信号对应的事件。基本概念对于理解和使用信号,对于理解信号机制都特别重要。下面就来看看什么是信号。

1、基本概念 

软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。 

收 到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处 理。第二种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信 号的缺省操作是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。 

在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号,当有信号发送给进程时,对应位置位。由此可以看出,进程对不同的信号可以同时保留,但对于同一个信号,进程并不知道在处理之前来过多少个。 

2、信号的类型 

发出信号的原因很多,这里按发出信号的原因简单分类,以了解各种信号: 

(1) 与进程终止相关的信号。当进程退出,或者子进程终止时,发出这类信号。 
(2) 与进程例外事件相关的信号。如进程越界,或企图写一个只读的内存区域(如程序正文区),或执行一个特权指令及其他各种硬件错误。 
(3) 与在系统调用期间遇到不可恢复条件相关的信号。如执行系统调用exec时,原有资源已经释放,而目前系统资源又已经耗尽。 
(4) 与执行系统调用时遇到非预测错误条件相关的信号。如执行一个并不存在的系统调用。 
(5) 在用户态下的进程发出的信号。如进程调用系统调用kill向其他进程发送信号。 
(6) 与终端交互相关的信号。如用户关闭一个终端,或按下break键等情况。 
(7) 跟踪进程执行的信号。 

Linux支持的信号列表如下。很多信号是与机器的体系结构相关的,首先列出的是POSIX.1中列出的信号: 

信号 值 处理动作 发出信号的原因 
---------------------------------------------------------------------- 
SIGHUP 1 A 终端挂起或者控制进程终止 
SIGINT 2 A 键盘中断(如break键被按下) 
SIGQUIT 3 C 键盘的退出键被按下 
SIGILL 4 C 非法指令 
SIGABRT 6 C 由abort(3)发出的退出指令  Process aborted

SIGBUS 7 Bus error: "access to undefined portion of memory object"
SIGFPE 8 C 浮点异常 
SIGKILL 9 AEF Kill信号  Kill (terminate immediately)
SIGSEGV 11 C 无效的内存引用 
SIGPIPE 13 A 管道破裂: 写一个没有读端口的管道 
SIGALRM 14 A 由alarm(2)发出的信号  Signal raised by alarm
SIGTERM 15 A 终止信号 
SIGUSR1 30,10,16 A 用户自定义信号1 
SIGUSR2 31,12,17 A 用户自定义信号2 
SIGCHLD 20,17,18 B 子进程结束信号 
SIGCONT 19,18,25 进程继续(曾被停止的进程) 
SIGSTOP 17,19,23 DEF 终止进程 
SIGTSTP 18,20,24 D 控制终端(tty)上按下停止键 ,停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号 
SIGTTIN 21,21,26 D 后台进程企图从控制终端读 
SIGTTOU 22,22,27 D 后台进程企图从控制终端写 

下面的信号没在POSIX.1中列出,而在SUSv2列出 

信号 值 处理动作 发出信号的原因 
-------------------------------------------------------------------- 
SIGBUS 10,7,10 C 总线错误(错误的内存访问) 
SIGPOLL A Sys V定义的Pollable事件,与SIGIO同义 
SIGPROF 27,27,29 A Profiling定时器到 
SIGSYS 12,-,12 C 无效的系统调用 (SVID) 
SIGTRAP 5 C 跟踪/断点捕获 
SIGURG 16,23,21 B Socket出现紧急条件(4.2 BSD) 
SIGVTALRM 26,26,28 A 实际时间报警时钟信号(4.2 BSD) 
SIGXCPU 24,24,30 C 超出设定的CPU时间限制(4.2 BSD) 
SIGXFSZ 25,25,31 C 超出设定的文件大小限制(4.2 BSD) 

(对于SIGSYS,SIGXCPU,SIGXFSZ,以及某些机器体系结构下的SIGBUS,Linux缺省的动作是A (terminate),SUSv2 是C (terminate and dump core))。 

下面是其它的一些信号 

信号 值 处理动作 发出信号的原因 
---------------------------------------------------------------------- 
SIGIOT 6 C IO捕获指令,与SIGABRT同义 
SIGEMT 7,-,7 
SIGSTKFLT -,16,- A 协处理器堆栈错误 
SIGIO 23,29,22 A 某I/O操作现在可以进行了(4.2 BSD) 
SIGCLD -,-,18 A 与SIGCHLD同义 
SIGPWR 29,30,19 A 电源故障(System V) 
SIGINFO 29,-,- A 与SIGPWR同义 
SIGLOST -,-,- A 文件锁丢失 
SIGWINCH 28,28,20 B 窗口大小改变(4.3 BSD, Sun) 
SIGUNUSED -,31,- A 未使用的信号(will be SIGSYS) 

(在这里,- 表示信号没有实现;有三个值给出的含义为,第一个值通常在Alpha和Sparc上有效,中间的值对应i386和ppc以及sh,最后一个值对应mips。信号29在Alpha上为SIGINFO / SIGPWR ,在Sparc上为SIGLOST。) 

处理动作一项中的字母含义如下 
A 缺省的动作是终止进程 
B 缺省的动作是忽略此信号 
C 缺省的动作是终止进程并进行内核映像转储(dump core) 
D 缺省的动作是停止进程 
E 信号不能被捕获 
F 信号不能被忽略 

上 面介绍的信号是常见系统所支持的。以表格的形式介绍了各种信号的名称、作用及其在默认情况下的处理动作。各种默认处理动作的含义是:终止程序是指进程退 出;忽略该信号是将该信号丢弃,不做处理;停止程序是指程序挂起,进入停止状况以后还能重新进行下去,一般是在调试的过程中(例如ptrace系统调 用);内核映像转储是指将进程数据在内存的映像和进程在内核结构中存储的部分内容以一定格式转储到文件系统,并且进程退出执行,这样做的好处是为程序员提 供了方便,使得他们可以得到进程当时执行时的数据值,允许他们确定转储的原因,并且可以调试他们的程序。 

注意 信号SIGKILL和SIGSTOP既不能被捕捉,也不能被忽略。信号SIGIOT与SIGABRT是一个信号。可以看出,同一个信号在不同的系统中值可能不一样,所以建议最好使用为信号定义的名字,而不要直接使用信号的值。 

二、信 号 机 制 

上 一节中介绍了信号的基本概念,在这一节中,我们将介绍内核如何实现信号机制。即内核如何向一个进程发送信号、进程如何接收一个信号、进程怎样控制自己对信 号的反应、内核在什么时机处理和怎样处理进程收到的信号。还要介绍一下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,使 得进程知道该次系统调用失败。这就是它们的作用。 

三、有关信号的系统调用 

前面两节已经介绍了有关信号的大部分知 识。这一节我们来了解一下这些系统调用。其中,系统调用signal是进程用来设定某个信号的处理方法,系统调用kill是用来发送信号给指定进程的。这 两个调用可以形成信号的基本操作。后两个调用pause和alarm是通过信号实现的进程暂停和定时器,调用alarm是通过信号通知进程定时器到时。所 以在这里,我们还要介绍这两个调用。 

1、signal 系统调用 

系统调用signal用来设定某个信号的处理方法。该调用声明的格式如下: 
void (*signal(int signum, void (*handler)(int)))(int); 
在使用该调用的进程中加入以下头文件: 
#include <signal.h> 

上述声明格式比较复杂,如果不清楚如何使用,也可以通过下面这种类型定义的格式来使用(POSIX的定义): 
typedef void (*sighandler_t)(int); 
sighandler_t signal(int signum, sighandler_t handler); 
但这种格式在不同的系统中有不同的类型定义,所以要使用这种格式,最好还是参考一下联机手册。 

在调用中,参数signum指出要设置处理方法的信号。第二个参数handler是一个处理函数,或者是 
SIG_IGN:忽略参数signum所指的信号。 
SIG_DFL:恢复参数signum所指信号的处理方法为默认值。 

传递给信号处理例程的整数参数是信号值,这样可以使得一个信号处理例程处理多个信号。系统调用signal返回值是指定信号signum前一次的处理例程或者错误时返回错误代码SIG_ERR。下面来看一个简单的例子: 

#include <signal.h> 
#include <unistd.h> 
#include <stdio.h> 
void sigroutine(int dunno) { /* 信号处理例程,其中dunno将会得到信号的值 */ 
switch (dunno) { 
case 1: 
printf("Get a signal -- SIGHUP "); 
break; 
case 2: 
printf("Get a signal -- SIGINT "); 
break; 
case 3: 
printf("Get a signal -- SIGQUIT "); 
break; 

return; 


int main() { 
printf("process id is %d ",getpid()); 
signal(SIGHUP, sigroutine); //* 下面设置三个信号的处理方法 
signal(SIGINT, sigroutine); 
signal(SIGQUIT, sigroutine); 
for (;;) ; 


其中信号SIGINT由按下Ctrl-C发出,信号SIGQUIT由按下Ctrl-发出。该程序执行的结果如下: 

localhost:~$ ./sig_test 
process id is 463 
Get a signal -SIGINT //按下Ctrl-C得到的结果 
Get a signal -SIGQUIT //按下Ctrl-得到的结果 
//按下Ctrl-z将进程置于后台 
[1]+ Stopped ./sig_test 
localhost:~$ bg 
[1]+ ./sig_test & 
localhost:~$ kill -HUP 463 //向进程发送SIGHUP信号 
localhost:~$ Get a signal – SIGHUP 
kill -9 463 //向进程发送SIGKILL信号,终止进程 
localhost:~$ 

2、kill 系统调用 

系统调用kill用来向进程发送一个信号。该调用声明的格式如下: 
int kill(pid_t pid, int sig); 
在使用该调用的进程中加入以下头文件: 
#include <sys/types.h> 
#include <signal.h> 

该 系统调用可以用来向任何进程或进程组发送任何信号。如果参数pid是正数,那么该调用将信号sig发送到进程号为pid的进程。如果pid等于0,那么信 号sig将发送给当前进程所属进程组里的所有进程。如果参数pid等于-1,信号sig将发送给除了进程1和自身以外的所有进程。如果参数pid小于- 1,信号sig将发送给属于进程组-pid的所有进程。如果参数sig为0,将不发送信号。该调用执行成功时,返回值为0;错误时,返回-1,并设置相应 的错误代码errno。下面是一些可能返回的错误代码: 
EINVAL:指定的信号sig无效。 
ESRCH:参数pid指定的进程或进程组不存在。注意,在进程表项中存在的进程,可能是一个还没有被wait收回,但已经终止执行的僵死进程。 
EPERM: 进程没有权力将这个信号发送到指定接收信号的进程。因为,一个进程被允许将信号发送到进程pid时,必须拥有root权力,或者是发出调用的进程的UID 或EUID与指定接收的进程的UID或保存用户ID(savedset-user-ID)相同。如果参数pid小于-1,即该信号发送给一个组,则该错误 表示组中有成员进程不能接收该信号。 

3、pause系统调用 

系统调用pause的作用是等待一个信号。该调用的声明格式如下: 
int pause(void); 
在使用该调用的进程中加入以下头文件: 
#include <unistd.h> 

该调用使得发出调用的进程进入睡眠,直到接收到一个信号为止。该调用总是返回-1,并设置错误代码为EINTR(接收到一个信号)。下面是一个简单的范例: 

#include <unistd.h> 
#include <stdio.h> 
#include <signal.h> 
void sigroutine(int unused) { 
printf("Catch a signal SIGINT "); 


int main() { 
signal(SIGINT, sigroutine); 
pause(); 
printf("receive a signal "); 


在这个例子中,程序开始执行,就象进入了死循环一样,这是因为进程正在等待信号,当我们按下Ctrl-C时,信号被捕捉,并且使得pause退出等待状态。 

4、alarm和 setitimer系统调用 

系统调用alarm的功能是设置一个定时器,当定时器计时到达时,将发出一个信号给进程。该调用的声明格式如下: 
unsigned int alarm(unsigned int seconds); 
在使用该调用的进程中加入以下头文件: 
#include <unistd.h> 

系 统调用alarm安排内核为调用进程在指定的seconds秒后发出一个SIGALRM的信号。如果指定的参数seconds为0,则不再发送 SIGALRM信号。后一次设定将取消前一次的设定。该调用返回值为上次定时调用到发送之间剩余的时间,或者因为没有前一次定时调用而返回0。 

注意,在使用时,alarm只设定为发送一次信号,如果要多次发送,就要多次使用alarm调用。 

对于alarm,这里不再举例。现在的系统中很多程序不再使用alarm调用,而是使用setitimer调用来设置定时器,用getitimer来得到定时器的状态,这两个调用的声明格式如下: 
int getitimer(int which, struct itimerval *value); 
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue); 
在使用这两个调用的进程中加入以下头文件: 
#include <sys/time.h> 

该系统调用给进程提供了三个定时器,它们各自有其独有的计时域,当其中任何一个到达,就发送一个相应的信号给进程,并使得计时器重新开始。三个计时器由参数which指定,如下所示: 
TIMER_REAL:按实际时间计时,计时到达将给进程发送SIGALRM信号。 
ITIMER_VIRTUAL:仅当进程执行时才进行计时。计时到达将发送SIGVTALRM信号给进程。 
ITIMER_PROF:当进程执行时和系统为该进程执行动作时都计时。与ITIMER_VIR-TUAL是一对,该定时器经常用来统计进程在用户态和内核态花费的时间。计时到达将发送SIGPROF信号给进程。 

定时器中的参数value用来指明定时器的时间,其结构如下: 
struct itimerval { 
struct timeval it_interval; /* 下一次的取值 */ 
struct timeval it_value; /* 本次的设定值 */ 
}; 

该结构中timeval结构定义如下: 
struct timeval { 
long tv_sec; /* 秒 */ 
long tv_usec; /* 微秒,1秒 = 1000000 微秒*/ 
}; 

在setitimer 调用中,参数ovalue如果不为空,则其中保留的是上次调用设定的值。定时器将it_value递减到0时,产生一个信号,并将it_value的值设 定为it_interval的值,然后重新开始计时,如此往复。当it_value设定为0时,计时器停止,或者当它计时到期,而it_interval 为0时停止。调用成功时,返回0;错误时,返回-1,并设置相应的错误代码errno: 
EFAULT:参数value或ovalue是无效的指针。 
EINVAL:参数which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一个。 

下面是关于setitimer调用的一个简单示范,在该例子中,每隔一秒发出一个SIGALRM,每隔0.5秒发出一个SIGVTALRM信号: 

#include <signal.h> 
#include <unistd.h> 
#include <stdio.h> 
#include <sys/time.h> 
int sec; 

void sigroutine(int signo) { 
switch (signo) { 
case SIGALRM: 
printf("Catch a signal -- SIGALRM "); 
break; 
case SIGVTALRM: 
printf("Catch a signal -- SIGVTALRM "); 
break; 

return; 


int main() { 
struct itimerval value,ovalue,value2; 
sec = 5; 

printf("process id is %d ",getpid()); 
signal(SIGALRM, sigroutine); 
signal(SIGVTALRM, sigroutine); 

value.it_value.tv_sec = 1; 
value.it_value.tv_usec = 0; 
value.it_interval.tv_sec = 1; 
value.it_interval.tv_usec = 0; 
setitimer(ITIMER_REAL, &value, &ovalue); 

value2.it_value.tv_sec = 0; 
value2.it_value.tv_usec = 500000; 
value2.it_interval.tv_sec = 0; 
value2.it_interval.tv_usec = 500000; 
setitimer(ITIMER_VIRTUAL, &value2, &ovalue); 

for (;;) ; 


该例子的屏幕拷贝如下: 

localhost:~$ ./timer_test 
process id is 579 
Catch a signal – SIGVTALRM 
Catch a signal – SIGALRM 
Catch a signal – SIGVTALRM 
Catch a signal – SIGVTALRM 
Catch a signal – SIGALRM 

Catch a signal –GVTALRM 


说明:

本文主要翻译自ULK 3rd chapter 11.

主要受 http://blog.csdn.net/yunsongice 影响,故发表在csdn.

另外,本文是最初版本,估计以后会有一个改进版本. 文中还有很多todo的地方.

另外,如果有版权问题,通知我,我马上删掉.


总结

信号分成两种: 
regular signal( 非实时信号 ), 对应的编码值为 [1,31]
real time signal 对应的编码值为 [32,64]

 

编码为 0 的信号 不是有效信号,只用于检查是当前进程否有发送信号的 权限 ,并不真正发送。

 

线程会有自己的悬挂信号队列 , 并且线程组也有一个信号悬挂队列 .

信号悬挂队列保存 task 实例接收到的信号 , 只有当该信号被处理后它才会从悬挂队列中卸下 .

 

信号悬挂队列还有一个对应的阻塞信号集合 , 当一个信号在阻塞信号集合中时 ,task 不会处理该被阻塞的信号 (但是该信号依旧在悬挂队列中 ). 当阻塞取消时 , 它会被处理 .

 

对一个信号 , 要三种处理方式 :

忽略该信号 ;

采用默认方式处理 ( 调用系统指定的信号处理函数 );

使用用户指定的方式处理 ( 调用用户指定的信号处理函数 ).

对于某些信号只能采用默认的方式处理 (eg:SIGKILL,SIGSTOP).

 

信号处理可以分成两个阶段 : 信号产生并通知到接收方 (generation), 接收方进行处理 (deliver)

.........


 

简介

Unix 为了允许用户态进程之间的通信而引入signal. 此外, 内核使用signal 给进程通知系统事件.近30 年来, signal 只有很小的变化 .

以下我们先介绍linux kernel 如何处理signal, 然后讨论允许进程间  exchange 信号的系统调用.

 

The Role of Signals

signal 是一种可以发送给一个进程或一组进程的短消息( 或者说是信号 , 但是这么容易和信号量混淆). 这种消息通常只是一个整数 , 而不包含额外的参数 .

linux 提供了很多种signal, 这些signal 通过宏来标识( 这个宏作为这个信号的名字). 并且这些宏的名字的开头是SIG.eg: 宏SIGCHLD , 它对应的整数值为17, 用来表示子进程结束时给父进程发送的消息 ( 即当子进程结束时应该向父进程发送标识符为17 的signal/ 消息/ 信号) . 宏SIGSEGV, 它对应的整数值为11, 当进程引用一个无效的物理地址时( 内核) 会向进程发送标识符为11 的signal/ 消息/ 信号 ( 参考linux 内存管理的页错误异常处理程序, 以及linux 中断处理).

信号有两个目的:
1. 使一个进程意识到一个特殊事件发生了( 不同的事件用不同的signal 标识) 
2. 并使目标进程进行相应处理(eg: 执行的信号处理函数 , signal handler). 相应的处理也可以是忽略它 .

当然 , 这两个目的不是互斥的 , 因为通常一个进程意识到一个事件发生后就会执行该事件相应的处理函数 .

下表是linux2.6 在80x86 上的前31 个signals 及其相关说明 . 这些信号中有些是体系结构相关的(eg:SIGCHLD,SIGSTOP), 有些则专门了某些体系结构才存在的(eg:SIGSTKFLT) ( 可以参考中断处理 , 里面也列出了一些异常对应的signal).

The first 31 signals in Linux/i386

#

Signal name

Default action

Comment

POSIX

1

SIGHUP

Terminate

Hang up controlling terminal or process

Yes

2

SIGINT

Terminate

Interrupt from keyboard

Yes

3

SIGQUIT

Dump

Quit from keyboard

Yes

4

SIGILL

Dump

Illegal instruction

Yes

5

SIGTRAP

Dump

Breakpoint for debugging

No

6

SIGABRT

Dump

Abnormal termination

Yes

6

SIGIOT

Dump

Equivalent to SIGABRT

No

7

SIGBUS

Dump

Bus error

No

8

SIGFPE

Dump

Floating-point exception

Yes

9

SIGKILL

Terminate

Forced-process termination

Yes

10

SIGUSR1

Terminate

Available to processes

Yes

11

SIGSEGV

Dump

Invalid memory reference

Yes

12

SIGUSR2

Terminate

Available to processes

Yes

13

SIGPIPE

Terminate

Write to pipe with no readers

Yes

14

SIGALRM

Terminate

Real-timerclock

Yes

15

SIGTERM

Terminate

Process termination

Yes

16

SIGSTKFLT

Terminate

Coprocessor stack error

No

17

SIGCHLD

Ignore

Child process stopped or terminated, or got signal if traced

Yes

18

SIGCONT

Continue

Resume execution, if stopped

Yes

19

SIGSTOP

Stop

Stop process execution

Yes

20

SIGTSTP

Stop

Stop process issued from tty

Yes

21

SIGTTIN

Stop

Background process requires input

Yes

22

SIGTTOU

Stop

Background process requires output

Yes

23

SIGURG

Ignore

Urgent condition on socket

No

24

SIGXCPU

Dump

CPU time limit exceeded

No

25

SIGXFSZ

Dump

File size limit exceeded

No

26

SIGVTALRM

Terminate

Virtual timer clock

No

27

SIGPROF

Terminate

Profile timer clock

No

28

SIGWINCH

Ignore

Window resizing

No

29

SIGIO

Terminate

I/O now possible

No

29

SIGPOLL

Terminate

Equivalent to SIGIO

No

30

SIGPWR

Terminate

Power supply failure

No

31

SIGSYS

Dump

Bad system call

No

31

SIGUNUSED

Dump

Equivalent to SIGSYS

No

 

上述signal 称为regular signal . 除此之外, POSIX 还引入了另外一类singal 即real-time signal . real time signal 的标识符的值从32 到64. 它们与reagular signal 的区别在于每一次发送的real time signal 都会被加入悬挂信号队列,所以多次发送的real time signal 会被缓存起来( 而不会导致后面的被忽略掉) . 而同一种( 即标识符一样) regular signal 不会被缓存, 即如果同一个signal 被发送多次 , 它们只有一个会被放入接受进程的悬挂队列 .

 

虽然linux kernel 并没有使用real time signal. 但是它也( 通过特殊的系统调用) 支持posix定义的real time signal.

 

有很多系统调用可以给进程发送singal, 也有很多系统调可以指定进程在接收某一个signal 时应该如何响应( 即实行哪一个函数). 下表给出了这类系统调用: ( 关于这些系统调用的更多信息参考下文)

System call

Description

kill( )

Send a signal to a thread group

tkill( )

Send a signal to a process

tgkill( )

Send a signal to a process in a specific thread group

sigaction( )

Change the action associated with a signal

signal( )

Similar to sigaction( )

sigpending( )

Check whether there are pending signals

sigprocmask( )

Modify the set of blocked signals

sigsuspend( )

Wait for a signal

rt_sigaction( )

Change the action associated with a real-time signal

rt_sigpending( )

Check whether there are pending real-time signals

rt_sigprocmask( )

Modify the set of blocked real-time signals

rt_sigqueueinfo( )

Send a real-time signal to a thread group

rt_sigsuspend( )

Wait for a real-time signal

rt_sigtimedwait( )

Similar to rt_sigsuspend( )

 

signal 可能在任意时候被发送给一个状态未知的进程 . 当信号被发送给一个当前并不正在执行的进程时, 内核必须把先把该信号保存直到该进程恢复执行. (to do ???????)

被阻塞的信号尽管会被加入进程的悬挂信号队列 , 但是在其被解除阻塞之前不会被处理(deliver),Blocking a signal (described later) requires that delivery of the signal be held off until it is later unblocked, which acer s the problem of signals being raised before they can be delivered.

 

内核把信号传送分成两个阶段: 
signal generation: 内核更新信号的目的进程的相关数据结构 , 这样该进程就能知道它接收到了一个信号. 觉得称为收到信号阶段更恰当. 这个generation 翻译成目的进程接收也不错 .

 

signal delivery(): 内核强制目的进程处理接收到的信号,这主要是通过修改进程的执行状态或者在目的进程中执行信号处理函数来实现的 . 觉得称为处理收到的信号阶段更恰当 . diliver 这里翻译成处理更恰当 .

deliver 的翻译: 有很多个 , 估计翻译成in computing 比较合理

 

一个genearated signal 最多只能deliver 一次( 即一个信号最多只会被处理一次) . signal 是可消耗资源 , 一旦一个signal 被deliver, 那么所有进程对它的引用都会被取消 .

已经产生但是还未被处理(deliver) 的信号称为pending signal ( 悬挂信号). 对于regular signal, 在某一个时刻 , 一种signal 在一个进程中只能有一个实例( 因为进程没有用队列缓存其收到的signal) . 因为有31 种regualar signal , 所以一个进程某一个时刻可以有31 个各类signal 的实例. 此外因为linux 进程对real time signal 采用不同的处理方式, 它会保存接收到的real time signal 的实例 , 所以可以同时有很多同种signal 的实例 .

问题: 不同种类的信号的优先级( 从值较小的开始处理) .

一般而言 , 一个信号可能会被悬挂很长是时间( 即一个进程收到一个信号后 , 该信号有可能在该进程里很久 , 因为进程没空来处理它), 主要有如下因素:

1. 信号通常被当前进程处理 . Signals are usually delivered only to the currently running process (that is, to the current process).

2. 某种类型的信号可能被本进程阻塞. 只有当其被取消阻塞好才会被处理 .

3. 当一个进程执行某一种信号的处理函数时 , 一般会自动阻塞这种信号 , 等处理完毕后才会取消阻塞 . 这意味着一个信号处理函数不会被同种信号阻塞 .

 

尽管信号在概念上很直观 , 但是内核的实现却相当复杂. 内核必须:

1. 记录一个进程阻塞了哪些信号

2. 当从核心态切换到用户态时 , 检查进程是否接受到了signal.( 几乎每一次时钟中断都要干这样的事 , 费时吗?).

3. 检查信号是否可以被忽略. 当如下条件均满足时则可被忽略:

   1). 目标进程未被其它进程traced( 即PT_PTRACED==0). 但一个被traced 的进程收到一个信号时 , 内核停止目标线程 , 并且给tracing 进程发送信号SIGCHLD. tracing 进程可能会通过SIGCONT来恢复traced 进程的执行

   2). 目标进程未阻塞该信号 .

   3). 信号正被目标进程忽略( 或者由于忽略是显式指定的或者由于忽略是默认操作).

4. 处理信号 . 这可能需要切换到信号处理函数

 

此外, linux 还需要处理BSD, System V 中signal 语义的差异性 . 另外 , 还需要遵守POSIX 的定义 .

 

 

 

处理信号的方式 (Actions Performed upon Delivering a Signal)

一个进程可以采用三中方式来响应它接收到的信号:

1.(ignore) 显示忽略该信号

2.(default) 调用默认的函数来响应该信号( 这些默认的函数由内核定义) , 一般这些默认的函数都分成如下几种( 采用哪一种取决于信号的类型 , 参考前面的表格): 
Terminate: The process is terminated (killed) 
Dump: The process is terminated (killed) and a core file containing its execution context is created, if possible; this file may be used for debug purposes. 
Ignore:The signal is ignored. 
Stop:The process is stopped, i.e., put in the TASK_STOPPED state. 
Continue:If the process was stopped (TASK_STOPPED), it is put into the TASK_RUNNING state.

3.(catch) 调用相应的信号处理函数 ( 这个信号处理函数通常是程序员在运行时指定的). 这意味着进程需要在执行时显式地指明它需要catch 哪一种信号. 并且指明其处理函数 . catch 是一种主动处理的措施 .

注意上述的三个处理方式被标识为:ignore, default, catch. 这三个处理方式以后会通过这三个标识符引用 .

 

注意阻塞一个信号和忽略一个信号是不同 , 一个信号被阻塞是就当前不会被处理 , 即一个信号只有在解除阻塞后才会被处理 . 忽略一个信号是指采用忽略的方式来处理该信号( 即对该信号的处理方式就是什么也不做) .

SIGKILL 和SIGSTOP 这两个信号不能忽略 , 不能阻塞 , 不能使用用户定义的函数(caught) . 所以总是执行它们的默认行为 . 所以 , 它们允许具有恰当特权级的用户杀死别的进程, 而不必在意被杀进程的防护措施 ( 这样就允许高特权级用户杀死低特权级的用户占用大量cpu 的时间) .

注: 有两个特殊情况. 第一 , 任意进程都不能给进程0( 即swapper 进程) 发信号 . 第二 , 发给进程1 的信号都会被丢弃(discarded), 除非它们被catch. 所以进程 0 不会死亡, 进程1 仅在int 程序结束时死亡 .

 

一个信号对一个进程而言是致命的(fatal) , 当前仅当该信号导致内核杀死该进程 . 所以,SIGKILL总是致命的. 此外 , 如果一个进程对一个信号的默认行为是terminate 并且该进程没有catch 该信号 , 那么该信号对这个进程而言也是致命的 . 注意 , 在catch 情况下 , 如果一个进程的信号处理函数自己杀死了该进程 , 那么该信号对这个进程而言不是致命的 , 因为不是内核杀死该进程而是进程的信号处理函数自己杀死了该进程.

 

POSIX 信号以及多线程程序

 

POSIX 1003.1 标准对多线程程序的信号处理有更加严格的要求: 
( 由于linux 采用轻量级进程来实现线程 , 所以对linux 的实现也会有影响)

1. 多线程程序的所有线程应该共享信号处理函数 , 但是每一个线程必须有自己的mask of pending and blocked signals

2. POSIX 接口kill( ), sigqueue( ) 必须把信号发给线程组 , 而不是指定线程. 另外内核产生的SIGCHLD, SIGINT, or SIGQUIT 也必须发给线程组 .

3. 线程组中只有有一个线程来处理(deliver) 的共享的信号就可以了 . 下问介绍如何选择这个线程 .

4. 如果线程组收到一个致命的信号 , 内核要杀死线程组的所有线程, 而不是仅仅处理该信号的线程 .

 

为了遵从POSIX 标准, linux2.6 使用轻量级进程实现线程组.

 

下文中 , 线程组表示OS 概念中的进程, 而线程表示linux 的轻量级进程. 进程也( 更多地时候)表示linux 的轻量级进程 . 另外每一个线程有一个私有的悬挂信号列表 , 线程组共享一个悬挂信号列表 .

 

与信号有关的数据结构

注:pending/ 悬挂信号, 表示进程收到信号 , 但是还没有来得及处理 , 或者正在处理但是还没有处理完成 .

对于每一个进程, 内核必须知道它当前悬挂(pending) 着哪些信号或者屏蔽(mask) 着哪些信号 .还要知道线程组如何处理信号. 为此内核使用了几个重要的数据结构( 它们可通过task 实例访问), 如下图:

The most significant data structures related to signal handling

Linux 信号signal处理机制_第1张图片

 

( 注意task 中的一些关于signal 的成员在上图中没有表现出来)

task 中关于signal 的成员列在下表中:

 

Process descriptor fields related to signal handling

Type

Name

Description

struct signal_struct *

signal

Pointer to the process's signal descriptor( 线程组共用 的信号)

struct sighand_struct *

sighand

Pointer to the process's signal handler descriptor(线程组共用 )

sigset_t

blocked

Mask of blocked signals( 线程私有)

sigset_t

real_blocked

Temporary mask of blocked signals (used by thert_sigtimedwait( ) system call) ( 线程私有)

structsigpending

pending

Data structure storing the private pending signal s

unsigned long

sas_ss_sp

Address of alternative signal handler stack.( 可以不提供)

size_t

sas_ss_size

Size of alternative signal handler stack( 可以不提供)

int (*) (void *)

Notifier

Pointer to a function used by a device driver to block some signals of the process

void *

notifier_data

Pointer to data that might be used by the notifier function (previous field of table)

sigset_t *

notifier_mask

Bit mask of signals blocked by a device driver through a notifier function

 

 

blocked 成员 保存进程masked out 的signal . 其类型为sigset_t , 定义如下:

    typedef struct {

        unsigned long sig[2];

    } sigset_t;

sizeof(long)==32, sigset_t 被当成了bit array 使用. 正如前文提到的,linux 有64 种信号 ,[1,31] 为regular signal, [32,64] 为real time signal. 每一种对应sigset_t 中一个bit.

 

信号描述符& 信号处理函数描述符

task 的signal, sighand 成员分别是信号描述符与信号处理函数描述符 .

signal 成员 是一个指针 , 它指向结构体signal_struct 的实例 , 该实例保存了线程组悬挂着的信号 . 也就是说线程组中的所有进程( 这里称为task 更合理) 共用同一个signal_struct 实例. signal_struct 中的shared_pending 成员保存了所有悬挂的信号( 以双向链表组织) . 此外signal_struct 中还保存了许多其它的信息(eg: 进程资源限制信息, pgrp, session 信息) .

下表列出了signal_struct 中与信号处理有关的成员:

 

The fields of the signal descriptor related to signal handling

Type

Name

Description

atomic_t

count

Usage counter of the signal descriptor

atomic_t

live

Number of live processes in the thread group

wait_queue_head_t

wait_chldexit

Wait queue for the processes sleeping in await4( ) system call

struct task_struct *

curr_target

Descriptor of the last process in the thread group that received a signal

structsigpending

shared_pending

Data structure storing the shared pending signals

int

group_exit_code

Process termination code for the thread group

struct task_struct *

group_exit_task

Used when killing a whole thread group

int

notify_count

Used when killing a whole thread group

int

group_stop_count

Used when stopping a whole thread group

unsigned int

flags

Flags used when delivering signals that modify the status of the process

 

 

除了signal 成员外 , 还有一个sighand 成员 用来指明相应的信号处理函数.

sighand 成员是一个指针 , 指向一个sighand_struct 变量 , 该变量为线程组共享 . 它描述了一个信号对应的信号处理函数.

 

sighand_struct 成员如下:

The fields of the signal handler descriptor

Type

Name

Description

atomic_t

count

Usage counter of the signal handler descriptor

struct k_sigaction [64]

action

Array of structures specifying the actions to be performed upon delivering the signals

spinlock_t

siglock

Spin lock protecting both the signal descriptor and the signal handler descriptor

sighand_struct 中的重要成员是action, 它是一个数组 , 描述了每一种信号对应的信号处理函数 .

sigaction 数据结构

某一些平台上, 会赋予一个signal 一些只能内核才可见的属性. 这些属性与sigaction( 它在用户态也可见) 构成了结构体k_sigaction. 在x86 上,k_sigaction 就是sigaction. 

注: 用户使用的sigaction 和内核使用的sigaction 结构体有些不同但是 , 它们存储了相同的信息( 自己参考一下用户态使用的sigaction 结构体吧).

内核的sigaction 的结构体的成员如下:

1)sa_handler: 类型为 void (*)(int):

    这个字段指示如何处理信号 . 它可以是指向处理函数的指针 , 也可以是SIG_DFL(==0) 表示使用默认的处理函数 , 还可以是SIG_IGN(==1) 表示忽略该信号

2)sa_flags: 类型为unsigned long:

   指定信号如何被处理的标志 , 参考下表 ( 指定信号如何处理的标志) .

3)sa_mask: 类型为sigset_t:

   指定当该信号处理函数执行时,sa_mask 中指定的信号必须屏蔽 .

 

指定信号如何处理的标志

注: 由于历史的原因 , 这些标志的前缀为SA_, 这和irqaction 的flag 类似 , 但其实它们没有关系.

Flags specifying how to handle a signal

Flag Name

Description

SA_NOCLDSTOP

Applies only to SIGCHLD ; do not send SIGCHLD to the parent when the process is stopped

SA_NOCLDWAIT

Applies only to SIGCHLD ; do not create a zombie when the process terminates

SA_SIGINFO

Provide additional information to the signal handler

SA_ONSTACK

Use an alternative stack for the signal handler

SA_RESTART

Interrupted system calls are automatically restarted

SA_NODEFER, SA_NOMASK

Do not mask the signal while executing the signal handler

SA_RESETHAND,

SA_ONESHOT

Reset to default action after executing the signal handler



 

悬挂的信号队列 (sigpending)

 

通过前文我们知道有些系统调用能够给线程组发信号(eg:kill, rt_sigqueueinfo), 有些操作给指定的进程发信号(eg:tkill, tgkill) .

 

为了区分这两类, task 中其实有两种悬挂信号列表: 
1.task 的 pending 字段表示了本task 上私有的悬挂信号( 列表) 
2.task 的signal 字段中的shared_pending 字段则保存了线程组共享的悬挂信号( 列表).

悬挂信号 列表用数据结构sigpending 表示 , 其定义如下: 
     struct sigpending { 
        struct list_head list; 
        sigset_t signal; 
    }

其signal 成员指明当前悬挂队列悬挂了哪些信号 .

其list 字段其实是一个双向链表的头 , 链表的元素的类型是sigqueue. sigqueue 的成员如下:

The fields of the sigqueue data structure

Type

Name

Description

struct list_head

list

Links for the pending signal queue's list

spinlock_t *

lock

Pointer to the siglock field in the signal handler descriptor corresponding to the pending signal

Int

flags

Flags of the sigqueue data structure

siginfo_t

info

Describes the event that raised the signal

struct

user_struct *

user

Pointer to the per-user data structure of the process's owner

( 注:sigqueue 的名字有queue, 但它其实只是悬挂队列的一个元素 . 它会记录一个被悬挂的信号的信息)

 

siginfo_t 是一个包含128 byte 的数据结构 , 用来描述一个指定信号的发生,其成员如下: 
si_signo: 信号ID

si_errno: 导致这个信号被发出的错误码. 0 表示不是因为错误才发出信号的 .

si_code: 标识谁发出了这个信号 . 参考下表 :

The most significant signal sender codes

Code Name

Sender

SI_USER

kill( ) and raise( )

SI_KERNEL

Generic kernel function

SI_QUEUE

sigqueue( )

SI_TIMER

Timer expiration

SI_ASYNCIO

Asynchronous I/O completion

SI_TKILL

tkill() and tgkill()

 

_sifields: 这个字段是一个union, 它有不少 成员 , 哪一个成员有效取决于信号 . 比如对于SIGKILL, 则它会记录信号发送者的PID,UID; 对于SIGSEGV, 它会存储导致访问出错的内存地址 .

 

操作信号数据结构的函数

一些宏和函数会使用信号数据结构 . 在下文的解说中, set 表示指向sigset_t 变量的指针, nsig表示信号的标识符( 信号的整数值).mask 是一个unsign long bit mask.

sigemptyset (set) and sigfillset (set)

把set 所有bit 设置为 0 或者1 .

sigaddset (set,nsig) and sigdelset (set,nsig)

把set 中对应与nsig 的bit 设置为1 或者 0. In practice, sigaddset( ) reduces to: 
    set->sig[(nsig - 1) / 32] |= 1UL << ((nsig - 1) % 32);

and sigdelset( ) to: 
    set->sig[(nsig - 1) / 32] &= ~(1UL << ((nsig - 1) % 32));

sigaddsetmask (set,mask) and sigdelsetmask (set,mask)

根据mask 的值设置set. 仅能设置1-32 个signal. The corresponding functions reduce to:    
      set->sig[0] |= mask;

and to: 
set->sig[0] &= ~mask;

 

sigismember (set,nsig)

返回set 中对应nsig 的bit 的值. In practice, this function reduces to:

    return 1 & (set->sig[(nsig-1) / 32] >> ((nsig-1) % 32));

 

sigmask (nsig)

根据信号标志码nsig 等到它的在sigset_t 中的bit 位的index.

sigandsets (d,s1,s2), sigorsets (d,s1,s2), and signandsets (d,s1,s2)

      伪代码如下:d=s1 & s2; d=s1|s2, d=s1 & (~s2)

sigtestsetmask (set,mask)

如果mask 中的为1 的位在set 中的相应位也为1, 那么返回1. 否则返回0. 只适用于1-32个信号.

siginitset (set,mask)

用mask 设置set 的1-32 个信号, 并把set 的33-63 个信号清空.

siginitsetinv (set,mask)

用(!mask) 设置set 的1-32 个信号, 并把set 的33-63 个信号设置为1.

signal_pending (p)

检查p 的 t->thread_info->flags 是否为 TIF_SIGPENDING. 即检查p 是否有 悬挂的非阻塞信号.

recalc_sigpending_tsk (t) and recalc_sigpending ( )

第一个函数检查 t->pending->signal 或者 t->signal->shared_pending->signal 上是否有悬挂的非阻塞信号. 若有设置 t->thread_info->flags 为 TIF_SIGPENDING.

recalc_sigpending( ) 等价于 recalc_sigpending_tsk(current) .

rm_from_queue (mask,q)

清掉悬挂信号队列q 中的由mask 指定的信号.

flush_sigqueue (q)

清掉悬挂信号队列q 中的信号.

flush_signals (t)

删除t 收到的所有信号. 它会清掉 t->thread_info->flags 中的TIF_SIGPENDING 标志, 并且调用flush_sigqueue 把t->pending 和 t->signal->shared_pending 清掉 .

 

Generating a Signal

很多内核函数会产生signal, 它完成处理处理的第一个阶段(generate a signal) , 即更新信号的目标进程的相应字段 . 但是它们并不直接完成信号处理的第二阶段(deliver the signal), 但是它们会根据目标进程的状态或者唤醒目标进程或者强制目标进程receive the signal .

注:generating a signal 这个阶段是从源进程发起一个信号 , 然后源进程在内核态下修改目标进程的相应状态, 然后可能源进程还会唤醒目的进程 .

无论一个信号从内核还是从另外一个进程被发送给另一个线程( 目标进程) , 内核都会执行如下的函数之一来发送信号:

Kernel functions that generate a signal for a process

Name

Description

send_sig( )

Sends a signal to a single process

send_sig_info( )

Like send_sig( ) , with extended information in a siginfo_tstructure

force_sig( )

Sends a signal that cannot be explicitly ignored or blocked by the process

force_sig_info( )

Like force_sig( ) , with extended information in a siginfo_tstructure

force_sig_specific( )

Like force_sig( ) , but optimized for SIGSTOP and SIGKILL signals

sys_tkill( )

System call handler of tkill( )

sys_tgkill( )

System call handler of tgkill( )

所有这些函数最终都会调用 specific_send_sig_info ( ) .

无论一个信号从内核还是从另外一个进程被发送给另一个线程组( 目标进程), 内核都会执行如下的函数之一来发送信号:

Kernel functions that generate a signal for a thread group

Name

Description

send_group_sig_info( )

Sends a signal to a single thread group identified by the process descriptor of one of its members

kill_pg( )

Sends a signal to all thread groups in a process group

kill_pg_info( )

Like kill_pg( ) , with extended information in a siginfo_tstructure

kill_proc( )

Sends a signal to a single thread group identified by the PID of one of its members

kill_proc_info( )

Like kill_proc( ) , with extended information in a siginfo_tstructure

sys_kill( )

System call handler of kill( )

sys_rt_sigqueueinfo( )

System call handler of rt_sigqueueinfo( )

这些函数最终都调用 group_send_sig_info ( ) .

 

specific_send_sig_info 函数说明

 

这个函数给指定的目标线程( 目标进程) 发送一个信号 . 它有三个参数:

参数sig: 信号( 即某一个信号) .

参数info: 或者是 siginfo_t 变量地址或者如下三个特殊值: 
0 : 表示信号由用户态进程发送; 
1 : 表示信号由核心态( 进程) 发送;
2 : 表示信号由核心态( 进程) 发送, 并且信号是SIGKILL 或者SIGSTOP.

参数t: 目标进程的task 实例指针

specific_send_sig_info 调用时必须禁止本cpu 的中断 , 并且获得t->sighand->siglock spin lock. 它会执行如下操作:

1. 检查目标线程是否忽略该信号, 若是返回0. 当如下三个条件均满足时则可认为忽略该信号:
   1). 目标线程未被traced( 即t->ptrace 不含PT_PTRACED 标志).
   2). 该信号未被目标线程阻塞( 即sigismember(&t->blocked, sig) == 0).
   3). 该信号被目标线程显式地忽略( 即t->sighand->action[sig-1].sa_handler == SIG_IGN)或者隐式忽略( 即handler==SIG_DFT 并且信号为SIGCONT, SIGCHLD, SIGWINCH, or SIGURG.).

2. 检查信号是否是非实时信号(sig<32) 并且同样的信号是否已经在线程的私有悬挂信号队列中了, 若是则返回0.

3. 调用send_signal(sig, info, t, &t->pending) 把信号加入目标线程的私有悬挂信号队列中.下文会详述.

4. 如果send_signal 成功并且信号未被目标线程阻塞, 则调用signal_wake_up ( ) 来通知目标进程有新的信号达到. 这个函数执行如下步骤:
   1). 把标志TIF_SIGPENDING 加到t->tHRead_info->flags 中
   2). 调用try_to_wake_up(). 如果目标线程处于TASK_INTERRUPTIBLE 或者TASK_STOPPED 并且信号是SIGKILL 则唤醒目标线程.
   3). 如果try_to_wake_up 返回0, 则目标线程处于runnable 状态, 之后检查目标线程是否在别的CPU 上执行, 如果是则向该CPU 发送处理器中断以强制该cpu 重调度目标线程( 注: 目前我们并未考虑多处理器的情况). 因为每一个线程在从schedule() 返回时都会检查是否存在悬挂的信号,所以这个处理器中断将会使目标线程很快就看到这个新的悬挂信号.

5. 返回1( 表示信号已经成功generated.)

 

send_signal 函数

这个函数接受四个参数:sig, info, t, signals. 其中sig, info,t 在specific_send_sig_info中已经介绍过了. signals 则是t 的pending queue 的首地址 . 它的执行流程如:

1. 若info==2, 那么这个信号是SIGKILL 或是SIGSTOP, 并且由kernel 通过force_sig_specific产生. 此时直接跳到9. 因为这种情况下, 内核会立即执行信号处理, 所以不用把该信号加入信号悬挂队列中.

2. 如果目标进程的用户当前的悬挂信号数目(t->user->sigpending) 小于目标进程的最大悬挂信号数目(t->signal->rlim[RLIMIT_SIGPENDING].rlim_cur), 则为当前信号分配一个sigqueue 变量,标识为q

3. 如果目标进程的用户当前的悬挂信号数目太大, 或者上一步中分配sigqueue 变量失败, 则跳到9.

4. 增加目标进程的用户当前的悬挂信号数目(t->user->sigpending) 以及t-user 的引用数.

5. 把信号q 加入目标线程的悬挂队列:
    list_add_tail(&q->list, &signals->list);

6. 填充q, 如下

    if ((unsigned long)info == 0) {
        q->info.si_signo = sig;
        q->info.si_errno = 0;
        q->info.si_code = SI_USER;
        q->info._sifields._kill._pid = current->pid;
        q->info._sifields._kill._uid = current->uid;
    } else if ((unsigned long)info == 1) {
        q->info.si_signo = sig;
        q->info.si_errno = 0;
        q->info.si_code = SI_KERNEL;
        q->info._sifields._kill._pid = 0;
        q->info._sifields._kill._uid = 0;
    } else
        copy_siginfo(&q->info, info);

函数copy_siginfo 用caller 传进来的info 填充q->info

7. 设置悬挂信号队列中的mask 成员的与sig 相应的位( 以表示该信号在悬挂信号队列中)
    sigaddset(&signals->signal, sig);

7. 返回0 以表示信号被成功加入悬挂信号队列.

9. 如果执行这一步, 则该信号不会被加入信号悬挂队列, 原因有如下三个:1) 有太多的悬挂信号了, 或者2) 没有空闲的空间来分配sigqueue 变量了, 或者3) 该信号的处理由内核立即执行. 如果信号是实时信号并且通过内核函数发送并且显式要求加入队列, 那么返回错误代码-EAGAIN( 代码类似如下):
    if (sig>=32 && info && (unsigned long) info != 1 &&
                   info->si_code != SI_USER)
        return -EAGAIN;

10. 设置悬挂信号队列中的mask 成员的与sig 相应的位( 以表示该信号在悬挂信号队列中)
    sigaddset(&signals->signal, sig);

11. 返回0. 尽管该信号没有放到悬挂信号队列中, 但是相应的signals->signal 中已经设置了

 

即使没有空间为信号分配sigqueue 变量,也应该让目标信号知道相应的信号已经发生, 这一点很重要. 考虑如下情形: 目标进程使用了很多内存以致于无法再分配sigqueue 变量了, 但是内核必须保证对目标进程依的kill 依然能够成功, 否则管理员就没有机会杀死目标进程了.

 

group_send_sig_info 函数

函数 group_send_sig_info 把一个信号发给一个线程组 . 这个函数有三个参数:sig, info, p . (和specific_send_sig_info 类似).

这个函数的执行流程如下 :

1. 检查参数sig 的正确性:

  if (sig < 0 || sig > 64)

     return -EINVAL;

2. 如果信号的发送进程处于用户态, 则检查这个发送操作是否允许. 仅当满足如下条件之一( 才视为允许):

  1). 发送者进程有恰当的权限( 通常发送者进程应该是system administrator).

  2). 信号为SIGCONT, 并且目标进程和发送者进程在同一个login session.

  3). 目标进程和发送者进程属于同一个用户

3. 如果用户态的进程不能发送此信号, 则返回-EPERM. 如果sig==0, 则立即返回.( 因为0 是无效的信号). 如果sighand==0, 也立即返回, 因为此时目标进程正在被杀死, 从而sighand 被释放.

    if (!sig || !p->sighand)

        return 0;

4. 获得锁 p->sighand->siglock, 并且关闭本cpu 中断.

5. 调用handle_stop_signal 函数, 这个函数检查sig 是否会和现有的悬挂的信号冲突, 会的话解决冲突. 这个函数的步骤如下:

  1). 如果线程组正在被杀死(SIGNAL_GROUP_EXIT) ,则返回.

  2). 如果sig 是IGSTOP, SIGTSTP, SIGTTIN, SIGTTOU 中的一种, 则调用rm_from_queue, 把线程组中所有悬挂的SIGCONT 删除. 注意: 包含线程组共享的悬挂信号队列中的(p->signal->shared_pending) 以及每一个线程私有悬挂队列中的.

  3). 如果sig 是SIGCONT, 则调用rm_from_queue, 把线程组中所有悬挂的SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU 删除. 注意: 包含线程组共享的悬挂信号队列中的(p->signal->shared_pending) 以及每一个线程私有悬挂队列中的. 之后为每一个线程调用try_to_wake_up.

6. 检查线程组是否忽略该信号, 如果忽略返回0.

7. 如果是非实时信号, 并且该线程组已经有这种悬挂的信号了, 那么返回0:

    if (sig<32 && sigismember(&p->signal->shared_pending.signal,sig))

        return 0;

8. 调用send_signal( ) 把信号加到线程组的共享悬挂信号队列中, 如果send_signal 返回非0 值,则group_send_sig_info 退出并把该非零值返回.

9. 调用_ _group_complete_signal( ) 来唤醒线程组中的一个轻量级进程. 参考下文.

10. 释放p->sighand->siglock 并且打开本地中断.

11. 返回 0 (success).

 

函数 _ _group_complete_signal ( ) 扫描目标线程组 , 并且返回一个能够处理 (receive) 该新信号的进程 . 这样的进程必须同时具备如下的条件 :

1) 该进程不阻塞新信号.

2) 进程的状态不是EXIT_ZOMBIE, EXIT_DEAD, TASK_TRACED, or TASK_STOPPED. 但是当信号是SIGKILL 是, 进程的状态允许是TASK_TRACED or TASK_STOPPED.

3) 进程不处于正在被杀死的状态, 即状态不是PF_EXITING.

4) 或者进程正在某一个cpu 上执行, 或者进程的TIF_SIGPENDING 的标志未被设置.

 

一个线程组中满足上诉条件的线程( 进程) 可能很多, 根据如下原则选择一个:

1) 如果group_send_sig_info 中的参数p 指定的进程满足上述条件, 则选择p.

2) 否则从最后一个接收线程组信号的线程(p->signal->curr_target) 开始查找满足上述条件的线程, 找到为止.

 

( 如果线程组中没有一个线程满足上述条件怎么办?)

 

如_ _group_complete_signal( ) 成功找到一个进程( 表示为selected_p), 那么:

1. 检查该信号是否是致命的, 若是, 通过给线程组中的每一个线程发送SIGKILL 来杀死线程组

2. 若不是, 调用signal_wake_up 来唤醒selected_p 并告知它有新的悬挂信号,

 

Delivering a Signal

通过上面的介绍, 内核通过修改目标进程的状态, 告知目标进程有新的信号到达. 但是目标进程对到达的新信号的处理(deliver signal) 我们还没有介绍. 下面介绍目标进程如何在内核的帮助下处理达到的新信号.

注意当内核( 代码) 要把进程从核心态恢复成用户态时( 当进程从异常/ 中断处理返回时), 内核会检查该进程的 TIF_SIGPENDING 标识 , 如果存在悬挂的信号 , 那么将先处理该信号 .

这里需要介绍一下背景: 当进程在用户态( 用U1 表示) 下由于中断/ 异常而进入核心态, 那么需要把U1 的上下文记录到该进程的内核堆栈中.

为了处理非阻塞的信号 , 内核调用do_signal 函数 . 这个函数接受两个参数:

regs: 指向U1 上下文在内核堆栈的首地址 ( 参考进程管理).

oldest:  保存了一个变量的地址, 该变量保存了被阻塞的信号的信息( 集合). 如果该参数为NULL, 那么这个地址就是&current->blocked ( 如下文). 注意当自定义信号处理函数结束后,会把oldest 设置为当前task 的阻塞信号集合.( 参考源代码, 以及rt_frame 函数).

我们这里描述的do_signal 流程将会关注信号delivery( 处理), 而忽略很多细节, eg: 竞争条件 ,产生core dump, 停止和杀死线程组等等 .

一般,do_signal 一般仅在进程即将返回用户态时执行 . 因此 , 如果一个中断处理函数调用do_signal, 那么do_signal 只要按如下方式放回:
    if ((regs->xcs & 3) != 3)
        return 1;

如果oldest 为NULL, 那么 do_signal 会把它设置为当前进程阻塞的信号:

    if (!oldset)
        oldset = &current->blocked;

 

do_signal 的核心是一个循环 , 该循环调用dequeue_signal 从进程的私有悬挂信号队列和共享悬挂队列获取未被阻塞的信号. 如果成功获得这样的信号, 则通过handle_signal 调用相应的信号处理函数, 否则退出do_signal .

( 这个循环不是用C 的循环语句来实现, 而是通过修改核心栈的regs 来实现. 大概的流程可以认为如下: 当由核心态时切换向用户态时, 检查是否有非阻塞的悬挂信号, 有则处理( 包含: 准备信号处理函数的帧, 切换到用户态以执行信号处理函数, 信号处理函数返回又进入核心态), 无则返回原始的用户态上下文)

 

dequeue_signal 先从私有悬挂信号列表中按照信号值从小到大取信号,取完后再从共享悬挂信号列表中取 . ( 注意取后要更新相应的信息)

 

接着我们考虑, do_signal 如何处理获得的信号( 假设用signr 表示) .

首先 , 它会检查是否有别的进程在监控(monitoring) 本进程 , 如果有 , 调用do_notify_parent_cldstop 和schedule 来让监控进程意识到本进程开始信号处理了.

接着,do_signal 获得相应的信号处理描述符( 通过current->sig->action[signr-1]) , 从而获得信号处理方式的信息 . 总共有三种处理方式: 忽略 , 默认处理 , 使用用户定义的处理函数 .

 

如果是忽略 , 那么什么也不做 :

if (ka->sa.sa_handler == SIG_IGN)

        continue;

 

执行默认的信号处理函数

如果指定的是默认的处理方式. 那么do_signal 使用默认的处理方式来处理信号 . ( 进程 0 不会涉及 , 参考前文)

对于init 进程除外 , 则它要丢弃信号:
    if (current->pid == 1)
        continue;

对于其它进程, 默认的处理方式取决于信号 .

第一类: 这类信号的默认处理方式就是不处理

    if (signr==SIGCONT || signr==SIGCHLD ||

            signr==SIGWINCH || signr==SIGURG)

        continue;//

第二类: 这类信号的默认处理方式如下:

    if (signr==SIGSTOP || signr==SIGTSTP ||

            signr==SIGTTIN || signr==SIGTTOU) {

        if (signr != SIGSTOP &&

               is_orphaned_pgrp(current->signal->pgrp))

            continue;

        do_signal_stop(signr);

    }

这里, SIGSTOP 与其他的信号有些微的区别.

SIGSTOP 停止整个线程组. 而其它信号只会停止不在孤儿进程组中的进程( 线程组).

孤儿进程组(orphand process group).

非孤儿进程组 指如果进程组A 中有一个进程有父亲, 并且该父进程在另外一个进程组B 中, 并且这两个进程组A,B 都在用一个会话(session) 中, 那么进程组A 就是非孤儿进程组. 因此如果父进程死了, 但是启动在进程的session 依旧在, 那么进程组A 都不是孤儿.

注: 这两个概念让我迷糊.

do_signal_stop 检查当前进程是否是线程组中的第一个正在被停止的进程, 如果是, 它就激活一个组停(group stop) 。本质上, 它会把信号描述符的 group_stop_count 字段设置为正值, 并且唤醒线程组中的每一个进程。每一个进程都会查看这个字段从而认识到正在停止整个线程组, 并把自己的状态改为 TASK_STOPPED, 然后调用schedule. do_signal_stop 也会给线程组的父进程发送SIGCHLD, 除非父进程已经被设置为SA_NOCLDSTOP flag of SIGCHLD.

默认行为是dump 的信号处理可能会进程工作目录下创建一个core 文件. 这个文件列出了进程的地址空间和cpu 寄存器的值. do_signal 创建这个文件后, 就会杀死整个线程组. 剩下18 个信号的默认处理是terminate, 这仅仅是简单地杀死整个线程组. 为此,do_signal 调用了do_group_exit。

 

使用指定的函数来处理信号(catching the signal)

如果程序为信号设置了处理函数 , 那么do_signal 将会通过调用handle_signal 来强制该信号函数被执行:

    handle_signal(signr, &info, &ka, oldset, regs);

    if (ka->sa.sa_flags & SA_ONESHOT)

        ka->sa.sa_handler = SIG_DFL;

return 1;

 

如果用户在为信号设置信号处理函数时指定了 SA_ONESHOT , 那么当该信号处理函数第一次执行后 ,其将会被reset. 即以后来的这样的信号将会使用默认的处理函数 .

Notice how do_signal( ) returns after having handled a single signal. Other pending signals won't be considered until the next invocation of do_signal( ) . This approach ensures that real-time signals will be dealt with in the proper order.

执行一个信号处理函数相当复杂 , 因为需要内核小心处理用户信号处理函数的调用栈, 然后把控制权交给用户处理函数( 注意这里涉及内核态到用户态的转换) .

用户的信号处理函数定义在用户态中并且包含在用户代码段中,它需要在用户态(U2) 下执行. hande_signal 函数在核心态下执行. 此外, 由于当前的核心态是在前一个用户态(U1) 转过来, 这意味着当信号处理函数(U2) 结束, 回到内核态, 然后内核态还需要回到U1, 而当从U2 进入核心态后, 内核栈存放的已经不再是U1 的上下文了( 而是U2), 此外一般信号处理函数中还会发生系统调用( 用户态到核心态的转换), 而系统调用结束后要回到信号处理函数. 

注意: 每一个内核态切换到用户态, 进程的内核堆栈都会被清空.

那么handle_signal 如何调用信号处理函数呢??

Linux 采用的方法如下: 每次调用信号处理函数之前, 把U1 的上下文拷贝到信号处理函数的栈中(一般信号处理函数的栈也是当前进程的用户态的栈, 但是程序员也可以在设置信号处理函数时指定一个自己定义的栈, 但是这里不影响这个方法, 所以我们只描述信号处理函数使用进程用户态的栈的情况). 然后再执行信号处理函数. 而当信号处理函数结束之后, 会调用sigreturn() 从U2 的栈中把U1 的上下文拷贝到内核栈中.

 

下图描述了信号处理函数的执行流程. 一个非阻塞的信号发给目标进程. 当一个中断或异常发生后, 目标进程从用户态(U1) 进入核心态. 在它切换回用户态(U1) 之前, 内核调用do_signal. 这个函数逐一处理悬挂的非阻塞信号. 而如果目标进程设置了对信号的处理函数, 那么它会调用handle_signal 来调用自定义的信号处理函数( 这期间需要使用 setup_frame 或setup_rt_frame来为信号处理函数设置栈 ), 此时当切换到用户态时, 目标进程执行的是信号处理函数而不是U1.当信号处理函数结束后, 位于 setup_frame 或setup_rt_frame 栈之上的返回代码 ( return code)被执行, 这返回代码会执行sigreturn 或者rt_sigreturn 从而把U1 的上下文从setup_frame或setup_rt_frame 栈中拷贝到核心栈. 而这结束后, 内核可以切换回U1.

注意: 信号有三种处理方式, 只有使用自定义处理函数才需要这样麻烦啊.

Linux 信号signal处理机制_第2张图片

 

接下来我们需要仔细瞧瞧这一切怎么发生的.

 

Setting up the frame

为了能恰当地为信号处理函数设置栈,handle_signal 调用setup_frame( 当信号没有相应的siginfo_t 时) 或者setup_rt_frame( 当信号有相应的siginfo_t 时). 为了判断采用哪一种, 需要参考 sigaction 中的sa_flag 是否包含SA_SIGINO.

setup_frame 接受四个参数, 如下:

sig: 信号标识

ka: 与信号相关的 k_sigaction 实例

oldest: 进程阻塞的信号

regs: U1 上下为在核心栈的地址.

 

setup_frame 函数会在用户栈中分配一个sigframe 变量, 该变量包含了能够正确调用信号处理函数的信息( 这些信息会被 sys_sigreturn 使用 ). sigframe 的成员如下( 其示意图如下):

pretcode : 信号处理函数的返回地址. 其指向标记为 kernel_sigreturn 的代码

sig : 信号标识.

sc : sigcontext 变量. 它包含了U1 的上下文信息, 以及被进程阻塞的非实时信号的信息.

fpstate : _fpstate 实例, 用来存放U1 的浮点运算有关的寄存器.

extramask : 被进程阻塞的实时信号的信息 .

retcode :8 字节的返回代码, 用于发射 sigreturn 系统调用. 早期版本的linux 用于信号处理函数返回后的善后处理.linux2.6 则用于特征标志, 所以调试器能够知道这是一个信号处理函数的栈.

Frame on the User Mode stack

 

 

setup_frame 函数首先获得sigframe 变量的地址, 如下:

frame =(regs->esp - sizeof(struct sigframe)) & 0xfffffff8

注意: 默认地信号处理函数使用得到栈是进程在用户态下的栈, 但是用户在设置信号处理函数时可以指定. 这里只讨论默认情况. 对于用户指定其实也一样.

另外由于栈从大地址到小地址增长, 所以上面的代码要看明白了. 此外还需要8 字节对齐.

之后使用 access_ok 来验证 frame 是否可用, 之后用__put_user 来填充frame 各个成员. 填充好之后, 需要修改核心栈, 这样从核心态切换到用户态时就能执行信号处理函数了, 如下:

    regs->esp = (unsigned long) frame;

    regs->eip = (unsigned long) ka->sa.sa_handler;

    regs->eax = (unsigned long) sig;

    regs->edx = regs->ecx = 0;

    regs->xds = regs->xes = regs->xss = _ _USER_DS;

    regs->xcs = _ _USER_CS;

 

setup_rt_frame 和setup_frame 类似, 但是它在用户栈房的是一个rt_sigframe 的实例, rt_sigframe 除了sigframe 外还包含了siginfo_t( 它描述了信号的信息). 另外它使用 _ _kernel_rt_sigreturn.

 

Evaluating the signal flags

设置好栈后,handle_signal 检查和信号有关的flags. 如果没有设置 SA_NODEFER , 那么在执行信号处理函数时, 就要阻塞sigaction.sa_mask 中指定的所有信号以及sig 本身. 如下:

    if (!(ka->sa.sa_flags & SA_NODEFER)) {

        spin_lock_irq(&current->sighand->siglock);

       sigorsets(&current->blocked, &current->blocked, &ka->sa.sa_mask);

        sigaddset(&current->blocked, sig);

        recalc_sigpending(current);

        spin_unlock_irq(&current->sighand->siglock);

}

如前文所述,recalc_sigpending 会重新检查进程是否还有未被阻塞的悬挂信号, 并依此设置进程的 TIF_SIGPENDING 标志.

 

注意: sigorsets(&current->blocked, &current->blocked, &ka->sa.sa_mask) 等价于current->blocked |= ka->sa.sa_mask. 而current->blocked 原来的值已经存放在frame 中了.

 

handle_signal 返回到do_signal 后,do_signal 也立即返回.

 

Starting the signal handler

do_signal 返回后, 进程由核心态切换到用户态, 于是执行了信号处理函数.

 

Terminating the signal handler

信号处理函数结束后, 因为其返回值的地址( pretcode 指定的 ) 是_ _kernel_sigreturn 指向的代码段, 所以就会执行_ _kernel_sigreturn 指向的代码. 如下:

    _ _kernel_sigreturn:

      popl %eax

      movl $_ _NR_sigreturn, %eax

      int $0x80

这会导致 sigreturn 被执行 ( 会导致从用户态切换到核心态).

sys_sigreturn 函数可以计算得到sigframe 的地址. 如下:

    frame = (struct sigframe *)(regs.esp - 8);

    if (verify_area(VERIFY_READ, frame, sizeof(*frame)) {

        force_sig(SIGSEGV, current);

         return 0;

    }

接着, 它要从frame 中把进程真正阻塞的信号信息拷贝到current->blocked 中. 结果那些在sigaction 中悬挂的信号解除了阻塞. 之后调用 recalc_sigpending.

接着 sys_sigreturn 需要调用restore_sigcontext 把frame 的sc( 即U1 的上下文) 拷贝到内核栈中并把frame 从用户栈中删除.

_ _kernel_sigreturn 的处理与这类似.

 

 

重新执行系统调用( 被信号处理掐断的系统调用 )

注: 当用核心态转向用户态时, 该核心态可能是系统调用的核心态.

小小总结 : 当内核使用用户指定的处理方式时 , 因为是从用户态转向内核态再转向用户态 , 所以其处理比较复杂 . 如下描述 : 当从用户态 (U1) 转入内核态后 , 在内核态试图回到 U1 时 , 会先判断是否有非阻塞的悬挂信号, 如果有就会先调用用户的处理函数 ( 即进入用户态 , 这里是用户态 2), 处理完后 , 再回到内核态 , 然后再回到U1.  注意在 U2 中也有可能发生系统调用从而再次进入内核态 . ( 注意在 U2 过程中 , 系统处于关中断状态 , 所以信号处理应该尽可能地快 ), 我们知道当用户态进入核心态时会把用户态的信息保存在核心态的栈中 ,  为了避免在从 U2 因系统调用再进入核心态是破坏 U1 在核心态中的信息 , 在进入 U2 之前 , 要不 U1 在核心栈中的信息拷贝到 U1 的栈中 , 并在 U2 返回后 , 再把 U2 栈中保存 U1 的信息拷贝会核心栈 .

注 :U2 使用的栈可以和 U1 是同一个栈 , 也可以是用户在设置信号处理函数时指定的一段内存 .

当一个进程调用某些并不能马上满足的系统调用(eg: 写文件) 时, 内核会把该进程的状态设置为TASK_INTERRUPTIBLE 或者TASK_UNINTERRUPTIBLE.

当一个进程( 表示为wp) 处于TASK_INTERRUPTIBLE 状态, 而另外一个进程又给它发信号, 那么内核会把wp 的状态的进程设置为TASK_RUNNING( 但是此时wp 的系统调用仍未完成). 而当wp 切换会用户态时, 这个信号会被deliver. 如果这种情况真的发生了, 则系统调用服务例程并没有成功完成任务, 但是会返回错误码EINTR , ERESTARTNOHAND , ERESTART_RESTARTBLOCK , ERESTARTSYS ,或 ERESTARTNOINTR. ( 参考中断处理的从中断返回部分).

从实践上看, 用户获得的错误代码是是EINTR, 这意味着系统调用没有成功完成. 程序员可以决定是否再次发起该系统调用. 其余的错误代码由内核使用来判断是否在信号处理之后自动重新执行该系统调用.

下表列出了这些错误代码在每一种可能的中断行为下对未完成系统调用的影响. 表中用的词定义如下:

Terminate: 该系统调用不会被内核自动重新执行. 而用户得到的该系统调用的返回值是-EINTER.对程序员而言该系统调用失败.

Reexecute: 内核会强制进程在用户态下自动重新执行该系统调用( 通过把中断号放到eax, 执行int 0x80 或者sysenter 指令). 但是这对程序员透明.

Depends: 如果当被deliver 的信号设置了 SA_RESTART 标志, 那么自动重新执行该系统调用. 否则中止系统调用并返回-EINTER.

 

Reexecution of system calls

Error codes and their impact on system call execution

Signal

Action

EINTR

ERESTARTSYS

ERESTARTNOHAND

ERESTART_RESTARTBLOCK

ERESTARTNOINTR

Default

Terminate

Reexecute

Reexecute

Reexecute

Ignore

Terminate

Reexecute

Reexecute

Reexecute

Catch

Terminate

Depends

Terminate

Reexecute

 

注: ERESTARTNOHAND , ERESTART_RESTARTBLOCK 使用不同的机制来重新自动执行系统调用( 参下文).

 

当 delivering 一个信号时, 内核必须确信进程正在执行系统调用中,这样它才能reexecute 该系统调用, 而 regs 中的成员orig_eax 就是干这个事情的. 回想一下这个成员在中断/ 异常时如何被初始化的:

Interrupt: 它等于 IRQ 数值 - 256.

0x80 exception ( 或者 sysenter): 它等于系统调用的编号.

Other exceptions: 它等于-1.

所以如果该值>=0, 那么可确定进程是在处于系统调用中被信号处理唤醒的( 即信号处理唤醒一个等待系统调用完成( 状态为 TASK_INTERRUPTIBLE ) 的进程). 所以内核在delivering 信号时, 能够返回上述的错误代码, 并作出恰当的挽救.

 

重启被非自定义信号处理函数中断的系统调用

注:上面语句的中断不是OS 中的中断, 而是日常生活中的中断的含义.

如果系统调用因为信号的默认处理函数或者信号的忽略处理而中断( 即由系统调用把task 的状态改为可中断状态, 但是却被信号的默认处理函数或者忽略信号操作把该task 的状态改为running,如前文所述), 那么do_signal 函数需要分析系统调用的错误码来决定是否自动重新执行被停止的系统调用. 如果需要重启该系统调用, 那么必须修改regs 中的内容, 从而在切换到用户态后, 在用户态下再次执行该系统调用( 即再次在用户态下让eax 存放系统调用的编号, 然后执行int 0x80或者sysenter). 如下代码:

    if (regs->orig_eax >= 0) {

        if (regs->eax == -ERESTARTNOHAND || regs->eax == -ERESTARTSYS ||

              regs->eax == -ERESTARTNOINTR) {

            regs->eax = regs->orig_eax;

            regs->eip -= 2;

        }

        if (regs->eax == -ERESTART_RESTARTBLOCK) {

            regs->eax = __NR_restart_syscall;

            regs->eip -= 2;

        }

    }

regs->eax 存放系统调用的编号 . 此外,int 0x80 或者sysreturn 均为2 字节. 所以regs->eip -=2 等价于切换到用户态后重新执行int 0x80 或者sysretrun 指令.

对于错误码 ERESTART_RESTARTBLOCK, 它需要使用restart_syscall 系统调用, 而不是使用原来的系统调用. 这个错误码只用在与时间有关的系统调用. 一个典型的例子是 nanosleep( ) : 想象一下, 一个进程调用这个函数来暂停20ms, 10ms 后由于一个信号处理发生( 从而激活这个进程), 如果这信号处理后重新启动这个系统调用, 那么它在重启的时候不能直接再次调用nanosleep, 否则将会导致该进程睡觉30ms. 事实上, nanosleep 会在当前进程的thread_info 的restart_block 中填写下如果需要重启nanosleep, 那么需要调用哪一个函数, 而如果其被信号处理中断, 那么它会返回-ERESTART_RESTARTBLOCK, 而在重启该系统调用时,sys_restart_syscall 会根据restart_block 中的信息调用相应的函数. 通常这个函数会计算出首次调用与再次调用的时间间距, 然后再次暂停剩余的时间段.

 

重启由自定义信号处理函数中断的系统调用

在这种情况下,handle_signal 会分析错误码以及 sigaction 中的标志是否包含了SA_RESTART, 从而决定是否重启未完成的系统调用. 代码如下:

    if (regs->orig_eax >= 0) {

        switch (regs->eax) {

            case -ERESTART_RESTARTBLOCK:

             case -ERESTARTNOHAND:

                regs->eax = -EINTR;

                break;

            case -ERESTARTSYS:

                if (!(ka->sa.sa_flags & SA_RESTART)) {

                    regs->eax = -EINTR;

                    break;

                 }

            /* fallthrough */

            case -ERESTARTNOINTR:

                regs->eax = regs->orig_eax;

                regs->eip -= 2;

        }

}

 

如果需要重启系统调用, 其处理与do_signal 类似. 否则向用户态返回 -EINTR.

 

 

问题 :

在信号处理函数中可以发生中断吗 , 可以再 发出系统调用吗,可以发出异常吗 ?

如果不行 会有什么影响 ??

 

 

与信号处理相关的系统调用

因为当进程在用户态时, 允许发送和接受信号. 这意味着必须定义一些系统调用来允许这类操作.不幸的是, 由于历史的原因这些操作的语义有可能会重合, 也意味着某些系统调用可能很少被用到. 比如,sys_sigaction, sys_rt_sigaction 几乎相同, 所以C 的接口sigaction 只调用了sys_rt_siaction. 我们将会描述一些重要的系统调用.

 

进程组 : Shell 上的一条命令行形成一个进程组 . 注意一条命令其实可以启动多个程序 . 进程组的 ID 为其领头进程的 ID.

 

kill( ) 系统调用

原型为: int kill(pid_t pid, int sig)

其用来给一个线程组( 传统意义上的进程) 发信息. 其对应的系统服务例程(service routine)是sys_kill. sig 参数表示待发送的信号,pid 根据其值有不同的含义, 如下:

pid > 0: 表示信号sig 发送到由pid 标识的线程组( 即线程组的PID==pid).

pid = 0: 表示信号sig 发送到发送进程所在的进程组中的所有线程组.

pid = -1: 表示信号sig 发送到除进程0, 进程1, 当前进程外的所有进程

pid < -1: 表示信号sig 发送到进程组-pid 中的所有线程组.

服务例程sys_kill 会初始化一个siginfo_t 变量, 然后调用kill_something_info. 如下:

    info.si_signo = sig;

    info.si_errno = 0;

    info.si_code = SI_USER;

    info._sifields._kill._pid = current->tgid;

    info._sifields._kill._uid = current->uid;

    return kill_something_info(sig, &info, pid);

 

kill_something_info 会调用kill_proc_info( 这个函数调用 group_send_sig_info 把信号发给线程组 ) 或者 kill_pg_info( 这个会扫描目标进程组然后逐一调用send_sig_info ) 或者为系统中的每一个进程调用group_send_sig_info( 当pid=-1 时).

系统调用kill 可以发送任意信号, 然而它不保证该信号被加到目标进程的悬挂信号队列中. ( 这个是指对于非实时信号 它也有可能会丢弃该信号吗???? ) 对于实时信号, 可以使用rt_sigqueueinfo.

System V and BSD Unix 还有killpg 系统调用, 它可以给一组进程发信号. 在linux 中, 它通过kill 来实现. 另外还有一个raise 系统调用, 它可以给当前进程发信号. 在linux 中,killpg, raise 均以库函数提供.

tkill( ) & tgkill( ) 系统调用

这两个函数给指定线程发信号. pthread_kill 使用它们之一来实现. 函数原型为:

int tkill(int tid, int sig);

long sys_tgkill (int tgid, int pid, int sig);

tkill 对应的服务例程是sys_tkill, 它也会填充一个siginfo_t 变量, 进程权限检查, 然后掉用specific_send_sig_info.

tgkill 与tkill 的差别在于它多了一个tgid 的参数, 它要求pid 必须是tgid 中的线程. 其对应的服务例程是sys_tgkill, 它做的事情和sys_tkill 类似, 但它还检查了pid 是否在tgid 中. 这种检查在某些情况下可以避免 race condition. 比如: 一个信号被发给了线程组A 中的一个正在被杀死的线程(killing_id), 如果另外一个线程组B 很快地创建一个新的线程并且其PID= killing_id,那么信号有可能会发送到线程组B 中的新建的线程. tgkill 可以避免这种情况, 因为线程组A,B的ID 不一样.

 

设置信号处理函数

程序员可以通过系统调用sigaction (sig,act,oact) 来为信号sig 设置用户自己的信号处理函数act. 当然如果用户没有设置, 那么系统会使用默认的信号处理函数. 其函数原型为:

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

oldact 用来保存信号signum 的旧的信号处理函数( 因为signum 的新的信号处理函数是act, 保存旧的是希望能够恢复使用旧的信号处理函数).

其对应的服务例程是sys_sigaction, 它首先检查act 地址的有效性, 然后act 的内容拷贝到一个类型为 k_sigaction 的 本地变量new_ka ,如下:

    _ _get_user(new_ka.sa.sa_handler, &act->sa_handler);

    _ _get_user(new_ka.sa.sa_flags, &act->sa_flags);

    _ _get_user(mask, &act->sa_mask);

    siginitset(&new_ka.sa.sa_mask, mask);

 

接着调用 do_sigaction 把new_ka 拷贝到current->sig->action[sig-1] 中的. 类似如下:

    k = &current->sig->action[sig-1];

    if (act) {

        *k = *act;

        sigdelsetmask(&k->sa.sa_mask, sigmask(SIGKILL) | sigmask(SIGSTOP));

        if (k->sa.sa_handler == SIG_IGN || (k->sa.sa_handler == SIG_DFL &&

         (sig==SIGCONT || sig==SIGCHLD || sig==SIGWINCH || sig==SIGURG))) {

        rm_from_queue(sigmask(sig), &current->signal->shared_pending);

            t = current;

            do {

                rm_from_queue(sigmask(sig), &current->pending);

                recalc_sigpending_tsk(t);

                t = next_thread(t);

            } while (t != current);

        }

    }

 

POSIX 规定当默认行为是忽略时, 把信号处理函数设置为SIG_IGN 或者SIG_DFT 会导致悬挂的信号被丢弃. 此外, SIKKILL 和SIGSTOP 永远不会被屏蔽 ( 参考上述代码).

此外, sigaction 系统调用还允许程序员初始化sigaction 中的sa_flags.

System V 也提供signal 系统调用. C 库的signal 使用rt_sigaction 来实现. 但是linux 仍然有相应的服务例程sys_signal. 如下:

    new_sa.sa.sa_handler = handler;

    new_sa.sa.sa_flags = SA_ONESHOT | SA_NOMASK;

    ret = do_sigaction(sig, &new_sa, &old_sa);

    return ret ? ret : (unsigned long)old_sa.sa.sa_handler;

 

获得被阻塞的悬挂信号

系统调用 sigpending () 允许 用户获得当前线程被阻塞的悬挂信号. 函数原型为:

int sigpending(sigset_t *set);

set 用来接收被阻塞的悬挂信号的信息.

其对应的服务例程是sys_sigpending, 其实现代码如下:

    sigorsets(&pending, &current->pending.signal,

                        &current->signal->shared_pending.signal);

    sigandsets(&pending, &current->blocked, &pending);

    copy_to_user(set, &pending, 4);

 

修改被阻塞的信号的集合

系统函数sigprocmask 可以用来修改当前线程的阻塞信号集合. 但是它仅适用于非实时信号. 函数原型为:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

假设在执行这个函数之前线程的阻塞信号的集合为bs. 执行这个函数之后线程的阻塞信号的集合为nbs.

 

oldsett: 用于返回( 返回) 线程当前阻塞的信号的集合(*oldest=bs)

set: 用于存储信号集合. 怎么用它还取决于how 参数.

how: 执行线程的新的阻塞信号集合如果通过set 参数获得. 其可能的值及其含义如下:

SIG_BLOCK: nbs=bs|set

SIG_UNBLOCK:nbs=bs-set

SIG_SETMASK:nbs=set

其对应的服务例程是 sys_sigprocmask( ) . 它调用copy_from_user 把set 值拷贝到本地变量new_set, 并把bs 拷贝到oldset 中. 其执行的代码类似如下:

    if (copy_from_user(&new_set, set, sizeof(*set)))

        return -EFAULT;

    new_set &= ~(sigmask(SIGKILL)|sigmask(SIGSTOP));

    old_set = current->blocked.sig[0];

    if (how == SIG_BLOCK)

        sigaddsetmask(&current->blocked, new_set);

    else if (how == SIG_UNBLOCK)

        sigdelsetmask(&current->blocked, new_set);

    else if (how == SIG_SETMASK)

        current->blocked.sig[0] = new_set;

    else

        return -EINVAL;

    recalc_sigpending(current);

    if (oset && copy_to_user(oset, &old_set, sizeof(*oset)))

            return -EFAULT;

    return 0;

 

悬挂( 暂停) 进程

系统调用 sigsuspend 的原型如下:

int sigsuspend(const sigset_t *mask);

其含义是: 把本线程的阻塞信号设置为mask 并把线程状态设置为 TASK_INTERRUPTIBLE. 并且只有当一个 nonignored, nonblocked 的信号发到本线程后才会把本线程唤醒(deliver 该信号, 系统调用返回).

其相应的服务例程为sys_sigsuspend, 执行的代码为:

    mask &= ~(sigmask(SIGKILL) | sigmask( SIGSTOP ));

    saveset = current->blocked;// saveset 本地局部变量

    siginitset(&current->blocked, mask);

    recalc_sigpending(current);

    regs->eax = -EINTR;

    while (1) {

        current->state = TASK_INTERRUPTIBLE;

        schedule( );

        if (do_signal(regs, &saveset))// 把阻塞信号集合恢复为saveset

            return -EINTR;

}

( 注意, 本系统调用本身期望它被信号处理函数中断.)

函数schedule 会导致执行别的进程( 线程), 而当本进程再次执行时( 即上面的schedule 返回了), 它会调用do_signal 来处理其未被阻塞的悬挂的信号, 然后恢复线程的阻塞信号集合(saveset). 如果do_signal 返回非0(do_signal 中调用用户自定义信号处理函数或者杀死本线程时返回非0), 那么该系统调用返回.

即只有当本线程处理完不被阻塞的信号( ==(!mask)| SIGKILL| SIGSTOP) 后, 它才会返回.

 

实时信号的系统调用

前面所述的系统调用仅适用于非实时信号,linux 还引入了支持实时信号的系统调用.

一些实时系统调用( 如: rt_sigaction, rt_sigpending, rt_sigprocmask, rt_sigsuspend) 与它们的非实时的版本类似( 只是在名字加了rt_). 下面仅简单描述两个实时信号的系统调用.

rt_sigqueueinfo( ): 把一个实时信号发给线程组( 放到线程组的共享悬挂信号列表中). 库函数sigqueue 利用这个系统调用来实现.

rt_sigtimedwait( ): 把阻塞的悬挂信号从悬挂信号队列中删除, 如果在调用这个系统调用时还没有相应的阻塞悬挂信号, 那么它会把本进程(task) 阻塞一段时间. 库函数sigwaitinfo,sigtimedwait 通过这个系统调用实现.


你可能感兴趣的:(Linux 信号signal处理机制)