4. 信号集操作

信号集操作,说白了就是一组管理信号状态的工具函数。我刚开始学这部分时,觉得不就是几个API嘛,用的时候查手册就行。直到有一次线上服务莫名其妙丢信号,排查了整整两天,才发现是信号集没处理好。嗯,从那以后我再也不敢小看这几个函数了。

4.1 信号集是什么?

信号集(sigset_t)本质上是一个位图。每个信号对应一个比特位,1表示该信号在集合中,0表示不在。Linux支持64种信号,所以sigset_t至少需要64位。

我个人习惯把信号集想象成一个「信号清单」——你要屏蔽哪些信号,就把它加到清单里。就这么简单。

4.2 信号集的基本操作

先看一组最基础的函数,它们负责构建信号集本身:

#include <signal.h>

int sigemptyset(sigset_t *set);    // 清空信号集
int sigfillset(sigset_t *set);     // 填满信号集
int sigaddset(sigset_t *set, int signum);   // 添加信号
int sigdelset(sigset_t *set, int signum);   // 删除信号
int sigismember(const sigset_t *set, int signum); // 检查信号是否存在

这里有个坑,我踩过:sigemptyset和sigfillset是初始化函数,不是重置函数。如果你对一个已经用过的信号集调用sigaddset,必须先调用sigemptyset或sigfillset初始化,否则结果是未定义的。

警告: 永远不要直接对未初始化的sigset_t变量进行操作。我曾经见过有人声明了一个局部sigset_t变量就直接调用sigaddset,结果在不同编译器下行为完全不一样。

来看一个完整的例子:

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

int main() {
    sigset_t set;
    
    // 初始化空集
    sigemptyset(&set);
    
    // 添加SIGINT和SIGTERM
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGTERM);
    
    // 检查SIGINT是否在集合中
    if (sigismember(&set, SIGINT)) {
        printf("SIGINT is in the set\n");
    }
    
    // 删除SIGTERM
    sigdelset(&set, SIGTERM);
    
    // 填满所有信号
    sigfillset(&set);
    
    return 0;
}

4.3 sigprocmask:信号屏蔽的艺术

sigprocmask是信号集操作的核心。它告诉内核:当前进程要屏蔽哪些信号。被屏蔽的信号不会立即递送,而是变成「挂起」状态。

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

参数how有三种模式:

how值 含义 实际场景
SIG_BLOCK 将set中的信号添加到当前屏蔽集 临时屏蔽某些信号,保护临界区
SIG_UNBLOCK 从当前屏蔽集中移除set中的信号 解除屏蔽,让信号可以递送
SIG_SETMASK 用set完全替换当前屏蔽集 彻底改变信号屏蔽策略

我个人最常用的是SIG_BLOCK和SIG_UNBLOCK的组合。为什么?因为SIG_SETMASK太粗暴了,容易把之前设置的屏蔽信息覆盖掉。

技巧: 如果你只是想临时屏蔽信号,记得保存旧的屏蔽集(oldset参数),操作完再恢复。这是最安全的做法。

来看一个实际例子:

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

void handler(int sig) {
    printf("Caught signal %d\n", sig);
}

int main() {
    sigset_t newmask, oldmask;
    
    // 设置SIGINT的处理函数
    signal(SIGINT, handler);
    
    // 初始化信号集,添加SIGINT
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGINT);
    
    // 屏蔽SIGINT,保存旧的屏蔽集
    sigprocmask(SIG_BLOCK, &newmask, &oldmask);
    
    printf("SIGINT is blocked. Press Ctrl+C...\n");
    sleep(5);  // 这5秒内按Ctrl+C,信号会被挂起
    
    // 恢复旧的屏蔽集(解除SIGINT的屏蔽)
    sigprocmask(SIG_SETMASK, &oldmask, NULL);
    
    printf("SIGINT is unblocked now.\n");
    sleep(3);  // 之前挂起的SIGINT会在这里递送
    
    return 0;
}

运行这段代码,你在第一个sleep期间按Ctrl+C,信号不会立即处理,而是等到sigprocmask恢复后才递送。这就是信号屏蔽的典型用法。

4.4 sigpending:查看挂起的信号

sigpending函数告诉你:当前有哪些信号被屏蔽了,正在排队等待处理。

int sigpending(sigset_t *set);

这个函数特别适合调试。我曾经在排查一个信号丢失的问题时,就是靠sigpending发现信号其实已经到达了,只是被屏蔽了没处理。

来看一个完整的实战例子:

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

int main() {
    sigset_t newmask, oldmask, pending;
    
    // 屏蔽SIGQUIT
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGQUIT);
    sigprocmask(SIG_BLOCK, &newmask, &oldmask);
    
    // 给自己发送SIGQUIT
    kill(getpid(), SIGQUIT);
    
    // 检查挂起的信号
    sigpending(&pending);
    if (sigismember(&pending, SIGQUIT)) {
        printf("SIGQUIT is pending\n");
    }
    
    // 解除屏蔽,信号会被递送(默认行为是终止进程)
    sigprocmask(SIG_SETMASK, &oldmask, NULL);
    
    printf("This line may not be reached\n");
    return 0;
}

4.5 知识体系总览

下面这张图总结了信号集操作的核心流程:

信号集操作核心流程 构建信号集 sigemptyset() sigfillset() sigaddset() / sigdelset() sigismember() 设置信号屏蔽 sigprocmask() SIG_BLOCK / SIG_UNBLOCK SIG_SETMASK 检查挂起 sigpending() 关键流程说明: 1. 先用 sigemptyset/sigfillset 初始化信号集 2. 用 sigaddset/sigdelset 调整信号集内容 3. 用 sigprocmask 将信号集应用到进程 4. 用 sigpending 查看哪些信号被挂起 5. 解除屏蔽后,挂起的信号会被递送

4.6 实战中的避坑指南

我在项目中遇到过几个典型问题,分享出来供你参考:

  • 信号丢失问题:标准信号不会排队。如果同一个信号被屏蔽期间多次产生,解除屏蔽后只会递送一次。实时信号(SIGRTMIN+)可以排队,但数量有限。
  • 线程与信号:sigprocmask是进程级别的。在多线程程序中,应该用pthread_sigmask来控制每个线程的信号屏蔽。
  • 不可屏蔽信号SIGKILL和SIGSTOP不能被屏蔽,也不能被捕获。这是内核的硬性规定,别想着绕过。
核心要点: 信号集操作是信号处理的基础。先构建集合,再设置屏蔽,最后检查挂起——这个流程要形成肌肉记忆。我每次写信号处理代码,都会先画一遍这个流程图。

嗯,信号集操作就讲到这里。记住一句话:信号集是工具,sigprocmask是核心,sigpending是调试利器。把这几个函数用熟了,信号处理这块你就掌握了八成。