信号处理

信号处理

1. 课程概述

本节课程聚焦于 Unix/Linux 系统中信号(signal)的处置(disposition)机制,重点讲解 POSIX 标准推荐的信号处理接口 sigaction。课程内容分为三部分:

  • sigaction 函数及其结构体 struct sigaction 的详细解析
  • 实例一:使用 sigaction 自定义 SIGINT(Ctrl+C)处理逻辑
  • 实例二:演示在信号处理器执行期间通过 sa_mask 屏蔽其他信号

后续还将简要介绍已被弃用但仍在广泛使用的旧接口 signal


2. sigaction 函数原型与参数详解

sigaction 是 POSIX 标准定义的、用于设置和获取信号处置方式的核心系统调用(或库封装)。其函数原型如下:

#include <signal.h>

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

参数说明:

  1. signum

    • 要操作的信号编号(如 SIGINT, SIGUSR1, SIGALRM 等)。
    • 不能用于 SIGKILLSIGSTOP(这两个信号无法被捕获或忽略)。
  2. act

    • 指向 struct sigaction 结构体的指针,用于设置新的信号处置方式
    • 若为 NULL,则不修改当前信号行为。
  3. oldact

    • 指向 struct sigaction 结构体的指针,用于保存旧的信号处置方式
    • 若不关心旧设置,可传入 NULL

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


3. struct sigaction 结构体详解

该结构体定义了信号的处置行为,主要包含以下成员(通常有 5 个,但最后一个为内部使用):

struct sigaction {
    void     (*sa_handler)(int);      // 信号处理函数指针(传统方式)
    void     (*sa_sigaction)(int, siginfo_t *, void *); // 扩展处理函数(POSIX RT)
    sigset_t sa_mask;                 // 在处理信号时要屏蔽的其他信号集合
    int      sa_flags;                // 控制信号行为的标志位
    void     (*sa_restorer)(void);    // 内部使用,应用程序不应设置(Linux 特有)
};

关键成员说明:

3.1 信号处理函数(Handler)

  • sa_handler:最常用的方式,函数签名为 void handler(int sig)
  • sa_sigaction:用于高级信号处理(需设置 SA_SIGINFO 标志),可获取更多信号信息(如发送者 PID)。
  • 二者互斥:只能设置其中一个。若同时设置,行为未定义(通常因共用体 union 实现而覆盖)。
  • 特殊值
    • SIG_DFL:恢复为默认行为(如终止、暂停等)。
    • SIG_IGN忽略信号(进程收到后无反应)。

默认行为示例:

  • SIGINT → 终止进程
  • SIGTSTP → 暂停进程(Ctrl+Z)
  • SIGCHLD → 忽略(某些系统)

3.2 sa_mask:信号屏蔽集

  • 类型:sigset_t(信号集)。
  • 作用:当信号处理函数正在执行时,自动将 sa_mask 中指定的信号加入进程的信号掩码(signal mask),防止这些信号在处理期间被递送。
  • 初始化必须使用 POSIX 标准函数(见下文),不能直接赋值。

3.3 sa_flags:行为控制标志

  • 常用值:0(表示不启用任何扩展功能)。
  • 其他标志(如 SA_RESTART, SA_SIGINFO)需查阅手册(man 2 sigaction)。
  • 若无需特殊行为,设为 0 即可。

3.4 sa_restorer(内部字段)

  • Linux 内核使用,应用程序不应访问或设置
  • 手册明确指出该字段“供 glibc 内部使用”。

4. 信号掩码(sigset_t)的正确初始化

为符合 POSIX 可移植性标准,必须使用以下函数初始化 sigset_t

sigemptyset(&act.sa_mask);  // 清空信号集(不屏蔽任何信号)
// 或
sigaddset(&act.sa_mask, SIGUSR2); // 添加特定信号到掩码

错误做法(虽在 Linux 可能工作,但不可移植):

