信号:内核机制与数据结构 —— signal_struct / sighand_struct
这是 task-struct 里
task->signal/task->sighand那一项的展开。信号(signal)是内核给进程的异步软件中断。本篇是导读页:讲信号内核机制的骨架——处理动作表、未决信号集、屏蔽字、投递给"整个进程"还是"某个线程"、实时信号排队。至于哪些信号会导致崩溃、怎么从信号反推 bug(面向排查),见 signals,二者角度互补。
本篇导航(太长拆成了四篇,各自独立成章):
| 拆分篇 | 内容 |
|---|---|
| 信号分类与全景 | 64 个信号怎么分类:标准 vs 实时、同步 vs 异步、六类默认动作 |
| 信号的完整传递路径 | 从产生到 handler 执行再到恢复现场,六阶段逐段拆解 |
| 数据结构与接口全景 | 内核数据结构的逐字段注释 + 全部内核接口 |
| 信号的性能问题 | 一次投递的开销模型、siglock/FPU/穿越代价、替代方案 |
零、一句话认知:信号的内核状态分两半——共享的处理表 + 各线程的未决集
一个多线程进程,信号相关的内核状态拆成两层:
sighand_struct:信号处理动作表(每个信号号 → 默认/忽略/自定义 handler)。整个进程共享一份(CLONE_SIGHAND),所以所有线程用同一套 handler。signal_struct:线程组共享的信号状态(发给整个进程的未决信号、资源限制、计时器)。
此外每个线程(task)自己还有一个私有的未决信号集(发给特定线程的信号)和屏蔽字。

