在 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 | kill -l |
信号值被定义在文件/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 |
信号的处理
信号的处理有三种办法、分别是:忽略、捕捉和默认动作,我们可以通过捕获程序接收到的信号,进行手动处理,而不是按照系统默认的方式来处理信号,因为系统大部分的默认处理方式都是“结束”程序,所以通过捕获信号,就能够自己编程进行信号处理。
忽略信号,⼤多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是
SIGKILL
和SIGSTOP
)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没⼈能管理的的进程,显然是内核设计者不希望看到的场景捕捉信号,需要告诉内核,用户希望如何处理某⼀种信号,说⽩了就是写⼀个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。
系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,⼤部分的处理方式都比较粗暴,就是直接杀死该进程。
具体的信号默认动作可以使用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 | //Defined in header <signal.h> |
根据函数原型可以看出由两部分组成,⼀个是真实处理信号的函数,另⼀个是注册函数了。
对于这个函数来说,sig 显然是信号的编号,handler 是中断函数的指针。
同样,中断函数的原型中,有⼀个参数是 int 类型,显然也是信号产生的类型,方便使用⼀个函数来处理多个信号。我们先来看看简单⼀个信号注册的代码示例吧。
1 |
|
使用Ctrl+C和Ctrl+Z可以看到这两个常用的快捷键发送的是2信号和20信号,而且程序并没有停止,说明信号已经被成功捕捉到。
通过 kill 命令发送信号之前,我们需要先查看到接收者,通过 ps 命令查看了之前所写的程序的 PID,通过 kill 函数来发送。对于已注册的信号,使用 kill 发送都可以正常接收到,但是如果发送了未注册的信号,则会使得应用程序终止进程。
那么,已经可以设置信号处理函数了,信号的处理还有两种状态,分别是默认处理和忽略,这两种设置很简单,只需要将 handler 设置为
SIG_IGN(忽略信号)或 SIG_DFL(默认动作)即可。在此还有两个问题需要说明⼀下:
当执行⼀个程序时,所有信号的状态都是系统默认或者忽略状态的。除非是 调用exec进程忽略了某些信号。exec 函数将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不会改变 。
当⼀个进程调动了 fork 函数,那么子进程会继承父进程的信号处理方式。
入门版的信号注册还是比较简单的,只需要⼀句注册和⼀个处理函数即可,那么,接下来看看,如何发送信号吧。
信号发送函数
kill 的函数原型
1 |
|
正如我之前所说的,信号的处理需要有接受者,显然发送者必须要知道发给谁,根据 kill 函数的远行可以看到,pid 就是接受者的 pid,sig 则是发送的信号的类型。从原型来看,发送信号要比接受信号还要简单些,那么我们直接上代码吧
1 |
|
接收信号的结果
总结⼀下:
根据以上的结果可看到,基本可以实现了信号的发送,虽然不能直接发送信号名称,但是通过信号的编号,可以正常的给程序发送信号了,也是初步实现了信号的发送流程。
关于 kill 函数,还有⼀点需要额外说明,上⾯的程序限定了 pid 必须为⼤于0的正整数,其实 kill 函数传入的 pid 可以是小于等于0的整数。
pid > 0:将发送个该 pid 的进程
pid == 0:将会把信号发送给与发送进程属于同⼀进程组的所有进程,并且发送进程具有权限想这些进程发送信号。pid < 0:将信号发送给进程组ID 为 pid 的绝对值得,并且发送进程具有权限向其发送信号的所有进程
pid == -1:将该信号发送给发送进程的有权限向他发送信号的所有进程。(不包括系统进程集中的进程)
关于信号,还有更多的话题,比如,信号是否都能够准确的送达到⽬标进程呢?答案其实是不⼀定,那么这就有了可靠信号和不可靠信号