应弃用 signal() 改用 sigaction():前者行为不可靠,可能重置信号、丢失信号、无法控制掩码和系统调用重启;后者可精确设置 sa_mask、SA_RESTART,并要求仅调用异步信号安全函数。

signal() 函数在 Linux 下的绑定行为很不可靠
Linux 下直接用 signal() 绑定信号处理函数,基本等于“听天由命”——它在不同系统(尤其是 glibc 版本差异)下语义不一致:signal() 可能重置为默认行为、可能不阻塞同信号递归、还可能丢失信号。POSIX 明确推荐弃用它,改用 sigaction()。
-
signal(SIGINT, handler)在某些 glibc 版本中注册后,收到一次SIGINT就自动恢复成SIG_DFL,第二次 Ctrl+C 直接终止进程 - 无法控制信号掩码,处理期间若再收到同信号(如快速连按 Ctrl+C),可能被忽略或引发未定义行为
- 不能指定是否重启被中断的系统调用(
SA_RESTART),导致read()、accept()等意外返回EINTR
用 sigaction() 实现可重入、可屏蔽、可重启的信号处理
真正可控的方式是 sigaction(),它能精确控制信号行为。关键点:显式清空 sa_mask、设置 SA_RESTART、避免使用非异步信号安全函数(如 printf、malloc)。
- 必须用
sigemptyset(&sa.sa_mask)初始化信号掩码,否则行为未定义 - 加上
SA_RESTART标志,让被中断的系统调用自动重试,避免满屏EINTR - 处理函数里只能调用异步信号安全函数(
write()、_exit()、sigprocmask()等),std::cout和std::string都不安全
struct sigaction sa;
sa.sa_handler = [](int sig) {
write(STDERR_FILENO, "Caught SIGINT\n", 16);
_exit(1); // 不要用 exit() — 它不是 async-signal-safe
};
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGINT, &sa, nullptr);
如何实现“优雅退出”:全局标志 + 主循环检查
信号处理函数不能做复杂逻辑,真正的清理工作必须回到主线程。通用做法是用 volatile sig_atomic_t 做原子标志位,主循环定期检查并执行析构、关闭资源等操作。
- 必须用
volatile sig_atomic_t(而非bool或int),确保读写不被编译器优化掉,且是原子访问类型 - 不要在信号处理函数中调用
close()、delete、std::thread::join()等——它们都不在 async-signal-safe 列表里 - 主循环应避免长时间阻塞(如无超时的
sleep()),可用poll()或epoll_wait()带超时来轮询标志位
volatile sig_atomic_t g_exit_requested = 0;
void signal_handler(int sig) {
g_exit_requested = 1;
}
// 注册
struct sigaction sa;
sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGTERM, &sa, nullptr);
sigaction(SIGINT, &sa, nullptr);
// 主循环
while (!g_exit_requested) {
// do_work();
poll(nullptr, 0, 100); // 每 100ms 检查一次
}
// 此处执行 close(), delete, join() 等安全清理
多线程环境下信号处理要格外小心
Linux 中信号是发给整个进程的,但最终由某个线程接收。默认情况下,任何线程都可能收到信号,导致行为难以预测。若想集中处理,必须显式屏蔽所有线程的信号,只在专用线程中 sigsuspend() 等待。
立即学习“C++免费学习笔记(深入)”;
- 主线程创建子线程前,先用
pthread_sigmask()屏蔽所有关心的信号 - 开一个专用信号处理线程,用
sigsuspend()或sigwait()同步等待信号 - 绝对不要在多个线程里分别调用
signal()或sigaction()—— 行为未定义 -
std::signal()(C++11 起)在多线程中效果与 C 的signal()相同,同样不推荐
信号处理本身不是难题,难的是理解哪些操作真正安全、哪些只是“碰巧没崩”。很多人用 signal() 加 exit() 跑了一年没出事,直到某次高负载下信号丢失或重入,服务静默崩溃——那才是最麻烦的。










