本篇所有内容均参考CSAPP,部分内容会加上博主理解(为了防止博主的理解有问题而误导读者,博主会在所有自己理解的地方标注是博主的理解,被标注的内容请谨慎阅读)。[博主吐槽: 本来想摘抄一些知识点的,没想到整个章节全部都是知识点]
Linux信号允许进程和内核中断其他进程。
信号术语
发送信号。内核通过更新目的进程上下文中的某个状态,发送(递送)一个信号给目的进程。发送信号可以有如下原因: 1)内核检测到一个系统事件,比如除零错误或者子进程终止。2) 一个进程调用了kill函数,显式的要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。
接收信号。 当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为*信号处理程序(signal handler)*的用户层函数捕获这个信号。[博主理解: 基本流程为: 进程正在运行中->进程接收到信号->控制传递到信号处理程序->信号处理程序运行->信号处理程序返回到下一条指令。 简单的来说就是当进程接收到一个信号时,会先执行这个信号对应的处理程序,执行完毕之后再返回到进程中下一条指令接着执行]
一个发出而没有被接收的信号叫做待处理信号(pending signal)。在任何时刻,一种类型至多只会有一个待处理信号。如果一个进程有一个类型为k的待处理信号,那么任何接下来发送到这个进程的类型为k的信号都不会排队等待,它们只是被简单的丢弃。一个进程可以有选择性地阻塞接收某种信号。当一种信号被阻塞时,它仍可以被发送,但是产生的待处理信号不会被接收,直到进程取消对这种信号的阻塞。
一个待处理信号最多只能被接收一次。内核为每个进程在pending位向量中维护着待处理信号的集合,而在blocked位向量中维护着被阻塞的信号集合。只要传送了一个类型为k的信号,内核就会设置pending中的第k位,而只要接收了一个类型为k的信号,内核就会清除pending中的第k位。
发送信号
Unix系统提供了大量向进程发送信号的机制。所有这些机制都是基于*进程组(process group)*这个概念的。
进程组
每个进程都只属于一个进程组,进程组是由一个正整数进程组ID来标识的。
1 | // getpgrp函数: 返回当前进程的进程组ID。 |
用/bin/kill程序发送信号
kill程序可以向另外的进程发送任意的信号,比如:
1 | /bin/kill -9 15213 |
这条命令会发送信号9(SIGKILL)给进程15213。一个为负的PID会导致信号被发送到进程组PID中的每个进程。比如:
1 | /bin/kill -9 -15213 |
这条命令发送一个SIGKILL给进程组15213中的每个进程。
有些shell可能有自己内置的kill命令,所以使用kill的时候最好指定路径。
从键盘发送信号
Unix Shell使用*作业(job)*这个抽象概念来表示为对一条命令行求值而创建的进程。在任何时刻,至多只有一个前台作业和0个或多个后台作业。
1 | ls | sort |
会创建一个由两个进程组成的前台作业,这两个进程是通过Unix管道连接起来的: 一个进程运行ls程序,另一个运行sort程序。shell为每个作业创建一个独立的进程组。进程ID通常取自作业中父进程中的一个。
在键盘上输入Ctrl + C会导致内核发送一个SIGINT信号到前台进程组中的每个进程。默认情况下,结果是终止前台作业。类似的,输入Ctrl + Z会发送一个SIGTSTP信号到前台进程组中的每个进程。默认情况下,结果是停止(挂起)前台作业。
用kill函数发送信号
进程通过调用kill函数发送信号给其他进程(包括它们自己)。
1 | // kill函数: 给指定进程发送信号。 |
如果pid大于零,那么kill函数发送信号号码sig给进程pid.如果pid等于零,那么kill发送信号sig给调用进程所在进程组中的每个进程,包括调用进程自己。如果pid小于0,kill发送信号sig给进程组 |pid|(pid的绝对值)中的每个进程。
用alarm函数发送信号
1 | // alarm函数: 进程可以通过alarm函数向它自己发送SIGALRM信号。 |
alarm函数安排内核在secs秒后发送一个SIGALRM信号给调用进程。如果secs是零,那么不会调度安排新的闹钟(alarm)。在任何情况下,对alarm的调用都将取消任何*待处理(pending)*闹钟,并且返回任何待处理闹钟在被发送前还剩下的秒数(如果这次对alarm的调用没有取消它的话);如果没有任何待处理的闹钟,就返回零。
接收信号
当内核把进程p从内核模式切换到用户模式时(例如: 从系统调用返回或是完成了一次上下文切换),它会检查进程p的未被阻塞的待处理信号的集合(pending &~blocked)。如果这个集合为空(通常情况下),那么内核将控制传递到p的逻辑控制流中的下一条指令。然而,如果集合是非空的,那么内核选择集合中的某个信号k(通常是最小的k),并且强制p接收信号k.收到这个信号会触发进程采取某种行为。一旦进程完成了这个行为,那么控制就传递回p的逻辑控制流中的下一条指令。每个信号都有一个预定义的默认行为,是下面的一种:
进程终止。
进程终止并转储内存。
进程停止(挂起)直到被SIGCONT信号重启。
进程忽略该信号。
1 | // signal函数: 修改和信号相关联的默认行为,SIGSTOP和SIGKILL的默认行为是不能修改的。 |
signal函数可以通过下列三种方法之一来改变和信号signum相关联的行为:
- 如果handler是SIG_IGN,那么忽略类型为signum的信号。
- 如果handler是SIG_DFL,那么类型为signum的信号行为恢复为默认行为。
- 否则,handler就是用户定义的函数的地址,这个函数被称为信号处理程序,只要进程接收到一个类型为signum的信号,就会调用这个程序[博主理解: CSAPP上写的是”调用这个程序”,博主认为改为”调用这个函数更恰当”]。通过把处理程序的地址传递到signal函数从而改变默认行为,这叫做设置信号处理程序(installing the handler)。调用信号处理程序被称为捕获信号,执行信号处理程序被称为处理信号。
当一个进程捕获了一个类型为k的信号时,会调用为信号k设置的处理程序,一个整数参数被设置为k.这个参数允许同一个处理函数捕获不同类型的信号。
信号处理程序可以被其他信号处理程序中断[博主注: 在信号处理程序执行完毕后,控制会返回到被中断的信号处理程序继续执行]。
阻塞和解除阻塞信号
Linux提供阻塞信号的隐式和显式的机制:
隐式阻塞机制。内核默认阻塞任何当前处理程序正在处理信号类型的待处理的信号。
显式阻塞机制。应用程序可以使用sigprocmask函数和它的辅助函数,明确的阻塞和解除阻塞选定的信号。
1 |
|
sigprocmask函数改变当前阻塞的信号集合。具体的行为依赖于how的值:
SIG_BLOCK: 把set中的信号添加到blocked中(blocked=blocked | set)。
SIG_UNBLOCK: 从blocked中删除set中的信号(blocked=blocked &~set)。
SIG_SETMASK: block=set。
如果oldset非空,那么blocked位向量值之前的值保存在oldset中。
未完待续。。。
编写信号处理程序
信号处理程序有几个属性使得它们很难推理分析: 1)处理程序与主程序并发运行,共享同样的全局变量,因此可能与主程序和其他处理程序相互干扰;2) 如何以及何时接收信号的规则常常有违人的直觉;3) 不同的系统有不同的信号处理语义。
安全的信号处理
保守编写处理程序的原则:
- G0. 处理程序要尽可能简单。避免麻烦的最好方法是保持处理程序尽可能小和简单。例如,处理程序可能只是简单的设置全局标志并立即返回,所有与接收信号相关的处理都由主程序执行,它周期性地检查(并重置)这个标志。
- G1. 在处理程序中只调用异步信号安全的函数。所谓异步信号安全的函数(或简称安全的函数)能够被信号处理程序安全地调用,原因有二: 要么它是可重入的(例如只访问局部变量),要么它不能被信号处理程序中断。[博主注: 可以百度搜索”异步信号安全函数”看看哪些是异步信号安全函数,这里博主就不写了,太多了。]
- G2. 保存和恢复errno。许多Linux异步信号安全的函数都会在出错返回时设置errno。在处理程序中调用这样的函数可能会干扰主程序中其他依赖于errno的部分。解决方法是在进入处理程序时把errno保存在一个局部变量中,在处理程序返回前恢复它。注意,只有在处理程序要返回时才有此必要。如果处理程序调用_exit终止该进程,那么就不需要这样做了。
- G3. 阻塞所有的信号,保护对共享全局数据结构的访问。如果处理程序和主程序或其他处理程序共享一个全局数据结构,那么在访问(读或写)该数据结构时,你的处理程序和主程序应该暂时阻塞所有的信号。这条规则的原因是从主程序访问一个数据结构d通常需要一系列的指令,如果指令序列被访问d的处理程序中断,那么处理程序可能会发现d的状态不一致,得到不可预知的结果。在访问d时展示阻塞信号保证了处理程序不会中断该指令序列。
- G4. 用volatile声明全局变量。考虑一个处理程序和一个main函数,它们共享一个全局变量g。处理程序更新g,main周期性地读取g。对于一个优化编译器而言,main中g的值看上去从来没有变化过,因此使用缓存在寄存器中g的副本来满足对g的每次引用是很安全的。如果这样,main函数可能永远都无法看到处理程序更新过的值。可以用volatile类型限定符来定义一个变量,告诉编译器不要缓存这个变量。volatile限定符强迫编译器每次在代码中引用g时,都要从内存中读取g的值。一般来说,和其他所有共享数据结构一样,应该暂时阻塞信号,保护每次对全局变量的访问。
- G5. 用sig_atomic_t声明标志。在常见的处理程序设计中,处理程序会写全局标志来记录收到了信号。主程序周期性地读这个标志,响应信号,再清除该标志。对于通过这种方式来共享的标志,C提供一种整型数据类型sig_atomic_t,对它的读和写保证会是原子的(不可中断的),因为可以用一条指令来实现它们: “volatile sig_atomic_t flag;”。因为它们是不可中断的,所以可以安全地读和写sig_atomic_t变量,而不需要暂时阻塞信号。注意,这里对原子性的保证只适用于单个的读和写,不适用于像flag++或flag=flag+10这样的更新,它们可能需要多条指令。