文章

C++信号处理

C++信号处理通过注册函数响应异步信号,如 SIGINT、SIGALRM,信号处理函数应只做原子操作或调用异步安全函数,主程序检查标志处理逻辑。

C++信号处理

C++ 信号处理

C++ 中的信号处理(signal handling)指的是程序在运行过程中响应特定异步事件(通常由操作系统发送的信号)的能力。信号机制在 UNIX/Linux 系统中较常见,主要用于处理诸如中断、终止、算术错误、非法访问等异常事件。

信号

信号是一种异步通信机制,由操作系统发送给进程,以通知发生了某种事件。每种信号都有一个编号和名称,例如:

信号名称编号含义
SIGINT2中断信号(如 Ctrl+C)
SIGTERM15请求终止进程
SIGSEGV11段错误,非法内存访问
SIGFPE8浮点异常(如除0)
SIGKILL9强制终止进程(不可捕获)
SIGABRT6程序异常终止(abort)

信号处理 API

C++ 使用 C 标准库中的 <csignal>(C 中为 <signal.h>)来处理信号。

基本函数:signal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <csignal>
#include <iostream>

void signalHandler(int signal) {
    std::cout << "Caught signal " << signal << std::endl;
    exit(signal);
}

int main() {
    signal(SIGINT, signalHandler);  // 捕获 Ctrl+C
    while (true) {
        std::cout << "Running...\n";
        sleep(1);
    }
}

函数签名

1
2
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

特殊处理器

  • SIG_DFL:默认处理方式。
  • SIG_IGN:忽略信号。

例如:

1
signal(SIGINT, SIG_IGN); // 忽略 Ctrl+C

信号处理函数注意事项

信号处理函数是异步调用的,可能随时打断程序执行。

  • 异步调用

    • 异步 = 不按照程序主流程顺序执行。

    • 例如,程序正在执行 x = a + b;,操作系统可能突然触发一个信号,然后暂停主程序,先执行信号处理函数。

  • 随时打断

    • 信号处理函数可以在任何指令中间被调用,程序不知道何时会被打断。

    • 因此,如果信号处理函数里调用了非安全操作(如 mallocprintf、加锁等),就可能破坏正在执行的代码状态,导致崩溃或死锁。

可以安全执行的操作

  • 修改全局或 volatile sig_atomic_t 变量
  • 调用异步信号安全函数(async-signal-safe),如 write_exit

不安全的操作

printf / fprintf
  • 这些函数使用 内部缓冲区(例如 stdout 缓冲)。
  • 如果主程序正好在刷新或写入缓冲区时被信号打断,再调用 printf,缓冲区可能处于不一致状态。
  • 可能导致输出错乱或程序崩溃。
malloc / free / new / delete
  • 内存分配函数内部通常使用全局堆管理结构(如 free list)。
  • 信号打断时,如果正在操作堆结构,再调用 malloc/free,可能破坏堆链表或分配状态,导致崩溃或内存泄漏。
文件 I/O (fopen / fclose / fwrite)
  • 标准库文件操作通常会加锁以保证多线程安全。
  • 信号中断时,如果线程持有锁,信号处理函数又尝试获取同一锁,就会死锁。
加锁操作(mutex、spinlock 等)
  • 信号可能在主线程持有锁时打断,如果信号处理函数再次尝试加锁,程序会死锁。
其他可能阻塞的系统调用
  • readwait 等,如果信号打断它们,可能导致未定义行为或阻塞不释放。

安全实践

  • 在信号处理函数中只设置标志位
  • 在主循环或程序正常流程中检查标志并执行具体处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <csignal>    // 信号处理相关函数和宏(如 SIGINT、signal)
#include <atomic>     // 原子类型,用于线程/信号安全的标志变量
#include <iostream>   // 输入输出流
#include <unistd.h>   // sleep 函数

// 使用原子布尔变量作为信号标志,保证信号处理函数中修改安全
std::atomic<bool> stopFlag(false);

// 信号处理函数,当接收到 SIGINT(Ctrl+C)时触发
void handler(int signum) {
    stopFlag = true; // 仅设置标志位,避免在信号处理函数中执行不安全操作
}

