在 Linux 中,理解信号的概念是非常重要的。这是因为,信号被用于通过 Linux 命令行所做的一些常见活动中。例如,每当你按 Ctrl+C 组合键来从命令行终结一个命令的执行,你就使用了信号。知道信号的基本原理是非常有用的。

概述

在 Linux 系统(以及其他类 Unix操作系统)中,信号被用于进程间的通信。信号是一个发送到某个进程或同一进程中的特定线程的异步通知,用于通知发生的一个事件。从 1970 年贝尔实验室的 Unix 面世便有了信号的概念,而现在它已经被定义在了 POSIX 标准中。

对于在 Linux 环境进行编程的用户或系统管理员来说,较好地理解信号的概念和机制是很重要的,在某些情况下可以帮助我们更高效地编写程序。对于一个程序来说,如果每条指令都运行正常的话,它会连续地执行。但如果在程序执行时,出现了一个错误或任何异常,内核就可以使用信号来通知相应的进程。

信号同样被用于通信、同步进程和简化进程间通信,在 Linux 中,信号在处理异常和中断方面,扮演了极其重要的角色。信号巳经在没有任何较大修改的情况下被使用了将近 30 年。

当一个事件发生时,会产生一个信号,然后内核会将事件传递到接收的进程。有时,进程可以发送一个信号到其他进程。除了进程到进程的信号外,还有很多种情况,内核会产生一个信号,比如文件大小达到限额、一个 I/O 设备就绪或用户发送了一个类似于 Ctrl+C 或 Ctrl+Z 的终端中断等。

运行在用户模式下的进程会接收信号。如果接收的进程正运行在内核模式,那么信号的执行只有在该进程返回到用户模式时才会开始。

发送到非运行进程的信号一定是由内核保存,直到进程重新执行为止。休眠的进程可以是可中断的,也可以是不可中断的。如果一个在可中断休眠状态的进程(例如,等待终端输入的进程)收到了一个信号,那么内核会唤醒这个进程来处理信号。如果一个在不可中断休眠状态的进程收到了一个信号,那么内核会拖延此信号,直到该事件完成为止。

当进程收到一个信号时,可能会发生以下 3 种情况:

  • 进程可能会忽略此信号。有些信号不能被忽略,而有些没有默认行为的信号,默认会被忽略。

  • 进程可能会捕获此信号,并执行一个被称为信号处理器的特殊函数。

  • 进程可能会执行信号的默认行为。例如,信号 15(SIGTERM) 的默认行为是结束进程。

当一个进程执行信号处理时,如果还有其他信号到达,那么新的信号会被阻断直到处理器返冋为止。

信号的名称和值

每个信号都有以SIG开头的名称,并定义为唯一的正整数。在 Shell 命令行提示符 下,输入kill -l命令,将显示所有信号的信号值和相应的信号名,类似如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kill -l
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

信号值被定义在文件/usr/include/bits/signum.h 中,其源文件是 /usr/src/linux/kernel/signal.c

在 Linux 下,可以查看 signal(7) 手册页来查阅信号名列表、信号值、默认的行为和它们是否可以被捕获。其命令如下所示:

1
man 7 signal

下标所列出的信号是 POSIX 标准的一部分,它们通常被缩写成不带SIG前缀,例如,SIGHUP 通常被简单地称为 HUP。

