信号
信号是对进程的通知机制,也被称为软中断,分成标准信号(传统信号,编号范围是 1~31)和实时信号。
部分信号:
- SIGINT:ctrl + c。
- SIGQUIT:ctrl + </kbd>,和 SIGINT 比多了核心转储。
- SIGTERM:
kill
命令默认发送的信号,程序可以注册处理这个信号,因此可以实现优雅退出。 - SIGKILL:必杀信号。
- SIGSTOP:必停信号。
- SIGPIPE:写管道有错,可能是读的进程把管道关了。
- SIGFPE:名字是浮点数错误,实际上泛指除 0 错误。而且 x86-64 上浮点数除 0 默认不出错,需要用
feenableexcept()
启用异常。 - SIGALRM:实时计时器过期。
- SIGVTALRM::虚拟计时器过期。
- SIGHUP:会话结束(终端断开)时发送给程序的信号,默认行为为杀死程序。在一些守护程序上还有重载配置的作用。
- SIGTSTP:ctrl + z。
- SIGUSR1 和 SIGUSR2:留给程序员自己用的,系统绝对不会发送的信号。
- SIGWINCH:终端环境的窗口尺寸发生变化。
一个信号默认的处理行为是三者之一:1. 忽略;2. 终止;3. 内核转储。
处理信号
使用 signal
或者 sigaction
注册信号处理函数,一般条件下 sigaction
是更好的选择。处理信号时一般不要调用 printf
等来自 stdio.h 的函数。
发送信号
kill
系统调用可以向指定进程发送给定的信号:
- 进程号 > 0:指定进程。
- 进程号 = 0:同组所有进程,包括自己。
- 进程号 < -1:取绝对值,然后向指定的进程组中所有进程。
- 进程号 = -1:系统范围内除了 init 和进程自己以外的、所有的当前进程有权限可以发送信号的目标进程。
- 信号值 = 0:不发送信号,但是检查是否能够成功向目标进程发送信号。可能的
errno
值有:EPERM
表示进程存在但是没有权限;ESRCH
表示进程不存在。
其他发送信号的规则:
- 特权级(
CAP_KILL
)进程可以向任何进程发送信号。 - init(1 号)进程是一个特例,只有它注册了某个信号的处理函数,才能向它发送这个信号。(原文是:It can be sent only signals for which it has a handler installed. 我觉得中文版翻译的不是很好,所以这里写的是我自己的理解。)
- 如果信号发送者和接收者满足一定关系,则信号是可以发送的。条件是:发送者的实际用户 ID 或者有效用户 ID 是接收者的实际用户 ID 或者保存的 set-user-ID,如下图所示。如果允许以接收者的有效用户 ID 作为判断依据,特权进程可能会设置自己的有效用户 ID 为普通用户,但是这时普通用户就能够向这个进程发送信号了。(书中说的是:将目标进程有效用户 ID 排除在检查范围之外,这一举措的辅助作用在于防止用户某甲向用户某乙的进程发送信号,而该进程正在执行的 set-user-ID 程序又属于用户某甲。我觉得这个说法很牵强,因为判断机制里面已经允许按照保存的 set-user-ID 来判断了。)

- SIGCONT 信号很特殊。非特权进程始终可以向同一个会话中的其他任何一个进程发送这个信号。
检查进程的存在
前面说了,使用 kill
向进程发送信号 0(实际上没有这个信号,但是 0 可以作为参数)可以测试是否能够成功发送信号。只要 errno
不是 ESRCH
就说明进程存在。
其他检查进程是否存在的方式:
wait()
系统调用:用来监控子进程。- 信号量和文件锁:如果能获取到锁说明进程已经终止。
- FIFO 等 IPC 通道:写入关闭说明进程终止。
- /proc/PID 接口:这一项由于用到了 PID,所以会受到循环使用 PID 的影响,导致结果在极端条件下不准确(但是大多数时候不会)。
其他发送信号的方式
raise()
函数可以用来进程自己发送信号。当系统只支持单线程时,相当于 kill(getpid(), sig)
,当系统支持多线程时,相当于 pthread_kill(pthread_self(), sig)
。相比起来,kill
总是将信号发给进程,具体线程可以是进程的任何一个。由于 raise()
函数是向进程自己发送信号,信号将立即传递。处理完成之后才会回到调用者。
killpg()
可以用于向某一进程组的所有成员发送信号,相当于 kill(-pgrp, sig)
。如果给的 pgrp 为 0,那么就会向当前进程所在进程组的所有进程发送信号。
显示信号字符串
和 errno 一样,我们可以调用函数来显示描述信号的字符串。
char *strsignal(int sig);
const char *sigdescr_np(int sig);
const char *sigabbrev_np(int sig);
书里只讲到 strsignal,它还会获取当前的 locale,但是在获取 locale 这一点上不是线程安全的。后面两个前者是获取详细描述,后者是获取简略描述(比如 SIGINT 的描述是 "INT"
),它们都不考虑 locale,是线程安全的。
psignal
和 perror
的功能则类似。psignal
也是考虑 locale 的。