signalfd 是 Linux 特有系统调用,将信号转为文件描述符以供 epoll 等 I/O 多路复用监听,避免 signal/sigwait 的异步中断、竞态、重入及线程兼容性问题。

signalfd 是什么,为什么不能直接用 signal 或 sigwait
signalfd 是 Linux 特有的系统调用,它把信号转成文件描述符(fd),让信号能像普通 I/O 事件一样被 epoll、poll 或 select 监听。这避免了传统信号处理函数(signal / sigaction)的异步中断特性带来的竞态、重入和可移植性问题。
直接在信号处理函数里调用非异步信号安全函数(比如 printf、malloc、std::cout)是危险的;而 sigwait 要求屏蔽信号并独占一个线程等待,和单线程事件循环不兼容。所以 signalfd 是更自然的选择——只要你的主循环基于 fd 就行。
如何创建并配置 signalfd
关键点:必须先用 sigprocmask 屏蔽目标信号,否则 signalfd 不会收到它们;且 signalfd 只捕获被当前线程阻塞的信号。
- 用
sigemptyset+sigaddset构造要监听的信号集(如SIGINT、SIGTERM) - 调用
sigprocmask(SIG_BLOCK, &set, nullptr)阻塞这些信号(不是忽略!) - 传入该信号集给
signalfd(-1, &set, SFD_CLOEXEC | SFD_NONBLOCK),返回一个非阻塞 fd - 错误时检查
errno:常见EINVAL(信号集为空或含无效信号)、ENOSYS(内核不支持,见下文兼容性)
示例片段:
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGTERM);
sigprocmask(SIG_BLOCK, &set, nullptr); // 必须!
int sfd = signalfd(-1, &set, SFD_CLOEXEC | SFD_NONBLOCK);
if (sfd == -1) { /* handle error */ }
怎么从 signalfd 读取信号事件
signalfd 的 fd 可读时,表示有信号到达;read() 返回一个或多个 struct signalfd_siginfo,每个对应一次信号递送。注意:它不区分同一信号多次发送(除非使用实时信号),也不提供栈回溯,但足够用于优雅退出或重载配置。
- 每次
read(sfd, &si, sizeof(si))至少读一个signalfd_siginfo;返回值为sizeof(si)表示成功,0 表示对端关闭(不可能),-1 且errno == EAGAIN表示无信号(因设了SFD_NONBLOCK) -
si.ssi_signo是信号编号,si.ssi_code区分来源(如SI_USER表示 kill,SI_KERNEL表示内核触发) - 不要用
sizeof(si)以外的长度读 —— 内核只写固定大小结构体 - 若需批量处理,可循环
read直到EAGAIN,但通常一次只来一个信号,不必过度优化
和 epoll 集成的关键细节与坑
把 signalfd fd 加入 epoll 和普通 fd 没区别,但容易漏掉两个硬性前提:
- 必须确保创建
signalfd前,目标信号已被当前线程sigprocmask阻塞 —— 子线程继承信号掩码,但主线程默认未阻塞任何信号 -
signalfd不支持EPOLLET(边缘触发):内核文档明确说明它只能工作在水平触发模式;用EPOLLET会导致后续信号丢失 - 如果程序 fork,子进程不会自动继承
signalfdfd,但信号掩码会继承 —— 所以子进程需重新调用signalfd(或显式close并忽略) - glibc 封装的
signalfd在旧内核(#ifdef __NR_signalfd 检测,运行时可 fallback 到sigwaitinfo+ 独立线程(但失去“统一事件循环”优势)
复杂点在于信号语义本身:比如 SIGHUP 可能被多次发送,而 signalfd 默认合并同一信号(非实时信号),你得靠业务逻辑决定是否需要计数或去抖。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











