24、信号进阶:sigaction()函数、信号集操作与sigprocmask()

信号处理,说白了就是进程和内核之间的一种“软中断”通信。上一章我们聊了 signal() 这个老接口,但说实话,它在实际项目中坑不少。我个人习惯是,只要涉及信号处理,直接上 sigaction()。为什么?因为它更可控、更安全,而且能解决很多 signal() 说不清道不明的问题。

这一章我们深入三个核心点:sigaction() 的用法、信号集的操作函数,以及如何用 sigprocmask() 屏蔽信号。嗯,这些都是你写健壮服务程序时绕不开的东西。

24.1 sigaction():更可靠的信号安装接口

sigaction() 是 POSIX 标准推荐的信号安装函数。它的原型长这样:

#include <signal.h>

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

参数不多,但每个都很关键:

  • signum:要捕获的信号编号,比如 SIGINT、SIGTERM。
  • act:新的信号处理方式,传入一个 struct sigaction 结构体。
  • oldact:如果不为 NULL,内核会把之前的处理方式填进去。方便你保存和恢复。

核心是这个结构体:

struct sigaction {
    void     (*sa_handler)(int);      // 信号处理函数
    void     (*sa_sigaction)(int, siginfo_t *, void *); // 带更多信息的处理函数
    sigset_t   sa_mask;               // 处理期间要屏蔽的信号集
    int        sa_flags;              // 控制行为标志
    void     (*sa_restorer)(void);    // 已废弃,不用管
};

这里有个容易混淆的点:sa_handlersa_sigaction 是共用体的,你只能用其中一个。如果你设了 SA_SIGINFO 标志,那就用 sa_sigaction,否则用 sa_handler

重点: sa_mask 字段指定了在信号处理函数执行期间,哪些信号会被自动屏蔽。这能防止信号处理函数被重入打断。我建议你至少把当前信号加进去,避免递归调用。

来看一个实际例子:

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

void handler(int sig) {
    printf("捕获到信号 %d\n", sig);
}

int main() {
    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);  // 先清空屏蔽集
    sa.sa_flags = 0;          // 默认行为

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

    while (1) {
        pause();  // 等待信号
    }
    return 0;
}

这段代码很简单,但已经体现了 sigaction() 的基本用法。你想想看,如果用 signal(),在某些系统上处理完一次信号后,处理函数会被重置为默认行为。而 sigaction() 不会,它会一直保持你设定的处理方式。

我的经验: 我在项目中遇到过一个问题——用 signal() 注册 SIGCHLD 处理函数,结果子进程退出时信号丢失了。后来换成 sigaction() 并设置 SA_NOCLDSTOP 标志,问题就解决了。所以,能用 sigaction() 就别用 signal()

24.2 信号集操作:五个基础函数

信号集(sigset_t)是一个位图,用来表示一组信号。你不能直接操作它的内部位,必须用标准函数。这五个函数是基础中的基础:

函数 作用 返回值
sigemptyset() 清空信号集,所有信号都不包含 成功返回0,失败返回-1
sigfillset() 填满信号集,包含所有信号 成功返回0,失败返回-1
sigaddset() 向信号集中添加一个信号 成功返回0,失败返回-1
sigdelset() 从信号集中删除一个信号 成功返回0,失败返回-1
sigismember() 检查某个信号是否在信号集中 在则返回1,不在返回0,出错返回-1

用法非常直观:

sigset_t set;

sigemptyset(&set);          // 清空
sigaddset(&set, SIGINT);   // 添加 SIGINT
sigaddset(&set, SIGTERM);  // 添加 SIGTERM

if (sigismember(&set, SIGINT)) {
    printf("SIGINT 在集合中\n");
}

sigdelset(&set, SIGINT);   // 删除 SIGINT

这里有个细节:sigemptyset()sigfillset() 必须在使用信号集之前调用。你不能假设一个未初始化的 sigset_t 是空的。我曾经见过有人忘了初始化,结果信号屏蔽行为完全不可预测——那调试起来真是头疼。

注意: 不同系统的 sigset_t 实现可能不同。有的用位数组,有的用结构体。所以一定要用标准函数操作,不要直接做位运算。否则移植性会出问题。

24.3 sigprocmask():屏蔽与解除屏蔽信号

sigprocmask() 用来检查或修改进程的信号屏蔽字。说白了,就是告诉内核:这段时间我不想被某些信号打扰。

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

how 参数有三种选择:

  • SIG_BLOCK:把 set 中的信号加到当前屏蔽集中。相当于“追加屏蔽”。
  • SIG_UNBLOCK:从当前屏蔽集中移除 set 中的信号。相当于“解除屏蔽”。
  • SIG_SETMASK:直接用 set 替换当前屏蔽集。相当于“覆盖”。

oldset 如果不为 NULL,会返回修改前的屏蔽集。方便你后续恢复。

来看一个典型场景:保护临界区代码。

sigset_t newmask, oldmask;

sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
sigaddset(&newmask, SIGTERM);

// 屏蔽 SIGINT 和 SIGTERM
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) == -1) {
    perror("sigprocmask");
    return 1;
}

// 临界区代码:这里不会被 SIGINT 或 SIGTERM 打断
printf("执行关键操作...\n");
sleep(5);

// 恢复原来的屏蔽集
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) == -1) {
    perror("sigprocmask");
    return 1;
}

printf("信号已恢复\n");

这段代码里,我先把 SIGINT 和 SIGTERM 屏蔽掉,等关键操作做完再恢复。为什么要恢复?因为你不恢复的话,后续代码可能一直屏蔽着这些信号,导致进程无法被正常终止。

关键点: 被屏蔽的信号不会丢失。它们会被内核“挂起”,一旦解除屏蔽,就会递送给进程。你可以用 sigpending() 函数查看当前有哪些信号被挂起。

24.4 知识体系总览

下面这张图帮你理清本章的核心脉络:

信号进阶 sigaction() 信号集操作 sigprocmask() sa_handler sa_mask sa_flags sigemptyset sigfillset sigaddset sigdelset sigismember SIG_BLOCK SIG_UNBLOCK SIG_SETMASK 核心目标:可靠地控制信号行为 安装 → 屏蔽 → 处理 → 恢复

24.5 避坑指南与实战建议

最后,分享几个我踩过的坑:

  • 不要在多线程中随意用 sigprocmask():在多线程程序里,sigprocmask() 的行为是未定义的。你应该用 pthread_sigmask()。这个坑我踩过一次,调试了两天才发现是线程信号屏蔽的问题。
  • 信号处理函数要尽量简单:只做最必要的事,比如设置一个全局标志。不要在里面调用 printf()malloc(),这些函数不是异步信号安全的。我见过有人因为信号处理函数里调了 printf(),导致死锁。
  • 记得保存和恢复信号屏蔽字:如果你在函数里修改了屏蔽字,退出前一定要恢复。否则调用者会莫名其妙地收不到信号。这是一个很隐蔽的 bug。
  • sigaction() 替代 signal():这已经说过很多次了,但值得再强调一遍。signal() 在不同系统上行为不一致,而 sigaction() 是 POSIX 标准,行为明确。

好了,这一章的内容就到这里。信号处理是系统编程里比较细腻的部分,多写多练,慢慢就能掌握其中的分寸。


公众号:蓝海资料掘金营,微信deep3321