信号处理

原理

操作系统信号模型

信号是 Unix/Linux 系统的异步通知机制——一个进程(或内核)向另一个进程发送信号,目标进程在合适的时机处理。

信号的三个阶段:

  1. 产生:内核或其他进程发送信号
  2. 未决:信号已发送但目标进程尚未处理,存储在进程的 task_struct->pending 位图和链表中
  3. 递达:目标进程执行信号处理函数

内核的信号递达路径

1. 内核设置 pending.signal 位图中对应位
2. 若目标进程睡眠 → 唤醒(TASK_INTERRUPTIBLE)
3. 进程被调度运行时,返回用户态之前:
 内核检查 pending 位图 → 若存在未决信号且未被屏蔽
 → 内核设置用户态栈帧
 → 进程返回到用户态时,执行信号处理函数
4. 处理函数执行完毕 → sigreturn 系统调用 → 恢复中断的执行流
```cpp
 
### 信号处理函数的执行上下文
 
信号处理函数使用进程的用户栈(或通过 `sigaltstack` 设置的内核信号栈),但它是**异步调用**的——不在正常调用链中,可以在 main()、malloc() 甚至 printf() 执行到一半时被调用。这是信号安全限制的根源。
 
### 异步信号安全
 
信号处理函数中只能调用**异步信号安全函数**(POSIX 标准定义)。不安全的函数(如 `printf`、`cout`、`malloc`、`std::string` 操作)内部使用锁——若主程序执行这些函数到一半时被信号打断,再调用同一函数会导致死锁。
 
安全函数包括:`write()`、`read()`、`_exit()`、`signal()`、`close()`、`fork()`、`kill()`、`waitpid()`、`alarm()`。
 
### sig_atomic_t 的语义
 
`sig_atomic_t` 是一个整数类型,对它的读写不会被信号中断——在 x86 上对应单条 `mov` 指令。`volatile` 告诉编译器不要优化对该变量的访问(不用寄存器缓存,每次都从内存读取)。主循环通过轮询此标志在信号处理函数外部安全执行复杂操作。
 
```cpp
volatile sig_atomic_t g_flag = 0;
void handler(int) { g_flag = 1; } // 安全:只写标志
// 主循环:安全处理 g_flag(不在信号处理函数内部)
```cpp
 
### sigaction vs signal
 
C 标准库的 `signal()` 在不同 Unix 系统上行为不一致——某些系统会在处理函数执行后恢复为默认处理。POSIX 的 `sigaction()` 提供精确控制:
- 可获取发送者的 PID 和 UID(`SA_SIGINFO`)
- 可控制信号处理期间屏蔽哪些信号(防止重入)
- 可控制自动重启被中断的系统调用(`SA_RESTART`)
- 支持实时信号排队(SIGRTMIN~SIGRTMAX)
 
### 信号与多线程
 
多线程环境下,信号处理有特别规则:`signal()` 和 `sigaction()` 设置的处理器是进程级别的——所有线程共享。但每个线程可有独立的信号掩码(`pthread_sigmask`)。信号只会递达给一个未屏蔽该信号的线程。推荐做法:在所有工作线程中屏蔽需要处理的信号,让主线程专门处理信号。
 
---
 
## 语法
 
### signal()
 
```cpp
#include <csignal>
void handler(int sig) { /* 只做极简操作 */ }
signal(SIGINT, handler); // 注册处理器
signal(SIGHUP, SIG_IGN); // 忽略信号
signal(SIGINT, SIG_DFL); // 恢复默认处理
```cpp
 
### 常见信号
 
| 信号 | 含义 | 可否捕获 |
|---|---|---|
| SIGINT | Ctrl+C 中断 | 可 |
| SIGTERM | kill 命令终止 | 可 |
| SIGKILL | 强制终止 | **不可** |
| SIGSTOP | 强制停止 | **不可** |
| SIGSEGV | 段错误 | 可(但无法安全恢复) |
| SIGUSR1/2 | 用户自定义 | 可 |
| SIGHUP | 终端关闭/重载配置 | 可 |
| SIGALRM | alarm() 定时器到期 | 可 |
| SIGCHLD | 子进程状态改变 | 可 |
 
### raise() 与 kill()
 
```cpp
raise(SIGTERM); // 向自身发送信号(等价于 kill(getpid(), signum))
kill(pid, SIGTERM); // 向指定进程发送信号(POSIX)
alarm(3); // 3 秒后向自身发送 SIGALRM
```cpp
 
### 进程终止函数
 
```cpp
exit(0); // 正常终止:调用 atexit 注册函数 + 全局析构
abort(); // 异常终止:发送 SIGABRT,不调用 atexit
_Exit(0); // 立即终止:不调用 atexit,不刷新缓冲区
```cpp
 
### 优雅退出模式
 
```cpp
volatile sig_atomic_t g_shutdown = 0;
void shutdown_handler(int sig) { g_shutdown = 1; }
 
// 主循环中安全处理
while (!g_shutdown) {
 doWork(); // 正常处理
}
// 在此安全使用 cout、delete、string 等
gracefulShutdown(); // 保存状态、关闭连接、释放资源
```cpp
 
---
 
## 实践
 
**练习**:无专属练习题。信号处理是系统编程技能,建议实践:写一个程序接受 `SIGUSR1` 信号后切换调试模式(控制台输出级别变化),接受 `SIGTERM` 后优雅退出(保存当前状态到文件)。