12.1 信号的概念

全称为 软中断信号,也被称为软中断,简称信号。

信号是发送给进程的软中断,操作系统用信号来通知正在运行的程序,发生了意外情况。如引用了不可用的内存地址,或异步事件(如电话断线)。

信号本质上是在软件层次上 对硬件中断机制的一种模拟

一个 进程收到一个信号处理器收到一个中断请求 可以说是 一样 的。

信号用于警告意外事件的发生,这些事件会 生成触发 信号:

GNU 的 C 函数库定义了各种信号类型,每种类型都适用于特定类型的事件。某些类型的事件使程序不能像往常一样继续进行,相应的信号通常会终止程序。其他类型的报告无害事件的信号在默认情况下被忽略。

如果用户要参与到会触发信号的事件中,可以事先定义 信号处理函数,当收到 特定信号 时,操作系统会运行它。有时称信号处理函数 捕获(catch) 了信号,当函数运行时,该信号通常处于 阻塞(blocked) 状态。

一个进程可以给另一个进程发信号,父进程可以借此抛弃子进程,或者两个相关进程借此通信并同步。

12.2 信号的种类

12.2.1 可靠信号与不可靠信号

早期Unix系统中的信号机制比较简单和原始,信号值小于 SIGRTMIN 的信号都是 不可靠信号。这就是 “不可靠信号” 的来源。它的主要问题是 信号可能丢失

信号值位于 SIGRTMINSIGRTMAX 之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。Linux 在支持新版本的信号安装函数 sigation() 以及信号发送函数 sigqueue() 的同时,仍然支持早期的 signal() 信号安装函数,支持信号发送函数 kill()

信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前 linux 中的 signal() 是通过 sigation() 函数实现的,因此,即使通过 signal() 安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由 signal() 安装的实时信号支持排队,同样不会丢失。

对于目前 linux 的两个信号安装函数:signal()sigaction() 来说,它们都不能把 SIGRTMIN 以前的信号变成可靠信号(都不支持排队,仍有可能丢失,仍然是不可靠信号),而且对SIGRTMIN以后的信号都支持排队。这两个函数的最大区别在于,经过sigaction安装的信号都能传递信息给信号处理函数,而经过signal安装的信号不能向信号处理函数传递信息。对于信号发送函数来说也是一样的。

12.2.2 实时信号与非实时信号

早期 Unix 系统只定义了 32 种信号,前 32 种信号已经有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的 CTRL ^C 时,会产生 SIGINT 信号,对该信号的默认反应就是进程终止。后 32 个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。

非实时信号 都不支持排队,都是 不可靠信号实时信号 都支持排队,都是 可靠信号

12.2.3 Linux 信号列表

Linux 内核中的信号,每一种都用一个数字标识,它们不会携带任何参数,名字大多是自我解释的。编号为 1 ~ 31 的信号为传统 UNIX 支持的信号,是 不可靠信号(非实时的),编号为 32 ~ 63 的信号是后来扩充的,为 可靠信号 (实时信号)。