int main() {
    // 注册信号处理函数,捕获 SIGINT
    signal(SIGINT, handler);

    // 主循环,持续工作直到收到信号
    while (!stopFlag) {
        std::cout << "Working...\n"; // 输出工作状态(非信号处理函数中安全)
        sleep(1);                     // 暂停 1 秒,模拟工作间隔
    }

    // 当 stopFlag 被设置为 true,跳出循环
    std::cout << "Exiting gracefully\n"; // 优雅退出提示

    return 0;
}
  • 信号处理函数必须只执行原子性操作或 async-signal-safe 函数
  • 不保证原子性的操作、使用共享资源或可能阻塞的系统调用都是 异步不安全 的。

sigaction

signal 有实现差异且不支持重入保护等特性,更推荐使用 sigaction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <csignal>   // 信号处理相关头文件(sigaction, SIGINT 等)
#include <iostream>  // 输入输出流
#include <unistd.h>  // sleep 函数

// 信号处理函数,当接收到指定信号时调用
void handler(int signo) {
    // 注意:cout 在信号处理函数中不安全,仅用于演示
    std::cout << "Signal " << signo << " caught\n";
}

int main() {
    struct sigaction sa;          // sigaction 结构体,用于定义信号行为

    sa.sa_handler = handler;      // 指定信号处理函数
    sigemptyset(&sa.sa_mask);     // 处理期间不屏蔽其他信号(空集)
    sa.sa_flags = 0;              // 默认行为(没有 SA_RESTART 或其他标志)

    // 注册信号处理函数,用于捕获 SIGINT(Ctrl+C)
    sigaction(SIGINT, &sa, nullptr);

    // 主循环,模拟程序持续运行
    while (true) {
        std::cout << "Running...\n";  // 输出运行状态
        sleep(1);                     // 暂停 1 秒,模拟工作
    }

    return 0;
}

对比:

特性signalsigaction
灵活性功能有限,只能简单注册处理函数可以精细控制信号行为(信号屏蔽、标志等)
行为一致性不同系统/编译器实现可能不完全一致标准化、跨平台行为更一致
信号屏蔽不能控制信号屏蔽可以设置处理信号时临时屏蔽其他信号
重新安装某些系统中处理函数会被恢复默认(信号处理仅一次)处理函数安装后不会被重置
支持的标志可以设置多个标志(例如 SA_RESTART 自动重启系统调用)
安全性有些实现中信号处理期间可能被其他信号打断支持信号掩码,处理过程更安全

常见用途

程序终止控制

  • 捕获 SIGINT(Ctrl+C)、SIGTERM 等,优雅关闭程序。
  • 做法:在信号处理函数中设置标志位,主循环检测后释放资源、保存状态。
1
2
3
4
5
volatile sig_atomic_t stopFlag = 0;

void handler(int) { stopFlag = 1; }

while (!stopFlag) { /* 程序工作 */ }

定时任务 / 闹钟

  • 使用 SIGALRM 配合 alarm() 定时触发信号。
  • 常用于定时轮询、超时控制。
1
2
void alarmHandler(int) { /* 执行定时任务 */ }
alarm(5); // 5 秒后触发 SIGALRM

子进程状态监控

  • 捕获 SIGCHLD 信号,检测子进程退出或异常。
  • 避免僵尸进程,及时回收子进程资源。
1
void childHandler(int) { waitpid(-1, nullptr, WNOHANG); }

异步 I/O / 外部事件

  • 捕获硬件中断或系统信号(如网络或文件事件)。
  • 在信号里仅设置标志位或调用 async-signal-safe 函数,主循环处理具体逻辑。

调试与日志

  • 捕获 SIGSEGVSIGFPE 等异常信号,打印简单信息或写入日志(仅 async-signal-safe 操作)。
  • 辅助排查程序崩溃原因。

信号 vs 异常 vs 中断

项目信号C++ 异常硬件中断
类型OS 级事件编译期语言特性CPU 层面
来源内核/外部事件程序代码硬件设备
响应方式异步触发同步抛出/捕获异步中断处理
例子SIGSEGVtry-catch鼠标点击
本文由作者按照 CC BY 4.0 进行授权