跳转至

编写信号处理程序

  • 处理程序有几个属性使其难以推理分析:
    1. 处理程序与主程序并发运行, 共享同样的全局变量, 因此可能与主程序和其他处理程序互相干扰
    2. 如何以及合适接收信号的规则常常违反直觉
    3. 不同的系统有不同的信号处理语义

接下来介绍编写安全, 正确和可以直接的信号处理程序的一些基本规则

安全的信号处理

[!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);
}

可移植的信号处理