应使用 sigaction() 而非 signal():前者提供可靠信号语义,可精确控制掩码、系统调用重启(sa_restart)和子进程等待(sa_nocldwait),且 posix 标准推荐;后者在 linux/glibc 下行为不一致、已过时。

signal() 和 sigaction() 到底该用哪个
别用 signal()。Linux 下它行为不一致,glibc 里默认是不可靠信号语义(比如可能自动重置 handler、不阻塞同类信号),而且 POSIX 已明确标注为过时。真正该用的是 sigaction()——它能精确控制信号掩码、是否重启系统调用、是否阻塞其他信号。
常见错误现象:signal(SIGTERM, handler) 在某些机器上注册后只生效一次,或者 handler 里调 write() 没问题,但调 printf() 就崩溃(因为 printf() 不是 async-signal-safe)。
- 必须用
struct sigaction显式设置sa_handler、sa_mask、sa_flags -
sa_flags至少要带SA_RESTART(避免read()被中断返回 -1 + EINTR)和SA_NOCLDWAIT(如果 fork 子进程) -
sa_mask建议填空(sigemptyset(&sa.sa_mask)),除非你真需要在 handler 执行期间屏蔽其他信号
信号处理函数里只能调用 async-signal-safe 函数
这是最容易踩的坑:在 SIGTERM handler 里调 std::cout 、<code>free()、malloc()、pthread_create(),甚至 std::string::c_str() 都可能让程序当场卡死或崩溃。因为 handler 是异步进入的,此时 libc 的内部锁(比如 malloc 的 arena 锁)可能正被主线程持有。
可用的函数极少,POSIX 明确列出的 async-signal-safe 函数不到 20 个。实际开发中几乎只靠这几个:
-
write()—— 直接写 fd,比如write(STDERR_FILENO, "shutting down...\n", 18) -
sigprocmask()、raise()、_exit()(注意不是exit()) - 自己定义的 volatile 标志位:用
volatile sig_atomic_t g_terminate_requested = 0;,handler 里只做g_terminate_requested = 1;
所以正确模式是:handler 只改一个 volatile sig_atomic_t,主循环定期检查它,再在安全上下文里做清理(关 socket、flush log、join 线程等)。
多线程下 SIGTERM 默认只发给主线程
Linux 默认把 SIGTERM 发给整个进程组,但信号实际只会被**某个线程接收**——通常是主线程(取决于谁没屏蔽该信号)。如果你的主线程长期阻塞在 pthread_cond_wait() 或 epoll_wait(),而工作线程完全没注册 handler,那 signal 就石沉大海。
解决方案不是给每个线程都设 handler(没必要且难维护),而是确保主线程能收到并响应:
- 主线程创建后,用
pthread_sigmask(SIG_UNBLOCK, &set, nullptr)解除对SIGTERM的屏蔽(子线程会继承掩码) - 避免在主线程里调用
sigwait()以外的阻塞系统调用前未设好 signal mask;更稳妥的做法是用signalfd()(Linux 特有),把信号转成文件描述符,用epoll统一管理 - 如果用了
std::thread,记得主线程不要 detach 所有子线程后直接退出——得显式join()或用std::atexit()注册清理,但atexit()不能替代 signal 处理
为什么 std::signal 不行,而 boost::asio::signal_set 也不适合 SIGTERM
std::signal 是 C++11 引入的包装,底层还是调 signal(),同样不可靠;boost::asio::signal_set 设计目标是配合 io_context 做异步事件分发,它依赖内部线程池和 event loop,一旦你的程序主逻辑不在 asio 控制下(比如混合了 raw pthread、epoll、或已有主循环),它就无法保证 SIGTERM 被及时捕获——甚至可能因线程竞争漏掉信号。
真正轻量、可控、符合 Unix 进程生命周期的做法,就是裸用 sigaction() + volatile 标志 + 主循环轮询。复杂点在于:信号到达和主循环检查之间存在窗口期,如果清理逻辑本身耗时(比如 flush 几百 MB 日志),得考虑用 alarm() 或单独监控线程防 hang,但这已是另一层健壮性设计了。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











