当向目标进程发送一枚信号SIGXXX时,Linux内核收到了产生的信号,然后再目标进程的进程描述符里记录了一笔:收到了信号SIGXXX,但是还没有递送给目标进程的这一段时间里,信号处于挂起状态,被称为( pending )信号,也称为未决信号。内核将信号递送给进程,进程就会暂停当前的控制流,转而去执行信号处理函数,这就是一个信号的完整生命周期。
典型信号可以会按照上面所述流程来处理,但是实际情况要复杂的多,还有许多场景需要考虑:
目标进程正在执行关键代码,不能被信号中断,需要阻塞某些信号,那么在这期间,信号就不允许被递送到进程,直到目标进程接触阻塞。
内核发现同一个信号已经存在,那么它该如何处理这种重复的信号,排队还是丢弃?
内核递送信号的时候,发现已有多个不同的信号被挂起,那它应该优先递送哪个信号?
对于多线程的进程,如果向该进程发送信号,应该由哪一个线程来负责响应?
上述场景都是使用信号前需要考虑的问题。
信号的产生
作为进程间通信的一种手段,进程之间可以互相发送信号,然而通常发给进程的信号,通常源于内核,包括:硬件异常、终端相关的信号和软件事件相关的信号。
硬件异常相关的信号:
信号 信号值 说明
SIGBUS | 7 | 总线错误,表示发生了内存访问错误
SIGFPE | 8 | 表示发生了算术错误
SIGILL | 9 | 进程尝试执行非法的机器语言指令
SIGSEGV | 11 | 段错误,表示应用程序访问了无效地址
这四种硬件异常,一般是由程序自身引发的,不是由其他进程发送的信号引发的,并且这些异常都比较致命,以至于进程无法继续下去,所以这些信号产生之后,会立即递送给进程。默认情况下,这四种信号都会使进程终止,并且产生core dump文件以供调试。对于这些信号,进程既不能忽略,也不能阻塞。
终端定义了如下几种信号生成字符:
Ctrl+C : 产生SIGINT信号
Ctrl+\ : 产生SIGQUIT信号
Ctrl+Z : 产生SIGTSTP信号
键入这些信号生成字符,相当于向前台进程组发送了对应的信号。
另一个和终端关系比较密切的信号是SIGHUP信号。很多程序员都遇到过这种问题:使用ssh登陆到远程的Linux服务器,执行比较耗时的操作(如编译项目代码),却因为网络不稳定,或者需要关机回家,ssh连接被断开,最终导致操作中途被放弃而失败。
之所以会这样,是因为一个控制进程在失去其终端之后,内核会负责向其发送一个SIGHUP信号。在登录会话中,shell通常是终端的控制进程,控制进程收到SIGHUP信号后,会引发如下的连锁反应:
shell收到SIGHUP后会终止,但是在终止之前,会向由shell创建的前台进程组和后台进程组发送SIGHUP信号,为了防止处于停止状态的任务接收不到SIGHUP信号,通常会在SIGHUP信号之后,发送SIGCONT信号,唤醒处于停止状态的任务。前台进程组和后台进程组的进程收到SIGHUP信号,默认的行为是终止进程,这也是前面提到的耗时任务中途失败的原因。注意,单纯地将命令放入后台执行(通过&符号),并不能摆脱被SIGHUP信号追杀的命运。
软件事件触发信号产生的情况比较多:
很多信号尤其是传统的信号,都会有默认的信号处理方式,如果我们不改变信号的处理函数,那么收到信号之后,就会执行默认的操作。信号的默认操作有一下几种:
ignore : 显示地忽略信号,内核将会丢弃该信号,信号不会对目标进程产生任何影响。
terminate : 终止进程,很多信号的默认处理是终止进程。
core : 生成核心转储文件并终止进程,进程会被杀死并且产生核心转储文件。核心转储文件记录了进程死亡现场的信息,用户可以使用核心转储文件来调试,分析进程死亡的原因。
stop : 停止进程不同于终止进程,终止进程是进程已经死亡,但是停止进程仅是使进程暂停,将进程的状态设置成TASK_STOPPED,一旦收到恢复执行的信号,进程还可以继续执行。
continue : 恢复进程的执行,和停止进程相对应,某些信号可以使进程恢复执行。
根据信号的默认处理,可以将传统信号进行如下分类:
| 信号 | 值 | 说明 |
———————————————————–
| SIGCHLD | 17 | 子进程终止、停止或恢复执行 |
———————————————————–
| SIGURG | 23 | 套接字上的紧急数据 |
———————————————————–
| SIGWINCH | 28 | 终端窗口大小发生变化 |
———————————————————–
| 信号 | 值 | 说明 |
———————————————————–
| SIGHUP | 1 | 挂起(hangup),多用于终端断开
———————————————————–
| SIGINT | 2 | 终端中断
———————————————————–
| SIGKILL | 9 | 杀死进程,该信号不能被忽略、屏蔽,该信号处
| | | 理函数也不能被用户自定义函数该别
———————————————————–
| SIGUSR1 | 10 | 用户自定义信号1
———————————————————–
| SIGUSR2 | 12 | 用户自定义信号2
———————————————————–
| SIGPIPE | 13 | 管道断开,多见于socket通信
———————————————————–
| SIGALAM | 14 | 定时器到期,该信号多用于实现定时器
———————————————————–
| SIGTERM | 15 | 终止进程,因为SIGKILL过于残暴,进程终止时
| | |可能需要先执行一些操作来保存现场信息,所以
| | |合理的杀死进程的方法是先发送SIGTERM信号,稍
| | |等片刻在发送SIGKILL信号
———————————————————–
| SIGSTKFLT | 16 | 协处理器栈错误,Linux并未使用该信号
———————————————————–
| SIGVTALRM | 26 | 虚拟定时器过期,setitimer函数的ITIMER_VIRTUAL模式
———————————————————–
| SIGPROF | 27 | 性能分析定时器过期,setitimer函数的ITIMER_PROF模式
———————————————————–
| SIGIO | 29 | I/O时可能发生
———————————————————–
| SIGPWR | 30 | 电量将要耗尽
———————————————————–
| 信号 | 值 | 说明 |
———————————————————–
| SIGQUIT | 3 | 终端Ctrl+\可产生该信号
———————————————————–
| SIGILL | 4 | 非法的指令
———————————————————–
| SIGTRAP | 5 | 跟踪/断点,gdb/strace一类工具会使用该信号,
| | | 这类工具会拦截或修改SIGTRAP信号处理函数
———————————————————–
| SIGABRT | 6 | 进程终止,进程调用abort函数会向自身发送
| | | SIGABRT信号,此外如果使用了assert,assert
| | | 失败时也会产生SIGABRT信号
———————————————————–
| SIGBUS | 7 | 总线错误
———————————————————–
| SIGFPE | 8 | 算术异常
———————————————————–
| SIGSEGV | 11 | 段错误,访问了非法的地址
———————————————————–
| SIGXCPU | 14 | 突破了对CPU时间的限制
———————————————————–
| SIGXFSZ | 25 | 突破了对文件大小的限制
———————————————————–
| SIGSYS | 31 | 无效的系统调用
———————————————————–
| 信号 | 值 | 说明 |
———————————————————–
| SIGSTOP | 19 | 确保进程会停止,该信号不能被忽略,不能将信号
| | |信号处理函数改写成用户指定的函数
———————————————————–
| SIGTSTP | 20 | 终端停止信号,和SIGSTOP功能类似,但是可以被
| | |进程忽略,可以被捕捉执行用户指定的信号处理函数
———————————————————–
| SIGTTIN | 21 | 用于作业控制,如果后台进程组尝试对终端执行
| | | read操作,终端驱动程序就会向该进程组发送
| | |SIGTTIN信号
———————————————————–
| SIGTTOU | 22 | 如果终端启动了TOSTOP(如通过stty tostop命令)
| | | 即不允许后台进程向终端写入,而某一后台进程
| | | 尝试写入终端时,终端驱动程序就会向进程组发送
| | | SIGTTOU信号
———————————————————–
| 信号 | 值 | 说明 |
———————————————————–
| SIGCONT | 18 | 如果目标进程处于停止状态,则恢复执行
———————————————————–
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
上面并没有列出32号信号和33号信号,这两个信号(SIGCANCEL 和 SIGSETXID)被NPTL这个线程库征用了,
用来实现线程的取消。从内核层来说,32号信号应该是最小的实时信号(SIGRTMIN),但是由于32号和33号
被glibc内部征用了,所以glibc将SIGRTMIN设置成了34号信号
可靠信号 :信号值在[ 1, 31 ]之间的所有信号,被称为不可靠信号;
不可靠信号 :在[ SIGRTMIN, SIGRTMAX]之间的信号被称为可靠信号;
不可靠信号是指发送的信号,内核不一定能递送给目标进程,信号可能会丢失。因为不可靠信号出现较早且应用广泛,处于兼容性考虑,不能改变这些信号的行为模式,所以只能新增信号。新增的信号就是[SIGRTMIN, SIGRTMAX]范围内的信号,它们被称为可靠信号。
可靠信号和不可靠信号的根本差异在于收到信号后,内核有不同的处理方式。
对于不可靠信号,内核用位图来记录该信号是否处于挂起状态。如果收到某不可靠信号,内核发现已经存在该信号处于未决状态,就会简单地丢弃该信号。因此发送不可靠信号,信号可能会丢失,即内核递送给目标进程的次数,可能小于信号发送的次数。
对于可靠信号,内核内部有队列来维护,如果收到可靠信号,内核会将信号挂到相应的队列中,因此不会丢弃。严格手来,内核也设有上限,挂起信号的个数也不能无限制地增大,不然耗费内核资源,因此只能说,在一定范围之内,可靠信号不会被丢弃。
可以通过如下命令获取内核对挂起信号的限制值,不同的系统会有不一样的值
ulimit -a
从下图可以看到,信号挂起队列的限制值是7713
《Linux环境编程 从应用到内核》高峰,李彬