信号 — 空消息、处置与阻塞
信号 — 空消息、处置与阻塞
这一章讲什么: 程序跑着跑着,外界(用户、内核、别的进程)有 事要通知它——通知的方式就是信号:一个编号、一个名字、没有 任何附带数据。本章把信号的一生讲透:它怎么来(CRTL-C 背后有 七步硬件旅程)、怎么被「挂起」而不是排队、进程能拿它怎么办 (终止/忽略/捕获)、怎么暂时挡住它(阻塞)、以及写信号处理函数 时必须遵守的军规。这是从「写脚本」跨向「写交互式程序」的 分水岭。
1. 顶层全景:信号的一生
事件(按键、定时器到期、子进程退出、除零、kill 命令……)
│ 只有内核能「生成」信号:更新目标进程的挂起位
▼
挂起(pending):进程此刻也许在睡觉/排队,信号等着
│ 同类型信号最多挂起 1 个——不排队,后来的丢弃
│ 被阻塞的信号继续挂着,直到解除阻塞
▼
递送(delivery):进程收到信号,三选一处置
├─ 默认动作:终止(多数)/ 忽略 / 暂停 / 转储核心(core dump) / 继续
├─ 忽略(SIG_IGN)——有些信号不可忽略
└─ 捕获:执行你注册的 handler(信号被 caught)
│ handler 里还能再挡别的信号(sa_mask)、
│ 决定同型信号来了怎么办(sa_flags)
▼
handler 返回 → 内核把程序恢复到被打断的下一条指令图说:生成≠递送,中间隔着「挂起」;递送≠终止,「处置」由进程 自己定(除了 SIGKILL 和 SIGSTOP,谁都拦不住)。
主线例子全章共用:你在终端按下 CTRL-C。它经过七步才变成 SIGINT,每一步都是具体的硬件/内核动作;然后看一个捕获了 SIGINT 的程序,在连续按 6 次 CTRL-C 时只交付 1 次——「不排队」铁律的 现场。
2. 主线例子第一步:CTRL-C 的七步旅程
① 键盘电路产生扫描码,经键盘控制器接收
② 控制器触发硬件中断 —— 每个键都中断一次,所以每击键都被即时处理
③ 内核短暂接管:找到这块键盘对应的终端,转交终端驱动
④ 驱动查「特殊字符表」:VINTR 位登记的码 == CTRL-C 的码
⑤ 查终端配置:isig 标志开着 → 让信号子系统给「控制终端是该终端
的所有进程」发 SIGINT
⑥ 内核发出 SIGINT
⑦ 你的进程收到:没装 handler 就按默认动作终止 —— 这就是 CTRL-C
杀死程序的全部机制图说:一次按键 = 一次硬件中断 + 一次内核查表 + 一次信号投递。 慢的是人,这段链条快得看不见。信号分两类:像 CTRL-C 这种异步 信号——什么时候到与进程在干什么无关;像除零这种同步信号—— 重跑程序它会在同一指令处再现1。
3. 信号是什么:空消息 + 一张分类表
信号除了编号没有任何内容——「像一封只有主题没有正文的邮件」,
SIGWINCH 的值本身就告诉你「窗口变了」2。编号随架构而异
(SIGINT 在大多数机器上是 2,但程序只许用符号名),kill -l 看
全表;<signal.h> 里 NSIG 是信号总数3。分类记这几个就够:
| 类别 | 代表 | 干什么 |
|---|---|---|
| 程序错误 | SIGSEGV(段错误)、SIGBUS、SIGFPE(算术错)、SIGILL、SIGABRT | 严重错误,给程序一个清理后事的机会 |
| 终止 | SIGINT(CTRL-C)、SIGQUIT(CTRL-,转储核心)、SIGKILL(不可捕获)、SIGTERM(kill 命令默认发的)、SIGHUP | 请进程退出 |
| 定时器 | SIGALRM(第 9 章)、SIGVTALRM | 计时器到期 |
| 作业控制 | SIGCHLD(子进程状态变化)、SIGSTOP(不可捕获)、SIGTSTP(CTRL-Z)、SIGCONT | 第 11 章的进程管理基础 |
| 杂项 | SIGWINCH(窗口改变)、SIGUSR1/2(用户自定义) | — |
默认动作五选一:终止 / 忽略 / 暂停 / core dump(把内存镜像写进 core 文件再死,gdb 能打开验尸)/ 继续4。
4. signal():能用但要知其病根
装 handler 两步:写个 void handler(int signum) 函数 → 用系统调用
注册。signal(信号, 处理) 第二个参数还能是 SIG_IGN(忽略)或
SIG_DFL(恢复默认)5。
signal() 的病在历史:老 Unix 里 handler 一跑,处置就重置回默认,
第二个同型信号来了直接按默认动作杀掉进程;程序员在 handler 里
重新 signal() 自救,但两个信号挤进来的窗口 race 依旧。4.4BSD 和
System V 各修各的,语义分裂;POSIX 采了 BSD 版。Linux 默认编译
出 BSD 语义(不重置);加 -D_XOPEN_SOURCE 或换 sysv_signal()
才得到 System V 语义——演示程序里第一次 CTRL-C 被捕获、第二次
直接死掉6。结论:signal() 只配教学,真代码用 sigaction()7。
5. 发送信号:kill 命令、kill()、raise()
kill PID 发 SIGTERM;kill -s SIGKILL PID(名字可写全名/去 SIG
前缀/编号);kill -1 给所有「你够得着」的进程发;pkill 按命令名
匹配8。程序里是 kill(pid, sig),权限规则:发送者的真实或
有效 UID 必须等于目标的 UID 才发得动(root 除外)9。第一个
参数的花样:0 = 全进程组、-1 = 所有够得着的、n<-1 = 进程组 |n|
——一条管道命令 last | grep | sort | uniq 里所有进程同组,
一次 kill 全端掉10。raise(sig) = kill(getpid(), sig),
典型用途:收到 SIGTSTP 时在 handler 里先做清理,再 raise(SIGTSTP)
把自己停掉(raise_demo 的模式)11。
6. 阻塞:信号掩码与「不排队」铁律
阻塞 ≠ 忽略:忽略是信号到了、进程丢弃;阻塞是信号根本不到,
攒着。内核给每个进程(第 15 章会看到每个线程也有)维护一张
信号掩码(可想象成每型信号一位的位图)12。API 三件套:
sigemptyset/sigfillset/sigaddset/sigdelset/sigismember 组装
sigset_t(别自己按位运算——实现不一定是位图),然后
sigprocmask(SIG_BLOCK|SIG_UNBLOCK|SIG_SETMASK, &set, &old)
换掩码13。
主线例子第二步:sigprocmask_demo1 的实验记录:
$ ./sigprocmask_demo1
SIGINT is blocked; sleeping for 5 seconds. Try entering a few CTRL-Cs.
^C^C^C^C^C^C Caught SIGINT ← 6 次 CTRL-C,只交付 1 次!
SIGINT is no longer blocked. Enter a few CTRL-Cs.
^C Caught SIGINT
^C Caught SIGINT ← 解除后,每次 CTRL-C 都即时交付三条规则解释输出14:
- 被阻塞的信号不排队——阻塞期间来 6 个同型信号,只挂起 1 个, 其余丢弃;
- 解除阻塞立即递送——POSIX 要求挂起的信号在 sigprocmask 返回 之前交给 handler,所以 "Caught SIGINT" 出现在 "no longer blocked" 之前;
- 慢系统调用被打断后不重启——
usleep被信号打断就直接返回 (Linux 上多数调用如此;signal(7) 有全表),所以那个循环最多 捕获 5 次。想让被打断的 read 之类的调用自动重来,装 handler 时 加SA_RESTART15。
用阻塞保护临界区的标准姿势:存旧掩码 → SIG_BLOCK → 临界区 →
SIG_SETMASK 恢复旧掩码(恢复比 SIG_UNBLOCK 安全——若该信号
本来就被别人挡着,恢复旧值不会误开)16。handler 和主程序共享
的变量,主程序访问时要挡信号;共享标志声明成
volatile sig_atomic_t——单条指令读写得完、且禁止编译器塞
寄存器17。
7. 等待信号:sigsuspend 与 sigwait
「解除阻塞 + pause() 睡觉等信号」有一个竞态:信号若恰好落在
两者之间,pause 会睡过头。sigsuspend(&mask) 原子地完成
「换掩码 + 挂起」两件事,等于把 sigprocmask; pause; 恢复
焊成一个不可分的系统调用;信号一到 handler 跑完,它带 EINTR
返回,掩码自动复原18。
另一种思路干脆不要 handler:sigwait(&set, &sig) 同步等信号—— 先把关心的信号全 BLOCK 掉,然后 sigwait 从挂起队列里收走 一个(它不再触发 handler),用返回值 switch 处理。程序按普通 控制流响应信号,没有异步打断,没有 async-signal-safe 问题。 这是「信号当事件队列用」的现代姿势19。
8. sigaction:现代的信号处置
struct sigaction {
void (*sa_handler)(int); /* 老式 handler */
void (*sa_sigaction)(int, siginfo_t *, void *); /* SA_SIGINFO 时用 */
sigset_t sa_mask; /* handler 运行期间额外挡住的信号 */
int sa_flags;
void (*sa_restorer)(void); /* 库内部用,忽略 */
};sa_handler 与 sa_sigaction 在某些机器上是 union——二选一。
sa_flags 四个开关20:
| flag | 效果 |
|---|---|
SA_SIGINFO | 装三参数版 handler,拿 siginfo_t |
SA_NODEFER | 处理中不自动挡同型信号 → 同型信号打断 handler,重入 nested(书挡式输出) |
SA_RESETHAND | 一交付就重置为 SIG_DFL → 第二次同型信号按默认杀掉进程(老式捕鼠夹) |
SA_RESTART | 被打断的慢系统调用自动重启 |
演示程序 sigact_demo3 用毫秒级时间戳当「书挡 ID」:不加 NODEFER
时输出是 进-出 进-出 进-出(同型信号被挡,排队一个个来);
加了 NODEFER 变成 进进进……出出出(handler 被重入,
像递归)。加 restart 后 read 不再报 EINTR——程序一直睡在 read 里
等你输入21。
siginfo_t:信号是谁、为什么发的
SA_SIGINFO 下 handler 收到 (int signo, siginfo_t *info, void *ctx)。
Linux 上保证有值的只有 si_signo/si_errno/si_code;谁发的填
si_pid/si_uid(kill 命令发的、raise 发的都填),硬件陷阱填
si_addr(出错指令地址)22。sigact_demo2 故意制造三种算术错:
$ ./sigact_demo2 → Code: FPE_INTDIV (Integer divide by zero)
$ ./sigact_demo2 f → Code: FPE_FLTDIV (Floating-point divide by zero)
$ ./sigact_demo2 o → Code: FPE_FLTOVF (Floating-point overflow)一个深刻的坑:handler 返回后,程序从被打断的指令继续——而同步 信号的那条指令本身就是错的,于是再错、再进 handler,死循环。 所以处理 SIGFPE 这类信号的 handler 必须自己终结进程(raise 一个 没 handler 的 SIGTERM)23。
9. handler 设计军规
- 越薄越好:正经做法是 handler 只置一个
volatile sig_atomic_t标志,主程序周期性检查并干活; - 只调 async-signal-safe 函数(signal-safety(7) 有全表);
printf不安全——要输出就用write(); - handler 通常该是「最后的代码」:跑完就该退出(要跳走看 sigsetjmp/siglongjmp);
- 一个 handler 捕多个信号类型是坏味道,重入设计加倍难24。
10. 作者的判断与证据
有明证的:
- CTRL-C 七步链条、终端驱动查 VINTR/isig,是可对照内核行为验证 的机制描述1。
- 阻塞期间 6 次 CTRL-C 只交付 1 次的实验、解除阻塞先于下一条 printf 的 POSIX 规定,直接可复现14。
- sigact_demo2 的 si_code 三值对照、同步信号重执行死循环,有 运行记录23。
- signal() 的 System V/BSD 语义分裂有历史出处6。
作者的判断:
- 「signal() 别用了,一律 sigaction」——业界共识,书中立场鲜明7。
- 「handler 里尽量只置标志」是保守的工程军规,牺牲实时性换正确性; 对于「主程序本来就空转等信号」的程序,重活放 handler 也成立, 作者留了口子24。
- printf 进 handler 的 UNSAFE 注释是明知故犯的教学简化, 书中明说25。
11. 边界与局限
- 本章只讲信号基础:实时信号(sigqueue 带数据)、sigtimedwait、 信号与线程的纠葛(哪个线程收信号)都推到第 15 章之后;
- SA_NOCLDSTOP/SA_NOCLDWAIT(子进程相关)只点了名,第 11 章用20;
- sigaltstack(alternate signal stack)未展开;
- sigsetjmp/siglongjmp 说「去读 man 页」24;
- 信号不排队、不带数据的本质局限意味着它只配当「通知」,不配当 「消息」——要传数据得用管道/共享内存(第 12、13 章)。
12. 可带走的
主线例子,一行复述: CTRL-C → 扫描码 → 硬件中断 → 终端驱动查 VINTR/isig → 内核给控制终端进程组发 SIGINT → 默认动作终止; 装 handler 捕获后,阻塞期连按 6 次只交付 1 次(不排队), 解除阻塞立即交付,usleep 被打断不重启(SA_RESTART 可救)。
- 信号 = 空消息;内核是唯一投递员;生成≠递送,中间是挂起。
- 同型信号不排队——挂起位只有一位;阻塞≠忽略。
- 处置三选一:默认 / SIG_IGN / handler;SIGKILL 与 SIGSTOP 谁也 拦不住。
- signal() 有历史语义分裂,真代码一律 sigaction()。
- kill(0/-1/n<-1) 按进程组投递;权限 = UID 匹配;raise 是自投。
- sigset_t 用 sigemptyset/sigaddset 组装,别按位自己搓。
- 保护临界区:存旧掩码→BLOCK→干活→SETMASK 恢复旧掩码。
- 解除阻塞立即递送(先于 sigprocmask 返回);慢系统调用被打断 返 EINTR,SA_RESTART 让它们自动重来。
- sigsuspend = 原子「换掩码+睡觉」,修掉 pause 的竞态;sigwait = 无 handler 的同步等信号,「信号当事件队列」。
- sa_flags:SA_SIGINFO 拿 siginfo_t(si_pid/si_uid 谁发的、 si_code 为什么);SA_NODEFER 允许重入;SA_RESETHAND 一次性。
- 同步信号(SIGFPE/SIGSEGV)handler 返回会重执行出错指令—— handler 必须终结进程。
- handler 军规:薄、只用 async-signal-safe 函数(write 不用 printf)、共享标志用 volatile sig_atomic_t、一类信号一个 handler。
13. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| CTRL-C 七步旅程、同步/异步 | Chapter 8: Introduction to Signals | text/14-ch08-chapter-8-introduction-to-signals.txt:100(搜「scan codes」) · :158(搜「synchronous signal」) |
| 空消息与信号类型值 | 同上 | :69(搜「subject and no body」) · :63(搜「SIGWINCH」) |
| 信号来源四类 | 同上 | :133(搜「source of a signal」) |
| 挂起/不排队位图 | 同上 | :200(搜「at most one pending」) |
| 默认动作五类、处置定义 | 同上 | :221(搜「default action」) · :242(搜「disposition」) |
| 信号分类与默认动作表 | 同上 | :273(搜「Signal Names and Default Actions」) |
| signal() 历史与语义分裂 | 同上 | :490(搜「reset to the default action」) · :605(搜「SVID」) |
| kill 命令/kill()/权限 | 同上 | :726(搜「SIGTERM signal」) · :787(搜「real or effective user」) · :793(搜「process group」) |
| raise 与清理后自停模式 | 同上 | :899(搜「raise」) · :926(搜「SIG_DFL」) |
| 阻塞与信号掩码 | 同上 | :990(搜「Blocking a signal」) · :1023(搜「signal mask」) |
| sigset 五函数 | 同上 | :1068(搜「sigemptyset」) |
| sigprocmask 三模式与恢复范式 | 同上 | :1111(搜「SIG_BLOCK」) · :1125(搜「restore the previous mask」) |
| 6 次 CTRL-C 实验与三条规则 | 同上 | :1222(搜「sigprocmask_demo1」) · :1233(搜「not queued」) · :1240(搜「immediately」) |
| EINTR 与系统调用重启 | 同上 | :1248(搜「interrupts that call」) · :702(搜「isn't restarted」) |
| volatile sig_atomic_t | 同上 | :1309(搜「sig_atomic_t」) |
| sigsuspend 原子语义 | 同上 | :1397(搜「sigsuspend」) · :1413(搜「atomically」) |
| sigwait 同步等待 | 同上 | :1435(搜「sigwait」) · :1457(搜「sigprocmask(SIG_BLOCK」) |
| sigaction 结构与 sa_flags | 同上 | :1515(搜「struct sigaction」) · :1568(搜「SA_NODEFER」) · :1582(搜「SA_RESTART」) |
| siginfo_t 成员保证 | 同上 | :1647(搜「si_signo, si_errno, and si_code」) |
| SIGFPE 三实验与重执行死循环 | 同上 | :1842(搜「sigact_demo2」) · :1836(搜「reexecuted」) |
| NODEFER/RESTART 书挡实验 | 同上 | :1909(搜「matching bookends」) · :2137(搜「still in the read」) |
| handler 设计军规 | 同上 | :2196(搜「as little as possible」) · :2206(搜「signal-safety」) · :2243(搜「more than one signal」) |
Footnotes
-
出处:「Chapter 8: Introduction to Signals」第 55–57 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:57,搜「synchronous signal」)与第 100–130 段的 CTRL-C 七步描述。 ↩ ↩2 -
出处:「Chapter 8: Introduction to Signals」第 68–72 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:69,搜「subject and no body」)。 ↩ -
出处:「Chapter 8: Introduction to Signals」第 58–67 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:63,搜「SIGWINCH」)与第 312–320 段(kill -l与 NSIG,搜「NSIG」)。 ↩ -
出处:「Chapter 8: Introduction to Signals」第 221–233 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:223,搜「Terminate」)。 ↩ -
出处:「Chapter 8: Introduction to Signals」第 512–539 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:517,搜「sighandler_t」) · :532(搜「Fake signal functions」)。 ↩ -
出处:「Chapter 8: Introduction to Signals」第 487–511 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:490,搜「reset to the default action」)与第 603–631 段(text/14-ch08-chapter-8-introduction-to-signals.txt:605,搜「SVID」)。 ↩ ↩2 -
出处:「Chapter 8: Introduction to Signals」第 505–511 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:509,搜「replaced by」)。 ↩ ↩2 -
出处:「Chapter 8: Introduction to Signals」第 712–755 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:726,搜「SIGTERM signal」) · :747(搜「pkill」)。 ↩ -
出处:「Chapter 8: Introduction to Signals」第 773–790 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:787,搜「real or effective user」)。 ↩ -
出处:「Chapter 8: Introduction to Signals」第 791–806 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:793,搜「process group」)。 ↩ -
出处:「Chapter 8: Introduction to Signals」第 899–911 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:901,搜「raise」) · :926(搜「SIG_DFL」)。 ↩ -
出处:「Chapter 8: Introduction to Signals」第 1016–1027 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:1023,搜「signal mask」)。 ↩ -
出处:「Chapter 8: Introduction to Signals」第 1034–1056 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:1042,搜「sigprocmask」)与第 1065–1093 段的 sigset 五函数(text/14-ch08-chapter-8-introduction-to-signals.txt:1068,搜「sigemptyset」)。 ↩ -
出处:「Chapter 8: Introduction to Signals」第 1180–1259 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:1222,搜「sigprocmask_demo1」) · :1233(搜「not queued」) · :1240(搜「immediately」)。 ↩ ↩2 -
出处:「Chapter 8: Introduction to Signals」第 1245–1259 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:1253,搜「not restarted」) · :702(搜「isn't restarted」)。 ↩ -
出处:「Chapter 8: Introduction to Signals」第 1122–1157 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:1125,搜「restore the previous mask」)。 ↩ -
出处:「Chapter 8: Introduction to Signals」第 1307–1328 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:1309,搜「sig_atomic_t」)。 ↩ -
出处:「Chapter 8: Introduction to Signals」第 1393–1425 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:1397,搜「sigsuspend」) · :1413(搜「atomically」)。 ↩ -
出处:「Chapter 8: Introduction to Signals」第 1426–1474 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:1435,搜「sigwait」) · :1323(搜「synchronously」)。 ↩ -
出处:「Chapter 8: Introduction to Signals」第 1511–1593 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:1515,搜「struct sigaction」) · :1568(搜「SA_NODEFER」) · :1582(搜「SA_RESTART」)。 ↩ ↩2 -
出处:「Chapter 8: Introduction to Signals」第 1866–2164 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:1909,搜「matching bookends」) · :2137(搜「still in the read」)。 ↩ -
出处:「Chapter 8: Introduction to Signals」第 1612–1668 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:1647,搜「si_signo, si_errno, and si_code」)。 ↩ -
出处:「Chapter 8: Introduction to Signals」第 1741–1864 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:1842,搜「sigact_demo2」) · :1836(搜「reexecuted」)。 ↩ ↩2 -
出处:「Chapter 8: Introduction to Signals」第 2193–2255 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:2196,搜「as little as possible」) · :2206(搜「signal-safety」) · :2243(搜「more than one signal」)。 ↩ ↩2 ↩3 -
出处:「Chapter 8: Introduction to Signals」第 592–599 段(
text/14-ch08-chapter-8-introduction-to-signals.txt:594,搜「async-signal-safe」)。 ↩