C 语言信号处理与可重入性
更新时间:2026-08-26。本文是
languages/c/主题专家层文档。信号是异步的——它可能在程序执行的任意时刻打断你,跳进信号处理器。这个"任意时刻"带来了一个致命问题:信号处理器里能调什么函数? 调错了,程序就在你最想不到的地方崩掉。本文讲透这个"禁区"。
本文要回答的问题
- 信号处理器里为什么不能随便调
printf?什么函数才"安全"? volatile sig_atomic_t是什么?为什么信号标志变量要用它?signal和sigaction有什么区别?为什么推荐后者?
一、信号的异步性
信号是异步事件:进程可能正在执行 malloc、正在 printf、正持有某把锁,这时信号到了,CPU 直接跳去执行信号处理器。

问题在于:如果处理器里又调用了 malloc,而主程序正执行到 malloc 一半(内部数据结构不完整),就会二次进入 malloc、破坏其内部状态 → 死锁或崩溃。
二、async-signal-safe 函数
POSIX 规定了一批**异步信号安全(async-signal-safe)**函数——它们在信号处理器里调用是安全的。核心清单:
| 类别 | 安全函数 |
|---|---|
| 写 | write、read(不是 printf/fread) |
| 进程 | _exit、fork、waitpid、kill |
| 信号 | signal、sigaction、raise |
| 标志 | sem_post |
| 系统 | getpid、alarm、sleep、socket/connect(部分) |
关键禁区:printf、malloc、free、fopen、pthread_mutex_lock 都不安全(因为它们内部有锁或共享状态)。
// 错误:信号处理器里调 printf(不安全)
static void handler(int sig) {
printf("caught %d\n", sig); // 危险!
}
// 正确:用 write(安全)
static void handler(int sig) {
const char msg[] = "caught signal\n";
write(1, msg, sizeof(msg) - 1); // write 是 async-signal-safe
}三、volatile sig_atomic_t
信号处理器和主程序共享的标志变量,必须用 volatile sig_atomic_t:
static volatile sig_atomic_t flag = 0;
static void handler(int sig) {
flag = 1; // 只做赋值,保证原子、可见
}
int main(void) {
signal(SIGINT, handler);
while (!flag) {
/* 主循环干活 */
}
/* 收到信号,退出 */
}| 要点 | 原因 |
|---|---|
volatile | 阻止编译器优化掉对 flag 的读取(缓存到寄存器) |
sig_atomic_t | 保证读写是原子的(单指令完成),不会被信号打断到一半 |
四、signal vs sigaction
| 对比 | signal | sigaction |
|---|---|---|
| 可移植性 | 语义因平台而异 | POSIX 标准、语义一致 |
| 控制粒度 | 粗 | 细(可设 SA_RESTART、SA_SIGINFO 等) |
| 推荐度 | 历史遗留 | 推荐 |
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask); // 处理器执行期间屏蔽哪些信号
sa.sa_flags = SA_RESTART; // 被信号打断的系统调用自动重启
sigaction(SIGINT, &sa, NULL);SA_RESTART 尤其重要:不加它,慢系统调用(如 read)被信号打断后返回 EINTR,需要手动重试(见 系统调用封装)。
五、段错误 SIGSEGV 实测
段错误是"访问了不该访问的内存",内核发 SIGSEGV:
int *p = NULL;
*p = 42; // 解引用空指针 → 段错误实测:
Segmentation fault ./signal-test
退出码: 139 (128 + 11,11 是 SIGSEGV 的信号编号)退出码 = 128 + 信号编号,这是 shell 报告"被信号杀死"的约定。SIGSEGV=11 → 139,SIGINT=2 → 130。
完整段错误排查见 crash 段错误排查 和 core dump。
六、与本站主线衔接
- 信号内核机制:信号的生成、递送、阻塞、处理流程,见 信号机制。
- crash 排查线:段错误、core dump 的完整排查,见 crash 排查线。
- EINTR 与重试:信号打断系统调用,见 系统调用封装。
- 可重入性:与线程安全的区别,见 并发编程。
一句话总结
信号处理器是"禁区"——只能调 async-signal-safe 函数(write 可以、printf/malloc 不行)、共享标志用 volatile sig_atomic_t、注册用 sigaction + SA_RESTART;违反这些,程序会在任意时刻莫名其妙地死锁或崩溃。