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初始化,否则结果是未定义的。
来看一个完整的例子:
#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太粗暴了,容易把之前设置的屏蔽信息覆盖掉。
来看一个实际例子:
#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 知识体系总览
下面这张图总结了信号集操作的核心流程:
4.6 实战中的避坑指南
我在项目中遇到过几个典型问题,分享出来供你参考:
- 信号丢失问题:标准信号不会排队。如果同一个信号被屏蔽期间多次产生,解除屏蔽后只会递送一次。实时信号(SIGRTMIN+)可以排队,但数量有限。
- 线程与信号:sigprocmask是进程级别的。在多线程程序中,应该用pthread_sigmask来控制每个线程的信号屏蔽。
- 不可屏蔽信号SIGKILL和SIGSTOP不能被屏蔽,也不能被捕获。这是内核的硬性规定,别想着绕过。
嗯,信号集操作就讲到这里。记住一句话:信号集是工具,sigprocmask是核心,sigpending是调试利器。把这几个函数用熟了,信号处理这块你就掌握了八成。