前言
信号是什么?
信号(signal)是很短的消息(通常是一个数,以此来标识信号),可以被发送到一个进程或一组进程。
信号可以干什么?
信号在最早的Unix系统中即被引入,用于在用户态进程间通信。内核也用信号通知进程系统所发生的事件。
信号的作用
名字前缀为SIG的一组宏用来标识信号:比如当一个进程引用无效的内存时,SIGSEGV宏产生发送给进程的信号标识符。
使用信号的两个主要目的是:
- 让进程知道已经发生了一个特定的事件。
- 强迫进程执行它自己代码中的信号处理程序。
这两个目的不是互斥的,因为进程经常通过执行一个特定的例程来对某一事件做出反应。
查看所有信号
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| $kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX
|
信号的分类
常规信号(regular signal)
:
- 编号为1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的)
- 如果一个常规信号被连续发送多次,那么,只有其中的一个发送到接收进程。
实时信号(real-time signal)
:
- 编号为32 ~ 64的信号是后来扩充的,称做可靠信号(实时信号)。
- 必须排队以便发送的多个信号能被接收到。
- 尽管Linux内核并不使用实时信号,它还是通过几个特定的系统调用完全实现了POSIX标准。
可以从两个不同的分类角度对信号进行分类:
- 可靠性方面:可靠信号与不可靠信号;
- 不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。
- 与时间的关系上:实时信号与非实时信号。
Linux中的前31个信号
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| 编号 信号名称 缺省操作 解释 POSIX 1 SIGHUP Terminate 挂起控制终或进程 是 2 SIGINT Terminate 来自键盘的中断 是 3 SIGQUIT Dump 从键盘退出 是 4 SIGILL Dump 非法指令 是 5 SIGTRAP Dump 跟踪的断点 否 6 SIGABRT Dump 异常结束 是 6 SIGIOT Dump 等价于SIGABRT 否 7 SIGBUS Dump 总线错误 否 8 SIGFPE Dump 浮点异常 是 9 SIGKILL Terminate 强迫进程终止 是 10 SIGUSR1 Terminate 对进程可用 是 11 SIGSEGV Dump 无效的内存引用 是 12 SIGUSR2 Terminate 对进程可用 是 13 SIGPIPE Terminate 向无读者的管道写 是 14 SIGALRM Terminate 实时定时器时钟 是 15 SIGTERM Terminate 进程终止 是 16 SIGSTKFLT Terminate 协处理器栈错误 否 17 SIGCHLD Ignore 子进程停止、结束或在被跟踪时获得信号 是 18 SIGCONT Continue 如果已经停止则恢复执行 是 19 SIGSTOP Stop 停止进程执行 是 20 SIGSTP Stop 从tty发出停止进程 是 21 SIGTTIN Stop 后台进程请求输入 是 22 SIGTTOU Stop 后台进程请求输出 是 23 SIGURG Ignore 套接字上的紧急条件 否 24 SIGXCPU Dump 超过CPU时限 否 25 SIGXFSZ Dump 超过文件大小的限制 否 26 SIGVTALRM Terminate 虚拟定时器时钟 否 27 SIGPROF Terminate 概况定时器时钟 否 28 SIGWINCH Ignore 窗口调整大小 否 29 SIGIO Terminate I/O现在可能发生 否 30 SIGPWR Terminate 等价于SIGIO 否 31 SIGSYS Dump 坏的系统调用 否 31 SIGUNUSED Dump 等价于SIGSYS 否
|
与信号相关的最重要的系统调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| kill() 向线程组发送一个信号 tkill() 向进程发送一个信号 tgkill() 向一个特定线程组中的进程发送信号 sigaction() 改变与信号相关的操作 signal() 类似于sigaction() sigpending() 检查是否有挂起信号 sigprocmask() 改动堵塞信号的集合 sigsuspend() 等待一个信号 rt_sigaction() 改变与实时信号相关的操作 rt_sigpending() 检测是否挂起实时信号 rt_sigprocmask() 修改阻塞的实时信号的集合 rt_sigqueueinfo() 向线程组发送一个实时信号 rt_sigsuspend() 等待一个实时信号 rt_sigtimedwait() 类似于rt_sigsuspend()
|
信号的传递
信号的一个重要特点是它们可以随时被发送给状态经常不可预知的进程。
- 发送给非运行进程的信号必须由内核保存,直到进程恢复执行。阻塞一个信号要求信号的传递拖延,直到随后解除阻塞,这使得信号产生一段时间之后才能对其传递这一问题变得更加严重。
因此,内核区分信号传递的两个不同阶段:
信号产生
:内核更新目标进程的数据结构以表示一个新信号已被发送。
信号传递
:内核强迫目标进程通过以下方式对信号做出反应:或改变目标进程的执行状态,或开始执行一个特定的信号处理程序,或两者都是。
每个所产生的信号至多被传递一次。信号是可消费资源:一旦它们已传递出去,进程描述符中有关这个信号的所有信息都被取消。
一般来说,信号可以保留不可预知的挂起时间。必须考虑下列因素:
- 信号通常只被当前正运行的进程传递(即由current进程传递)。
- 给定类型的信号可以由进程选择性地阻塞(blocked)。这种情况下,在取消阻塞前进程将不接收这个信号。
- 当进程执行一个信号处理程序的函数时,通常“屏蔽”相应的信号,即自动阻塞这个信号直到处理程序结束。因此,所处理的信号另一次出现不能中断信号处理程序,所以,信号处理函数不必是可重入的。
挂起信号(pending signal)
:已经产生但还没有传递的信号称为挂起信号。
- 不论什么时候,对于某个常规类型的信号,一个进程仅存在其一个挂起信号。同一进程同种类型的其它信号不被排队,仅仅被简单的丢弃。
- 可是,对于实时信号时不同的,其同种类型的挂起信号能够多个。
尽管信号的表示比较直观,但内核的实现相当复杂。内核必须:
- 记住每个进程阻塞哪些信号。
- 当从内核态切换到用户态时,对任何一个进程都要检查是否有一个信号已到达。这几乎在每个定时中断时都发生(大约每毫秒发生一次)。
- 确定是否可以忽略信号。这发生在下列所有的条件都满足时:
- 目标进程没有被另一个进程跟踪(进程描述符中ptrace字段的PT_PTRACED标志等于0)。
- 信号没有被目标进程阻塞。
- 信号被目标进程忽略(或者因为进程已显示地忽略了信号,或者因为进程没有改变信号的缺省操作且这个缺省操作就是“忽略”)。
- 处理这样的信号,即信号可能在进程运行期间的任一时刻请求把进程切换到一个信号处理函数,并在这个函数返回以后恢复原来执行的上下文。
传递信号之前所执行的操作
进程应答
,进程以三种方式对一个信号做出应答:
- 显示地忽略信号。
- 执行与信号相关的缺省操作。由内核预定义的缺省操作取决于信号的类型,可以是下列类型之一:
- Terminate:进程被终止(杀死)。
- Dump:进程被终止(杀死),并且,如果可能,创建包含进程执行上下文的核心转储文件。这个文件可以用于调试。
- Ignore:信号被忽略。
- Stop:进程被停止,即把进程置为TASK_STOPPED状态。
- Continue:如果进程被停止(TASK_STOPPED),就把它置为TASK_RUNNING状态。
- 通过调用相应的信号处理函数捕获信号。
- 注意,被对一个信号的阻塞和忽略是不同的:只要信号被阻塞,它就不被传递;只有在信号解除阻塞后才传递它。而一个被忽略的信号总是被传递,只是没有进一步的操作。
- 如果信号的传递会引起内核杀死一个进程,那么这个信号对该进程就是致命的。
- SIGKILL信号总是致命的;
- 而且,缺省操作为Terminate的每个信号,以及不被进程捕获的信号对该进程也是致命的。
- 注意,如果一个被进程捕获的信号,其对应的信号处理函数终止了这个进程,那么这个信号就不是致命的,因为进程自己选择了终止,而不是被内核杀死。
POSIX信号和多线程应用
POSIX 1003.1标准对多线程应用的信号处理有一些严格的要求:
- 信号处理程序必须在多线程应用的所有线程之间共享;不过,每个线程必须有自己的挂起信号掩码和阻塞信号掩码。
- POSIX库函数kill()和sigqueue()必须向所有的多线程应用而不是某个特殊的线程发出信号。所有由内核产生的信号如此(如:SIGCHLD、SIGINT或SIGQUIT)。
- 每个发送给多线程应用的信号仅传递给一个线程,这个线程是由内核在从不会阻塞该信号的线程中随意选择出来的。
- 如果向多线程应用发送了一个致命的信号,那么内核将杀死该应用的所有线程,而不仅仅是杀死接收信号的那个线程。
线程组
如果一个挂起信号被发送给了某个特定进程,那么这个信号是私有的;如果被发送给了整个线程组,它就是共享的。
与信号相关的数据结构
对系统中的每个进程来说,内核必须跟踪什么信号当前正在挂起或被屏蔽,以及每个线程组是如何处理所有信号的。
为了完成这些操作,内核使用几个从处理器描述符可存取的数据结构:
信号描述符和信号处理程序描述符
进程描述符的signal字段
指向信号描述符(signal descriptor)
——一个signal_struct类型
的结构,用来跟踪共享挂起信号。
信号描述符还包括与信号处理关系并不密切的一些字段,如:
- 每进程的资源限制数组rlim
- 用于存放进程的组领头进程
- 会话领头进程PID的字段pgrp和session。
信号描述符被属于同一组线程组的所有进程共享:
- 也就是被调用clone()系统调用(CLONE_THREAD标志置位)创建的所有进程共享,因此,对属于同一线程组的每个进程而言,信号描述符中的字段必须都是相同的。
信号处理程序描述符(signad handler descriplor)
:每个进程还引用它,它是一个sighand_struct类型
的结构,用来描述每个信号必须怎样被线程组处理。
- 在调用clone()系统调用时设置CLONE_SIGHAND标志,信号处理程序描述符就可以由几个进程共享。
- 描述符的count字段表示共享该结构的进程个数。
在一个POSIX的多线程应用中,线程组中的所有轻量级进程都引用相同的信号描述符和信号处理程序描述符。
挂起信号队列
为了跟踪当前的挂起信号是什么,内核把两个挂起信号队列与每个进程相关联:
共享挂起信号队列
,它位于信号描述符的shared_pending字段,存放整个线程组的挂起信号。
私有挂起信号队列
,它位于进程描述符的pending字段,存放特定进程(轻量级进程)的挂起信号。
与信号处理相关的系统调用
kill()系统调用:
一般用kill(pid, sig)系统调用
向普通进程或多线程应用发送信号,其相应的服务例程是sys_kill()函数
。整数参数pid的几个含义取决于它的值:
- pid > 0:把sig信号发送到其PID等于pid的进程所属的线程组。
- pid = 0:把sig信号发送到与调用进程同组的进程的所有线程组。
- pid = -1:把信号发送到所有进程,除了swapper(PID 0)、init(PID 1)和current以外。
- pid < -1:把信号发送到进程组-pid进程的所有线程组
kill()系统调用能发送任何信号,即使是编号在32~64之间的实时信号。然而,kill()系统调用不能确保把一个新的元素加入到目标进程的挂起信号队列,因此,挂起信号的多个实例可能被丢失。实时信号应当通过rt_sigqueueinfo()系统调用进行发送。
tkill()和tgkill()系统调用:
- tkill()和tgkill()系统调用向线程组中的指定进程发送信号。
- 所有遵循POSIX标准的pthread库的
pthread_kill()函数
,都是调用这两个系统调用中的任意一个向指定的轻量级进程发送信号。
改变信号的操作
sigaction(sig, act, oact)系统调用
允许用户为信号指定一个操作。
- 当然,如果没有自定义的信号操作,那么内核执行与传递的信号相关的缺省操作。
- POSIX标准规定,当缺省操作是”Ignore”时,把信号操作设置成
SIG_IGN
或SIG_DFL
将引起同类型的任一挂起信号被丢弃。
- 此外还要注意,对信号处理程序来说,不论请求屏蔽信号是什么,SIGKILL和SIGSTOP从不被屏蔽。
检查挂起的阻塞信号:
sigpending系统调用允许进程检查挂起的阻塞信号的集合,也就是说检查信号被阻塞时已产生的那些信号。
修改阻塞信号的集合:
sigprocmask()系统调用
允许进程修改阻塞信号的集合。这个系统调用只应用于常规信号(非实时信号)。相应的sys_sigprocmask()服务例程作用于三个参数:
- oset:进程地址空间的一个指针,指向存放以前位掩码的一个位数组。
- set:进程地址空间的一个指针,指向包含新位掩码的位数组。
- how:一个标志,可以有下列的一个值:
- SIG_BLOCK:*set位掩码数组,指定必须加到阻塞信号的位掩码数组中的信号。
- SIG_UNBLOCK:*set位掩码数组,指定必须从阻塞信号的位掩码数组中删除的信号。
- SIG_SETMASK:*set位掩码数组,指定阻塞信号新的位掩码数组。
挂起进程:
sigsuspend()系统调用
把进程置为TASK_INTERRUPTBLE状态,当然这是把mask参数指向的位掩码数组所指定的标准信号阻塞以后设置的。只有当一个非忽略、非阻塞的信号发送到进程以后,进程才被唤醒。
参考资料
【Linux】Linux进程信号详解
Linux信号(signal) 机制分析