使用signal()函数可以捕获 Ctrl+C(即SIGINT信号)并执行清理操作,但仅能调用异步信号安全函数;推荐在跨平台场景下使用sigaction()(POSIX 系统)或 Windows 平台的SetConsoleCtrlHandler()来更可靠地处理控制台退出信号。
先来看 signal() 的具体用法。通常 Ctrl+C 会直接终止进程,但通过注册自定义信号处理函数,你完全可以拦截这个 SIGINT 信号。关键在于,目标不是“阻止程序退出”,而是“在退出前完成必要的资源清理与日志保存”。signal() 最大的优势是轻量高效,几乎在所有 POSIX 系统上都能工作,适合大多数控制台程序的信号处理需求。
signal(SIGINT, handler)必须在main()开头尽早调用,否则首次按下 Ctrl+C 时,程序可能来不及响应就直接被系统终止了。- 处理函数
handler内部只能调用异步信号安全函数——例如write()、_exit()。像printf()、std::cout或new这些都不属于信号安全函数,调用它们可能导致死锁或更严重的程序崩溃。 - 注册之后,每次按 Ctrl+C 都会触发同一个 handler。如果你希望只响应一次,需要在 handler 内部将信号处理方式重置为
SIG_DFL(默认行为)或SIG_IGN(忽略),从而避免重复捕获。
一个典型的 C++ 信号捕获示例:
void sigint_handler(int) {
write(STDOUT_FILENO, "Caught SIGINT, cleaning up...\n", 32);
// 在这里释放资源、落日志等
_exit(0); // 注意:不要用 exit(),它不安全
}
int main() {
signal(SIGINT, sigint_handler);
while (true) { /* 主循环 */ }
}
sigaction() 比 signal() 更可靠
为什么推荐使用 sigaction()?因为 signal() 在不同系统上的行为存在差异——比如有的系统在 handler 返回后会自动重置为默认处理,而有的则不会。而 sigaction() 的行为是明确且可预测的,还能在捕获信号时临时屏蔽其他信号,提供更精细的控制,是更现代、更推荐的 C++ 信号处理方案。
- 使用
struct sigaction结构体来配置,不能简单传一个函数指针了事,需要设置sa_handler或sa_sigaction。 sa_flags推荐设为SA_RESTART,这样被信号中断的系统调用(比如read())会自动重试,避免程序因EINTR错误而异常退出。sa_mask可以临时阻塞其他信号,例如屏蔽SIGTERM,防止在 handler 处理过程中被另一个信号打断,造成嵌套中断或数据竞争。
值得留意的是,C++11 及之后的标准并不保证 signal() 的可移植性。而 sigaction() 是 POSIX 标准接口,Linux 和 macOS 都原生支持。如果是 Windows 环境,则需要换用另一套方案,不能直接套用 signal() 来处理控制台退出信号。
Windows 下不能用 signal() 捕获 Ctrl+C
Windows 控制台对 Ctrl+C 的处理逻辑完全不同。即使在 MinGW 或 MSVC 中调用 signal(SIGINT, ...),也可能毫无反应,或者行为不符合预期。正确的做法是使用 Windows API SetConsoleCtrlHandler() 来注册控制台事件处理函数。
- handler 的函数签名是
BOOL WINAPI HandlerRoutine(DWORD dwCtrlType)。 dwCtrlType可能是Ctrl+C、Ctrl+Break、关闭控制台窗口等,必须显式判断dwCtrlType == CTRL_C_EVENT才能精准捕获SIGINT信号。- 返回
TRUE表示你已经处理了这个信号,系统不再执行默认的终止操作;返回FALSE则系统继续走默认流程,可能导致进程被立即终止。
如果需要跨平台,建议在代码层做一个封装:Linux/macOS 使用 sigaction(),Windows 使用 SetConsoleCtrlHandler(),尽量避免混用或依赖不可移植的行为,确保程序在不同操作系统上都能正确捕获控制台退出信号。
常见陷阱:异步信号安全与资源竞争
信号处理的最大难点在于,信号是异步的,随时可能打断你正在执行的任何代码。这意味着:
- 绝对不要在 handler 里调用 STL 容器、智能指针、
malloc()或std::string::c_str()——这些都不是异步信号安全的函数,调用它们会引发未定义行为。 - 如果主循环和 handler 同时读写同一个全局变量,必须用
volatile sig_atomic_t类型来声明。这是唯一一种保证原子读写的类型,其他类型都可能引发数据竞争或内存屏障问题。 - 不要依赖 RAII 机制在信号处理中自动执行析构——信号发生时,栈展开并不会发生,
std::unique_ptr的析构函数不会被调用,资源会泄漏。
真正安全、可控、可调试的做法是:handler 内部只做一件事——设置一个 volatile sig_atomic_t flag = 0,然后主循环定期检查这个 flag,如果发现信号被捕获,就主动进行清理和退出。这样一切都在主线程的掌控中,既避免了异步安全问题,也让程序逻辑更清晰,是 C++ 信号处理实践中的推荐模式。
