1、信号简介
一个信号就是一条小消息, 它通知进程系统中发生了一个某种类型的事件。
低层的硬件异常是由内核异常处理程序处理的,正常情况下, 对用户进程而言是不可见的。信号提供了一种机制, 通知用户进程发生了这些异常。
1. 常见信号
- SIGFPE
一个进程试图除以0 - SIGILL
一个进程执行一条非法指令 - SIGSEGV
进程进行非法内存引用 - SIGINT
如果当进程在前台运行时,那么内核就会发送一个SIGINT信号给这个前台进程组中的每个进程 - SIGKILL
一个进程可以通过向另一个进程发送一个SIGKILL信号强制终止它 - SIGCHLD
当一个子进程终止或者停止时, 内核会发送一个SIGCHLD信号给父进程
2、信号术语
传送一个信号到目的进程是由两个不同步骤组成的
1. 发送信号
1.发送信号的原因
- 内核检测到了一个系统事件
- 一个进程调用了kill函数
2. 说明
- 内核通过更新目的进程上下文中的某个状态发送信号
- 一个进程可以发送信号给自己
2. 接受信号
当目的进程被内核强迫以某种方式对信号的发送做出反应时, 它就接收了
信号。
1. 进程的操作
- 忽略这个信号
- 终止
- 执行一个称为信号处理程序(signalhandler)的用户层函数捕获这个信号
2. 待处理信号
一个发出而没有被接收的信号叫做待处理信号(pending signal) 。
1. 在任何时刻,一种类型至多只会有一个待处理信号
其含义是
- 如果进程有一个类型为k的待处理信号, 那么任何接下来发送到这个进程的类型为k的信号都不会排队等待,它们被简单地丢弃。
- 如果进程有选择性地阻塞接收某种信号时,它仍可以被发送, 但是产生的待处理信号不会被接收, 直到进程取消对这种信号的阻塞。
2. 一个待处理信号最多只能被接收一次
内核为每个进程在pending 位向量中维护着待处理信号的集合, 而在blocked 位向量(也称为信号掩码(signal mask)) 。中维护着被阻塞的信号集合。只要传送了一个类型为K 的信号, 内核就会设pending 中的第k 位, 而只要接收了一个类型为K 的信号, 内核就会清除pending 中的第k 位。
3、发送信号
1.进程组
- 进程组用一个正整数表示
- 每个进程只能属于一个进程组
- 父进程和子进程默认属于一个进程组
2. 使用/bin/kill程序发送信号
linux> /bin/kill -9 12123
linux> /bin/kill -9 -12123
- 正的pid代表发送信号9向进程号为12123的进程
- 负的pid代表发送信号9向进程组号为12123的进程组的所有进程
3. 从键盘发送信号
比如我们使用shell的时候按下Ctrl + c就会发送一个信号到前台进程组的每个进程中。
Unix shell的 作业
Unix shell使用作业(job)这个抽象概念来表示为对一条命令行求值而创建的进程
在任何时刻,至多只有一个前台作业和0个或多个后台作业。
比如,键入
linux> ls I sort
会创建一个由两个进程组成的前台作业,这两个进程是通过Unix管道连接起来的: 一个
进程运行ls程序,另一个运行sort程序。
shell 为每个作业创建一个独立的进程组。进程组ID 通常取自作业中父进程中的一个。
4. 用kill函数发送信号
可以使用该函数将信号发送给其他进程,包括他自己。
pid大于0,则是发送信号到进程pid。等于0发送到调用该函数的进程所在进程组的所有进程。小于0则是给|pid|进程组的所有进程。
5.使用alarm函数发送信号
alarm函数安排内核在secs秒后发送一个SIGALRM信号给调用进程。
4、接收信号
当内核把进程p从内核模式切换到用户模式时。它会检查进程p 的未被阻塞的待处理信号的集合
- 如果这个集合为空(通常情况下), 那么内核将控制传递到p的逻辑控制流中的下一条指令
- 如果集合是非空的, 那么内核选择集合中的某个信号k(通常是最小的k )并且强制p接收信号k
1. 信号的默认行为
每个信号都有一个默认的行为
举例
SIGKILL 默认类型就是终止接收进程
SIGCHLD 默认就是忽略这个信号
信号可以通过signal函数修改默认行为,但是SIGSTOP和SIGKILL的默认行为不可以修改。
信号处理程序可以被其他信号处理程序中断
5、阻塞和解除阻塞信号
1. 隐式阻塞机制
默认阻塞任何当前处理程序正在处理信号类型的待处理的信号
2. 显式阻塞机制
应用程序可以使用sigprocmask函数和它的辅助函数, 明确地阻塞
和解除阻塞选定的信号
相关函数如下
1. 原理
sigprocmask函数改变当前阻塞的信号集合,也就是blocked位向量,来显示的阻塞信号。
具体的行为依赖how的值,有以下三种情况
6、编写信号处理程序
1. 安全的信号处理
因为信号处理程序要跟主程序以及其他信号处理程序并发,所以我们需要进行安全的信号处理。书中给出了一些原则:
- 处理程序要尽量简单
- 处理程序中只调用异步信号安全的函数
要么它是可重入的(例如只访问局部变量),要么它不能被信号处理程序中断 - 阻塞所有的信号,保护对共享全局数据结构的访问。
如果处理程序和主程序或其 他处理程序共享 个全局数据结构,那么在访问(读或者写)该数据结构时,你的处理程序和主程序应该暂时阻塞所有的信号 - 用volatile 声明全局变量
考虑一个处理程序和一个main 函数,它们共享一个全局变量g。处理程序更新g, main 周期性地读g。对于一个优化编译器而言,main 中g的值看上去从来没有变化过, 因此使用缓存在寄存器中g的副本来满足对g的每次引用是很安全的。如果这样, main 函数可能永远都无法看到处理程序更新过的值。可以用volatile类型限定符来定义一个变量,告诉编译器不要缓存这个变量。例如:
volatile int g;
volatile 限定符强迫编译器每次在代码中引用g 时, 都要从内存中读取g的值。一般来说, 和其他所有共享数据结构一样,应该暂时阻塞信号,保护每次对全局变量的访问。 - 用sig_atomic_t 声明标志。
在常见的处理程序设计中, 处理程序会写全局标志来记录收到了信号。主程序周期性地读这个标志, 响应信号,再清除该标志。对通过这种方式来共享的标志, C 提供一种整型数据类型sig_atomic_t, 对它的读和写保证会是原子的(不可中断的),因为可以用一条指令来实现它们:volatile sig_atomic_t flag;
因为它们是不可中断的, 所以可以安全地读和写该变量, 而不需要暂时阻塞信号。注意, 这里对原子性的保证只适用于单个的读和写, 不适用于像flag++或flag=flag+10 这样的更新, 它们可能需要多条指令。
2. 正确的信号处理
信号是不排队的,当pending位向量中某个类型的信号已经在处理中,后面到的信号就会被简单丢弃。所以如果出现了一个信号的时候,就证明至少有一个信号到达了,而不是只有一个信号到达了