本文是一篇科普文章,介绍什么是 Linux 信号,以及它的基本用法。原文链接见底部参考。
Linux中有许多处于不同状态的进程。这些进程属于用户应用程序或操作系统。我们需要一种机制让内核和这些进程协调它们的活动。其中一种方式是在一个进程有重大改变时通知其他进程,因此我们有了 信号 的概念。
信号基本上是一种单向通知。信号可以由内核发送给一个进程,或由一个进程发送给另一个进程,或者一个进程发送给它自己。
Linux信号的概念来源于Unix。在后来的Linux版本中,加入了实时(real-time)信号。信号是一种简单和轻量级的进程间通信形式,因此适用于嵌入式系统。
总共有 31 个标准信号,编号为 1-31。每个信号命名为“SIG”开头,后跟一个后缀(如INT、HUP、KILL等)。从 2.2 版开始,Linux 内核支持 33 种不同的实时信号,编号为 32-64,但应用程序应改为使用 SIGRTMIN + n 表示法。标准信号有特定用途,但 SIGUSR1 和 SIGUSR2 的使用可以由程序自定义。实时信号也可由程序定义。
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会被发送到父进程。
一些编程语言能够使用诸如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中,会优先处理标准信号。对于实时信号,编号较低的信号首先被传递,如果一个信号类型有很多在排队,最早的一个会被首先传递。
// 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); } }