Linux 信号机制完全指南
一、信号的本质
什么是信号?
信号是 Linux 下的一种软中断机制,用于异步通知进程发生了某个事件。
类比:信号就像老师上课时给你使个眼色——告诉你"待会有事",但你不用立刻停下正在做的事,等一个合适的时机(比如下课后)再去处理。
| 概念 | 说明 |
|---|---|
| 软中断 | 不是硬件中断,是软件层面模拟的中断 |
| 异步通知 | 信号产生的时间是随机的,进程无法预知 |
| 紧急程度 | 比普通函数调用更紧急,会打断当前执行流 |
二、信号的四大产生方式
| 方式 | 示例 | 产生的信号 |
|---|---|---|
| ① 键盘输入 | Ctrl+C | SIGINT (2) |
Ctrl+\ | SIGQUIT (3) | |
Ctrl+Z | SIGTSTP (20) | |
| ② 命令/函数 | kill -9 PID | SIGKILL (9) |
kill -15 PID | SIGTERM (15) | |
raise(SIGSTOP) | 任意信号 | |
| ③ 硬件异常 | 除 0 | SIGFPE (8) |
| 段错误(访问空指针) | SIGSEGV (11) | |
| 非法指令 | SIGILL (4) | |
| ④ 软件条件 | alarm(5) 定时到 | SIGALRM (14) |
| 写已关闭的管道 | SIGPIPE (13) | |
| 子进程退出 | SIGCHLD (17) |
三、信号的处理时机(重点!)
信号产生后立刻处理吗?
不是! 信号处理有延迟。
具体时机
用户态 ──────────────► 内核态 ──────────────► 用户态
│
▼
检查待处理信号
│
▼
有信号?→ 执行信号处理函数
│
▼
恢复用户态执行
核心原则:信号在进程从内核态返回用户态之前被检查和处理。
为什么这样设计?
- 安全:内核态不能执行用户态代码(信号处理函数属于用户态代码)
- 及时性:系统调用返回、中断处理完成、进程调度切换时都会检查
被阻塞的信号怎么办?
// 使用 sigprocmask 阻塞信号后,信号被挂起
sigprocmask(SIG_BLOCK, &set, NULL);
// 解除阻塞后,信号才会被处理
sigprocmask(SIG_UNBLOCK, &set, NULL);
四、可重入函数(高频面试题)
什么是可重入函数?
定义:一个函数可以在执行过程中被中断,并在中断返回后安全地再次执行,而不会导致数据损坏或逻辑错误。
可重入函数的条件
| 条件 | 说明 |
|---|---|
| ✅ 不修改静态/全局变量 | 只使用局部变量(在栈上分配) |
| ✅ 不调用 malloc/free | 堆操作可能被中断 |
| ✅ 不调用标准 I/O(printf/scanf) | 使用全局缓冲区 |
| ✅ 不调用非可重入函数 | 链式传染 |
典型的不可重入函数
// ❌ 不可重入:使用了全局变量
int global = 0;
int func() {
global++; // 被中断时 global 处于中间状态
return global;
}
// ❌ 不可重入:printf 使用全局缓冲区
printf("hello"); // 中断时可能正在写入缓冲区
// ❌ 不可重入:malloc 操作全局堆链表
malloc(100); // 中断时堆结构可能不完整
✅ 可重入函数示例
// ✅ 可重入:只用局部变量
int func(int a, int b) {
int c = a + b;
return c;
}
// ✅ 可重入:只读取全局变量(不修改)
int func() {
return global_readonly; // 只读是安全的
}
五、中断中为什么不能用 printf?
❌ 中断服务函数中能否用 printf?为什么?
不能! 原因有三:
| 原因 | 详细说明 |
|---|---|
| ① 不可重入 | printf 使用全局缓冲区,中断发生时主程序可能正在执行 printf,再次调用会导致缓冲区状态错乱 |
| ② 可能阻塞 | printf 需要写终端,可能等待 I/O 完成,中断上下文不能阻塞 |
| ③ 浮点运算 | printf 格式化浮点数时可能触发浮点协程操作,中断中禁用浮点 |
✅ 中断中应该做什么?
void interrupt_handler() {
// 1. 清中断标志
clear_interrupt_flag();
// 2. 读取硬件状态(只读寄存器)
int status = read_hardware_status();
// 3. 记录状态到共享变量
g_interrupt_flag = 1;
// 4. 触发下半部(软中断/tasklet/工作队列)
schedule_work(&work);
}
六、常用信号速查表
| 信号名 | 编号 | 产生方式 | 默认动作 | 能否捕获 |
|---|---|---|---|---|
| SIGINT | 2 | Ctrl+C | 终止 | ✅ |
| SIGQUIT | 3 | Ctrl+\ | 终止 + core | ✅ |
| SIGILL | 4 | 非法指令 | 终止 + core | ✅ |
| SIGTRAP | 5 | 断点/调试 | 终止 + core | ✅ |
| SIGFPE | 8 | 除 0/浮点异常 | 终止 + core | ✅ |
| SIGKILL | 9 | kill -9 | 终止 | ❌ 不可捕获 |
| SIGSEGV | 11 | 段错误(空指针) | 终止 + core | ✅ |
| SIGPIPE | 13 | 写已关闭管道 | 终止 | ✅ |
| SIGALRM | 14 | alarm 定时到 | 终止 | ✅ |
| SIGTERM | 15 | kill(默认) | 终止 | ✅ |
| SIGCHLD | 17 | 子进程退出/暂停 | 忽略 | ✅ |
| SIGSTOP | 19 | Ctrl+Z 或 kill | 暂停进程 | ❌ 不可捕获 |
| SIGCONT | 18 | 继续暂停的进程 | 继续 | ✅ |
七、SIGKILL vs SIGTERM(必考)
| 对比项 | SIGKILL (9) | SIGTERM (15) |
|---|---|---|
| 可否捕获 | ❌ 不可捕获 | ✅ 可捕获 |
| 可否忽略 | ❌ 不可忽略 | ✅ 可忽略 |
| 默认动作 | 立即终止 | 终止 |
| 能否清理资源 | ❌ 不能 | ✅ 可以(在信号处理函数中) |
| 使用场景 | 强制杀死 | 优雅退出 |
生产环境最佳实践
// 1. 先发 SIGTERM,让进程优雅退出
kill(pid, SIGTERM);
// 2. 等待 3 秒,如果进程还在,再发 SIGKILL
sleep(3);
kill(pid, SIGKILL);
// 进程端:捕获 SIGTERM,清理资源
void sigterm_handler(int sig) {
// 关闭文件描述符
close(fd);
// 释放内存
free(buffer);
// 通知其他进程
notify_others();
// 退出
exit(0);
}
signal(SIGTERM, sigterm_handler);
八、信号处理函数相关 API
1. signal() — 最简单的注册方式
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
// 示例
void handler(int sig) {
printf("收到信号: %d\n", sig);
}
signal(SIGINT, handler);
2. sigaction() — 更强大的注册方式
#include <signal.h>
struct sigaction {
void (*sa_handler)(int); // 处理函数
void (*sa_sigaction)(int, siginfo_t*, void*); // 扩展版
sigset_t sa_mask; // 处理时阻塞的信号
int sa_flags; // 标志位
};
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
3. sigprocmask() — 信号屏蔽
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
// how 参数:
// SIG_BLOCK - 将 set 中的信号加入阻塞集
// SIG_UNBLOCK - 将 set 中的信号从阻塞集移除
// SIG_SETMASK - 将阻塞集设为 set
// 示例:屏蔽 SIGINT
sigset_t set, oldset;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_BLOCK, &set, &oldset);
// ... 临界区代码 ...
sigprocmask(SIG_SETMASK, &oldset, NULL); // 恢复
4. kill() — 发送信号
#include <signal.h>
int kill(pid_t pid, int sig);
// 示例
kill(1234, SIGTERM); // 向 PID 1234 发送 SIGTERM
kill(0, SIGINT); // 向当前进程组所有进程发送
kill(getpid(), SIGUSR1); // 向自己发送 SIGUSR1
5. alarm() — 定时发送 SIGALRM
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
// 示例:5 秒后收到 SIGALRM
alarm(5);
九、常见信号处理场景
1. 忽略 SIGPIPE(防止程序退出)
// 网络编程中,写已关闭的 socket 会收到 SIGPIPE
signal(SIGPIPE, SIG_IGN); // 忽略,写操作返回 -1,errno = EPIPE
2. 子进程退出不产生僵尸进程
// 方式1:忽略 SIGCHLD
signal(SIGCHLD, SIG_IGN); // 子进程退出后由内核自动回收
// 方式2:捕获 SIGCHLD,调用 wait
void sigchld_handler(int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0) {
// 循环回收所有已退出的子进程
}
}
signal(SIGCHLD, sigchld_handler);
3. 超时功能(alarm + SIGALRM)
void alarm_handler(int sig) {
printf("超时了!\n");
// 做超时处理
}
signal(SIGALRM, alarm_handler);
alarm(5); // 5 秒后超时
十、信号机制的局限性
| 问题 | 说明 |
|---|---|
| 不可靠信号 | signal() 在不同系统行为不一致,建议用 sigaction() |
| 信号丢失 | 相同信号多次产生,可能只处理一次 |
| 系统调用中断 | 信号可能打断系统调用,需处理 EINTR |
| 线程间信号 | 信号是进程级,多线程时信号发给任意线程 |
处理 EINTR
ssize_t ret;
do {
ret = read(fd, buf, size);
} while (ret == -1 && errno == EINTR); // 被信号中断则重试
汇总
| 题目 | 答案要点 |
|---|---|
| 信号是什么? | 软中断,异步通知机制 |
| 信号产生方式? | 键盘、kill、硬件异常、软件条件 |
| 信号处理时机? | 内核态返回用户态前检查 |
| 什么是可重入函数? | 可被中断并安全再次调用 |
| printf 为什么不能用在信号处理函数中? | 不可重入、可能阻塞、可能引发浮点运算 |
| SIGKILL 和 SIGTERM 区别? | SIGKILL 不可捕获,SIGTERM 可捕获 |
| 如何优雅退出进程? | 先 SIGTERM,超时再 SIGKILL |
| 僵尸进程如何避免? | 忽略 SIGCHLD 或捕获并 wait |
| alarm() 有什么作用? | 定时发送 SIGALRM,可用于超时检测 |
🎯 总结
信号是 Linux 下重要的异步通知机制,掌握产生方式、处理时机、可重入性是面试高频考点。记住:
信号处理函数中只做最轻量、最安全的操作:设置标志位、记录状态、触发下半部。