编号 信号 说明 产生方法 默认动作 可忽略 可处理 可阻塞
1 SIGHUP 通知同一会话内的各个作业,告知它们与控制终端不再关联 终端连接正常或非正常结束 终止进程      
2 SIGINT 中断进程 ^C 优雅结束进程 N N  
3 SIGQUIT 终止进程,内核转储 ^\ 终止进程 N N  
4 SIGILL 执行了非法指令,无法恢复到默认动作 可执行文件出现错误,试图执行数据段,堆栈溢出 终止进程      
5 SIGTRAP 调试器用信号,无法恢复到默认动作 断点指令,其它陷阱指令 终止进程      
6 SIGABRT 程序本身检测到错误 调用 abort 函数 终止进程      
7 SIGBUS 非法地址,包括内存地址对齐错误   终止进程      
8 SIGFPE 致命算术错误   终止进程      
9 SIGKILL 立即终止进程,致命 kill -9 终止进程 N N N
10 SIGUSR1 预留给用户,用于任何自定义功能   终止进程      
11 SIGSEGV 分割违规 访问不属于自己存储空间或只读存储空间 终止进程      
12 SIGUSR2 预留给用户,用于任何自定义功能   终止进程      
13 SIGPIPE 管道已损坏   终止进程 Y Y Y
14 SIGALRM 测量真实时间或钟表时间的计时器超时 alarm 终止进程      
15 SIGTERM 立即终止进程 kill 优雅结束进程 Y Y Y
16 SIGSTKFLT            
17 SIGCHLD 子进程终止或暂停   忽略      
18 SIGCONT 让进程继续运行   什么也不做   Y Y
19 SIGSTOP 暂停进程 kill -STOP   N N N
20 SIGTSTP 挂起进程 ^Z   Y Y Y
21 SIGTTIN 后台作业的进程想读取终端   暂停进程 Y Y Y
22 SIGTTOU 后台作业的进程想写到终端   暂停进程 Y Y Y
23 SIGURG 紧急或带外数据到达套接字   忽略      
24 SIGXCPU 超过 CPU 时间限制   终止进程      
25 SIGXFSZ 超出文件大小限制   终止进程      
26 SIGVTALRM 测量当前进程使用 CPU 时间的计时器超时   终止进程      
27 SIGPROF 测量当前进程使用的 CPU 时间,以及系统花在该进程上的 CPU 时间的计时器超时   终止进程      
28 SIGWINCH 终端窗口大小发生改变   忽略      
29 SIGIO 文件描述符为输入输出准备就绪 fcntl 忽略      
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            
60 SIGRTMAX-4            
59 SIGRTMAX-5            
61 SIGRTMAX-3            
62 SIGRTMAX-2            
63 SIGRTMAX-1            
64 SIGRTMAX            

根据信号功能的不同,可以大致分成以下几类:

程序错误信号

当操作系统检测到严重的程序错误时会产生以下的信号。通常这些信号表明,程序以某种方式崩溃了,一般没有办法继续运行下去了。

一些程序会处理程序错误信号,以便在终止之前进行清理。

终止 是大多数程序中程序错误的最终结果。

所有这些信号的 默认操作是导致进程终止

如果您 阻止或忽略 这些信号,或为它们建立了正常返回的处理程序,那么当这些信号发生时,您的程序可能会发生可怕的破坏,除非它们是通过 raisekill 函数,而不是真正的错误产生的。

raise 函数用于进程给自己发送信号,kill 函数用于把信号发送给进程或进程组,它不仅可以中止进程,也可以向进程发送其他信号。

内核转储:core dump,当进程被这些程序错误信号之一终止运行时,操作系统会将此时进程地址空间的内容以及有关进程状态写出一个文件,文件名为 core,可保存在进程的任何当前目录。用调试器检查它们,可以调查导致错误的原因。

SIGFPE

该信号报告一个 致命的算术错误。包括几乎所有的算术错误,包括被零除及溢出。如果程序在某个位置保存了整数,然后用在了浮点运算,就会产生 “非法运算”,因为处理器无法把数据识别为浮点数字。

SIGILL

名字来源于 非法指令,Illegal Instruction。通常意味着程序在尝试执 行垃圾指令或特权指令。因为 C 编译器产生的程序只会执行合法指令,因此 SIGILL 通常表示可执行文件被损坏了,或者在尝试执行数据,而不是程序。

当发生栈区溢出时,或当系统无法运行信号处理函数时,也会产生 SIGILL 信号,

SIGSEGV

分割违规,Segmentation Violation。

当程序 尝试在为其分配的内存以外读或写 时,或者,尝试写入只读内存 时,会产生该信号。

实际上,程序走的太远,才能被系统的内存保护机制检测到,此时,信号才会出现。

获取 SIGSEGV 条件的常用方法包括:解引用空指针或未初始化的指针,或者当你使 用指针逐步遍历数组,但未能检查数组的末尾时。取消引用空指针会生成 SIGSEGV 还是 SIGBUS,在不同的系统中是不同的。

SIGBUS

该信号的名称是 “总线错误” 的缩写。当 无效指针被解除引用时 会生成此信号。

SIGSEGV 一样,该信号通常是解引用未初始化的指针的结果。两者之间的区别在于 SIGSEGV 表示对有效内存的无效访问,而 SIGBUS 表示访问无效地址。特别是,SIGBUS 信号通常是由于 引用一个未对齐的指针 而引起的,比如指向一个不能被 4 整除的地址的四字整数。(每种计算机都有自己的地址对齐要求。)

SIGABRT

