1 前言
写作本文的目的和其它文章略有不同,不是为了系统和全面的介绍”信号”这个子系统,--虽然它不复杂,其内容也不是一篇短短的文章所能够覆盖的,而是要回答自己的疑惑,解决工作中遇到的一些问题,理解那些不能够马上了解的部分。说到底,可以将这篇文章看作问题的答案。
曾经遇到的问题放在最后一节”问题与答案”中,在阅读正文之前先扫描一下问题可能更加有助于理解文章中的内容。
欢迎大家对这篇文章提出意见和指正,我的email是:shisheng_liu@yahoo.com.cn。
2 许可协议
本文的许可协议遵循GNU Free Document License。协议的具体内容请参见http://www.gnu.org/copyleft/fdl.html。在遵循GNU Free Document License的基础上,可以自由地传播或发行本文,但请保留本文的完整性。
3 什么是信号
信号是UNIX进程间通信的一种标准方式,在最早期的UNIX系统中已经存在。信号的出现允许内核和其它进程通知进程特定事件的发生。现代UNIX中也存在其它的进程间通信方式,但由于信号相对简单和有效,它们仍然被广泛使用。
信号是最简单的消息,当一个信号被发送时,它没有参数等附加信息,唯有一个整数来表示信号的值。信号的值在所有的UNIX系统中已经标准化了,每一个信号都有一个名字,它以三个字母SIG开头,对应于特定的事件。例如:SIGKILL表示终止进程;SIGBUS代表硬件故障;SIGCHLD代表子进程状态改变。
UNIX中常用的信号有31个,除了前面提到的三个信号外,还有如下信号是文章中用到的,其它的不再一一列出。
SIGSTOP 暂停程序执行
SIGCONT 恢复程序执行
3.1 进程对信号的处理
进程可以对每一个信号设置单独的处理方式,它可以忽略该信号,也可以设置自己的信号处理程序(称为捕捉),或者对信号什么也不做,信号发生的时候执行系统默认动作。
进程还可以设置对某些信号的阻塞(block)标志。如果一个信号被设置为阻塞,当信号发生的时候,它会与正常的信号一样被递送(deliver)给进程,但只有进程解除对信号的阻塞时才会被处理。
从一个非阻塞信号被递送给进程到信号得到处理之间的时间间隔,称为信号未决(pending)。有的资料将pending翻译为”信号挂起”。
所有的信号中,有两个信号SIGSTOP和SIGKILL是特殊的,他们既不能被捕捉,也不能被忽略,也不能被阻塞,这个特性保证了系统管理员在任何时候都可以用信号暂停和结束程序。
3.2 进程数据结构中与信号相关的部分
从这一部份开始,我们转入对linux内核部分的分析。在内核中,一个基本的数据结构sigset_t用来表示信号。不同的CPU架构中sigset_t的长度略有不同,对Intel i386架构来说,sigset_t是一个64位整数,每一位表示一个信号。Sigset_t中的后32位表示实时信号,它和普通信号的唯一区别是支持同一个信号的排队,这保证了发送的多个实时信号都被接收。实时信号是POSIX标准的一部分,但linux并没有对它做单独的处理,除了支持排队以外,因此它不是我们关心的重点。
进程结构task_struct中包含下列数据成员与信号相关
l sigpending
类型为int的一个标志,如果非0表示进程中存在着未决的非阻塞信号等待处理。
l pending
类型为struct sigpending的变量。可以看作进程的信号队列,它存放所有未决的信号,对某些信号来说,还包括相关的信息。例如SIGCHLD信号是子进程结束时发送给父进程的,它的附加信息中包括了子进程的ID和结束值。
l blocked
类型为sigset_t的变量。表示当前进程中哪些信号被阻塞。
l sig
类型为struct signal_struct的变量。描述进程怎样处理每个信号。
3.3 相关函数
下面是进程处理信号时经常用到的系统调用
l sigaction
设置或读取进程对某个信号的处理方式。进程可以忽略或用默认方式处理该信号,也可以设置自己的信号处理程序。
l Sigprocmask
设置或读取进程的信号阻塞掩码。
l Sigpending
返回当前未决的信号集。被阻塞的未决信号并不返回。
4 信号的发送
信号是异步事件,当一个信号被发送给进程时,接收进程可能运行在任何时刻,处于任何一种状态。为了使程序处于不同状态时都能够正确的处理信号,系统在信号发送的时候需要进行适当的预处理。我们讨论的过程实际上包括信号的产生和信号的递送,下面会进行详细的分析。
4.1 进程的状态
从进程本身的状态来看,它有可能处在运行态(RUNNING),等待态(INTERRUPTBLE &UNINTERRUPTBLE), 停止态(STOPPED)和僵死态(ZOMBIE)。而从进程运行的模式(mode)来看,又可能位于内核模式和用户模式的任一种,
4.2 程序分析
有几种方法可以将信号发送给某个进程,但它们最终会调用内核中的一个函数send_sig_info来完成发送。在这个函数里,内核会进行一系列检查,只有满足适当条件*的信号才会被放在进程的信号队列中;接着检查程序是否处于INTERRUPTABLE态,如果是就唤醒该进程,将进程的状态改为RUNNING态,并且放在系统运行队列内。
系统对发送信号的处理有两点比较有意思:
1) 只有SIGKILL和SIGCONT信号能够被状态为STOPPED进程接收。
所有其他信号都被忽略。这是由STOPPED状态本身的特性决定的,它被设置来控制进程的执行和暂停,SIGCONT信号能够使暂停的程序恢复执行,而SIGKILL被接收则提供杀死暂停进程的能力。
2)所谓满足适当的信号是满足一系列检查条件的信号:
.a)发送信号的进程满足POSIX.1中对发送者的要求
.b)该进程没有显式/隐含忽略该信号。
.c)该进程没有阻塞该信号。
.d)同样的信号没有已经在进程中挂起。同一进程的同一信号是不排队的,如果一个信号被连续发送多次,而它已经在接收进程中被挂起时,后续的信号被简单丢弃。
4.3 相关函数
4.3.1 内核部分
l send_sig_info(kernel/signal.c)
完成信号发送的入口函数,其他所有函数最终都通过它完成信号的发送。
l force_sig_info(kernel/signal.c)
仅供内核函数使用的强制信号发送函数。它做了一些努力以确保进程既不能忽略,又不能阻塞该信号,然后调用send_sig_info来完成信号的发送。
4.3.2 用户部分
l kill系统调用
进程通过kill系统调用向其他进程发送信号,kill系统调用在内核中的实现参见kernel/signal.c中的sys_kill() 函数,它调用send_sig_info来完成信号的发送。
Kill能够向一个进程或整个进程组发送信号。通过在kill系统调用时指定负的接收进程ID(”pid”),信号被发送给ID为”-pid”的进程组;如果将接收进程ID指定为-1,则信号被发送给除自己外的全体进程,这显然是一个不应该经常使用的功能。
5 信号的处理
5.1 信号处理程序
信号处理程序是进程本身的执行程序的一部分,只有进程正在CPU上运行的时候才能得到执行,也就是说,如果进程得不到执行的机会,例如状态是UNINTERRUPTABLE, STOPPED(如前面所说,可以接收SIGKILL, SIGCONT信号)等状态,发给进程的信号永远不会得到处理的机会。
5.2 何时执行
内核会在一些特定的时间点,-- 具体的说,在每一个系统调用结束,程序从内核态返回到用户态之前和程序从中断和异常代码中返回的时候[参见arch/kernel/entry.S文件中的ret_from_sys_call]代码 --, 检查当前进程是否有信号未被处理,然后调用信号处理的主函数do_signal。
5.3 程序分析
作为一个主函数,do_signal有相当多值得注意的地方。它循环地工作,每一个循环都从进程中取出一个未决的信号,取信号的顺序是由小到大,然后加以处理。处理的过程如下:
1) 如果程序正在被跟踪,程序会被转入STOPPED态,同时一个SIGCHLD信号被发送给跟踪者。一个有意思的事情是:当程序再次得到运行权后,它是怎样运行的呢?
2) 如果信号的处理操作是忽略该信号,则立刻完成本次循环,但SIGCHLD信号是一个例外,它调用sys_wait4函数,强迫这个进程读子进程的信息,借此清理由终止的子进程所留下的内存。
3) 如果信号的处理操作是默认处理,do_signal会执行信号的缺省操作,一个例外当接收进程是init时,信号被丢弃,本次循环会立即完成。
不同信号的缺省操作是不同的,可以分为四类,第一类信号的缺省操作是忽略,例如SIGCHLD信号;第二类信号会将进程转入停止态(注意,并不是结束进程),例如SIGSTOP信号;第三类信号会结束进程并将结束前的状态写入core文件,例如表示非法内存访问的SIGSEGV信号;而另一些信号仅仅简单的结束进程(do_exit函数),例如SIGKILL信号。
4)最后,调用进程本身的信号处理程序来处理信号,在完成了这个调用后,do_signal直接返回,也就是说,与其他被通过循环处理的信号不同,带有自己的信号处理函数的信号在一次do_signal调用中只能处理一个。这样做的原因是,与其它信号处理方式不同,进程的信号处理函数为用户态函数,它不可能直接在do_signal循环中被调用,否则会带来严重的安全性问题,do_signal能做的是设置程序的执行寄存器环境和堆栈代码,使进程回到用户态首先执行信号处理函数。设置进程的栈环境是一项相当复杂的工作,值得用另一篇文章来单独说明了,在此不再赘述,可以参考具体代码arch/kernel/signal.c中的handle_signal函数。
5.4 相关函数
所有的相关函数都位于内核态。
l ret_from_syscall
系统调用结束,返回到用户态前执行的函数。它检查进程中是否存在信号未决,并调用do_signal函数来处理未决的信号。Ret_from_syscall位于汇编文件arch/kernel/entry.S中,它其实并不是一个函数,而是一个汇编语言程序点,调用者用jmp指令来进入它。
l do_signal
信号处理的主函数。位于arch/kernel/signal.c文件中。
l handle_signal
处理有”自定义信号处理函数”的信号,完成建立堆栈等复杂工作。
6 信号与系统调用
6.1 允许被信号中断的系统调用
正常执行的系统调用是不会被信号中断的,但某些系统调用可能需要很长的等待时间来完成,对于这种情况,允许信号中断它的执行提供了更好的程序控制能力。要达到允许被信号中断的功能,系统调用的实现上需要满足如下条件:
系统调用必须在需要等待的时候将进程转入睡眠状态,主动让出CPU。因为作为内核态的程序,系统调用的执行是不可抢占的,不主动放弃CPU的系统调用会一直运行直到结束。而且当前进程的睡眠状态必须设置为TASK_INTERRUPTABLE,只有在这个状态下,当信号被发送给进程时,进程的状态被(信号发送函数)唤醒,并重新在运行队列中排队等待调度。
当进程获得了一个CPU时间片后,它接着睡眠时的下一条指令开始运行(还在系统调用内),系统调用判断出收到了信号,会设置一个与信号有关的退出标志并迅速结束。退出标志为:ERESTARTNOHAND,ERESTARTSYS,ERESTARTNOINTR中的一种,这些标志都是和接收信号程序do_signal通信用的,它们和进程对特定信号的处理标志一起,决定了系统调用中断后是否重新执行。
ERESTARTNOINTR:要求系统调用被无条件重新执行。
ERESTARTSYS: 要求系统调用重新执行,但允许信号处理程序根据自己的标志来做选择。
ERESTARTNOHAND:在信号没有设置自己的信号处理函数时系统调用重新执行。
6.2 系统调用的重新执行
在系统调用结束的时候,do_signal函数得以执行,它会与系统调用的退出标志一起来决定是否重新执行系统调用。do_sigal函数需要对两种情况进行处理,一种是进程对收到的信号设置了自己的信号处理程序,而另一种是进程没有设置收到的信号的信号处理程序,而且在处理完所有信号后,进程没有被停止或者结束掉。在后一种情况中,当系统调用退出的标志为ERESTARTNOHAND,ERESTARTSYS,ERESTARTNOINTR中的任意一个时,do_signal会使系统调用重新启动。而对前一种情况,只有系统调用的退出标志为ERESTARTNOINTR或者ERESTARTSYS并且信号处理标志也满足条件的时候系统调用才能重新启动。
6.3 相关函数
l sys_sigaction
系统调用sigaction的实现。通过sigaction系统调用,进程可以设置特定信号的处理程序和处理标志,其中一个标志SA_RESTART影响是否允许系统调用的重新执行。
7 问题与答案
l 为什么有时候进程阻塞于系统调用中时,该阻塞可以被信号中断;而阻塞于另一些系统调用的时候就不可以被中断?
如”信号与系统调用”一节所述,只有将进程以TASK_INTERRUPTABLE方式阻塞的系统调用才能够被中断。而设置以TASK_UNINTERRUPTABLE方式阻塞的系统调用不能中断。
l 系统是如何确保SIGKILL/SIGSTOP信号不能被捕捉和忽略的?
进程通过系统调用sigaction来设置对信号的处理方式,sigaction在内核中的实现函数是sys_sigaction(kernel/signal.c),它对进程传递信号值进行检查,确保信号SIGKILL和SIGSTOP不能被设置。
另外两个系统调用sigprocmask和sigsuspend会更改信号的屏蔽字,与sigaction类似,它们在内核中的实现函数会检查参数,确保SIGKILL和SIGSTOP不被屏蔽。
l 阻塞信号和忽略信号两种操作有何不同?
阻塞信号允许信号被发送给进程,但不进行处理,需要等到阻塞解除后再处理。而忽略信号是进程根本不接收该信号,所有被忽略的信号都被丢弃。
系统调用sigprocmask和sigsuspend被用来设置信号的阻塞与否;而系统调用sigaction则设置进程是否忽略一个信号。