CSAPP第八章信号部分
信号基本含义
linux中信号就是一段消息,通知进程发生了某些事情,类比软件层面的异常(软中断)。
信号是一种软件形式的异常,允许进程和内核中断其他进程,可以用来通知用户进程发生了某些异常
Linux系统基本信号及其行为
常见的信号有:
- SIGINT(利用ctrl+c键发送给前台组的每个进程,默认是终止进程)
- SIGCHLD(子进程终止时,内核会发送这个信号给父进程)
信号术语
发送信号:内核更新进程的某个上下文;
接收信号:一个进程可以忽略一个信号,或者执行一段处理程序。
内核为每个进程维护了两个位向量,一个用来标记待处理信号pending,一个标记被阻塞信号blocked。只要传送了一个类型为k的信号,那么进程的pending的第k位就会设置为1。
这个实现有什么问题?一个信号没有被及时处理,那么后面几个信号就会丢失。对一个进程而言,同一时刻,同种类型的信号最多只能有一个处于待处理状态,多余的会被直接抛弃。
发送信号
在了解发送信号的方式前,需要了解两个概念:进程组和前后台作业
进程组
每个进程都属于且只属于一个进程组,一个进程组由一个正整数标识。
前后台作业
作业这个概念其实就是进程。不过作业是加了限定词的进程:unix shell用作业表示对一条命令行求值而创建的进程。
在任何时刻,至多有一个前台作业和0至多个后台作业。
发送信号的方式
(1)/bin/kill程序
linux> /bin/kill -9 15213
表示发送信号9(SIGKILL)给进程24818
linux> /bin/kill -9 -15213
表示发送信号9(SIGKILL)给进程组24818
(2)键盘发送
Crtl+C会导致内核发送一个SIGINT信号到前台进程组中的每一个进程。
(3)标准库接口
可以在函数中调用kill
函数来对目的进程发送信号
1 |
|
用alarm
函数发送SIGALARM
信号
1 |
|
接收信号
接收信号:当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了信号。
接收信号的时机:内核把进程从内核模式切换到用户模式时(例如系统调用返回),它会检查进程的未被阻塞的待处理信号的集合(pending & ~blocked),如果集合非空,那么就会选择某个信号k,强制进程处理信号k。
接收信号的默认行为:
- 进程终止
- 忽略该信号
- 通过用户层函数信号处理程序捕获这个信号
修改信号的默认行为
1 |
|
signum为信号序号,handler为信号处理程序。我们可以使用自定义的信号处理程序,也可以使用预定义的几个处理程序(SIG_IGN
进程忽略该信号、SIG_DFL
恢复该信号的默认行为)。
用户自定义的信号处理程序要遵从sighandler_t的信号处理原型,接收一个整数入参,无出参。
例子:
1 |
|
这里只要在main
函数开始调用一次signal
,就相当于从此以后改变了SIGCHLD
信号的默认行为,让它去执行handler
处理程序。当子进程终止或停止时,内核就会发送一个SIGCHLD
信号到父进程中,此时就能让父进程去执行自己的工作,当子进程终止或停止时,发送SIGCHLD
信号到父进程,则父进程会调用handler
函数来对该子进程进行回收。
阻塞信号
Linux提供阻塞信号的隐式和显示的机制:
- 隐式阻塞机制:内核默认阻塞当前正在处理信号类型的待处理信号。
- 显示阻塞机制:应用程序通过
sigprocmask
函数来显示阻塞和解阻塞选定的信号。
1 |
|
通过how
来决定如何改变阻塞的信号集合blocked
:
当
how=SIG_BLOCK
时,blocked = blocked | set
当
how=SIG_UNBLOCK
时,blocked = blocked & ~set
当
how=SETMASK
时,block = set
编写健壮的信号处理程序
如何编写安全、正确和可移植的信号处理程序?
并发认知
信号也是并发的一个例子,信号处理程序是一个独立的逻辑流(不是进程),与主程序并发运行。比如我们在进程A中执行一个while
循环,当该进程受到一个信号时,内核会将控制权转移给该信号的处理程序,所以该信号处理程序是并发执行的,当信号处理程序结束时,再将控制转移给主程序。由于信号处理程序与主程序在同一进程中,所以具有相同的上下文,所以会共享程序中的所有全局变量。
原则
- 处理程序尽可能简单
- 处理程序使用异步信号安全的函数
所谓异步信号安全,要么可重入,要么不可中断。
- 保存和恢复errno
- 暂时阻塞所有信号,保护对全局数据结构的访问
- volatile声明全局变量
- sig_atomic_t 声明全局标志,确保原子操作
正确的信号处理
信号位向量的特性导致同类型信号丢弃现象
处理方法:一次信号处理函数调用尽可能多的处理信号
可移植的信号处理
不同的系统有不同的信号处理语义:
signal
函数的语义各有不同。- 系统调用可以被中断。
要解决这些问题,定义了sigaction
函数,它允许用户在设置信号处理时,明确指定他们想要的信号处理语义。
1 | int sigaction(int signum, struct sigaction *act, struct sigaction *oldact) |
信号同步
案例一:父子进程竞争
场景:子进程结束出发SIGCHLD信号,导致信号处理函数先运行,造成同步错误。
解决方法:在fork之前,阻塞SIGCHLD信号,等到某个特定位置运行完,再解锁信号。
启发:如果想要确保某些code(比如addjob)能在信号handler之前被运行,那么就阻塞该信号,直到该code执行完毕。
缺点:子进程需要额外解锁该信号。
案例二:显式等待某信号
场景:父进程等待子进程执行完毕。
1 | while(!pid) |
浪费CPU
1 | while(!pid) |
死锁风险:在while后收到信号,然后pause,永远收不到信号,无法返回。
1 | while(!pid) |
sleep无法确定睡眠间隔
最佳实践:
1 | while(!pid) |
解读sigsuspend:
- 从语义上看,表示对某信号延迟处理,等待一段时间
- 从代码上来看,
sigsuspend(&mask)
等价于原子版本的以下三句组合:
1 | sigprocmask(SIG_SETMASK, &mask, &prev); |
其中原子性是第一句和第二句,这两者之间的执行不会被中断。
实际使用
在调用sigsuspend前阻塞SIGCHLD信号
在调用sigsuspend时取消阻塞SIGCHLD信号(利用mask),那么SIGCHLD信号都会被等到pause执行完处理,然后又恢复阻塞SIGCHLD信号。