翻译自Linux Signals

概念

Linux计算机系统具有许多不同状态的进程。 这些进程属于用户应用程序或操作系统。 我们需要一种内核机制和这些进程来协调它们的活动。 实现此目的的一种方法是在重要事件发生时通知其他人的过程。 这就是我们有Signals的原因。

Signals基本上是单向通知。 内核可以将Signals发送到进程,或者由一个进程发送到另一个进程,或者进程将Signals发送给自身。

Linux Signals追溯到Unix Signals。 在后来的Linux版本中,添加了实时Signals。 Signals是一种简单而轻量级的进程间通信,因此适用于嵌入式系统。

img

Common sources of Linux signals

讨论

Signals 分类

img

共有31个标准Signals,编号为1-31。 每个Signal都被命名为“SIG”,后跟一个后缀。 从2.2版开始,Linux内核支持33种不同的实时Signals。 这些数字为32-64,但程序员应使用SIGRTMIN + n来表示。 标准Signals具有特定目的,但SIGUSR1和SIGUSR2的使用可由应用程序定义。 实时Signals也由应用程序定义。

编号为0的Signal,被POSIX.1称为空信号,一般不使用,但可以作为kill函数一个特例。 没有Signal被发送但可以使用它(相当不可靠)来检查进程是否仍然存在。

Linux Signal的实现完全符合POSIX标准。 较新的实现应该优先使用sigaction而不是传统的signal接口。

正如硬件子系统可以中断处理器一样,Signal中断进程执行。 因此,它们被视为软中断(software interrupts)。 正如中断处理程序处理硬件中断,Signal处理程序处理Signal。

一些Signal被映射到特定的输入:

  • ctrl + c -> SIGINT 发送给给前台进程组中的所有进程。常用于终止正在运行的程序。

  • ctrl + z -> SIGSTOP 发送给给前台进程组中的所有进程,常用于挂起一个进程

  • ctrl + \ -> SIGQUIT 发送给前台进程组中的所有进程,终止前台进程并生成 core 文件

Signals如何影响linux进程状态

一些Signal终止接收此Signal的process:SIGHUP,SIGINT,SIGTERM,SIGKILL。

一些Signal终止进程以及核心转储(core dump)以帮助程序员调试出错的地方:SIGABRT(中止信号),SIGBUS(总线错误),SIGILL(非法指令),SIGSEGV(无效内存引用),SIGSYS(系统调用错误)。

Core dump:操作系统在进程收到某些信号而终止运行时,将此时进程地址空间的内容以及有关进程状态的其他信息写出的一个磁盘文件

其他停止进程的Signal还有:SIGSTOP,SIGTSTP。

SIGCONT则是一个恢复已停止进程的Signal。

程序可以覆盖Signal的默认行为。 例如,可以编写交互式程序来忽略SIGINT(由ctrl + c输入生成)。 两个值得注意的例外是SIGKILL和SIGSTOP信号,这些信号不能被忽略,阻止或覆盖。

让我们考虑父进程及其子进程的示例。

假设孩子将SIGSTOP发送给自己,子进程将被停止。 这反过来触发SIGCHLD到父母。 然后父母可以通知孩子继续使用SIGCONT。 当孩子离开停止状态时,另一个SIGCHLD被发送给父母。 如果稍后,子进程退出,则最终的SIGCHLD将发送给父进程。

img

Example exchange of signals between parent and child processes

Signal与异常

一些编程语言能够使用诸如try-throw-catch之类的结构进行异常处理。

信号则与异常不同。 相反,失败的系统或库调用返回非零的退出码。 当进程终止时,它的退出代码将是128加Signal编号。 例如,由SIGKILL杀死的进程将返回137(128 + 9)。

同步和异步

生成信号时,可以将它们视为同步或异步。

同步

由于指令导致不可恢复的错误(例如非法地址访问)则发生同步Signal。 这些Signal被发送到导致它的线程。 这些也称为traps,因为它们也会导致陷入内核。

异步

异步Signal位于当前执行上下文的外部。 从另一个进程发送SIGKILL就是一个例子。 这些也称为软中断。

Signal的典型生命周期

Signal经历三个阶段:

img

Generation

内核或任何进程都可以生成Signal。无论谁生成Signal,都将其发送到特定进程。Signal由其编号表示,没有额外的数据或参数。因此,Signal是轻量级的。但是,可以为POSIX实时Signal传递额外的数据。可以生成Signal的系统调用和函数包括raise,kill,killpg,pthread_kill,tgkill和sigqueue。

Delivery

Signal在传递之前被称为pending。通常情况下,内核会尽快将Signal传递给进程。但是,如果进程阻止了Signal,它将保持挂起状态直到解除阻塞。

Processing

一旦传递Signal,就会以多种方式之一进行处理。每个Signal都有一个相关的默认动作:

  • 忽略Signal

  • 终止流程

  • core dump

  • 停止/继续进程。

对于非默认行为,相应的处理函数将被调用。具体行为则通过sigaction函数指定。

阻塞和非阻塞

img

Signal中断正常的程序执行流程。 当进程执行某些关键代码或更新与Signal处理程序共享的数据时,此时不应该接受信号。 阻塞正是为了解决了这个问题,代价则是Signal的处理被延迟了。

每个进程都可以指定是否要阻塞特定Signal。 如果阻塞并且Signal确实发生,操作系统将把Signal保持为挂起状态。 一旦进程解除阻塞,就会传递Signal。 当前被阻塞的信号集合称为Signal mask。

没有必要无限期地阻塞Signal。 为此,该进程可以在Signal被接受后忽略该Signal。

被一个进程阻塞的Signal不会影响其他进程的正常接收Signal。

可以使用sigprocmask(单线程)或pthread_sigmask(多线程)设置Signal mask。 当进程具有多个线程时,可以基于每个线程阻止Signal。 Signal将被传递到任何未阻止它的线程上。即:

Signal handlers are per process, signal masks are per thread.

多个待处理Signal

许多标准Signal可以等待进程的处理(pending for process)。 但是,给定Signal类型时,只有一个实例可以挂起。 这是因为Signal的挂起和阻塞实现为位掩码,每个Signal类型一位。

例如,我们可以同时将SIGALRM和SIGTERM挂起,但我们不能有两个SIGALRM信号等待进程处理。 即使有多个SIGALRM,该过程也只会收到一个SIGALRM信号。

对于实时Signal,Signal可以与数据一起进入队列,从而可以单独传送和处理Signal的每个实例。

POSIX没有规定标准Signal的传送顺序,也没有规定如果标准Signal和实时Signal都处于pending状态会发生什么。 Linux优先考虑标准Signal。 对于实时Signal,首先传送较低编号的Signal,如果许多同一个类型Signal在队列中等待,则发送最早的Signal,即先进先出。

SAMPLE CODE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
*// Adapted from source:* *http://www.firmcodes.com/signals-in-linux///* *Accessed: 2019-07-09*
*// Example shows a custom handler for SIGINT// but the handler reverts to default action for future signals.// Thus, first ctrl+c will allow program to continue// and second ctrl+c will terminate the program.*
#include <unistd.h>
#include <stdio.h>
#include <signal.h>

void sig_handler1(int num)
{
printf("You are here becoz of signal: %d**\n**", num);
signal(SIGINT, SIG_DFL);
}

int main()
{
signal(SIGINT, sig_handler1);
while(1)
{
printf("Hello**\n**");
sleep(2);
}
}

Reference

Linux Signals