该信号表示程序本身检测到错误,并通过调用 abort 进行报告。请参阅中止程序。

SIGIOT

由 PDP-11 iot 指令生成。在大多数机器上,这只是 SIGABRT 的另一个名称。

SIGTRAP

由机器的 断点指令(breakpoint)生成,可能还有其他 陷阱指令(trap)。这个信号被 调试器 使用。如果程序以某种方式执行错误的指令,你的程序可能只会看到 SIGTRAP

【 breakpoint 】:软件开发中,断点指程序中故意设计的暂停位置,用于调试,有时干脆就叫暂停(pause)。

【 trap 】:陷阱,也称异常或故障,是一种典型的同步中断,通常是由异常情况(如断点、零除、无效的内存访问)产生的。陷阱通常会导致切换到内核态,操作系统在返回原始进程之前会进行一些操作。在系统进程中的陷阱要比用户进程中的陷阱更严重,在某些系统中是致命的。在有些用法中,陷阱这个词单指某个中断,该中断用于启动上下文切换,到监控程序或调试器。

SIGEMT

模拟器陷​​阱。这是由某些未实现的指令造成的,这些指令可能在软件中模拟,或操作系统未能正确模拟它们。

SIGSYS

坏的系统调用。也就是说,执行了向操作系统施加陷阱的指令,但系统调用执行的代码号无效。

终止信号

这些信号都用于以不同的方式通知进程终止。它们的目的稍有不同,程序也希望以不同的方式来处理这些信号。

处理这些信号的原因通常是为了让程序在终止之前,能够清理一下资源和数据。如,保存状态信息、删除临时文件、恢复之前的终端模式等。这一类的信号处理函数结尾时应该 指定响应信号的默认动作,然后 再重新给自己发信号(reraise)。

以下所有信号的 默认动作均为终止进程

SIGTERM

通用的终止进程的信号,与 SIGKILL 不同,该信号可被阻塞、处理、忽略。

优雅地要求进程终止。

kill 会产生该信号。

SIGINT

中止进程,在终端按下 ^C 时产生。

SIGQUIT

由 “退出字符” 控制,终端按下 ^\ 会产生。它会在终止进程时产生内核转储,就像程序错误信号一样。可以认为是被用户检测到的程序错误情形。

由该信号终止的进程会省略一些清理工作,如不会删除临时文件,以便用户连同内核转储一起查看。

SIGKILL

立即终止进程。不能被处理、忽略、阻塞,因此是致命的信号。

通常只由专门的请求来产生,因为它不能被处理,应该做为最后的措施来使用,之前应该先尝试使用不这么激烈的办法,如 ^C。如果其它所有信号对进程都不管用时,使用该信号往往会让它马上终止。

在某些异常情况下,如程序已经完全无法继续运行时(连一个信号处理函数都运行不了),系统会自动为进程产生 SIGKILL 信号。

SIGHUP

hang-up,用于报告用户终端已经断开连接,也许因为网络故障。

该信号还用于将 “终端上的控制进程的终止” 报告给与该会话相关联的作业,该终止在控制终端上有效地断开了会话中的所有进程。

用户终端连接正常或非正常结束时发出,通常是在终端的控制进程结束时,通知同一会话内的各个作业,告知它们与控制终端不再关联。

此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

闹铃信号

这些信号用于提醒计时器已超时。默认动作为促使进程终止,但很少使用默认动作,大多数用来交给信号处理函数执行。

SIGALRM

该信号通常表示,用来测量真实时间或钟表时间的计时器超时了,如使用 alarm 函数。

SIGVTALRM

该信号通常表示,用来测量当前进程使用 CPU 时间的计时器超时了,名字源于 virtual time alarm。

SIGPROF

该信号通常表示,用于测量当前进程使用的 CPU 时间,以及系统花在该进程上的 CPU 时间的计时器超时了,这样的定时器用于实现代码分析工具。

异步 I/O 信号

这些信号用于和异步 I/O 设备的连接。必须通过调用 fcntl 来采取明确的动作,以促使特定的文件描述符产生这些信号,默认动作为忽略。

SIGIO

当一个文件描述符准备就绪,可以进行输入或输出时产生。

在大多数操作系统中,终端和 socket 是能生 SIGIO 信号的唯一类型的文件,其它类型,包括普通文件,即便你要求它们,也永远也不会产生该信号。

