土法炼钢兴趣小组的算法知识备份

【操作系统百科】信号:Unix 最拧巴的抽象

文章导航

分类入口
os
标签入口
#signals#sigaction#signalfd#tgkill#async-signal-safe#pidfd#sigqueue

目录

signal 是 Unix 1970 年代的设计遗产。它同时做了三件事——异步通知、进程控制、错误递送——这三者本应是不同的抽象,但被塞进了同一套 API。结果就是每个资深工程师都被 signal 坑过至少一次。

本文不复述 man page,只讲三件事:内核怎么递送用户态怎么接如何在现代代码里尽量绕开 signal 的地雷

一、先看图

flowchart LR
    subgraph Producer[发送端]
      K[kill/tgkill/<br/>pidfd_send_signal] --> Q
      KI[内核产生<br/>SIGSEGV/CHLD/...] --> Q
    end
    Q[pending set<br/>per-task + per-tgroup]
    Q --> D{blocked?}
    D -->|yes| W[等待解除屏蔽]
    D -->|no| R[返回用户态前<br/>check signal]
    R --> H{handler?}
    H -->|SIG_DFL| DA[默认动作:<br/>term / core / stop / ignore]
    H -->|SIG_IGN| IG[丢弃]
    H -->|自定义| UH[切 sigframe 到用户栈<br/>调用 handler]
    UH --> RT[sigreturn 恢复]
    classDef kernel fill:#388bfd22,stroke:#388bfd,color:#adbac7;
    classDef user fill:#3fb95022,stroke:#3fb950,color:#adbac7;
    classDef warn fill:#f0883e22,stroke:#f0883e,color:#adbac7;
    class Q,D,R,RT kernel
    class UH user
    class DA warn

关键点:signal 只在从内核返回用户态前被检查。这意味着一个 busy loop 不 enter kernel 就永远不会被软中断(但 preempt 仍会把它踢下来,调度 tick 算 kernel entry)。

二、信号分类

2.1 标准信号 1-31

历史包袱:SIGHUP/SIGINT/SIGKILL/SIGTERM/SIGCHLD/… 这些是 SysV/BSD 流传下来的固定号。关键属性:

2.2 实时信号 32-64(SIGRTMIN..SIGRTMAX)

POSIX RT 扩展:

适合要数据、要计数的异步通知。但注意 glibc 会偷用 SIGRTMIN..SIGRTMIN+2(pthread cancel、setxid)。实际可用从 SIGRTMIN+3 开始。systemd 用一大堆 SIGRTMIN+N 做运行时控制。

2.3 同步 vs 异步

三、发送 API

3.1 kill(2) 和 tgkill(2)

kill(pid, sig):送给进程(thread group)。内核选一个未屏蔽此信号的线程递送。坑:你不能指定哪个线程接。

tgkill(tgid, tid, sig):送给特定线程。glibc 的 pthread_kill 底层就是 tgkill。

3.2 sigqueue(3)

带 payload 的发送,走 RT signal 机制。

3.3 pidfd_send_signal(2)

前文说的 pidfd 加持:

int pidfd = pidfd_open(pid, 0);
pidfd_send_signal(pidfd, SIGTERM, NULL, 0);

优势:pid 不会误杀(见 B-12 第 7.2 节)。5.1+ 可用。

四、接收 handler 的雷区

4.1 signal(2) vs sigaction(2)

永远不要用 signal(2)——语义在 BSD / SysV 不一致,且有些平台重置 handler 为 SIG_DFL 后再次递送会丢。sigaction(2) 是 POSIX 正规 API。

struct sigaction sa = {0};
sa.sa_sigaction = my_handler;
sa.sa_flags = SA_SIGINFO | SA_RESTART;
sigemptyset(&sa.sa_mask);
sigaction(SIGCHLD, &sa, NULL);

关键 flags:

4.2 async-signal-safe

signal handler 里只能调 POSIX 标为 AS-safe 的函数。这是硬约束,不是建议。违反后果:

AS-safe 子集很小:write、read、_exit、sigaction、sem_post、cfsetospeed、几何上一把。printf 不 safe,malloc 不 safe,任何 FILE* 操作都不 safe

所以 handler 的正确姿势:

复杂逻辑禁止放 handler 里。

4.3 errno 陷阱

signal handler 执行时会被各种 syscall 夹住,syscall 可能改 errno。handler 里如果调了 write 等会改 errno 的函数,必须先保存 errno、退出前恢复

