Linux 信号捕捉

Linux 信号捕捉

七月 23, 2025 次阅读

信号捕捉流程

信号的捕捉流程图如下:

signal_capture

信号捕捉是指当进程收到信号时,内核将控制流转交给用户自定义的信号处理函数,并在处理完成后恢复原始执行流程的过程。整个过程涉及多次用户态↔内核态切换,并确保信号不会丢失。以下是详细流程:


1. 主程序在用户态执行(如 main 函数)

  • 进程正常执行用户代码(如 main 函数的指令)。
  • 可能被中断:由于硬件中断(如时钟中断)、异常(如缺页异常)或系统调用,CPU 从用户态切换到内核态。

2. 进入内核态处理中断/异常

  • 内核完成中断或异常的处理(如系统调用执行完毕)。
  • 在返回用户态前,检查待处理信号(do_signal
    • 如果没有待处理信号,直接返回用户态,恢复 main 的执行。
    • 如果发现待处理信号(如 SIGQUIT),且该信号的处理方式是用户自定义函数,进入信号递送流程。

3. 内核准备执行信号处理函数

  • 修改用户态返回地址
    • 内核不会直接返回到 main,而是修改栈和寄存器,使得 CPU 返回用户态时跳转到信号处理函数(如 sighandler
    • 信号处理函数运行在独立的栈帧(避免破坏 main 的栈)。
  • 记录原始上下文
    • 内核保存 main 的完整执行现场(寄存器、栈指针等),以便后续恢复。

4. 信号处理函数在用户态执行

  • 信号处理函数(如 sighandler)开始执行,完成用户定义的逻辑(如打印日志、清理资源等)。
  • 信号处理函数执行完毕后,不会直接返回 main,而是自动调用 sigreturn 系统调用,再次进入内核态

5. sigreturn:内核恢复主程序上下文

  • sigreturn 的作用
    • 告诉内核“信号处理已完成”,并恢复之前保存的 main 的上下文
    • 但在此之前,内核会再次检查是否有新的待处理信号(防止信号丢失)。
      • 如果没有新信号:直接恢复 main 的执行现场,返回用户态继续执行。
      • 如果有新信号:重复信号递送流程(可能再次进入另一个信号处理函数)。

6. 最终返回主程序继续执行

  • 如果没有更多信号需要处理,内核恢复 main 的寄存器、栈指针等,并返回到用户态。
  • 主程序从被中断的位置继续执行,就像信号从未发生过一样(除非信号处理函数修改了全局状态)。

总结

  1. 信号是异步的,内核在每次返回用户态前(无论是从中断、异常还是 sigreturn)都会检查信号。
  2. 信号处理函数和主程序是独立的控制流,它们使用不同的栈,没有直接的调用关系。
  3. sigreturn 是信号处理的关键
  • 它不仅是恢复现场,还会再次检查信号,确保不会遗漏新到达的信号。
  1. 递归信号处理
  • 如果信号处理函数执行期间又收到同一信号,可能会导致递归调用(除非使用 sigaction 阻塞该信号)。

sigaction 函数

sigaction 是比 signal 更强大、更灵活的信号处理函数,它允许精确控制信号的处理行为,包括:

  • 设置信号处理函数(sa_handlersa_sigaction
  • 指定在执行信号处理函数时自动阻塞哪些信号sa_mask
  • 配置额外的信号处理选项(sa_flags

函数原型

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数说明

参数 说明
signum 要操作的信号编号(如 SIGINTSIGTERM
act 指向 struct sigaction,用于设置新的信号处理方式(若为 NULL,则仅读取旧设置)
oldact 用于保存旧的信号处理方式(若为 NULL,则不保存)

返回值

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno

struct sigaction 结构体

struct sigaction {
    void (*sa_handler)(int);                  // 普通信号处理函数
    void (*sa_sigaction)(int, siginfo_t *, void *);  // 实时信号处理函数
    sigset_t sa_mask;                         // 额外阻塞的信号集
    int sa_flags;                             // 控制信号行为的标志位
    void (*sa_restorer)(void);                // 已废弃,通常置 NULL
};

关键字段说明

  1. sa_handler

    • 指定信号处理函数,可以是:
      • SIG_IGN(忽略信号)
      • SIG_DFL(恢复默认行为)
      • 自定义函数(格式:void handler(int signo)
    • 例如:
      void handler(int sig) {
          printf("Received signal %d\n", sig);
      }
      struct sigaction act;
      act.sa_handler = handler;  // 设置自定义处理函数
  2. sa_sigaction

    • 用于实时信号(如 SIGRTMIN 以后的信号),提供更多信息(siginfo_t)。
    • 需配合 sa_flags = SA_SIGINFO 使用。
  3. sa_mask

    • 在执行信号处理函数时,自动阻塞的信号集(防止信号嵌套)。

    • 默认阻塞当前信号(无需手动添加)。

    • 例如:

      sigemptyset(&act.sa_mask);  // 清空信号集
      sigaddset(&act.sa_mask, SIGQUIT);  // 阻塞 SIGQUIT
  4. sa_flags

    • 控制信号行为的标志位,常见选项:

      • SA_RESTART:被信号中断的系统调用自动重启。
      • SA_NOCLDSTOP:仅对 SIGCHLD 有效,子进程停止时不通知父进程。
      • SA_SIGINFO:使用 sa_sigaction 而非 sa_handler
    • 例如:

      act.sa_flags = SA_RESTART;  // 系统调用被中断后自动恢复

signal 函数的对比

特性 signal sigaction
可移植性 不同系统行为可能不同 行为一致
信号屏蔽 无法指定额外阻塞的信号 支持 sa_mask
实时信号 不支持 支持(sa_sigaction
系统调用重启 依赖实现 可通过 SA_RESTART 控制

信号捕捉底层细节

在信号捕捉的底层实现中,内核会在调用信号处理函数之前(而非之后)将 pending 表中对应信号的位置清零(即 pending &= ~(1 << signo))。这一设计是为了正确处理信号的重复触发,同时结合**信号屏蔽字(blocking mask)**的自动管理机制。以下是详细解释:


步骤 1:信号到达,pending 位置 1

  • 当信号(如 SIGINT)到达时,内核会:
    1. 检查该信号是否被阻塞(通过 blocking mask):
      • 如果未被阻塞,准备递送。
      • 如果被阻塞,仅将 pending 对应位置 1,暂不处理。
    2. 若信号未被阻塞,内核将其加入待处理队列。

步骤 2:准备调用信号处理函数

  • 从内核态返回用户态执行信号处理函数前,内核会:
    1. 清零 pending 表中的对应位(表示该信号正在被处理)。
    2. 自动阻塞该信号(将 blocking mask 中对应位置 1),防止嵌套调用。

步骤 3:执行信号处理函数

  • 信号处理函数(如 handler)在用户态执行:
    • 如果在此期间同一信号再次到达
      • 由于该信号已被阻塞(blocking mask1),内核不会立即递送。
      • 但会将 pending 表中对应位重新置 1(记录信号到达)。

步骤 4:信号处理函数返回

  • 函数返回时,内核通过 sigreturn 系统调用恢复现场:
    1. 恢复原来的 blocking mask(取消对该信号的阻塞)。
    2. 再次检查 pending
      • 如果发现该信号位为 1(表示在处理期间信号又到达了),则重新递送
      • 否则,正常恢复主程序执行。

3. 为什么要在调用处理函数前清零 pending?

原因 1:避免信号丢失

  • 如果在处理函数执行后才清零 pending,可能会出现:
    • 信号 A 正在处理时,同一信号 A 再次到达。
    • 如果第一次的 pending 未被清零,第二次到达的信号可能被忽略(内核认为“已在处理”)。
  • 提前清零确保后续到达的信号能正确记录在 pending 中。

原因 2:与阻塞机制配合

  • 内核通过自动阻塞当前信号 + 提前清零 pending,实现以下行为:
    • 信号处理期间,同一信号不会被嵌套调用(避免递归)。
    • 但后续到达的信号会被记录,并在处理完成后重新递送。

示例场景

假设进程收到两次 SIGINT

  1. 第一次 SIGINT
    • pending1 → 内核调用处理函数前清零 pending 并阻塞 SIGINT
  2. 处理函数执行期间,第二次 SIGINT 到达:
    • 由于 SIGINT 被阻塞,内核仅将 pending1,不立即处理。
  3. 处理函数返回后:
    • 内核发现 pending1,重新递送 SIGINT,再次触发处理函数。

下面是验证示例:

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstring>
#include <sys/wait.h>

using namespace std;

// 打印当前进程的未决信号集(pending signals)
void print_pending_map()
{
    sigset_t sigset;
    sigpending(&sigset);  // 获取当前未决信号集
    
    // 从31到1遍历所有可能的信号
    for(int i = 31; i >= 1; --i)
    {
        if(sigismember(&sigset, i)) 
            cout << 1 << " ";  // 该信号处于未决状态
        else 
            cout << 0 << " ";   // 该信号未产生
    }
    cout << endl << endl;
}

// 信号处理函数
void myhandler(int sign)
{
    // 打印收到的信号信息
    cout << "signal " << strsignal(sign) << " begin" << endl;
    
    // 无限循环打印信号状态
    while(true)
    {
        print_pending_map();  // 打印当前未决信号集
        cout << "Processing signal: " << strsignal(sign) << endl;
        sleep(1);  // 每秒打印一次
    }
}

int main()
{
    // 设置SIGINT的信号处理
    struct sigaction act, oact;
    sigemptyset(&act.sa_mask);  // 初始化信号屏蔽字
    sigemptyset(&oact.sa_mask);
    
    // 关键设置:在执行SIGINT处理函数时阻塞SIGQUIT
    sigaddset(&act.sa_mask, SIGQUIT);  // 将SIGQUIT加入阻塞集
    
    act.sa_handler = myhandler;  // 设置信号处理函数
    sigaction(SIGINT, &act, &oact);  // 注册SIGINT的处理方式
    
    // 为SIGQUIT设置相同的处理函数(用于演示)
    signal(SIGQUIT, myhandler);  // 注意:这里使用signal而不是sigaction

    // 创建子进程
    pid_t ret = fork();
    if(ret == 0)  // 子进程代码
    {
        // 子进程无限循环打印自己的PID
        while(true)
        {
            sleep(1);
            cout << "Child process " << getpid() << " running..." << endl;
        }
    }

    // 父进程代码
    sleep(2);  // 等待2秒让子进程启动
    
    // 第一次向子进程发送SIGINT
    cout << "Parent sending SIGINT to child process" << endl;
    kill(ret, SIGINT);  // 发送SIGINT信号
    sleep(2);  // 等待2秒
    
    // 第二次向子进程发送SIGINT
    cout << "Parent sending SIGINT to child process again" << endl;
    kill(ret, SIGINT);
    sleep(2);  // 等待2秒
    
    // 向子进程发送SIGQUIT
    cout << "Parent sending SIGQUIT to child process" << endl;
    kill(ret, SIGQUIT);  // 发送SIGQUIT信号
    sleep(2);  // 等待2秒
    
    // 向子进程发送SIGKILL(无法被捕获或忽略)
    cout << "Parent sending SIGKILL to child process" << endl;
    kill(ret, SIGKILL);  // 发送SIGKILL信号强制终止子进程
    
    wait(nullptr);  // 等待子进程结束
    return 0;
}
代码设计目标

本实验通过父子进程模型,演示以下Linux信号处理核心机制:

  • 信号处理函数的注册方式差异(signal vs sigaction
  • 信号阻塞掩码(sa_mask)的实际效果
  • 未决信号集(pending signals)的动态变化
  • 不可阻塞信号(SIGKILL)的特殊性
核心组件说明

信号监控函数

void print_pending_map() {
    sigset_t sigset;
    sigpending(&sigset);  // 获取当前未决信号集
    
    for(int i = 31; i >= 1; --i) {
        cout << (sigismember(&sigset, i) ? 1 : 0) << " ";
    }
    cout << endl;
}

功能:实时打印信号未决状态,每个bit代表对应信号是否处于未决状态。

信号处理函数

void myhandler(int sign) {
    cout << "Handler executing for: " << strsignal(sign) << endl;
    while(true) {  // 用于保持处理状态便于观察
        print_pending_map();
        sleep(1);
    }
}

设计意图:通过无限循环模拟长时间信号处理,便于观察信号阻塞效果。

关键配置
// 配置SIGINT处理
struct sigaction act;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT);  // 关键设置!
act.sa_handler = myhandler;
sigaction(SIGINT, &act, NULL);

// 配置SIGQUIT处理(对比组)
signal(SIGQUIT, myhandler);

核心设置:当处理SIGINT时,自动阻塞SIGQUIT信号。

实验流程
sequenceDiagram
    participant Parent
    participant Child
    Parent->>Child: fork()
    loop 子进程运行
        Child->>Child: 打印运行状态
    end
    Parent->>Child: kill(SIGINT)
    Child->>Child: 执行myhandler
    Parent->>Child: kill(SIGINT) 
    Parent->>Child: kill(SIGQUIT)
    Note right of Child: SIGQUIT被阻塞
保持pending状态 Parent->>Child: kill(SIGKILL)

结果输出如下:

╭─ljx@VM-16-15-debian ~/linux_review/sign
╰─➤  ./signal_cap.o
Child process 703969 running...
Parent sending SIGINT to child process
signal Interrupt begin
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Processing signal: Interrupt
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Processing signal: Interrupt
Parent sending SIGINT to child process again
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0

Processing signal: Interrupt
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0

Processing signal: Interrupt
Parent sending SIGQUIT to child process
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0

Processing signal: Interrupt
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0

Processing signal: Interrupt
Parent sending SIGKILL to child process