GNU 系统中,如果使用 fcntl 成功地设定了异步模式,该信号就会产生。

SIGURG

当 “紧急(urgent)” 或带外数据(out-of-band)到达套接字时,会发送此信号。

SIGPOLL

这是一个 System V 的信号名字,与 SIGIO 类似,仅为兼容性定义。

作业控制信号

这些信号用于支持作业控制。如果操作系统不支持作业控制,则这些信号无法被处理或 raise。

如果不是很懂作业控制,最好不要使用这些信号。

SIGCHLD

子进程终止或暂停时,会把该信号发给父进程。

默认动作为忽略。

如果为该信号准备了信号处理函数,但此时有被终止的子进程仍未通过 waitwaitpid 报告自己的状态,那么,该信号处理函数是否要管理这些进程决定于不同的操作系统。

SIGCLD

已过时,被 SIGCHLD取代。

SIGCONT

用于使进程继续运行。比较特殊的信号,如果进程已暂停,在信号传送之前 ,它永远会使其继续。

默认动作为什么也不做。

可以阻塞该信号,可以为其设定处理函数,但它永远会不管不顾地使进程继续运行。

SIGSTOP

暂停进程,不可捕获、忽略、阻塞。

SIGTSTP

交互式暂停信号,与 SIGSTOP 不同,该信号可捕获、忽略。

如果你想在进程暂停时,让文件或系统表处于安全状态,就需要用函数来捕获该信号。例如,关闭屏显的程序应该捕获 SIGTSTP,以便它们暂停之前能恢复屏显。

当用户键入 SUSP 字符时(^Z)会产生。

SIGTTIN

当进程作为后台作业运行时,无法从用户终端读取。如果后台进程尝试读取终端,作业中的所有进程都会收到一个 SIGTTIN 信号,默认动作为暂停进程。

SIGTTOU

当后台进程尝试向终端写入,或尝试设置终端模式时,产生该信号。

默认动作为暂停进程。

只有在开启了 TOSTOP 模式的情况下,尝试写入终端才会产生该信号。

当进程暂停时,不会再给它传递任何信号,直到它又继续运行,当然,除了 SIGKILLSIGCONT

暂停期间,想要传递给进程的信号会标记为挂起(pending),直到其运行再传递给它。

SIGKILL 信号永远会让进程终止,不能被阻塞、捕获、忽略。

SIGCONT 虽然可以被忽略,但它不管怎么样都会让暂停的进程继续运行。

给进程发送 SIGCONT 信号,会促使任何挂起的暂停信号被丢弃。同样,任何挂起的 SIGCONT 信号,当进程收到暂停信号时都会被丢弃。

如果孤儿进程组中的一个进程收到 SIGTSTPSIGTTINSIGTTOU 信号,但不予捕获,进程就不会暂停。暂停该进程其实没什么用了,因为不会有 shell 程序会注意到它暂停,然后再允许用户继续运行它。取而代之的是,取决于在用的操作系统,有些系统什么也不会做,还有些系统会发送另一个信号,如 SIGKILLSIGHUP。在 GNU 系统中,进程遇到 SIGKILL 就会死掉,由此避免了过多的暂停进程或孤儿进程在系统驻留可能引发的问题。

【 孤儿进程组 】:当一个控制进程终止时,它的终端变得可用,可在上面新建起一个会话。实际上,其它用户可以登陆到该终端。如果原会话中的任何进程仍然尝试使用该终端时,会造成问题。为了防止问题的发生,当会话发起者被终止时,进程组仍需继续运行,称为孤儿进程组。 当进程组变成孤儿时,其中的进程都会收到一个 SIGHUP 信号,一般情况会造成进程终止,但如果进程忽略该信号,或建立一个信号处理函数,它就可以继续运行于孤儿进程组中,即便其控制进程已经终止,但它仍然再也无法访问终端了。

操作错误信号

这些信号用于报告由于程序操作引起的各种错误。并不是程序内部的程序错误,而是导致操作系统调用无法顺利完成的错误。

所有信号的默认动作均为终止进程。

SIGPIPE

管道已损坏。

如果使用管道或 FIFO,你必须精心设计你的程序,必须先用一个进程来打开管道读取,然后才能允许其它进程开始写入。如果读取进程始终没有运行,或异常终止,写入管道会产生 SIGPIPE 信号。