void h(int s) {
    int saved = errno;
    write(pipefd, "X", 1);
    errno = saved;
}

4.4 多线程语义

五、现代替代:signalfd + pidfd

signalfd(2) 把 signal 变成 fd

sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGCHLD);
pthread_sigmask(SIG_BLOCK, &mask, NULL);

int sfd = signalfd(-1, &mask, SFD_CLOEXEC);
// 加入 epoll

好处:

坑:signalfd 需要先 block 对应 signal,否则 kernel 优先走 handler 路径。

pidfd + epoll 已在 B-12 讲;在只关心”子进程退出”时,pidfd 比 SIGCHLD 干净得多。

六、SIGCHLD 的三种姿势

  1. SIG_DFL:父忽略,子变僵尸
  2. SIG_IGN 或 SA_NOCLDWAIT:子退出即 reap,失去 exit code
  3. 自定义 handler + waitpid 循环:传统姿势
void sigchld(int s) {
    int saved = errno;
    while (waitpid(-1, NULL, WNOHANG) > 0) {}
    errno = saved;
}

要循环 wait——SIGCHLD 不排队,短时间多子退出可能只收到一次。

七、SIGSEGV:故障处理的艺术

进程访问非法地址 → kernel 产生 SIGSEGV 送自己。默认是 core dump。

但高级用法:

handler 里要用 sigaltstack,否则栈溢出触发 SEGV 时没地方跑 handler。

八、SIGPIPE 与 EPIPE

写入已 close 的 pipe/socket → 内核送 SIGPIPE(默认 term)。网络服务里这是常见踩雷:

signal(SIGPIPE, SIG_IGN);  // 或 sigaction
// 或每次 send 用 MSG_NOSIGNAL

否则客户端一断,服务端进程就死。

九、容器里的 signal 流转

Docker stop / kubectl delete pod 的流程:

  1. 发 SIGTERM 给容器 PID 1
  2. 等 grace period(默认 10/30s)
  3. 发 SIGKILL

PID 1 不装 SIGTERM handler 时,kernel 忽略 SIGTERM(PID 1 对信号有特殊待遇——无 handler 的默认都忽略,防止宕内核)。

解决:

十、常见 signal bug 模板

bug A:log rotation 后 nginx 没切日志 原因:reload 发的是 SIGUSR1 给 master;master 处理了但 worker 没 解决:按软件文档 reload 模式

bug B:gdb attach 后进程被卡 原因:ptrace 会 stop;detach 后若 deliver 了 SIGSTOP 就需要 SIGCONT 解决kill -CONT <pid>

bug C:pthread_cancel 不生效 原因:cancel 通过 SIGRTMIN 实现;被屏蔽或 handler 被覆盖 解决:不要手动玩 SIGRTMIN..+2

bug D:strace 下进程行为不一样 原因:strace 改了 signal delivery timing;某些竞争被隐藏 解决:用 perf trace 或 eBPF 替代,干扰更小

十一、小结

下一篇 B-14 讲管道、FIFO、socketpair——Unix 风格进程间通信的老牌三剑客。


参考文献

工具


上一篇进程生命周期 下一篇管道、FIFO、socketpair

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-05-21 · os

【操作系统百科】fd 化抽象

Linux 把信号、事件、定时器、进程都变成 fd——signalfd/eventfd/timerfd/pidfd。本文讲每种 fd 的用途、与 epoll 组合、KVM 的 eventfd、systemd 的 pidfd、以及 fd 化哲学。

2026-05-10 · os

【操作系统百科】fd 表与 struct file

fd 是用户态访问文件的句柄——但 fd 的共享语义在 fork/exec/dup/CLOEXEC 下极其微妙。本文讲 files_struct、fdtable、close_range、pidfd_getfd、cloexec 默认化趋势。

2026-04-17 · os

【操作系统百科】进程生命周期:clone → exec → exit → reap

一个进程从诞生到尸体被回收,在内核里走过六个阶段:clone → run → exec(可选)→ exit → zombie → release。本文按阶段讲 do_fork、bprm_execve、do_exit、release_task,以及 waitpid/pidfd/subreaper 的收尸规则、孤儿与僵尸的语义、systemd PID 1 的特殊性。

2026-04-27 · os

【操作系统百科】内存回收

Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。


By .