信号处理

原理

操作系统信号模型

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

信号的三个阶段:

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

内核的信号递达路径

1. 内核设置 pending.signal 位图中对应位
2. 若目标进程睡眠 → 唤醒(TASK_INTERRUPTIBLE)
3. 进程被调度运行时,返回用户态之前:
   内核检查 pending 位图 → 若存在未决信号且未被屏蔽
   → 内核设置用户态栈帧
   → 进程返回到用户态时,执行信号处理函数
4. 处理函数执行完毕 → sigreturn 系统调用 → 恢复中断的执行流

信号处理函数的执行上下文

信号处理函数使用进程的用户栈(或通过 sigaltstack 设置的内核信号栈),但它是异步调用的——不在正常调用链中,可以在 main()、malloc() 甚至 printf() 执行到一半时被调用。这是信号安全限制的根源。

异步信号安全

信号处理函数中只能调用异步信号安全函数(POSIX 标准定义)。不安全的函数(如 printfcoutmallocstd::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);   // 恢复默认处理

常见信号

信号含义可否捕获
SIGINTCtrl+C 中断
SIGTERMkill 命令终止
SIGKILL强制终止不可
SIGSTOP强制停止不可
SIGSEGV段错误可(但无法安全恢复)
SIGUSR1/2用户自定义
SIGHUP终端关闭/重载配置
SIGALRMalarm() 定时器到期
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 解释为什么不能在信号处理函数中调用 mallocprintf——从锁和可重入性角度回答;2) 写一段代码实现”带自动保存的文本编辑器”,接收到 SIGTERM 后自动保存编辑内容并退出。

建议先阅读13_多线程 — 多线程环境中信号的行为(pthread_sigmask vs sigprocmask)。

C 语言关联:C 语言的信号处理完全通过 <signal.h> 实现,与 C++ 的 <csignal> 语义相同。signal() 函数在 C 和 C++ 中都可用,但习惯上 C++ 中推荐用 sigaction(POSIX)` 语义相同。详情见 C 标准库深度