关键指针关系:
task_struct->sighand→ 同一个sighand_struct(CLONE_SIGHAND,所有线程共享 handler 表)task_struct->signal→ 同一个signal_struct(CLONE_THREAD,共享进程级未决信号)task_struct->pending—— 每线程私有,发给特定线程的信号落在这里task_struct->blocked—— 每线程私有,独立的信号屏蔽字signal_struct->thread_head—— 链表头,链起线程组内所有 task_struct
sighand_struct — 信号处理动作表(进程共享,所有线程同一套 handler)
sighand_struct 是全进程共享的信号处理动作表。各线程通过 task_struct->sighand 指针指向同一份实例——所有线程看到同一套 handler。
核心字段说明:
refcount_t count:引用计数,记录有多少线程共享这份动作表。clone(CLONE_SIGHAND) 时 +1,task 退出时 -1,归零后 kfree。这就是为什么线程可以安全退出——只要还有别的线程活着,sighand_struct 就不会被释放。struct k_sigaction action[_NSIG]:信号编号 1~64 的处置动作数组(_NSIG=64),全进程共享。action[SIGINT-1]就是 SIGINT 的处理方式(handler / SIG_DFL / SIG_IGN),action[SIGKILL-1]的处置带有 SA_IMMUTABLE 标志,不可修改。用户态 sigaction() 最终改写这里。spinlock_t siglock:保护全部信号状态的自旋锁——send_signal、do_sigaction、get_signal 全部先拿这把锁,是信号操作序列化的根。
// include/linux/sched/signal.h (简化, 仅核心字段)
struct sighand_struct {
refcount_t count; // 引用计数
struct k_sigaction action[_NSIG]; // 动作数组, _NSIG=64
spinlock_t siglock; // 保护全部信号状态的锁
};分析要点:
- "全进程共享一份"意味着:线程 A sigaction(SIGINT, ...) 之后,线程 B 收到 SIGINT 也走同一个 handler。不存在"每线程独立 handler"。
- count 的引用计数机制保证:只要还有任一线程活着,sighand_struct 就不会被提前释放。
- siglock 序列化全部信号操作——这就是"同一时刻每个线程组至多一个线程在处理信号路径"的原因。
signal_struct — 线程组共享的信号状态(发给整个进程的未决信号 + 资源限制)
signal_struct 是线程组共享的信号状态容器。各线程通过 task_struct->signal 指针指向同一份实例。
核心字段说明:
refcount_t sigcnt:引用计数,记录有多少线程引用此 signal_struct。struct sigpending shared_pending(★核心):发给"整个进程"的信号落地处。kill(pid)发信号时,pending 放在这里,再由 complete_signal() 选一条线程处理。而tkill(tid)则写目标线程的私有 pending,不走这里。这是信号机制最核心的分野:
kill(pid) → shared_pending → 任选一条线程处理
pthread_kill → task->pending → 就是发给这条线程
sigqueue(pid)→ shared_pending → RT 信号可排队int group_exit_code:组退出码,SIGKILL 时记录。struct rlimit rlim[RLIM_NLIMITS]:进程级资源限制。SIGXCPU(CPU 超限)和 SIGXFSZ(文件过大)的触发依赖这里——内核在资源超限的检查点 send_signal 到当前进程。struct list_head thread_head:线程组链表头。complete_signal() 遍历它来找合适的投递目标——找一条!(sig blocked) && 可唤醒的线程。如果全部屏蔽,信号留在 shared_pending 等人解除屏蔽。
// include/linux/sched/signal.h (简化, 仅核心字段)
struct signal_struct {
refcount_t sigcnt; // 引用计数
struct sigpending shared_pending; // 发给线程组的未决信号集
int group_exit_code; // 组退出码
int group_stop_count; // 正在执行组停止的线程数
struct rlimit rlim[RLIM_NLIMITS]; // 进程级资源限制
struct hlist_head posix_timers; // timer_create() 定时器链表
struct list_head thread_head; // 线程组链表头
};每线程 (task_struct) 私有的信号字段 — private pending + blocked
每个线程(task_struct)除了通过指针共享 sighand_struct 和 signal_struct,还拥有私有的信号状态:
核心字段说明:
struct sigpending pending:本线程的未决信号集。pthread_kill(tid, sig)经由 send_signal(PIDTYPE_PID) 写到这里。包含两部分:bitmask(sigset_t 位掩码,每种信号是否有未决)和 list(sigqueue 链表,RT 信号的排队队列;普通信号不分配 sigqueue)。sigset_t blocked:本线程当前屏蔽的信号位掩码。sigprocmask() / pthread_sigmask() 直接改写这里。核心语义:如果sig & blocked,该信号不会在本线程被投递,留在 pending 里等人解除屏蔽。struct sighand_struct *sighand:指向进程共享的处理动作表(action[64] 数组)。struct signal_struct *signal:指向线程组共享的状态(shared_pending / thread_head)。
// include/linux/sched.h (task_struct 中与信号相关的私有字段)
struct task_struct {
// ...
struct sigpending pending; // 本线程的未决信号集
sigset_t blocked; // 信号屏蔽字
struct sighand_struct *sighand; // → 共享的 action[64] 数组
struct signal_struct *signal; // → shared_pending / thread_head
// ...
};分析要点:
- 三级未决信号的查询顺序(get_signal() dequeue 的遍历优先级)——这决定了"专门发给线程的信号优先于发给进程的,RT 信号优先于普通信号":
① task->pending.list (RT 信号, 本线程)
② signal->shared_pending.list (RT 信号, 线程组)
③ task->pending.signal (普通信号, 本线程, 位掩码)
④ signal->shared_pending.signal(普通信号, 线程组)- **"屏蔽 ≠ 丢弃" **:sig 被 blocked 只是暂不投递,信号留在 pending 里。解除屏蔽(sigprocmask(SIG_UNBLOCK))后,recalc_sigpending() 检查
pending & ~blocked,如果有就 set TIF_SIGPENDING,下次返回用户态投递。这就是为什么 SIGKILL 不可屏蔽——内核不检查 blocked,直接 force_sig。 - sighand 和 signal 都是指针:两个不同的 task_struct 可以指向同一个 sighand_struct(CLONE_SIGHAND)和 signal_struct(CLONE_THREAD)。这就是"线程"和"进程"的资源共享本质——看 clone_flags。
核心记忆:处理动作(怎么处理)全进程共享一份;未决信号(有哪些待处理)分两处——发给进程的放共享集、发给线程的放私有集;屏蔽字每线程独立。 这套划分决定了"信号送给谁、哪个线程执行 handler"。
三、信号的默认动作与三种处置
进程对每个信号可以选三种处置(存在 sighand_struct 的动作表里):
| 处置 | 含义 |
|---|---|
| SIG_DFL | 默认动作(Term 终止 / Core 终止+转储 / Ign 忽略 / Stop 暂停) |
| SIG_IGN | 忽略该信号 |
| 自定义 handler | 执行你 sigaction 注册的函数 |
- 默认动作的四类(Term/Core/Ign/Stop)及哪些信号产 core,见 signals。
- 六类默认动作的完整划分与 64 个信号的逐一归类,见 信号分类与全景。
SIGKILL(9) 和SIGSTOP(19) 无法被捕获/忽略/屏蔽——内核硬保证,这样kill -9总能杀掉进程(D 状态不可中断睡眠除外,见 scheduling)。
四、一个信号的生命周期:产生 → 未决 → 投递 → 处理

