C++ 信号处理
C++ 信号处理
Section titled “C++ 信号处理”信号是操作系统提供的一种机制,用于通知进程发生了异步事件。这些事件范围广泛,可以来自用户操作(如按下 Ctrl+C)、程序错误(如除以零)或来自其他进程的终止请求。
当信号发送给进程时,操作系统通常会中断进程的正常执行流程。然后进程可以:
- 忽略信号。
- 执行默认动作(通常是终止进程)。
- 执行一个自定义函数,称为信号处理器。
C++ 标准库通过 <csignal> 头文件提供对标准 C 信号的访问。一些常见信号(并非全部由 C++ 标准本身保证,通常是 POSIX 定义的)包括:
注意:并非所有信号都可以被捕获或忽略(例如,POSIX 系统上的 SIGKILL、SIGSTOP)。
std::signal 函数
Section titled “std::signal 函数”注册信号处理器的主要函数是来自 <csignal> 的 std::signal。
// 简化签名:signal_handler_t signal(int sig, signal_handler_t func);其中:
sig:整数信号编号(例如,SIGINT、SIGTERM)。func:当信号sig发生时要调用的函数的指针。此函数(处理器)必须接受一个int类型的参数(信号编号),并返回void。func有特殊值:SIG_DFL用于恢复默认动作,SIG_IGN用于忽略信号。
返回值为与该信号关联的先前处理器,或在出错时为 SIG_ERR。
关于信号处理器需要注意的重要事项
Section titled “关于信号处理器需要注意的重要事项”信号处理器在非常受限的环境中运行。 因为它们几乎可以在程序的任何时候中断执行,在其中执行复杂操作是极其危险的,并且经常导致未定义行为、崩溃或死锁。
主要限制和最佳实践:
- 可重入性: 只能调用保证是异步信号安全的函数。大多数标准库函数(包括
iostream、内存分配new/delete、互斥锁操作等)都不是异步信号安全的。 - 保持处理器简单: 理想情况下,处理器应该只做最少的事情,例如设置一个标志(类型为
volatile std::sig_atomic_t或std::atomic_flag)然后返回。 - 谨慎修改全局状态: 修改共享数据需要极其小心(例如,使用原子类型)。
- 终止: 直接从处理器中调用
exit()或std::terminate()会绕过正常的栈展开,这意味着析构函数(和 RAII 清理)不会运行。如果需要立即退出,std::_Exit或std::quick_exit(C++11) 会执行最少量的清理,但仍有数据丢失/损坏的风险。 - 可移植性: 信号行为在不同操作系统之间可能差异很大。
示例:安全捕获 SIGINT (Ctrl+C)
Section titled “示例:安全捕获 SIGINT (Ctrl+C)”此示例展示了一种更安全的方式来处理 SIGINT:在处理器中设置一个标志,并在主程序循环中检查该标志。
#include <iostream>#include <csignal> // 用于信号处理函数和类型#include <atomic> // 用于 std::atomic_flag (线程安全且信号安全标志)#include <chrono> // 用于 std::chrono::seconds#include <thread> // 用于 std::this_thread::sleep_for
// 使用原子标志在处理器和主线程之间安全通信// std::atomic_flag 保证是无锁的,因此是信号安全的std::atomic_flag keep_running = ATOMIC_FLAG_INIT; // 初始化为清除状态
// 信号处理函数void signalHandler(int signum) { // 在处理器内部不应该做什么: // std::cout << "Interrupt received!" << std::endl; // 不安全 (iostream) // exit(signum); // 不安全 (没有清理)
// 安全操作:设置原子标志以发出终止信号 if (signum == SIGINT) { keep_running.clear(std::memory_order_release); // 将标志设置为 false (清除它) }}
int main() { // 设置标志的初始状态 (true = 运行中) keep_running.test_and_set(std::memory_order_acquire);
// 注册 SIGINT 的信号处理器 signal(SIGINT, signalHandler);
std::cout << "Program running. Press Ctrl+C to interrupt." << std::endl;
// 主循环检查标志 while (keep_running.test_and_set(std::memory_order_acquire)) { // 当标志为 true 时循环 std::cout << "Working..." << std::endl; // 模拟工作 std::this_thread::sleep_for(std::chrono::seconds(1)); }
// 清理在这里发生,在循环优雅退出之后 std::cout << "\nInterrupt signal received. Exiting gracefully." << std::endl; // main 作用域中对象的析构函数将在此处运行
return 0;}当你运行此代码并按下 Ctrl+C 时,signalHandler 会将 keep_running 标志设置为 false。while 循环条件最终将评估为 false,允许程序退出循环并在终止前执行清理。
raise() 函数
Section titled “raise() 函数”你可以使用 <csignal> 中的 std::raise() 函数以编程方式向自己的进程发送信号。
int raise(int sig);此函数将信号 sig 发送到调用进程。成功时返回 0,失败时返回非零值。有时用于测试信号处理器或在内部触发特定的信号相关操作。
使用 raise() 的示例:
#include <iostream>#include <csignal>#include <chrono>#include <thread>#include <atomic>
std::atomic_flag keep_running_raise = ATOMIC_FLAG_INIT;
void signalHandlerRaise(int signum) { if (signum == SIGTERM) { // 这次处理一个不同的信号 keep_running_raise.clear(std::memory_order_release); // 在某些系统上,如果信号处理器重置,可能需要重新注册 // signal(SIGTERM, signalHandlerRaise); }}
int main() { keep_running_raise.test_and_set(std::memory_order_acquire); signal(SIGTERM, signalHandlerRaise); // 注册 SIGTERM 的处理器
int counter = 0; while (keep_running_raise.test_and_set(std::memory_order_acquire)) { std::cout << "Counter: " << ++counter << std::endl; if (counter == 3) { std::cout << "Raising SIGTERM..." << std::endl; raise(SIGTERM); // 向此进程发送 SIGTERM } std::this_thread::sleep_for(std::chrono::milliseconds(500)); }
std::cout << "\nSIGTERM received or loop finished. Exiting." << std::endl; return 0;}信号的替代方案
Section titled “信号的替代方案”虽然信号对于处理某些操作系统级事件是必要的,但它们通常不是 C++ 中用于通用错误处理或线程间通信的首选方式。
- 异常: 标准 C++ 中用于处理同步错误的机制。它们允许适当的栈展开和 RAII 清理。
- 条件变量和 Future (
<condition_variable>,<future>): 更适合同一进程内线程之间的信号传递。 - 应用程序级逻辑: 对于平滑关机请求,通常专用的机制(如检查共享原子标志,或使用专用线程)比单独依赖
SIGTERM或SIGINT更清晰、更安全。
仅当你确实需要与操作系统的异步事件机制交互时才使用信号,但处理它们时需极其小心。