如果该信号被阻塞、捕获或忽略,则违规呼叫将以 EPIPE 而失败。

如果尝试向一个已断开的套接字输出也会产生该信号。

SIGLOST

资源丢失。

当你对 NFS 文件进行咨询锁定时,NFS 服务器重启了,并忘记锁定这回事了,此时会产生该信号。

在 GNU 系统中,当服务进程异常死掉时会产生该信号,可以忽略该信号。

无论使用了什么调用都会返回一个错误。

根据 man SIGNAL(7),Linux 中,SIGIOSIGLOST 使用同一个编号,29。SIGLOST 在内核中被注释掉了,但某些软件编译过程中,仍然会把 29 当作 SIGLOST

SIGXCPU

超出了 CPU 时间限制。

当进程超过了其在 CPU 时间上的软件资源限制时会产生该信号。

SIGXFSZ

超出文件大小限制。

当进程尝试扩展文件大小,超出进程软件资源在文件大小的限制时,会产生该信号。

其他信号

这些信号用于各种不同的目的,它们通常不会影响进程,除非为了什么明确地使用它们。

SIGUSR1、SIGUSR2

预留给用户,用于任何自定义功能。

可用于简单的进程间通讯。

默认动作为终止进程。

SIGWINCH

Window size change,改变窗口大小。

当终端驱动程序记录屏幕上的行数和列数发生变化时,产生该信号。

默认动作为忽略。

SIGINFO

显示系统、进程状态信息

当用户在终端的规范模式(canonical mode)下键入 STATUS 字符时,该信号被发送到控制终端的前台进程组中的所有进程。

如果该进程是进程组的首领(leader),默认动作是显示一些关于系统及进程当前行为的状态信息,否则默认动作为什么都不做。

该信号只对 alpha 和 sparc 架构的系统可用。

12.3 信号的生命周期

对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来说,可以分为三个阶段:

12.3.1 信号的产生

产生信号的事件

通常来说,产生信号的事件主要可归结为以下三类:错误、外部事件、显式请求

错误

错误意味着某个程序做了非法的事情,无法继续执行下去了。但并非所有类型的错误都会产生信号 – 事实上,大多数情况下不会。例如,打开一个不存在的文件是一个错误,但它不会产生一个信号;相反,open 返回 -1。通常,通过返回指示错误的值,来报告与某些库函数相关的错误。引发信号的错误可能发生在程序中的任何地方,而不仅仅是在库调用中。这些包括除以零和无效的内存地址。

外部事件

部事件通常与 I/O 或其他进程有关。这些包括输入的到达,计时器的到期以及子进程的终止。

显式请求

显式请求意味着使用库函数,如 kill,其目的很明确,就是为了生成一个信号。

信号产生的时机

信号可以 同步生成异步生成

同步

同步信号 与程序中的特定操作有关,并在该操作期间传递(除非被阻塞)。

大多数 错误 会同步地生成信号,进程为自己生成信号的 显式请求 也是同步产生。

在某些机器上,某些类型的硬件错误(通常是浮点异常)不会完全同步地报告,而是会在运行几个指令之后。。

异步

生成异步信号的事件,往往 不受进程的控制

在进程执行期间,这些信号到达的时间是不可预测的。外部事件 会异步生成信号,对进程的 显式请求 也是异步生成信号。

给定类型的信号通常要么是同步的,要么是异步的。例如,错误信号通常是同步的,因为错误会同步地产生信号。但是任何类型的信号都可以通过 显示请求 同步或异步生成

生成信号

除了由于硬件陷阱或中断而产生的信号之外,程序可以显示地(explicitly)将信号发送给自己或另一个进程。

给自己发信号

进程可以给使用 raise 函数来自己发信号,该函数声明于 signal.h

raise 的用途之一是 重现 你已捕获的信号的 默认行为

比如可以做到:当收到一个暂停信号时,可以把响应重新设为默认动作,然后执行一些清理工作,再重新发送一遍暂停信号。当进程重新继续时,再恢复信号处理器。

给另一进程发信号

