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