c++多线程中直接用signal()或sigaction()处理sigint极易死锁,因信号可能中断持锁线程;终端ctrl+c仅发给主线程,子线程不自动接收;正确做法是用signalfd+epoll统一收信号,或在handler中仅设volatile sig_atomic_t标志并由主逻辑轮询清理。

多线程 C++ 程序中直接用 signal() 或 sigaction() 注册 SIGINT 处理函数,极大概率导致死锁或未定义行为——因为信号可能在任意线程、任意时刻(比如正持有互斥锁时)被递送,而处理函数里调用的 printf、std::cout、pthread_mutex_lock 全都不属于 async-signal-safe 函数。
为什么主线程收 SIGINT 后子线程还在跑?
POSIX 明确规定:终端触发的 SIGINT 默认只发给进程的主线程(即执行 main() 的那个线程),子线程不会自动收到。你以为按了 Ctrl+C 全局退出,其实只是主线程进了 handler,其他线程照常运行、甚至卡在锁上。
- 子线程不会继承主线程的信号处理函数;它们有自己的信号掩码,默认不屏蔽
SIGINT,但也不会被内核主动投递 - 若主线程在 handler 里调用
exit(),整个进程终止,子线程来不及清理;若只是设标志,必须确保所有线程都轮询该标志 - 用
pthread_kill(tid, SIGINT)手动发信号给子线程?不行——SIGINT是不可靠信号,不排队,且子线程未必注册了 handler
正确做法:用 signalfd + epoll 统一收信号
把信号“转成 I/O 事件”,让某个专用线程(通常是主线程)在 epoll 循环里安全地读取,彻底避开异步中断上下文。这是 Linux 下最可靠、最可控的方式。
- 先用
pthread_sigmask()在所有线程启动前屏蔽SIGINT(包括主线程和每个pthread_create()前) - 主线程调用
signalfd(-1, &set, SFD_CLOEXEC)创建一个 fd,&set是仅含SIGINT的信号集 - 把这个 fd 加入 epoll 实例,
epoll_wait()返回后,用read(sfd, &info, sizeof(info))拿到struct signalfd_siginfo - 此时已回到常规线程上下文,可安全调用
std::cout、加锁、通知子线程、调用join()等
如果只能用传统 signal handler,怎么保命?
仅限嵌入式、兼容旧代码等无法改架构的场景。必须严格限制 handler 行为,否则就是定时炸弹。
- handler 内只写一个
volatile sig_atomic_t g_sigint_received = 0;,赋值为 1 - 绝不能出现
std::atomic、std::cout、malloc、pthread_mutex_*、printf,连write(1, ...)都要慎用(虽属 async-signal-safe,但可能干扰 stdout 缓冲) - 主逻辑(如 while 循环)必须定期检查
g_sigint_received,并在安全位置做退出清理 -
signal()已废弃,务必用sigaction()替代,设置sa_flags = SA_RESTART避免系统调用被中断
真正麻烦的不是注册 handler,而是信号与线程生命周期、资源释放、状态同步的耦合。哪怕用了 signalfd,也要确保子线程能响应退出信号——比如用 std::condition_variable + std::atomic_bool 通知,而不是依赖信号本身跨线程传递。信号永远只是“触发器”,真正的协调必须在用户态完成。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











