信号分类与全景:1~64 号信号
这是 signals-kernel(信号内核总纲)的拆分篇之一,专门讲"信号怎么分类"。配套拆分篇:传递路径 · 数据结构 · 性能;姊妹篇 signals-user(用户态编程)。
总览:64 个信号,两大阵营
Linux 一共定义了 64 个信号,编号 1~64(_NSIG = 65,0 号保留不发送),按优先级和能力分成两大阵营:
| 标准信号(1~31) | 实时信号(SIGRTMIN ~ 64) | |
|---|---|---|
| 数量 | 31 个(部分号位未定义) | 最多 33 个(取决于 glibc 配置) |
| 排队 | ✗ 不排队——同信号来多次只记一次 | ✓ 排队——每次分配 sigqueue,依次投递 |
| 投递顺序 | 信号编号小的优先 | 按到达顺序投递(FIFO) |
| 附带数据 | ✗ 不能 | ✓ 可携带 int/pointer(sigqueue()) |
| 可靠性 | 可能丢失 | 可靠 |
glibc 的 RT 信号范围:x86-64 glibc 默认
SIGRTMIN = 34,把 32、33 留给了pthread的取消信号(SIGCANCEL)和setxid信号。所以用户可用的 RT 信号是 34~64,共 31 个。

信号分类(按产生来源):同步信号 vs 异步信号
这是最根本的分类——信号是怎么来的。它决定了投递时机、目标线程和内核代码路径。
| 同步信号 (Synchronous) | 异步信号 (Asynchronous) | |
|---|---|---|
| 产生者 | CPU 硬件异常 / 当前指令触发 | 另一个进程 / 内核定时器 / 终端驱动 / 外部事件 |
| 触发时机 | 精确发生在当前指令执行时 | 任意时刻,与当前执行流无关 |
| 投递时机 | 立刻——在同一个内核入口内投递 | 下次返回用户态时——挂在 pending 里等待 |
| 目标线程 | 永远是 faulting 线程(current) | 可在线程组内选任意线程(或不指定) |
| 内核入口 | 异常/陷阱(trap/exception handler)→ force_sig | 系统调用(kill/tkill/pthread_kill)或中断上下文 → send_signal |
| 丢失风险 | 无——指令不完成,信号必须被处理 | 标准信号可能被合并(同信号多次只记一次) |
| 典型信号 | SIGSEGV, SIGFPE, SIGILL, SIGBUS, SIGTRAP, SIGSYS | SIGINT, SIGTERM, SIGKILL, SIGCHLD, SIGALRM, SIGHUP, SIGUSR1/2 |
同步信号的内核路径(以 SIGSEGV 为例):
用户态: mov (null), %rax ← 触发 #PF 缺页异常
│
▼
内核异常入口: do_page_fault()
→ bad_area() / do_sigsegv()
→ force_sig_fault(SIGSEGV, SEGV_MAPERR, 0)
→ force_sig() ← 目标固定是 current
→ send_signal() ← 同一底层函数
→ complete_signal()
→ signal_wake_up(current)
→ 异常返回路径上
→ exit_to_user_mode_loop() ← 同一趟内核入口,不返回用户态
→ do_signal() → get_signal() ← 检测到 SIGSEGV 未决
→ handle_signal()
→ 如果注册了 handler → setup_rt_frame → iretq 进 handler
→ 如果没注册 → do_coredump() → do_group_exit() ← 直接死在这里关键区别:
┌──────────────────────────────────────────────────────┐
│ 同步信号: 内核入口(异常) → force_sig → 在同一趟 │
│ 出口路径上投递 → 不进用户态直接处理 │
│ │
│ 异步信号: 内核入口(syscall/中断) → send_signal │
│ → 设置 TIF_SIGPENDING → 返回用户态 │
│ → 用户代码继续跑 → 下次内核入口时 │
│ → 在出口路径上检测到 TIF_SIGPENDING │
│ → 投递 │
└──────────────────────────────────────────────────────┘为什么同步信号不经过"下次返回用户态"的等待? 因为触发同步信号的指令本身就是从用户态进来的(异常入口),异常处理完成后必然要从内核返回到用户态——这个"出口"天然存在,信号可以直接在这一趟出口路径上投递,不需要再等"下一次"。
异步信号的目标线程选择(complete_signal() 的逻辑):
// kernel/signal.c: complete_signal()
// ① 优先:找当前线程或指定线程
if (wants_signal(sig, current)) // 当前线程没屏蔽该信号
t = current;
else if (t == NULL)
// ② 遍历线程组找第一个没屏蔽该信号的线程
for_each_thread(p, t) { ... }
// ③ 唤醒选中的线程
signal_wake_up(t, ...);同步信号的特殊细节:
| 信号 | 触发条件 | force_sig 参数 | si_code |
|---|---|---|---|
| SIGSEGV | 空指针 / 非法地址 | force_sig_fault(SEGV_MAPERR) | SEGV_MAPERR |
| SIGSEGV | 写只读页 | force_sig_fault(SEGV_ACCERR) | SEGV_ACCERR |
| SIGBUS | 非对齐访问 / mmap 文件被截断 | force_sig_fault(BUS_ADRERR) | BUS_ADRERR |
| SIGFPE | 除零 | force_sig_fault(FPE_INTDIV) | FPE_INTDIV |
| SIGILL | 非法指令 | force_sig_fault(ILL_ILLOPC) | ILL_ILLOPC |
| SIGTRAP | int3 / 单步 | force_sig_fault(TRAP_BRKPT) | TRAP_BRKPT |
| SIGSYS | seccomp kill | force_sig_fault(SYS_SECCOMP) | SYS_SECCOMP |
同步信号的"不可忽略"特性:如果用户对 SIGSEGV/SIGBUS/SIGFPE/SIGILL 设置了
SIG_IGN,内核会强制改成SIG_DFL——这些信号无法被忽略,否则进程会在同一指令上无限循环(忽略 → 返回重试 → 再触发 → 再忽略 → 死循环)。
信号分类(按默认动作 + 典型用途)
第一类:终止 + Core Dump —— "崩溃类"信号
这类信号的默认动作是终止进程并产生 core dump。它们通常由硬件异常或编程错误触发,是排查崩溃的根本线索。
| 信号 | 编号 | 典型原因 |
|---|---|---|
| SIGSEGV | 11 | 非法内存访问:空指针解引用、写只读页、访问未映射地址 |
| SIGBUS | 7 | 总线错误:未对齐访问(严格架构上)、mmap 文件被截断后的访问、物理地址不可用 |
| SIGABRT | 6 | abort() 调用、assert() 失败、双 free / 堆损坏检测 |
| SIGILL | 4 | 非法指令:执行垃圾数据、特权指令在用户态执行、CPU 不支持的指令 |
| SIGFPE | 8 | 算术异常:整数除零、浮点异常(被屏蔽则不触发)、整数溢出(-ftrapv) |
| SIGTRAP | 5 | 断点/单步调试(gdb 的 int3)、__builtin_trap() |
| SIGSYS | 31 | 错误系统调用号(seccomp 过滤触发时常见) |
| SIGXCPU | 24 | CPU 时间超限(RLIMIT_CPU) |
| SIGXFSZ | 25 | 文件大小超限(RLIMIT_FSIZE) |
SIGSEGV vs SIGBUS 的核心区别:
| 场景 | 信号 |
|---|---|
*(int *)0 空指针解引用 | SIGSEGV |
mmap 一个文件,之后文件被 truncate,再访问已映射区域 | SIGBUS |
write() 到只读映射页 | SIGSEGV |
在 SPARC/ARM 上做非对齐 int 访问 | SIGBUS |
| x86-64 上非对齐访问(硬件自动处理,不触发信号) | 无 |
SIGABRT 的三个常见来源:
1. abort() → 用户主动调用
2. assert(ptr) 失败 → glibc 内部调用 abort()
3. glibc 堆检测失败 → "double free or corruption" → abort()第二类:终止 + 不 Core —— "优雅退出类"信号
默认动作是终止但不产生 core dump。这类信号通常用于进程管理和用户交互。
| 信号 | 编号 | 典型场景 |
|---|---|---|
| SIGTERM | 15 | kill 默认信号,"请优雅退出",程序可捕获做清理 |
| SIGINT | 2 | Ctrl+C,前端进程被中断,可捕获 |
| SIGQUIT | 3 | Ctrl+\,退出并 core(注意:这其实产 core,但默认也可捕获) |
| SIGHUP | 1 | 终端关闭 / 守护进程重新加载配置 |
| SIGPIPE | 13 | 向已关闭的管道/socket 写入 |
| SIGALRM | 14 | alarm(2) / setitimer() 超时 |
| SIGUSR1/USR2 | 10/12 | 用户自定义信号,无固定语义 |
| SIGPROF | 27 | ITIMER_PROF 超时(CPU 时间统计) |
| SIGVTALRM | 26 | ITIMER_VIRTUAL 超时(用户态 CPU 时间) |
| SIGPWR | 30 | UPS 即将断电,系统关机关联 |
SIGHUP 的两面性:
- 终端关闭:shell 退出时向前台进程发 SIGHUP → 默认终止
- daemon 重载:守护进程捕获 SIGHUP 来重新加载配置文件(如
nginx -s reload最终走到 SIGHUP)
SIGPIPE 的陷阱:
// 常见崩溃场景:写入已关闭的 socket/pipe
signal(SIGPIPE, SIG_IGN); // 不忽略 → 进程被 SIGPIPE 杀死
write(fd, buf, len); // fd 对端已关闭 → write 返回 -EPIPE → 内核发 SIGPIPE
// 如果不处理 SIGPIPE,进程直接终止第三类:不可捕获/忽略 —— "内核硬保证类"信号
这两个信号由内核强制保护,handler 带 SA_IMMUTABLE 标志,do_sigaction() 拒绝修改。
| 信号 | 编号 | 默认动作 | 为什么不可改 |
|---|---|---|---|
| SIGKILL | 9 | 终止(不可 core) | 保证总有一种方式杀死进程。D 状态(不可中断睡眠)除外 |
| SIGSTOP | 19 | 停止 | 保证总有一种方式暂停进程,ptrace/job control 的根本 |
// do_sigaction() 中的保护:
if (sig == SIGKILL || sig == SIGSTOP)
return -EINVAL; // 拒绝修改 handlerSIGKILL 和 D 状态:
kill -9不是万能钥匙。进程在不可中断睡眠(TASK_UNINTERRUPTIBLE)中时,信号留在 pending 里,等它从磁盘 I/O 等操作中醒来后才处理。
第四类:停止/继续 —— "作业控制类"信号
用于 shell 作业控制(Ctrl+Z / bg / fg):
| 信号 | 编号 | 含义 |
|---|---|---|
| SIGSTOP | 19 | 强制停止进程,不可捕获(SA_IMMUTABLE) |
| SIGTSTP | 20 | 终端停止(Ctrl+Z),可捕获 |
| SIGTTIN | 21 | 后台进程读终端 |
| SIGTTOU | 22 | 后台进程写终端 |
| SIGCONT | 18 | 继续被停止的进程 |
SIGCONT 的特殊语义:即使没有被停止的进程,SIGCONT 也会被忽略(默认 Ign)。它的真正作用是让 TASK_STOPPED 的进程回到 TASK_RUNNING,同时自动清除所有 pending 的 stop 信号(否则进程刚复活又被停住)。
第五类:默认忽略 —— "异步通知类"信号
这类信号的默认动作是 Ignore,程序通常按需捕获来接收异步事件:
| 信号 | 编号 | 典型用途 |
|---|---|---|
| SIGCHLD | 17 | 子进程状态变化(退出/停止/继续),waitpid() 的驱动信号 |
| SIGURG | 23 | 带外数据到达 socket(MSG_OOB) |
| SIGWINCH | 28 | 终端窗口大小改变 |
| SIGIO | 29 | 异步 I/O 事件(F_SETOWN + FASYNC) |
SIGCHLD 的实际行为:子进程退出时内核自动向父进程发 SIGCHLD。默认是 Ignore 所以不写 handler 也无害,但如果写了 handler,必须在 handler 里调用 waitpid() 回收子进程——否则变僵尸。
第六类:实时信号(SIGRTMIN ~ SIGRTMAX)
实时信号(34~64)是可靠、排队的信号。和标准信号的核心差异:
- 排队:来的 N 次各分配一个
sigqueue节点链入pending.list,依次投递。标准信号的位掩码同信号只记一次——来 100 次 SIGUSR1 只投递一次。 - 附带数据:
sigqueue(pid, sig, value)携带union sigval { int sival_int; void *sival_ptr; },handler 通过info->si_value拿到。 - 投递顺序:内核保证 RT 信号按到达顺序投递(同一个信号 FIFO),且 RT 信号优先于标准信号投递——
get_signal()先遍历pending.list(RT 信号的 sigqueue 链)再检查pending.signal位掩码(标准信号)。
// RT 信号带数据的发送与接收
union sigval value;
value.sival_int = 42; // 或 value.sival_ptr = &my_data;
sigqueue(pid, SIGRTMIN + 3, value);
// handler 里拿到:
void handler(int sig, siginfo_t *info, void *ctx) {
int my_value = info->si_value.sival_int; // 42
}一句话总结:信号先分两大阵营(标准不排队 / 实时排队可靠),再按来源分同步/异步(同步只找 faulting 线程、立刻投递,异步可任选线程、等下次回用户态),最后按默认动作分六类,排查崩溃时先对号入座。