kill 函数可用于向另一个进程发送信号。虽然它的名字代表杀掉进程,但实际上它还可以用于许多其它的目的。比如:

  • 父进程启动一个子进程来执行一项任务,也许让其处于一个无限循环,当不再需要该任务时,就结束它。
  • 一个进程做为进程组的一部分执行任务时,需要终止掉,或遇到错误及其它事件时,需要通知其它进程。
  • 两个进程协同工作时,需要同步。

当进程给自己发一个信号时,如果该信号没有被阻塞,在进程返回之前,kill 会传递至少一个信号给进程,但该信号不一定非的是刚刚发给自己的这个信号,也有可能是其它挂起的、未阻塞的信号。

如果信号可以成功发送,kill 的返回值为零。否则,不会发送信号,返回值为 -1

如果 pidkill 函数的参数)指定了向多个进程发送一个信号,如果它能向至少一个进程发送信号,则 kill 就成功了。你无法区分是一个进程收到了信号,还是所有进程都收到了。

使用 kill 的权限

你是无法用 kill 把信号发送给任意的随机进程的,因为存在一些限制。这些限制旨在防止反社会行为,例如任意杀死属于另一用户的进程。kill 比较典型的使用,还是在父、子、兄弟进程之间传递信号,在这些情况下,你通常有权发送信号。唯一常见的例外,是在子进程中运行 setuid 程序。如果程序更改了其真实 UID 以及其有效 UID,你就可能不再有权限来发送信号了。su 就是这样的程序。

一个进程是否有权给另一个进程发信号,由两个进程的 UID 决定的。

通常,一个进程要想给另一个进程发信号,要么 发送进程属于特权用户(如 root),要么 发送进程的真实 UID 或有效 UID 与接收进程的相匹配。如果接收进程已从其映像文件上的 SUID 模式位更改了其有效 UID,则进程映像文件的所有者会被用来代替其当前有效 UID。在一些实施方案中,即使 UID 不匹配,父进程也能够将信号发送到子进程,而其他实施方案则可能会用别的限制。

SIGCONT 信号是一种特殊情况。如果发送进程与接收进程处于同一个会话,则可以发送,而不考虑 UID。

12.3.2 信号在目标进程中注册

进程表的表项 中有一个 软中断信号域,该域中每一位对应一个信号。

内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。

  • 如果信号发送给一个正在睡眠的进程,如果进程为可中断睡眠,则唤醒进程
  • 如果进程为不可中断睡眠,则仅设置进程表中信号域相应的位,而不唤醒进程
  • 如果进程处于就绪状态,则仅设置进程表中信号域相应的位

当一个 实时信号 发送给一个进程时,不管该信号是否已经在进程中注册,都会被 再注册一次,因此,信号不会丢失,因此,实时信号又叫做 “可靠信号”。

当一个 非实时信号 发送给一个进程时,如果该信号已经在进程中注册,则该信号将被 丢弃,造成信号丢失。因此,非实时信号又叫做 “不可靠信号”。

因此,信号是否可靠只与信号值有关

12.3.3 信号的执行和注销

内核处理一个进程收到的软中断信号是在该 进程的上下文 中,因此,进程必须处于运行状态。当其由于被信号唤醒或者正常调度重新获得 CPU 时,在其从内核空间返回到用户空间时,会检测是否有信号等待处理。如果存在未决信号等待处理,且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。

对于 非实时信号 来说,由于在未决信号信息链中最多只 占用一个 sigqueue 结构,因此该结构被释放后,应该 把信号在进程未决信号集中删除(信号注销完毕);

而对于 实时信号 来说,可能在未决信号信息链中占用多个 sigqueue 结构,因此应该针对占用 sigqueue 结构的数目区别对待:如果只 占用一个 sigqueue 结构(进程只收到该信号一次),则执行完相应的处理函数后,应该 把信号在进程的未决信号集中删除(信号注销完毕)。如果 占用多个,则 要等待该信号的所有 sigqueue 处理完毕 后再在进程的未决信号 集中删除 该信号。

信号屏蔽字:就是进程中被阻塞的信号集,这些信号不能发送给该进程,它们在该进程中被 “屏蔽” 了.

当所有未被屏蔽的信号都处理完毕后,即可返回用户空间。对于被屏蔽的信号,当取消屏蔽后,在返回到用户空间时会再次执行上述检查处理的一套流程。

内核处理 一个进程收到的 信号的时机,是在一个 进程从内核态返回用户态 时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态进程在用户态下不会有未处理完的信号

