ctrl+c 默认发送sigint直接终止程序,不执行析构或清理;需用sigaction()拦截并设volatile sig_atomic_t标志位,主循环检测后优雅退出。

Ctrl+C 会直接终止程序,除非你显式拦截 SIGINT
默认情况下,按下 Ctrl+C 会向进程发送 SIGINT 信号,内核直接终止程序——不会调用析构函数、不执行 atexit 注册函数、资源不释放。想优雅退出(比如保存状态、关闭文件、清理线程),必须用 signal() 或更安全的 sigaction() 拦截。
signal(SIGINT, handler) 的基本写法和致命缺陷
看似简单:signal(SIGINT, [](int){ /* 处理 */ });,但 C++ 中这样写几乎一定出问题:
-
signal()不保证信号处理函数是可重入的,而 C++ lambda(尤其捕获变量的)通常生成非 POD 类型,不能安全用作信号处理函数 - POSIX 明确要求信号处理函数只能调用异步信号安全函数(
write()、_exit()、sigprocmask()等),std::cout、new、printf、std::string构造全都不安全 - 部分系统(如 macOS)中
signal()安装后可能自动重置为默认行为,第二次 Ctrl+C 就又退出了
正确做法是用函数指针 + 全局/静态标志位:
volatile sig_atomic_t g_stop_requested = 0;
<p>void signal_handler(int) {
g_stop_requested = 1;
}</p><p>int main() {
signal(SIGINT, signal_handler);
while (!g_stop_requested) {
// 主循环逻辑
std::this_thread::sleep_for(100ms);
}
// 这里做安全的清理(析构、close、join等)
}</p>
为什么推荐 sigaction() 而不是 signal()
sigaction() 是 POSIX 标准接口,行为可移植、可预测,且能规避 signal() 的多个陷阱:
- 不会被自动重置:设置
sa_flags = 0即可保持 handler 长期有效 - 可屏蔽其他信号:通过
sa_mask在 handler 执行期间阻塞指定信号,避免嵌套中断 - 支持
SA_RESTART:让被中断的系统调用(如read()、accept())自动重试,而不是返回 -1 +EINTR
最小可用示例:
<pre class="brush:php;toolbar:false;">struct sigaction sa; sa.sa_handler = signal_handler; sa.sa_flags = SA_RESTART; // 关键:避免 read()/accept() 被打断后失败 sigemptyset(&sa.sa_mask); sigaction(SIGINT, &sa, nullptr);
C++ 多线程下 SIGINT
只发给主线程,别指望子线程收到POSIX 规定:终端产生的 SIGINT 默认只发送给进程的主线程(即调用 main() 的那个线程)。子线程即使调用了 sigaction() 也不会触发 handler。
- 若需通知子线程,必须靠共享变量(如
volatile sig_atomic_t)+ 条件检查,或用pthread_kill()手动转发(不推荐) - 不要在信号 handler 里调用
pthread_cancel()或std::thread::join()—— 这些都不是异步信号安全函数 - 如果主线程阻塞在
std::condition_variable::wait()或epoll_wait(),记得在循环里检查g_stop_requested,不能只依赖超时唤醒
真正可靠的退出流程是:信号 handler 设置标志 → 主循环检测标志 → 主动调用清理逻辑 → 正常 return。
最易被忽略的一点:sig_atomic_t 是唯一能跨信号上下文安全读写的整型;用 bool 或 int 替代,在优化开启时可能被编译器缓存寄存器值,导致子线程永远看不到变化。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