act.sa_mask = 0; // 不符合 POSIX

5. 实例一:自定义 SIGINT 处理

目标

捕获 SIGINT(Ctrl+C),打印信号信息后退出,而非默认终止。

代码逻辑

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

void handle_sigint(int sig) {
    printf("Caught signal: %s\n", strsignal(sig)); // 使用 strsignal 获取信号名
    exit(0);
}

int main() {
    struct sigaction act;
    act.sa_handler = handle_sigint;
    sigemptyset(&act.sa_mask); // 不屏蔽其他信号
    act.sa_flags = 0;

    if (sigaction(SIGINT, &act, NULL) == -1) {
        perror("sigaction failed");
        exit(1);
    }

    // 主循环:持续休眠,等待信号
    while (1) {
        sleep(1); // 注意:sleep 可能被信号中断
    }
    return 0;
}

关键点

  • strsignal(sig):将信号编号转换为可读字符串(如 "Interrupt")。
  • sleep() 是系统调用封装,可能被信号中断(返回剩余秒数),因此需循环调用以确保长时间等待。

6. 实例二:信号处理期间屏蔽其他信号

目标

在处理 SIGINT 时,屏蔽 SIGUSR2,验证其被阻塞直至处理函数结束。

代码逻辑

void handle_sigint(int sig) {
    printf("Handling SIGINT... (PID: %d)\n", getpid());
    sleep(20); // 模拟长时间处理
    printf("Finished handling SIGINT.\n");
}

int main() {
    struct sigaction act;
    act.sa_handler = handle_sigint;
  
    // 初始化并添加 SIGUSR2 到掩码
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, SIGUSR2);
  
    act.sa_flags = 0;

    sigaction(SIGINT, &act, NULL);

    printf("PID: %d\n", getpid()); // 方便使用 kill 发送信号
    pause(); // 等待任意信号
}

实验步骤

  1. 运行程序,记录 PID。
  2. 按 Ctrl+C 触发 SIGINT,进入处理函数(开始 20 秒休眠)。
  3. 在另一终端执行 kill -USR2 <PID>
  4. 观察:SIGUSR2 不会立即触发,而是在 handle_sigint 返回后才被处理(若其处置为默认,则进程终止)。

补充说明

  • 信号继承:子进程继承父进程的信号处置方式。例如,若 shell(父进程)将 SIGUSR1/2 设为终止,则子进程也会如此。
  • 信号队列:标准信号不排队。多次发送同一信号,可能只处理一次。

7. 对比:已弃用的 signal 函数

尽管 sigaction 是推荐接口,但旧代码中常见 signal

#include <signal.h>
sighandler_t signal(int signum, sighandler_t handler);

特点

  • 简单:仅两个参数(信号 + 处理函数)。
  • 不安全/不可移植
    • 不同 Unix 系统对其语义实现不一致(如是否自动重启被中断的系统调用)。
    • 无法指定信号掩码或标志。
  • 返回值:返回之前的信号处置函数指针;出错时返回 SIG_ERR

示例

void timer_handler(int sig) {
    printf("Timer triggered!\n");
    alarm(2); // 重新设置 2 秒定时器
}

int main() {
    signal(SIGALRM, timer_handler);
    alarm(2); // 2 秒后发送 SIGALRM
    while(1) pause();
}

建议:始终优先使用 sigaction,仅在维护旧代码或简单脚本时考虑 signal


8. 总结

  • 核心接口sigaction 是现代 Unix 信号处理的标准方式。
  • 关键结构struct sigaction 中重点关注 sa_handlersa_mask
  • 可移植性:使用 sigemptyset/sigaddset 初始化信号集。
  • 调试工具:利用 strsignal() 打印信号名称,getpid() 获取进程 ID 便于测试。
  • 学习建议:熟练查阅 man 2 sigactionman 7 signal 获取权威文档。
信号机制 2026-03-10

评论区