本页目录

信号处理用户态

覆盖: sigaction → sigframe (用户栈) → SA_RESTART → signal stack (sigaltstack) → signalfd → timerfd → real-time signals 适用: glibc + Linux, 用户态视角 (内核部分见 kernel 文档)

概述

信号是 Unix 最古老但仍在活跃使用的 IPC/通知机制。用户态编程中信号的关键问题不是"怎么发"(kill 系统调用),而是"信号什么时候被递送、handler 执行时栈是怎么样的、handler 返回后程序从哪里继续"。本文聚焦用户态信号 handler 的执行模型。

sigaction: 安装信号 handler

#include <signal.h>

struct sigaction sa = {
    .sa_sigaction = handler,   // void handler(int sig, siginfo_t *info, void *ucontext)
    .sa_flags     = SA_SIGINFO | SA_RESTART,
    .sa_mask      = 0,          // handler 执行期间 block 的信号
};
sigemptyset(&sa.sa_mask);
sigaction(SIGINT, &sa, NULL);

// sa_flags 关键位:
//   SA_SIGINFO:   使用 3-参数 handler (获得 siginfo + ucontext)
//   SA_RESTART:   被打断的 syscall 自动重试
//   SA_NODEFER:   handler 执行时不自动 block 本信号
//   SA_RESETHAND: 一次性 handler (执行后恢复 SIG_DFL)
//   SA_ONSTACK:   使用 sigaltstack (独立信号栈)

Signal Frame: 信号 handler 的"幽灵栈"

// 信号递送时,内核在用户栈上创建 sigframe:
//
// 正常栈 (往下增长):
//   [user data]  ← 原来 RSP
//   [sigframe]   ← 内核构造的"快照"
//     ├─ 保存的寄存器: RIP, RSP, RFLAGS, 通用 regs
//     ├─ siginfo_t (si_signo, si_code, si_addr, ...)
//     └─ FPU state (XSAVE)
//   ← 新的 RSP (信号 handler 从这里开始)

// 信号 handler 返回:
//   sigreturn() → 从 sigframe 恢复所有寄存器 → 程序继续

关键⁠: 信号 handler 跑在用户栈上(不是内核栈)——它是普通的用户态函数。只是内核在调用它之前保存了整个寄存器上下文,handler 返回后才恢复。

信号与系统调用

// 系统调用在"慢"操作 (read from pipe/socket) 中被信号打断:

ssize_t ret;
do {
    ret = read(fd, buf, size);
} while (ret == -1 && errno == EINTR);  // 手动重试

// 或者: SA_RESTART → 内核自动重试 (但不是所有 syscall 都可重启)
//   可重启: read/write/wait/pause/select/poll/...
//   不可重启: sleep (返回剩余时间), connect (返回错误), accept

// pselect / ppoll / epoll_pwait:
//   原子操作: unblock signal → wait → block signal
//   避免 race: signal between unblock and wait

sigaltstack: 独立信号栈

// 问题: 栈溢出 → SIGSEGV → 信号 handler 需要在栈上运行 → 但栈已满!
// 解决: 给信号 handler 分配一个独立的栈 (sigaltstack)

stack_t ss = {
    .ss_sp    = sigstack_mem,          // 独立内存区域
    .ss_size  = SIGSTKSZ,              // 通常 8KB+
    .ss_flags = 0,
};
sigaltstack(&ss, NULL);

struct sigaction sa = {
    .sa_sigaction = handler,
    .sa_flags     = SA_ONSTACK | SA_SIGINFO,  // ← SA_ONSTACK!
};
sigaction(SIGSEGV, &sa, NULL);

// 用途:
//   - SIGSEGV handler (安全记录 crash 信息)
//   - Go runtime: goroutine 抢占 (SIGURG + 独立信号栈)
//   - Profiling: SIGPROF handler

signalfd: 信号变 fd

// 传统信号的问题: 异步 → handler 只能调用 async-signal-safe 函数
// signalfd: 把信号当成 fd 读 (同步) → 可 poll/epoll

#include <sys/signalfd.h>

sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
sigprocmask(SIG_BLOCK, &mask, NULL);    // 先 block (否则默认行为)

int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);

// event loop:
struct signalfd_siginfo fdsi;
read(sfd, &fdsi, sizeof(fdsi));
printf("got signal %d\n", fdsi.ssi_signo);

// 与 epoll 配合 → 所有事件统一处理

timerfd: 定时器变 fd

#include <sys/timerfd.h>

int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);

struct itimerspec its = {
    .it_interval = { .tv_sec = 1, .tv_nsec = 0 },  // 周期 1s
    .it_value    = { .tv_sec = 1, .tv_nsec = 0 },  // 首次 1s
};
timerfd_settime(tfd, 0, &its, NULL);

// 每次到期 → fd 可读 → read 返回 1 (到期次数)
uint64_t expirations;
read(tfd, &expirations, sizeof(expirations));

实时信号 (SIGRTMIN ~ SIGRTMAX)

POSIX 实时信号 vs 传统信号:
  传统 (SIGINT, SIGTERM, ...): 多次发送 → 可能合并为一次递送 (排队=1)
  实时信号 (SIGRTMIN+):       多次发送 → 按顺序排队 → 不会丢失

实时信号可以携带额外数据:
  union sigval value;
  value.sival_int = 42;
  sigqueue(target_pid, SIGRTMIN, value);

调试

# 进程的信号状态
cat /proc/<pid>/status | grep Sig
# SigBlk: blocked, SigIgn: ignored, SigCgt: caught (有 handler)
# SigPnd: pending per-thread, ShdPnd: pending per-process

# 发信号
kill -s SIGUSR1 <pid>
kill -s SIGRTMIN+1 <pid>   # 实时信号

# strace 看信号
strace -e trace=signal ./prog

参考

  • man: sigaction(2), sigaltstack(2), signalfd(2), timerfd_create(2)
  • 源码⁠: glibc signal/sigaction.c, sysdeps/unix/sysv/linux/signalfd.c
  • LWN: "Signalfd and timerfd", "POSIX real-time signals"

关键词: sigaction, sigframe, SA_RESTART, sigaltstack, SA_ONSTACK, signalfd, timerfd, real-time signals, SIGEV_THREAD