ctrl+c 触发 sigint,应通过 sigaction() 注册信号处理函数,仅设置 volatile sig_atomic_t 标志位,主循环轮询该标志并执行 raii 清理;需同时处理 sigterm(容器/ systemd 默认),确保多线程协作退出且清理在超时内完成。

如何捕获 Ctrl+C 并触发清理逻辑
直接 kill -9 或强制关机时无法拦截,但用户按 Ctrl+C 会发 SIGINT,这是最常需要响应的优雅退出入口。C++ 没有内置信号处理机制,得用 signal() 或更安全的 sigaction() 注册回调。
常见错误是:在信号处理函数里调用非异步信号安全函数(如 std::cout、new、malloc),导致未定义行为甚至死锁。正确做法是只设置一个 volatile sig_atomic_t 标志位,主循环定期检查它。
- 用
sig_atomic_t g_stop_requested = 0;声明全局标志,确保读写原子 - 注册 handler:
signal(SIGINT, [](int) { g_stop_requested = 1; }); - 主逻辑中轮询:
while (!g_stop_requested) { /* 工作 */ },退出前执行清理 - 避免在 handler 中调用
std::exit()、std::cout、std::thread::join()等
RAII 资源清理必须覆盖所有退出路径
即使加了信号处理,仍可能因异常、return、或未捕获的 SIGTERM 提前退出。靠人工在每个出口写 close(fd); delete ptr; 容易遗漏。C++ 的 RAII 是唯一可靠手段。
关键不是“有没有析构函数”,而是“析构函数是否真能跑完”。比如线程未 join() 就销毁 std::thread 对象,会直接调用 std::terminate() —— 这不算优雅退出。
- 文件句柄用
std::fstream或 RAII 封装类,而非裸int fd - 动态内存优先用
std::unique_ptr,避免裸new/delete - 线程对象退出前必须
join()或detach();推荐在析构函数里if (t.joinable()) t.join(); - 若使用第三方库(如 libuv、asio),确认其资源类是否支持 RAII;否则需手动 wrap
多线程环境下怎么保证清理不竞争
主线程收到信号后设标志,其他工作线程可能正持有锁、写文件、或阻塞在系统调用上。强行 pthread_cancel() 或杀线程会导致资源泄漏或状态不一致。
真正可行的是“协作式退出”:所有线程都主动检查同一个退出标志,并在安全点(如循环开头、I/O 后)响应。不要依赖线程被外部终止。
- 共享标志必须是
std::atomic<bool></bool>(比sig_atomic_t更通用,且支持wait()) - 阻塞调用(如
epoll_wait()、pthread_cond_wait())需配合超时或中断机制,避免卡死 - 若用
std::condition_variable,唤醒时检查标志:cv.wait(lock, []{ return stop_flag.load(); }); - 日志、网络连接等长生命周期对象,提供
shutdown()方法并由主线程统一调用
systemd 或容器环境下的 SIGTERM 处理差异
在 Docker 或 systemd 服务中,kill 默认发的是 SIGTERM,不是 SIGINT。如果只处理 Ctrl+C,程序在容器里会被强制 SIGKILL 终止,来不及清理。
另外,systemd 有 TimeoutStopSec= 配置,超时后发 SIGKILL。你的清理逻辑必须在这个时间内完成,否则照样粗暴退出。
- 同时注册
SIGTERM和SIGINT,指向同一套退出逻辑 - 避免在退出流程中做耗时操作(如同步刷磁盘、等待远程响应);可先关闭监听、断开连接,再异步保存状态
- 用
atexit()补充兜底,但它不接收信号参数,也不能处理多线程,仅适合极简的单线程收尾 - 测试时用
kill -TERM $(pidof your_app)验证,别只测Ctrl+C
最容易被忽略的是:信号处理函数的可重入性、多线程下标志检查的时机、以及容器环境中 SIGTERM 的默认行为。这三个点没对齐,所谓“优雅退出”就只是本地调试时的幻觉。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