信 号 默认行为 描 述 信号值
SIGABRT 生成 core 文件然后终止进程 这个信号告诉进程终止操作。ABRT 通常由进程本身发送,即当进程调用 abort() 函数发出一个非正常终止信号时 6
SIGALRM 终止 警告时钟 14
SIGBUS 生成 core 文件然后终止进程 当进程引起一个总线错误时,BUS 信号将被发送到进程。例如,访问了一部分未定义的内存对象 10
SIGCHLD 忽略 当了进程结束、被中断或是在被中断之后重新恢复时,CHLD 信号会被发送到进程 20
SIGCONT 继续进程 CONT 信号指不操作系统重新开始先前被 STOP 或 TSTP 暂停的进程 19
SIGFPE 生成 core 文件然后终止进程 当一个进程执行一个错误的算术运算时,FPE 信号会被发送到进程 8
SIGHUP 终止 当进程的控制终端关闭时,HUP 信号会被发送到进程 1
SIGILL 生成 core 文件然后终止进程 当一个进程尝试执行一个非法指令时,ILL 信号会被发送到进程 4
SIGINT 终止 当用户想要中断进程时,INT 信号被进程的控制终端发送到进程 2
SIGKILL 终止 发送到进程的 KILL 信号会使进程立即终止。KILL 信号不能被捕获或忽略 9
SIGPIPE 终止 当一个进程尝试向一个没有连接到其他目标的管道写入时,PIPE 信号会被发送到进程 13
SIGQUIT 终止 当用户要求进程执行 core dump 时,QUIT 信号由进程的控制终端发送到进程 3
SIGSEGV 生成 core 文件然后终止进程 当进程生成了一个无效的内存引用时,SEGV 信号会被发送到进程 11
SIGSTOP 停止进程 STOP 信号指示操作系统停止进程的执行 17
SIGTERM 终止 发送到进程的 TERM 信号用于要求进程终止 15
SIGTSTP 停止进程 TSTP 信号由进程的控制终端发送到进程来要求它立即终止 18
SIGTTIN 停止进程 后台进程尝试读取时,TTIN 信号会被发送到进程 21
SIGTTOU 停止进程 后台进程尝试输出时,TTOU 信号会被发送到进程 22
SIGUSR1 终止 发送到进程的 USR1 信号用于指示用户定义的条件 30
SIGUSR2 终止 同上 31
SIGPOLL 终止 当一个异步输入/输出时间事件发生时,POLL 信号会被发送到进程 23
SIGPROF 终止 当仿形计时器过期时,PROF 信号会被发送到进程 27
SIGSYS 生成 core 文件然后终止进程 发生有错的系统调用时,SYS 信号会被发送到进程 12
SIGTRAP 生成 core 文件然后终止进程 追踪捕获/断点捕获时,会产生 TRAP 信号。 5
SIGURG 忽略 当侖一个 socket 有紧急的或是带外数据可被读取时,URG 信号会被发送到进程 16
SIGVTALRM 终止 当进程使用的虚拟计时器过期时,VTALRM 信号会被发送到进程 26
SIGXCPU 终止 当进程使用的 CPU 时间超出限制时,XCPU 信号会被发送到进程 24
SIGXFSZ 生成 core 文件然后终止进程 当文件大小超过限制时,会产生 XFSZ 信号 25

信号的处理

信号的处理有三种办法、分别是:忽略、捕捉和默认动作,我们可以通过捕获程序接收到的信号,进行手动处理,而不是按照系统默认的方式来处理信号,因为系统大部分的默认处理方式都是“结束”程序,所以通过捕获信号,就能够自己编程进行信号处理。

  • 忽略信号,⼤多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是SIGKILLSIGSTOP)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没⼈能管理的的进程,显然是内核设计者不希望看到的场景

  • 捕捉信号,需要告诉内核,用户希望如何处理某⼀种信号,说⽩了就是写⼀个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。

  • 系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,⼤部分的处理方式都比较粗暴,就是直接杀死该进程。

具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。

《UNIX 环境⾼级编程(第三部)》的 P251——P256中间对于每个信号有详细的说明。

了解了信号之后,如何使用信号呢?

最常用的kill命令就是一个发送信号的工具。比如,我在后台运行了⼀个 top ⼯具,通过ps 命令可以查看他的 PID,通过kill 9 PID 来发送了⼀个终止进程的信号来结束了 top 进程。如果查看信号编号和名称,可以发现9对应的是 9) SIGKILL,正是杀死该进程的信号。⽽以下的执行过程实际也就是执行了9号信号的默认动作——杀死进程。

信号处理函数的注册

信号注册函数不只⼀种方法,常用的是函数signal

信号发送函数也不止⼀个,常用的是kill 函数

信号注册函数

在正式开始了解这两个函数之前,可以先来思考⼀下,处理中断都需要处理什么问题。

按照我们之前思路来看,可以发送的信号类型是多种多样的,每种信号的处理可能不⼀定相同,那么,我们肯定需要知道到底发生了什么信号。