12.3.4 信号的传递

信号被生成时,一开始先是被 挂起(pending)。但通常只在挂起状态停留很小一段时间,然后就会被 传递 给进程。然而,如果这类的信号当前 被阻塞,它就有可能无限期地停留在挂起状态,直到这种信号被 解阻(unblocked)。一旦被解阻,它就会立即被传递出去。

传递信号时,无论是立即传递出去,还是长时间延迟之后才传递出去,都会引起程序的 特定动作

对于某些信号,如 SIGKILLSIGSTOP,采取的动作是固定的,但对于大多数信号,程序有一个选择:忽略 信号、指定处理函数接受 该类信号的 默认动作。程序会使用 signalsigaction 等函数来设定其选择。我们有时会说处理函数 捕捉到 信号。当运行处理程序时,该信号通常处于被阻塞状态。

如果对某种信号设定的默认动作为忽略,则所产生的任何此类信号当即就会被丢弃。即使信号在此时被阻塞,也会发生这种情况。以这种方式被丢弃的信号将永远不会被传送,即使程序随后为该类信号指定了不同的动作,然后解除阻止。

如果信号到达时,程序既没有处理也没有忽略,其默认动作就上场了。每种信号都有其自己的默认动作。对于大多数信号,默认操作是终止进程。对于那些代表 “无害” 事件的信号,默认行为是什么都不做。

当信号终止进程时,其父进程可以通过检查由 waitwaitpid 函数报告的终止状态代码来确定终止的原因。它可以通过获得的信息来确认:子进程的终止,是由一个信号及其涉及的同类信号引起的。如果在 shell 中运行的程序被信号终止,shell 通常会打印出一些错误消息。

通常用于代表程序错误的信号具有一个特殊的属性:当一个信号终止进程时,它还会写入一个核心转储文件,该文件会记录终止时进程的状态。你可以使用调试器来检查核心转储,以调查导致错误的原因。

如果你通过显示请求引发了 “程序错误” 信号,并且导致了进程的终止,那么它就会生成一个核心转储文件,就好像该信号是由于错误直接引发的一样。

如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回、弹出栈顶时,就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方。这样做的原因是,用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。

传递信号的命令

kill

kill -signal PID

用 kill 把信号传递给指定作业(%jobnumber)或进程(PID)。

kill -SIGHUP $(ps aux | grep 'rsyslogd' | grep -v 'grep'| awk '{print $2}')
killall

killall -signal command

killall 可以直接指定发送信号的进程名称。可以杀掉所有指定名称的进程,用于杀服务。

killall [-iIe] [command name]

-i 交互式

-e 指定里程准确名称

-I 进程名称忽略大小写

范例:

killall -1 rsyslogd 给 rsyslogd 启动的 PID 发送 SIGHUP 信号

killall -9 httpd 强制终止所有 httpd 启动的进程

killall -i -9 bash 依次询问每个 bash 进程是否需要被终止

12.4 信号的阻塞

阻塞信号意味着告诉操作系统先按住它(hold),稍后再传送。一般来说,程序不会无限期地阻塞信号 – 也有可能通过设置动作为 SIG_IGN 来忽略它们。但是暂时阻塞信号是很有用的,可以防止它们中断敏感的操作。如:

  • 在你修改全局变量时,如果这些变量也会被某些信号的处理器修改,可以使用函数来阻塞这些信号
  • 在特定的信号处理器运行时,你可以在 sigaction 调用中设置 sa_mask,来阻塞某些信号,以保证信号处理器运行时不会被信号中断

12.4.1 为什么要阻塞

临时阻塞信号可以 防止运行重要代码时被中断,此收到的信号会在解阻之后传递给进程。

有时阻塞会使 程序变的更加可靠。如信号处理器以外的其它代码在检查或修改数据期间,可以阻塞特定的信号,不让信号处理函数在引期间运行。

如果希望只有当 信号尚未到达时,才想执行特定的操作,则阻塞信号也是必要的。

想要 测试信号是否到达 的唯一可靠方法,就是在信号被阻塞时进行测试。

12.4.2 信号集

所有的信号阻塞函数都使用一种数据结构,称为信号集(signal set),用来 指定哪些信号会受到影响。因此,每个活动都涉及两个阶段:创建信号集,然后将其做为参数传给一个库函数。

