C 库函数 – sigaction()(超详细)

C 库函数 – sigaction() 的深度解析:掌握信号处理的核心机制

在编写系统级程序或需要对异常行为做出响应的 C 程序时,信号(Signal)是一个无法绕开的概念。信号是操作系统向进程发送的一种异步通知机制,用于告知程序发生了某些事件,比如用户按下 Ctrl + C、除零错误、子进程结束等。而 sigaction() 就是 C 语言中用于精确控制信号处理行为的核心函数之一。

相比更古老的 signal() 函数,sigaction() 提供了更强大、更灵活的接口,尤其适合对信号处理行为有严格要求的场景。本文将带你从零开始理解 sigaction() 的工作原理,并通过真实代码示例逐步掌握其使用方法。


为什么需要 sigaction()?

在早期的 Unix 系统中,signal() 是唯一可用的信号处理函数。但它存在一些设计缺陷,例如:

  • 信号处理函数在被调用后,系统会自动重置为默认行为(即 SIG_DFL),这可能导致某些信号被意外忽略。
  • 在多线程环境下,signal() 的行为不可预测。
  • 无法精细控制信号的阻塞行为或处理上下文。

这就像你家的门铃系统:如果每次有人按铃,门铃就自动“重置”成默认声音,那你就无法自定义铃声,也无法在铃声响起时防止其他干扰。sigaction() 正是为了解决这类问题而生,它让你可以“定制门铃行为”,并且保证在每次响铃时都按你的设定来执行。


sigaction() 函数原型与参数详解

sigaction() 的函数原型如下:

#include <signal.h>

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

我们逐个分析参数:

  • signum:要设置处理方式的信号编号,如 SIGINT(对应 Ctrl + C)、SIGTERM(终止信号)、SIGUSR1(用户自定义信号)等。
  • act:指向一个 struct sigaction 结构体的指针,用于定义新信号处理行为。
  • oldact:可选参数,用于保存原来对该信号的处理方式。如果不需要保留旧设置,可传入 NULL

struct sigaction 结构体包含多个字段,核心部分如下:

struct sigaction {
    void (*sa_handler)(int);        // 处理函数指针
    sigset_t sa_mask;               // 临时阻塞的信号集
    int sa_flags;                   // 标志位,控制行为细节
    void (*sa_restorer)(void);      // 保留字段,通常不用
};

⚠️ 注意:sa_handler 不能设为 SIG_DFLSIG_IGN,如果要使用默认或忽略行为,应直接传入对应值。


实际案例:捕获 Ctrl + C 信号

让我们写一个简单的程序,当用户按下 Ctrl + C 时,程序不立即退出,而是打印一条提示信息。

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

// 信号处理函数:当收到 SIGINT 时被调用
void sigint_handler(int signum) {
    printf("\n⚠️  捕获到信号 SIGINT (Ctrl + C)!程序正在优雅退出...\n");
    // 可在此处执行清理操作,如关闭文件、释放内存等
    _exit(0); // 立即终止进程,避免无限循环
}

int main() {
    struct sigaction sa;

    // 初始化 sigaction 结构体
    sa.sa_handler = sigint_handler;  // 设置处理函数
    sigemptyset(&sa.sa_mask);        // 清空阻塞信号集
    sa.sa_flags = 0;                 // 不设置特殊标志

    // 注册信号处理:SIGINT = Ctrl + C
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction failed");
        return 1;
    }

    printf("📌 程序已启动,按 Ctrl + C 可触发自定义处理逻辑。\n");
    printf("⏳ 程序将持续运行,直到收到信号...\n");

    // 无限循环,等待信号
    while (1) {
        sleep(1); // 每秒打印一次,模拟工作
        printf("⏰ 运行中... %d 秒\n", (int)time(NULL));
    }

    return 0;
}