另外,虽然我们知道了系统发出来的是哪种信号,但是还有⼀点也很重要,就是系统产生了⼀个信号,是由谁来响应?

如果系统通过Ctrl+C产生了⼀个 SIGINT(中断信号),显然不是所有程序同时结束,那么,信号⼀定需要有⼀个接收者。对于处理信号的程序来说,接收者就是自己。

开始的时候,先来看看入门版本的信号注册函数,他的函数原型如下:

signal 的函数原型

1
2
//Defined in header <signal.h>
void (*signal( int sig, void (*handler) (int))) (int);

根据函数原型可以看出由两部分组成,⼀个是真实处理信号的函数,另⼀个是注册函数了。

对于这个函数来说,sig 显然是信号的编号,handler 是中断函数的指针。

同样,中断函数的原型中,有⼀个参数是 int 类型,显然也是信号产生的类型,方便使用⼀个函数来处理多个信号。我们先来看看简单⼀个信号注册的代码示例吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<stdio.h>
#include<signal.h>
#include<unistd.h>

void handler(int sig) {
printf("接收到了%d信号", sig);
}

int main() {
for(int i = 1; i <= 64; i++) {
signal(i, handler);
}
while(1) {
printf("执行了一次\n");
sleep(1);
}
return 0;
}

使用Ctrl+CCtrl+Z可以看到这两个常用的快捷键发送的是2信号和20信号,而且程序并没有停止,说明信号已经被成功捕捉到。

通过 kill 命令发送信号之前,我们需要先查看到接收者,通过 ps 命令查看了之前所写的程序的 PID,通过 kill 函数来发送。对于已注册的信号,使用 kill 发送都可以正常接收到,但是如果发送了未注册的信号,则会使得应用程序终止进程。

那么,已经可以设置信号处理函数了,信号的处理还有两种状态,分别是默认处理和忽略,这两种设置很简单,只需要将 handler 设置为

SIG_IGN(忽略信号)或 SIG_DFL(默认动作)即可。在此还有两个问题需要说明⼀下:

  1. 当执行⼀个程序时,所有信号的状态都是系统默认或者忽略状态的。除非是 调用exec进程忽略了某些信号。exec 函数将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不会改变 。

  2. 当⼀个进程调动了 fork 函数,那么子进程会继承父进程的信号处理方式。

入门版的信号注册还是比较简单的,只需要⼀句注册和⼀个处理函数即可,那么,接下来看看,如何发送信号吧。

信号发送函数

kill 的函数原型

1
2
3
4
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

正如我之前所说的,信号的处理需要有接受者,显然发送者必须要知道发给谁,根据 kill 函数的远行可以看到,pid 就是接受者的 pid,sig 则是发送的信号的类型。从原型来看,发送信号要比接受信号还要简单些,那么我们直接上代码吧

1
2
3
4
5
6
7
8
9
#include<stdlib.h>                                                                                       #include<signal.h>
#include<sys/types.h>

int main(int argc, char *argv[]) {
int sig = atoi(argv[1]);
int pid = atoi(argv[2]);
kill(sig, pid);
return 0;
}

接收信号的结果

总结⼀下:

根据以上的结果可看到,基本可以实现了信号的发送,虽然不能直接发送信号名称,但是通过信号的编号,可以正常的给程序发送信号了,也是初步实现了信号的发送流程。

关于 kill 函数,还有⼀点需要额外说明,上⾯的程序限定了 pid 必须为⼤于0的正整数,其实 kill 函数传入的 pid 可以是小于等于0的整数。

  • pid > 0:将发送个该 pid 的进程

  • pid == 0:将会把信号发送给与发送进程属于同⼀进程组的所有进程,并且发送进程具有权限想这些进程发送信号。pid < 0:将信号发送给进程组ID 为 pid 的绝对值得,并且发送进程具有权限向其发送信号的所有进程

  • pid == -1:将该信号发送给发送进程的有权限向他发送信号的所有进程。(不包括系统进程集中的进程)

关于信号,还有更多的话题,比如,信号是否都能够准确的送达到⽬标进程呢?答案其实是不⼀定,那么这就有了可靠信号和不可靠信号