- 产生(generate):kill/
tgkill、CPU 异常(缺页无法满足→SIGSEGV,见 signals)、内核事件(子进程退出→SIGCHLD)。信号被置位到未决集。 - 未决(pending):普通信号的未决集是位集合——同一种信号来多次只记一位,不排队(可能"丢")。
- 投递(deliver):目标线程从内核态返回用户态的那一刻检查未决且未屏蔽的信号,执行处置。所以信号不是"立刻打断",而是"下次回用户态时处理"。
- 屏蔽(block):
sigprocmask/pthread_sigmask设置的屏蔽字里被挡住的信号,留在未决集,解除屏蔽后才投递。
从产生到恢复现场的六阶段完整路径(send_signal → complete_signal → 伪造栈帧 → sigreturn),见 信号的完整传递路径。
五、多线程:信号送给哪个线程?
这是信号在多线程下最容易错的点:
| 发送方式 | 送到 | 谁处理 |
|---|---|---|
kill(pid) / 终端 Ctrl-C(SIGINT) | 进程(shared pending) | 任选一个没屏蔽该信号的线程处理 |
pthread_kill(tid) / tgkill | 特定线程(private pending) | 那个指定线程处理 |
| 同步异常(SIGSEGV/SIGFPE) | 触发异常的那个线程 | 该线程自己 |
- 进程级信号任选线程:所以多线程程序常用一个专门线程处理信号(其余线程屏蔽它),避免不确定性——
sigwait+ 专职线程是常见模式。 - fork 只带走调用线程、继承信号 handler 但清空未决集的坑,见 fork-and-threads。
深入阅读:signal-multithread —— 把本节拆开讲透:
complete_signal()怎么遍历线程组选线程、TIF_SIGPENDING标志到setup_rt_frame伪造返回路径的完整时序(含内核↔用户态切换每一帧)、sigreturn 恢复现场机制、三种编程模式(sigwait/指定线程/默认)。
六、实时信号:会排队的信号
- 普通信号(1–31):未决集是位集合,不排队——屏蔽期间同种信号来 100 次,解除后只投递 1 次。
- 实时信号(SIGRTMIN–SIGRTMAX,32–64):会排队,每个都投递,还能带一个数据值(
sigqueue)。用于不能丢事件的场景。
实时信号与标准信号的完整对比、glibc 的 RT 范围、排队实现细节,见 信号分类与全景。
八、观测
cat /proc/<pid>/status | grep -E 'Sig|Shd'
# SigBlk: 屏蔽字(blocked) SigIgn: 忽略集 SigCgt: 已捕获(有 handler)集 SigPnd/ShdPnd: 未决集
# 十六进制位掩码,每一位对应一个信号号
kill -l # 列出所有信号号↔名字SigCgt(caught)的位 = 装了自定义 handler 的信号;SigBlk= 当前屏蔽的。排查"信号为什么没反应"先看这两个。ShdPnd(进程共享未决)/SigPnd(本线程未决)不为 0 = 有信号卡在未决(可能被屏蔽了)。- 用 perf 观测信号吞吐与 siglock 争用(
signal:signal_generate等事件),见 信号的性能问题。
九、和其它文档的关系
- 父篇:task-struct(signal/sighand 是 task 的资源对象)。
- 本篇拆分篇:分类 · 传递路径 · 数据结构 · 性能。
- 姊妹篇:signals-user —— 用户态编程指南:API 速查 + 十大关键坑 + 5 个实战 Demo。本篇讲内核机制,那篇讲"怎么写信号代码"。
- 性能替代篇:signals-alternatives —— 为什么不直接用信号?signalfd / eventfd / timerfd / 共享内存+eventfd / futex 各自的性能来源(逐项对比内核代码路径),含量化表和选择决策树。
- 崩溃排查视角:signals(哪些信号杀进程、产 core、反推 bug)——本篇讲机制,那篇讲排查。
- 多线程 fork 的信号坑:fork-and-threads。
- 投递时机:scheduling(返回用户态时检查)、interrupts(信号是"软件中断",与硬件中断对照)、syscall(内核态返回时处理)。
- 不可中断睡眠:scheduling(D 状态连 SIGKILL 都不理)。
十、一句话总结
信号的内核状态分两半:处理动作表
sighand_struct(全进程共享一套 handler,sigaction 改它)+ 未决信号(发给进程的进共享集、发给线程的进私有集)+ 每线程独立的屏蔽字。一个信号经历产生→置位未决→(线程返回用户态时)检查未屏蔽则投递→执行处置(默认/忽略/handler);普通信号未决集是位集合不排队,实时信号会排队。多线程下 kill(pid) 任选一个没屏蔽的线程处理(故常用专职线程 sigwait),pthread_kill 定向,同步异常给触发线程。SIGKILL/SIGSTOP 不可捕获。透过/proc/<pid>/status的 SigBlk/SigCgt/SigPnd 观测。