代码解析:

  • sigint_handler 是一个普通函数,接收信号编号作为参数。虽然我们只关心 SIGINT,但函数签名必须与 void (*)(int) 一致。
  • sigemptyset(&sa.sa_mask) 清空信号集,表示在处理信号期间不额外阻塞其他信号。
  • sa.sa_flags = 0 表示使用默认行为。后续我们会介绍如何用标志位增强功能。
  • sigaction(SIGINT, &sa, NULL) 成功注册后,程序将不再默认退出,而是调用我们的自定义函数。

运行此程序后,按 Ctrl + C,你会看到:

⚠️  捕获到信号 SIGINT (Ctrl + C)!程序正在优雅退出...

然后程序退出。这比直接终止更可控。


高级特性:使用 sa_flags 控制行为

sa_flagssigaction() 中最强大的部分。通过设置不同的标志,你可以改变信号处理的执行方式。

标志名 作用说明
SA_RESTART 使被信号中断的系统调用(如 read()write())自动重启,避免失败
SA_NOCLDSTOP 当子进程停止时,不发送 SIGCHLD 信号
SA_SIGINFO 使用带信息的信号处理函数(void (*sa_sigaction)(int, siginfo_t *, void *)

举个例子,使用 SA_RESTART 修复被中断的 read() 调用:

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

void sigint_handler(int signum, siginfo_t *info, void *context) {
    printf("\n💡 收到信号 %d,来源进程 PID: %d\n", signum, info->si_pid);
}

int main() {
    struct sigaction sa;

    sa.sa_sigaction = sigint_handler;  // 使用带信息的处理函数
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO;          // 启用信息模式

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

    printf("📌 启动成功,等待 Ctrl + C...\n");

    // 模拟阻塞式读取
    char buffer[128];
    ssize_t n = read(0, buffer, sizeof(buffer) - 1);
    if (n == -1) {
        perror("read failed");
    } else {
        buffer[n] = '\0';
        printf("✅ 读取到输入:%s\n", buffer);
    }

    return 0;
}

✅ 提示:当使用 SA_SIGINFO 时,必须将 sa_handler 改为 sa_sigaction,并传入三个参数。


信号屏蔽与 sa_mask 的妙用

sa_mask 允许你在处理某个信号时,临时屏蔽其他信号。这在避免信号嵌套或竞态条件时非常有用。

例如,如果你正在处理 SIGUSR1,但不希望此时被 SIGINT 打断,可以这样做:

struct sigaction sa;

sa.sa_handler = my_handler;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGINT);  // 处理 SIGUSR1 时屏蔽 SIGINT
sa.sa_flags = 0;

sigaction(SIGUSR1, &sa, NULL);

这相当于“锁门”:当处理 SIGUSR1 时,SIGINT 被暂时“锁住”,不会打断当前逻辑。


与 signal() 的对比:为何推荐使用 sigaction()

特性 signal() sigaction()
是否可重置 是(默认重置) 否(可保持)
是否支持信号屏蔽 有限 支持 sa_mask
是否支持信息传递 支持 SA_SIGINFO
行为是否可预测 不稳定 稳定、可移植

✅ 建议:在任何新项目中,优先使用 sigaction(),除非你明确知道 signal() 足够满足需求。


总结与最佳实践

C 库函数 – sigaction() 是信号处理的“标准工具箱”。它不仅功能强大,而且行为可预测,是编写健壮系统程序的基石。

  • sigaction() 替代 signal(),提升代码可靠性。
  • 通过 sa_mask 控制信号阻塞,防止嵌套中断。
  • 使用 SA_SIGINFO 获取更丰富的信号上下文。
  • 在信号处理函数中避免调用不可重入函数(如 printfmalloc)。

最后提醒:信号处理函数应当尽量简洁,只做必要的操作,例如设置标志位、写日志、发送通知等。复杂的逻辑应通过主程序轮询或事件机制处理。

掌握 sigaction(),你就真正迈入了 C 语言系统编程的大门。它不仅是一个函数,更是一种思维方式:让程序在不可预测的环境中,依然保持可控与优雅

继续深入学习,你将能构建出真正稳定、高效的后台服务、守护进程和实时系统。