信号处理函数在多线程中默认不安全,因其运行在不确定线程上下文且不可调用非异步信号安全函数;应屏蔽信号后用signalfd转为事件,或仅用volatile sig_atomic_t通信。

信号处理函数在多线程中默认不安全
Linux 下 signal() 或 sigaction() 注册的信号处理函数,本质上是进程级全局的,且运行在「不确定线程」的上下文中——不是你期望的那个线程,甚至可能在锁持有期间被异步唤起。一旦处理函数里调用了 pthread_mutex_lock()、printf()、malloc() 等非异步信号安全(async-signal-safe)函数,就极易触发死锁或未定义行为。
典型现象:程序收 SIGINT 后卡住不动,gdb 查看线程栈发现主线程停在 pthread_mutex_lock,而另一个线程正持有该锁,且它的调用栈末端是 __sighandler ——说明信号处理函数在锁未释放时被强行插入执行。
- 所有标准 I/O 函数(
printf、fprintf、write除外)都不是 async-signal-safe -
pthread_mutex_lock/pthread_cond_wait等线程同步原语绝对禁止在信号处理函数内调用 -
signal()在多线程中行为不可靠,应始终用sigaction()替代
用 signalfd() 把信号转成文件描述符事件
这是最干净的解法:把信号“拉平”为可读事件,由某个专用线程(通常是主线程或独立 signal thread)通过 epoll 或 poll 统一接收并处理,完全避开异步中断上下文。
关键步骤:
立即学习“C++免费学习笔记(深入)”;
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
- 调用
pthread_sigmask()在所有线程中屏蔽目标信号(如SIGINT、SIGTERM) - 在主/专用线程中调用
signalfd()创建一个 fd,传入已屏蔽的信号集 - 用
read()从该 fd 读取struct signalfd_siginfo,每次读到一个信号事件 - 所有业务逻辑(如设置退出标志、调用
pthread_cancel()、清理资源)都在常规线程上下文中执行
示例片段:
sigset_t set; sigemptyset(&set); sigaddset(&set, SIGINT); pthread_sigmask(SIG_BLOCK, &set, nullptr); // 全局屏蔽 int sfd = signalfd(-1, &set, SFD_CLOEXEC); // 后续在 epoll_wait() 中监听 sfd,read() 得到信号
若必须用传统信号处理函数,只做最小动作
极少数场景(如兼容老代码、无法改架构)仍需 signal() 或 sigaction(),那只能严格约束处理函数行为:
- 仅写入一个
volatile sig_atomic_t全局变量(如volatile sig_atomic_t g_exit_requested = 0;) - 绝不调用任何 libc 动态分配/IO/锁函数;连
std::atomic<bool>都不行(它底层可能用锁或非 async-signal-safe 指令) - 主逻辑轮询该变量,在安全上下文中响应退出请求
- 注意:
sig_atomic_t仅保证读写原子性,不提供内存序;若需跨线程可见,仍要搭配memory_order_relaxed的 fence 或直接用std::atomic+ 主线程轮询(但信号 handler 内仍只能用sig_atomic_t赋值)
调试时快速定位信号相关死锁
别靠猜。用以下组合确认是否信号引发:
- 启动时加
strace -e trace=rt_sigaction,rt_sigprocmask,signalfd,看信号掩码和处理注册逻辑 - gdb 中
info threads+thread apply all bt,重点找含__sighandler、do_sigtimedwait或卡在pthread_mutex_lock的栈 - 检查所有信号处理函数内是否出现
printf、std::cout、new、mutex.lock()等高危调用 - 用
cat /proc/<pid>/status | grep Sig查看当前信号掩码(SigBlk)是否符合预期
真正麻烦的不是写错信号处理,而是以为“只打个日志没事”,结果日志函数内部悄悄 malloc 或加锁——这种隐式依赖最容易漏检。

















