编写信号处理程序
- 处理程序有几个属性使其难以推理分析:
- 处理程序与主程序并发运行, 共享同样的全局变量, 因此可能与主程序和其他处理程序互相干扰
- 如何以及合适接收信号的规则常常违反直觉
- 不同的系统有不同的信号处理语义
接下来介绍编写安全, 正确和可以直接的信号处理程序的一些基本规则
安全的信号处理
[!attention] 麻烦的信号处理程序 信号处理程序与其主程序以及其他的信号处理程序并发地运行 如果处理程序和主程序并发地访问同样的全局数据结构, 那么结果可能是不可预知的
- 一些保守的编写处理程序的原则:
- 处理程序要尽可能简单: 保持处理程序尽可能小和简单
- 在处理程序中只调用异步信号安全的函数
- 可重入的(只访问局部变量)
- 不能被信号处理程序中断
- 保护和恢复
errno
: 许多Linux异步信号安全的函数都会在出错返回时设置errno
- 在处理程序中调用这样的函数可能会干扰主程序中其他依赖于
errno
的部分, 因此, 在进入处理程序时, 把errno
保存在一个局部变量中, 在处理程序返回前恢复他
- 在处理程序中调用这样的函数可能会干扰主程序中其他依赖于
- 阻塞所有信号, 保护对共享全局数据结构的访问
- 原因: 从主程序访问一个数据结构
d
通常需要一系列的指令, 如果指令序列被访问d
的处理程序中断, 那么处理程序可能会发现d
的状态不一致, 导致不可预知的结果. 因此, 在访问d
时暂时阻塞信号保证处理程序不会中断该指令序列
- 原因: 从主程序访问一个数据结构
- 用
volatile
声明全局变量: 使用volatile
高速编译器不要缓存这个变量volatile int g
:volatile
限定符强迫编译器每次在代码中引用g
时, 都要从内存中读取g
的值- 但一般来说, 和其他所有共享数据结构一样, 应该暂时阻塞信号, 保护每次对全局变量的访问
- 使用
sig_atomic_t
声明标志volatile sig_atomic_t flag
:- 在常见的处理程序设计中, 处理程序会写全局标志来记录收到了信号. 主程序周期性地读取这个标志, 相应信号, 再清楚该标志
- 对于
sig_atomic_t
, 对它的读和写保证会是原子的 - 因为其实不可中断的, 所以可以安全的读写, 而不需要暂时阻塞信号
正确的信号处理
- 未处理的信号是不进行排队的
- 因为
pending
位向量中每种类型的信号只对应有一位, 所以每种类型最多只能有一个未处理的信号 - 多余的信号会被直接丢弃
- 因为
signal1
- 基本结构:
- 父进程创建一些子进程, 这些子进程各自独立运行一段时间, 然后终止
- 父进程必须回收子进程以避免在系统中留下僵死进程
- 同时, 希望父进程能够在子进程运行时, 自由的去做其他工作, 因此, 使用
SIGCHLD
处理程序来回收子进程, 而不是显示地等待子进程终止.- 只要有一个子进程终止或停止, 内核就回发送一个
SIGCHLD
信号给父进程#include <stdio.h> #include <signal.h> #include <unistd.h> #include <errno.h> #include <stdlib.h> #include "csapp.h" void handler1(int sig){ int olderrno = errno; if ((waitpid(-1, NULL, 0)) < 0) Sio_error("waitpid error"); Sio_puts("Handler reaped child\n"); Sleep(1); errno = olderrno; } int main(){ int i, n; char buf[MAXBUF]; if (signal(SIGCHLD, handler1) == SIG_ERR) unix_error("signal error"); for (i = 0; i < 3; i++){ if (fork() == 0){ printf("Hello from child %d\n", (int)getpid()); exit(0); } } if ((n = read(STDIN_FILENO, buf, sizeof(buf))) < 0) unix_error("read"); printf("Parent processing input\n"); while(1); exit(0); }
- 只要有一个子进程终止或停止, 内核就回发送一个
- 会发生什么?
- 父进程接收并捕获了第一个信号. 当处理程序还在处理第一个信号时候, 第二个信号就传送并添加到了待处理信号集合中.
- 然而, 因为
SIGCHLD
信号被SIGCHLD
处理程序阻塞了, 所以第二个信号就不会被接收. - 当处理程序还在处理第一个信号时候, 第三个信号到达了, 由于已经有了一个待处理的
SIGCHLD
, 第三个SIGCHLD
信号被丢弃 - 一段时间后, 处理程序返回, 进行第二次处理程序的执行
- 处理程序完成对第二个信号的处理后, 已经没有待处理的
SIGCHLD
信号了
- 教训: 不可以使用信号来对其他进程中发生的事件计数
signal2
- 在回收时候, 应该回收尽可能多的僵死子进程
void handler2(int sig){ int olderrno = errno; whlie(waitpid(-1, NULL, 0) > 0){ Sio_puts("Handler reaped child\n"); } if (errno != ECHILD) Sio_error("waitpid error"); Sleep(1); errno = olderrno; }
[!note] waitpid 返回值: waitpid(): on success, returns the process ID of the child whose state has changed; if WNOHANG was specified and one or more child(ren) specified by pid exist, but have not yet changed state, then 0 is returned. On error, -1 is returned.
waitpid()
: - 成功时: 返回结束的子进程 - 错误时: -1, 并且设置errno
- WNOHANG: [[OS/进程#^164805|WNOHANG]]
signalprob0
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include "csapp.h"
volatile long counter = 2;
// this program prints 2 1 3
void handler1(int sig){
sigset_t mask, prev_mask;
Sigfillset(&mask); // add all signals to mask
Sigprocmask(SIG_BLOCK, &mask, &prev_mask); // Block sigs, and save previous mask in `prev_mask`
Sio_putl(--counter); // 1
Sigprocmask(SIG_SETMASK, &prev_mask, NULL); // Restore sigs
_exit(0);
}
int main(){
pid_t pid;
sigset_t mask, prev_mask;
printf("%ld", counter); //2
fflush(stdout);
signal(SIGUSR1, handler1);
if ((pid = Fork()) == 0){ // if child process, infinite loop until a signal
while(1) ;
}
Kill(pid, SIGUSR1); // send a signal to child process
Waitpid(-1, NULL, 0); // wait for child process to terminate
Sigfillset(&mask);
Sigprocmask(SIG_BLOCK, &mask, &prev_mask); // Block sigs
printf("%ld", ++counter); // 3
Sigprocmask(SIG_SETMASK, &prev_mask, NULL); // Restore sigs
exit(0);
}