12.4.3 进程信号掩码

当前被阻塞的信号集合 称为信号掩码(signal mask),每个进程都有其自己的信号掩码,创建新进程时,它会继承父进程的信号掩码。通过设置信号掩码,你可以 灵活地施加阻塞或解阻

在多线程进程中,每个线程都有其自己的信号掩码,不存在唯一的进程掩码。

12.4.4 测试信号的传递

阻塞可用于测试信号的传递,可以临时把会影响测试的信号阻塞,避免对测试造成中断。

12.4.5 为信号处理器阻塞

如果要使用信号处理器,你通常希望它能够完成其运行,期间不受其它信号的打扰。从信号处理器开始运行,直到其结束,期间必须阻塞可能引起混乱或损坏其数据的信号。

如果要拿信号处理函数来对付信号,在处理函数运行期间,该信号会自动阻塞(加入到该进程的信号掩码中)。例如,如果针对 SIGTSTP 设定了处理函数,则信号到来时,处理函数运行期间,将会迫使随后的所有 SIGTSTP 信号全部等待。

然而,默认情况下,其它类型的信号不会被阻塞,在处理函数运行期间,它们可以正常抵达。

在任何情况下,当处理程序返回时,系统将恢复在处理程序之前就位的掩码。如果有任何未决信号在挂起,则该进程将立即收到这些信号,然后才继续执行下面的代码。

12.4.6 检查挂起的信号

可以随时通过系统调用来查看哪些信号正在挂起。检查某个信号是否在挂起的测试,通常并没有什么用处。如果在该信号没有被阻塞的情况下来测试,则更是个坏主意。

如果有一种特定的信号在为进程挂起,随后到达的更多相同类型的信号很可能会被丢弃。比如,如果有一个 SIGINT 信号正在挂起,此时另一个 SIGINT 信号到来,当解阻该信号时,程序很有可能只会看到其中的一个。

12.4.7 记住一个信号

记住一个信号,稍后再有所行动。

除了使用库功能来阻塞信号,还可以让处理程序设置一个标签随后来测试,同样可以直到和阻塞相同的效果。

12.5 等待信号

如果程序是由外部事件驱动的,或者需要使用信号来同步,当它无事可做时,可能需要等待,直到某个信号到来。

12.5.1 使用 pause 函数

让进程进入等待直到信号到来,最简单的方法是调用 pause 函数。它会暂停程序的执行,直到收到信号处理器被执行,或是进程终止。

如果信号促使处理函数被执行,则 pause 返回。这被认为是一个不成功的返回,(因为成功的行为应该是永远暂停程序),因此返回值为 -1

12.5.2 pause 的问题

pause 的简单性会隐藏严重的时间错误,这些错误会使程序神秘地挂起。

如果程序的真正工作是由信号处理器自己完成的,则可以安全的使用 pause,主程序除了调用 pause 就什么也不做。每次有信号被传递时,处理程序会做后继的批量工作,然后再返回,于是程序的主循环再次调用 pause

直到等来一个或多个信号,才能安全地使用 pause,然后再继续真正的工作。即便想办法让信号处理程序通过设置标签来从中协调,也仍然无法可靠地使用 pause

12.5.3 使用 sigsuspend

等待信号的干净、可靠的方法就是阻塞它,然后再使用 sigsuspend。通过在循环中使用 sigsuspend 函数,可以等待特定类别的信号,同时允许其他类型的信号交给各自的处理程序。

12.6 信号的安装

如果 进程要处理某一信号,那么就 要在进程中安装该信号

12.6.1 为什么要安装

安装信号主要用来确定 信号值 及进程针对该信号值的 动作 之间的 映射关系,即进程将要处理哪个信号,该信号被传递给进程时,将执行何种操作。

12.6.1 如何安装

linux 主要有两个函数实现信号的安装:signal()sigaction()

  • signal():只有两个参数,不支持信号传递信息,主要是用于前 32 种 非实时信号的安装
  • sigaction():是 较新 的函数,由 sys_signalsys_rt_sigaction 这两个系统调用实现,有三个参数,支持信号传递信息,主要用来与 sigqueue() 系统调用配合使用。sigaction() 同样 支持非实时信号的安装

sigaction() 优于 signal() 主要体现在 支持信号携带参数