Linux中有许多处于不同状态的进程。这些进程属于用户应用程序或操作系统。我们需要一种机制让内核和这些进程协调它们的活动。其中一种方式是在一个进程有重大改变时通知其他进程,因此我们有了 信号 的概念。
Linux 信号的常见来源如图所示:
信号基本上是一种单向通知。信号可以由内核发送给一个进程,或由一个进程发送给另一个进程,或者一个进程发送给它自己。
Linux信号的概念来源于Unix。在后来的Linux版本中,加入了实时(real-time)信号。信号是一种简单和轻量级的进程间通信形式,因此适用于嵌入式系统。
总共有 31 个标准信号,编号为 1-31。每个信号命名为“SIG”开头,后跟一个后缀(如INT、HUP、KILL等)。从 2.2 版开始,Linux 内核支持 33 种不同的实时信号,编号为 32-64,但应用程序应改为使用 SIGRTMIN + n 表示法。标准信号有特定用途,但 SIGUSR1 和 SIGUSR2 的使用可以由程序自定义。实时信号也可由程序定义。
Linux 信号的列表如下:
信号编号 | 信号名称 | 描述 |
---|---|---|
1 | SIGHUP | 控制终端挂起或者断开连接 |
2 | SIGINT | 中断信号,通常由 Ctrl+C 发送 |
3 | SIGQUIT | 退出信号,通常由 Ctrl+\ 发送 |
4 | SIGILL | 非法指令信号 |
5 | SIGTRAP | 跟踪异常信号 |
6 | SIGABRT | 中止信号 |
7 | SIGBUS | 总线错误信号 |
8 | SIGFPE | 浮点错误信号 |
9 | SIGKILL | 强制退出信号(无法忽略或捕获) |
10 | SIGUSR1 | 用户定义信号1 |
11 | SIGSEGV | 段错误信号 |
12 | SIGUSR2 | 用户定义信号2 |
13 | SIGPIPE | 管道破裂信号 |
14 | SIGALRM | 闹钟信号 |
15 | SIGTERM | 终止信号(无法忽略或捕获) |
16 | SIGSTKFLT | 协处理器栈错误信号 |
17 | SIGCHLD | 子进程状态改变信号 |
18 | SIGCONT | 继续执行信号 |
19 | SIGSTOP | 暂停进程信号(无法忽略或捕获) |
20 | SIGTSTP | 终端停止信号,通常由 Ctrl+Z 发送 |
21 | SIGTTIN | 后台进程尝试读取终端输入信号 |
22 | SIGTTOU | 后台进程尝试写入终端输出信号 |
23 | SIGURG | 套接字上的紧急数据可读信号 |
24 | SIGXCPU | 超时信号 |
25 | SIGXFSZ | 文件大小限制超出信号 |
26 | SIGVTALRM | 虚拟定时器信号 |
27 | SIGPROF | 分析器定时器信号 |
28 | SIGWINCH | 窗口大小变化信号 |
29 | SIGIO | 文件描述符上就绪信号 |
30 | SIGPWR | 电源失效信号 |
31 | SIGSYS | 非法系统调用信号 |
32 | SIGRTMIN | 实时信号最小编号 |
… | … | … |
64 | SIGRTMAX | 实时信号最大编号 |
0号信号,即 POSIX.1 标准中所说的null信号,一般不使用,但在 kill 函数中有个特殊的用途。使用时没有信号被发送,但可以用来(相当不可靠)检查进程是否仍然存在。
Linux中的信号实现完全符合 POSIX 标准。最新的实现应该倾向于使用 sigaction 而不是传统的信号接口。
正如硬件子系统可以中断处理器一样,信号可以中断进程的执行。因此,它们被看作是软件中断。一般来说,中断处理程序(interrupt handlers)处理硬件中断,而信号处理程序(signal handlers)则处理信号导致的中断。
通常信号被映射到特定的按键输入,比如,SIGINT代表ctrl+c,SIGSTOP代表ctrl+z,SIGQUIT代表ctrl+\。
信号如何影响进程的状态?
一些信号会终止正在接受信号的进程:SIGHUP、SIGINT、SIGTERM、SIGKILL。有一些信号不仅可以终止进程还会输出一些内核信息,以帮助程序员调试出错的地方,如SIGABRT(abort)、SIGBUS(bus error)、SIGILL(illegal instruction)、SIGSEGV(invalid memory reference无效内存引用)、SIGSYS(bad system call错误的系统调用) )。用于停止进程的信号有:SIGSTOP、SIGTSTP。 SIGCONT 是恢复已停止的进程。
一个程序可以覆盖信号的默认行为。例如,一个交互式程序可以忽略SIGINT(由ctrl+c输入产生)。不过有两个例外需要注意,SIGKILL和SIGSTOP,它们不能被忽略、阻止或用这种方式覆盖。
让我们看一个父进程和其子进程的例子。假设子进程向自己发送了SIGSTOP,子进程将被停止。这反过来又会触发SIGCHLD到父进程。然后,父进程可以使用SIGCONT向子进程发出继续运行的信号。当子进程从停止状态重新运行时,另一个SIGCHLD被发送到父进程。如果后来,子进程退出了,最后的SIGCHLD会被发送到父进程。
信号类似于异常(exception)吗?
一些编程语言能够使用诸如try-throw-catch这样的结构进行异常处理。
但信号与异常并不类似。相反,失败的系统或库调用会返回非零的退出代码。当一个进程被终止时,它的退出代码是128加信号编号。例如,一个被SIGKILL杀死的进程将返回137(128+9)。
信号是同步还是异步的?
信号既可以是同步,也可以是异步。
同步信号的出现是由于指令导致了一个无法恢复的错误,如非法地址访问。这些信号被发送到导致它的线程。这些信号也被称为陷阱(trap),因为它们也会导致陷阱进入内核的陷阱处理程序(trap handler)。
异步信号是对当前执行环境的外部信号。从另一个进程中发送 SIGKILL 就是这样一个例子。这些也被称为软件中断。
信号的生命周期是什么?
一个信号经历三个阶段:
- Generation(生成):信号可以由内核或任何进程生成,生成后会将其发送给特定的进程。信号由其编号表示,没有额外的数据或参数。因此,信号是轻量级的。但是,POSIX 实时信号传递额外的数据。可以生成信号的系统调用和函数包括 raise、kill、killpg、pthread_kill、tgkill 和 sigqueue。
- Delivery(传递):信号在传递之前一直处于待处理状态。通常,内核会尽快将信号传递给进程。但是,如果对应的进程阻塞了信号,它将保持未处理状态直到解除阻塞。
- Processing(处理):一旦信号被传递到,就会以多种方式中其中一种进行处理。每个信号都有一个默认的行为:忽略信号;或终止进程,有时使用核心转储(core dump);或停止/继续该过程。对于非默认行为,对应的处理函数会被调用。通过 sigaction 函数指定究竟采用哪一种处理方式。
什么是信号阻塞和解除阻塞?
信号打断了程序执行的正常流程。当进程正在执行一些关键代码或更新与信号处理程序共享的数据时,这是不希望看到的。阻断的引入解决了这个问题。不过代价是,信号处理被延迟了。
每个进程都可以指定它是否要阻塞一个特定的信号。如果被阻断,而信号确实发生了,操作系统将把该信号作为待处理信号。一旦进程解除阻断,该信号将被传递。当前被屏蔽的信号集合被称为信号屏蔽(signal mask)。
无限期地阻断一个信号是没有意义的。为了这个目的,进程可以在接受到信号后选择忽略它,被一个进程屏蔽的信号不会影响其他进程,他们可以正常接收信号。
信号屏蔽(Signal mask)可以用 sigprocmask(单线程)或 pthread_sigmask(多线程)来设置。 当一个进程有多个线程时,信号可以针对每个线程分别设置是否屏蔽。信号将被传递给任何一个没有阻断它的线程。从本质上讲,信号处理程序是针对某个进程的,信号掩码是针对某个线程的。
一个进程可以有多个待处理的信号吗?
是的,许多标准信号可以在进程中被挂起。然而,一个给定的信号类型只能有一个实例被挂起。这是因为信号的挂起和阻塞是作为位掩码(bitmask)实现的,每个信号类型只有一个位。例如,我们可以让 SIGALRM 和 SIGTERM 同时挂起,但我们不能有两个 SIGALRM
信号挂起。进程将只收到一个SIGALRM信号,即使是多次抛出。
通过实时信号,信号可以和数据一起排队,这样每个信号的实例都可以单独传递和处理。
POSIX没有规定标准信号的传递顺序,也没有规定如果标准信号和实时信号都在等待中会如何处理。然而在Linux中,会优先处理标准信号。对于实时信号,编号较低的信号首先被传递,如果一个信号类型有很多在排队,最早的一个会被首先传递。
信号历史发展
- 1990 信号在 POSIX.1-1990 标准中得到了描述。可以追溯至 IEEE标准1003.1-1988。
- 1993 实时扩展作为 POSIX.1b 发布。其中包含实时信号。
- 1999 随着内核版本 2.2 的发布,Linux 开始支持实时信号。
- 2001 POSIX.1-2001 标准中增加了更多信号:SIGBUS、SIGPOLL、SIGPROF、SIGSYS、SIGTRAP、SIGURG、SIGVTALRM、SIGXCPU、SIGXFSZ。
简单 C 语言信号处理程序
1 | // Example shows a custom handler for SIGINT |