目录
[1. signal() ------ 老古董](#1. signal() —— 老古董)
[2. kill() / raise() ------ 发送信号](#2. kill() / raise() —— 发送信号)
[3. alarm() / pause() ------ 定时与等待](#3. alarm() / pause() —— 定时与等待)
[4. sigaction() ------ 真正的王者](#4. sigaction() —— 真正的王者)
故事:(在介绍信号处理方式中有使用到printf函数(已标注不安全))
[Async-Signal-Safe 函数白名单:](#Async-Signal-Safe 函数白名单:)
[1. 优雅退出](#1. 优雅退出)
[2. 超时控制](#2. 超时控制)
[3. 子进程回收(防僵尸进程)](#3. 子进程回收(防僵尸进程))
一、学习路线图
别急着写代码,先看地图。我们按这个顺序来,保证学习不迷路:

二、什么是信号?
一句话定义: 信号是Linux系统发给进程的一种异步通知,告诉进程"嘿,出事了,处理一下!"
生活类比:
想象你在专心打游戏(主进程运行),突然电话响了(信号产生)。
- 你暂停游戏,去接电话(阻塞 当前进程,递送信号)。
- 你处理电话里的事(执行信号处理函数)。
- 挂了电话,继续打游戏(恢复现场)。
一句话总结: 信号就是软件层面的"中断",它是异步的,你永远不知道它下一秒会不会来。
三、必须掌握的信号
Linux有几十种信号,但常用的就那么几个。记住这张表,能应付90%的场景:
| 信号名 | 编号 | 默认动作 | 能否捕获 | 使用场景 |
|---|---|---|---|---|
| SIGINT | 2 | 终止 | ✅ | 用户按 Ctrl+C |
| SIGTERM | 15 | 终止 | ✅ | kill 命令默认发送,请求优雅退出 |
| SIGKILL | 9 | 强制终止 | ❌ | 核弹选项,系统强制杀进程 |
| SIGSTOP | 19 | 暂停 | ❌ | 暂停进程(类似 Ctrl+Z) |
| SIGSEGV | 11 | 终止+CoreDump | ✅ | 段错误(访问非法内存) |
| SIGCHLD | 17 | 忽略 | ✅ | 子进程退出,通知父进程收尸 |
| SIGPIPE | 13 | 终止 | ✅ | 往已关闭的管道/Socket写数据 |
| SIGALRM | 14 | 终止 | ✅ | 定时器到期 |
⚠️ 注意:
SIGKILL(9) 和SIGSTOP(19) 是"上帝信号",进程自己无法捕获或忽略它们,这是操作系统保留的最后手段。
一句话总结: SIGTERM 是礼貌的"请离开",SIGKILL 是直接"拔电源"。
四、信号的生命周期
信号不是发出去就立刻执行的,它有4个阶段:
- 产生: 键盘按下
Ctrl+C,或者代码调用了kill()。- 注册: 内核在进程的PCB(进程控制块)里把信号标记为"未决"(Pending)。
- 递送: 进程从内核态返回用户态时,检查有没有未决信号。如果有且没被阻塞,就递送给进程。
- 处理: 进程执行对应的动作(忽略、默认、或自定义函数)。
一句话总结: 信号产生后不一定马上执行,可能会在"未决"队列里排队。
五、三种处理方式
收到信号后,进程通常有三种选择:
- 忽略 (
SIG_IGN): 假装没听见。
- 例子: 服务器程序通常忽略
SIGPIPE,防止因为客户端断开导致服务崩溃。- 默认 (
SIG_DFL): 听系统的安排。
- 例子:
SIGINT默认就是终止进程。- 自定义 (函数指针): 自己写个函数处理。
- 例子: 捕获
SIGTERM来保存数据再退出。
代码示例:
cpp
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void my_handler(int signum) {
// 注意:printf 在信号处理函数中是不安全的!
printf("收到信号 %d,但我偏不退出!\n", signum);
}
int main() {
// 1. 自定义处理 SIGINT (Ctrl+C)
signal(SIGINT, my_handler);
// 2. 忽略 SIGTERM (kill 默认信号)
signal(SIGTERM, SIG_IGN);
while(1) {
printf("运行中...\n");
sleep(1);
}
return 0;
}
测试:
- 测试 SIGINT (Ctrl+C)
- 测试 SIGTERM(需要另开终端)
cpp# 运行程序后,直接在终端按 Ctrl + C # 终端2:查看进程PID并发送SIGTERM ps aux | grep test kill <PID>因为SIGTERM被忽略,kill <PID>无法杀死进程,进程继续运行,
SIGKILL(9)无法被捕获,可通过kill -9 <PID>强制杀死
六、核心API
1. signal() ------ 老古董
这是最早期的接口,简单但不可靠(不同系统实现不一样)。
cppsignal(SIGINT, handler); // 注册handler处理SIGINT缺点:信号处理完一次后可能会自动重置为默认行为(取决于系统),导致第二次按
Ctrl+C就退出了。
2. kill() / raise() ------ 发送信号
kill(pid, sig):给指定进程发信号。raise(sig):给自己发信号。
cppkill(1234, 9); // 强制杀死PID为1234的进程 raise(SIGUSR1); // 自己给自己发个自定义信号
3. alarm() / pause() ------ 定时与等待
alarm(seconds):设置一个定时器,时间到发SIGALRM。pause():让进程挂起,直到有信号到来。
4. sigaction() ------ 真正的王者
这是POSIX标准推荐的接口,稳定、可靠。
完整例子:
cpp
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void handler(int sig, siginfo_t *info, void *context) {
printf("收到信号 %d, 来自进程 %d\n", sig, info->si_pid);
}
int main() {
struct sigaction sa;
// 1. 清空结构体
sigemptyset(&sa.sa_mask);
// 2. 设置标志位:SA_SIGINFO表示使用sa_sigaction而不是sa_handler
sa.sa_flags = SA_SIGINFO;
// 3. 注册处理函数
sa.sa_sigaction = handler;
// 4. 生效
sigaction(SIGUSR1, &sa, NULL);
printf("等待信号...\n");
while(1) pause();
return 0;
}
七、信号集与阻塞(进阶)
有时候,我们不希望信号立刻打断我们(比如正在写关键数据),这时候需要阻塞信号。
核心类型:
sigset_t
5个操作函数:
sigemptyset():清空集合sigfillset():填满集合(全选)sigaddset():加入某个信号sigdelset():删除某个信号sigprocmask():设置阻塞掩码(核心!)例子:临时屏蔽
SIGINT5秒
cppsigset_t block_set, old_set; sigemptyset(&block_set); sigaddset(&block_set, SIGINT); // 把SIGINT加入屏蔽名单 // 阻塞 SIGINT sigprocmask(SIG_BLOCK, &block_set, &old_set); printf("接下来的5秒,按Ctrl+C无效...\n"); sleep(5); // 解除阻塞,恢复原状 sigprocmask(SIG_SETMASK, &old_set, NULL); printf("恢复响应Ctrl+C\n");阻塞不是丢弃,信号会在"未决"队列里等着,解除阻塞后立刻处理。
八、可重入函数(最重要的坑)
这是新手最容易死的地方!
故事:(在介绍信号处理方式中有使用到printf函数(已标注不安全))
主程序正在
printf("Hello"),写到一半,信号来了!信号处理函数里也调用了
printf("Signal!")。
printf内部有锁(线程安全),此时主程序的锁还没释放,处理函数又要加锁。
结果: 死锁!程序卡死。
什么是可重入?
一个函数在被中断后 再次被调用,依然能正常工作。
Async-Signal-Safe 函数白名单:
在信号处理函数里,只能调用这些安全的函数:
write()(注意不是printf)read()_exit()(注意不是exit)signal()(部分系统)黑名单(千万别用):
printf,malloc,new,strtok,exit。
正确模式:
信号处理函数只做一件事:设置全局标志位。
cpp#include <signal.h> #include <stdio.h> #include <unistd.h> #include <atomic> // 或者用 volatile sig_atomic_t // 必须是 volatile,防止编译器优化 volatile sig_atomic_t quit_flag = 0; void handler(int sig) { quit_flag = 1; // 只做这一件事! } int main() { signal(SIGINT, handler); while (!quit_flag) { // 主循环正常干活 printf("Working...\n"); sleep(1); } printf("检测到退出标志,优雅退出。\n"); return 0; }一句话总结: 信号处理函数里不要做任何复杂的事,只改一个
volatile变量。
九、常见编程模式
1. 优雅退出
如上例所示,利用
volatile sig_atomic_t标志位跳出循环,释放资源。
2. 超时控制
结合
alarm和sigaction。
cpp
// 设置5秒闹钟
alarm(5);
// 如果5秒内没收到信号,pause会返回
pause();
// 如果超时,信号处理函数会介入
3. 子进程回收(防僵尸进程)
父进程不需要
wait阻塞,而是捕获SIGCHLD。
cpp
void sigchld_handler(int sig) {
// 必须用 while 循环,因为可能多个子进程同时退出
while(waitpid(-1, NULL, WNOHANG) > 0);
}
int main() {
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigaction(SIGCHLD, &sa, NULL);
// ... fork子进程 ...
}
十、调试命令
程序跑飞了?用这些命令看看:
kill -l:列出所有支持的信号。cat /proc/<pid>/status | grep Sig:查看进程当前的信号掩码和未决信号。strace -e signal=all ./你的程序:跟踪程序接收到的所有信号(调试神器!)。
十一、常见错误表
| 现象 | 原因 | 解决 |
|---|---|---|
| 信号处理函数没被调用 | 函数签名写错了 | 确保函数签名是void handler(int) |
| 程序莫名其妙被杀掉 | 写入了关闭的Socket,内核会发送 SIGPIPE 信号,其默认动作是终止进程 | signal(SIGPIPE, SIG_IGN) |
printf 卡死或乱码 |
在信号处理函数中调用了 printf、malloc 等不可重入函数导致死锁 | 改用 write 或只设标志位 |
read/accept 返回 -1 |
系统调用被信号打断,函数返回 -1 并将 errno 设置为 EINTR | 检查 errno == EINTR 并 continue |

