﻿# 信号：内核机制与数据结构 —— signal_struct / sighand_struct

> 这是 [../task-struct.md](/concepts/process/task-struct.md) 里 `task->signal` / `task->sighand` 那一项的展开。信号(signal)是内核给进程的**异步软件中断**。本篇讲**内核怎么实现信号**:处理动作表、未决信号集、屏蔽字、投递给"整个进程"还是"某个线程"、实时信号排队——即信号机制的**数据结构与流程**。至于**哪些信号会导致崩溃、怎么从信号反推 bug**(面向排查),见 [../../crash/signals.md](/crash/signals.md),二者角度互补。

## 零、一句话认知：信号的内核状态分两半——共享的处理表 + 各线程的未决集

一个多线程进程,信号相关的内核状态拆成两层：

- **`sighand_struct`**:信号**处理动作表**(每个信号号 → 默认/忽略/自定义 handler)。**整个进程共享一份**(`CLONE_SIGHAND`),所以所有线程用同一套 handler。
- **`signal_struct`**:线程组**共享**的信号状态(发给整个进程的未决信号、资源限制、计时器)。

此外**每个线程(task)自己**还有一个**私有的未决信号集**(发给特定线程的信号)和**屏蔽字**。

```plantuml
@startuml
skinparam class {
    BorderColor #333
    HeaderBackgroundColor #E8E8E8
}
skinparam package {
    BorderColor #666
    FontStyle bold
}
package "一个多线程进程" {
    class "task_struct\n(线程 A)" as tA {
        + pending : sigpending  <<私有>>
        + blocked : sigset_t    <<私有>>
    }
    class "task_struct\n(线程 B)" as tB {
        + pending : sigpending  <<私有>>
        + blocked : sigset_t    <<私有>>
    }
    class "sighand_struct\n<<全进程共享一份>>" as sighand {
        + count : refcount_t
        + action[64] : k_sigaction
        + siglock : spinlock_t
    }
    class "signal_struct\n<<全进程共享一份>>" as signal {
        + sigcnt : refcount_t
        + shared_pending : sigpending
        + thread_head : list_head
        + rlim[] : rlimit
        + group_exit_code : int
    }
    class "sigpending\n(每线程私有)" as pending {
        + list : list_head
        + signal : sigset_t
    }
    class "k_sigaction" as ksa {
        + sa : sigaction
        + flags : unsigned int
    }
    tA --> sighand : sighand
    tA --> signal : signal
    tA --> pending : 私有
    tB --> sighand : sighand
    tB --> signal : signal
    tB --> pending : 私有
    signal --> signal : thread_head\n(链起所有线程)
    sighand --> ksa : action[0..63]
}
note right of tA
  pthread_kill(tid, sig)
  信号落在这里
end note
note right of signal
  kill(pid) 信号落在这里
  complete_signal() 遍历
  thread_head 选线程投递
end note
@enduml
```

**关键指针关系：**

- `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 全部先拿这把锁，是信号操作序列化的根。

```c
// include/linux/sched/signal.h (简化, 仅核心字段)
struct sighand_struct {
    refcount_t              count;          // 引用计数
    struct k_sigaction      action[_NSIG];  // 动作数组, _NSIG=64
    spinlock_t              siglock;        // 保护全部信号状态的锁
};
```

**分析要点：**

1. "全进程共享一份"意味着：线程 A sigaction(SIGINT, ...) 之后，线程 B 收到 SIGINT 也走同一个 handler。不存在"每线程独立 handler"。
2. count 的引用计数机制保证：只要还有任一线程活着，sighand_struct 就不会被提前释放。
3. 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，不走这里。这是信号机制最核心的分野：
```bash
  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 等人解除屏蔽。

```c
// 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）。

```c
// 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
    // ...
};
```

**分析要点：**

1. **三级未决信号的查询顺序**（get_signal() dequeue 的遍历优先级）——这决定了"专门发给线程的信号优先于发给进程的，RT 信号优先于普通信号"：
```bash
   ① task->pending.list          (RT 信号, 本线程)
   ② signal->shared_pending.list (RT 信号, 线程组)
   ③ task->pending.signal         (普通信号, 本线程, 位掩码)
   ④ signal->shared_pending.signal(普通信号, 线程组)
   ```

2. **"屏蔽 ≠ 丢弃" **：sig 被 blocked 只是暂不投递，信号留在 pending 里。解除屏蔽（sigprocmask(SIG_UNBLOCK)）后，recalc_sigpending() 检查 `pending & ~blocked`，如果有就 set TIF_SIGPENDING，下次返回用户态投递。这就是为什么 SIGKILL 不可屏蔽——内核不检查 blocked，直接 force_sig。
3. **sighand 和 signal 都是指针**：两个不同的 task_struct 可以指向同一个 sighand_struct（CLONE_SIGHAND）和 signal_struct（CLONE_THREAD）。这就是"线程"和"进程"的资源共享本质——看 clone_flags。

> **核心记忆**:**处理动作(怎么处理)全进程共享一份;未决信号(有哪些待处理)分两处——发给进程的放共享集、发给线程的放私有集;屏蔽字每线程独立。** 这套划分决定了"信号送给谁、哪个线程执行 handler"。

---

## 零·五、信号分类与全景：1~64 号信号

### 总览：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 个。

```plantuml
@startuml
skinparam class {
    BorderColor #333
    HeaderBackgroundColor #E8E8E8
}
class "Linux 信号编号空间 (1~64)" as sigspace #Transparent
package "标准信号\n1~31 (位掩码, 不排队)" as standard {
    class hup as "1  SIGHUP"      #FFCCCC
    class int as "2  SIGINT"      #FFCCCC
    class quit as "3  SIGQUIT"    #FFCCCC
    class ill as "4  SIGILL"      #FF9999
    class trap as "5  SIGTRAP"    #FFCCCC
    class abrt as "6  SIGABRT"    #FF9999
    class bus as "7  SIGBUS"      #FF9999
    class fpe as "8  SIGFPE"      #FF9999
    class kill as "9  SIGKILL"    #CC0000
    class usr1 as "10 SIGUSR1"    #CCFFCC
    class segv as "11 SIGSEGV"    #FF9999
    class usr2 as "12 SIGUSR2"    #CCFFCC
    class pipe as "13 SIGPIPE"    #FFCCCC
    class alrm as "14 SIGALRM"    #FFCCCC
    class term as "15 SIGTERM"    #FFCCCC
    class stk as "16 SIGSTKFLT"   #FFCCCC
    class chld as "17 SIGCHLD"    #CCFFFF
    class cont as "18 SIGCONT"    #CCFFFF
    class stop as "19 SIGSTOP"    #CC0000
    class tstp as "20 SIGTSTP"    #CCFFFF
    class ttin as "21 SIGTTIN"    #CCFFFF
    class ttou as "22 SIGTTOU"    #CCFFFF
    class urg as "23 SIGURG"      #CCCCFF
    class xcpu as "24 SIGXCPU"    #FFCCCC
    class xfsz as "25 SIGXFSZ"    #FFCCCC
    class vtal as "26 SIGVTALRM"  #FFCCCC
    class prof as "27 SIGPROF"    #FFCCCC
    class winch as "28 SIGWINCH"  #CCCCFF
    class io as "29 SIGIO"        #CCCCFF
    class pwr as "30 SIGPWR"      #FFCCCC
    class sys as "31 SIGSYS"      #FF9999
}
package "实时信号\n34~64 (排队, 可靠)" as rt {
    class rtrange as "34~64\nSIGRTMIN~\nSIGRTMAX" #CCE5FF
}
note right of standard
  #FF9999  → 默认 Core (触发崩溃)
  #FFCCCC  → 默认 Term (终止进程)
  #CC0000  → 不可捕获/忽略
  #CCFFFF  → 默认 Stop/Cont
  #CCFFCC  → 默认 Term (用户自定义用途)
  #CCCCFF  → 默认 Ign (异步通知)
end note
note right of rt
  每个实例分配 sigqueue
  按到达顺序 FIFO 投递
  可携带附加数据
end note
@enduml
```

### 信号分类（按产生来源）：同步信号 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 为例）：

```bash
用户态: 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()  ← 直接死在这里
```

```bash
关键区别：
  ┌──────────────────────────────────────────────────────┐
  │  同步信号: 内核入口(异常) → force_sig → 在同一趟    │
  │             出口路径上投递 → 不进用户态直接处理      │
  │                                                      │
  │  异步信号: 内核入口(syscall/中断) → send_signal     │
  │            → 设置 TIF_SIGPENDING → 返回用户态        │
  │            → 用户代码继续跑 → 下次内核入口时          │
  │            → 在出口路径上检测到 TIF_SIGPENDING       │
  │            → 投递                                     │
  └──────────────────────────────────────────────────────┘
```

> **为什么同步信号不经过"下次返回用户态"的等待？** 因为触发同步信号的指令本身就是从用户态进来的（异常入口），异常处理完成后必然要从内核返回到用户态——这个"出口"天然存在，信号可以直接在这一趟出口路径上投递，不需要再等"下一次"。

**异步信号的目标线程选择**（`complete_signal()` 的逻辑）：

```c
// 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 的三个常见来源**：

```bash
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 的陷阱**：

```c
// 常见崩溃场景：写入已关闭的 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 的根本 |

```c
// do_sigaction() 中的保护:
if (sig == SIGKILL || sig == SIGSTOP)
    return -EINVAL;  // 拒绝修改 handler
```

> **SIGKILL 和 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` 位掩码（标准信号）。

```c
// 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
}
```

---

## 一、信号的完整传递路径：从产生到 handler 执行再到恢复

> 信号不是"魔法般地"立刻跳到你的 handler 函数——内核需要经历六个阶段：**① 产生** → `send_signal` 将信号置入未决集；**② 选目标** → `complete_signal` 设 `TIF_SIGPENDING` 标志并唤醒目标线程；**③ 内核退出** → `exit_to_user_mode_loop` 检查到标志 → `do_signal`；**④ 伪造栈帧** → `handle_signal` → `setup_rt_frame` 在用户栈上构建 sigframe（保存完整 pt_regs + 返回地址指向 sigreturn）；**⑤ 跳转执行** → 修改 `pt_regs` 让 CPU "以为"即将返回的地址就是 handler；**⑥ 恢复现场** → handler 返回时经由 VDSO sigreturn trampoline → `sys_rt_sigreturn` → 将保存的 pt_regs 写回 → iret/sysret 回到原始用户代码。

### 1.1 全景架构图：一次信号传递的所有角色与数据流

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<user>> #C8E6C9
  BorderColor<<user>>   #388E3C
  BackgroundColor<<kern-send>> #BBDEFB
  BorderColor<<kern-send>>   #1976D2
  BackgroundColor<<kern-signal>> #E1BEE7
  BorderColor<<kern-signal>>  #7B1FA2
  BackgroundColor<<kern-exit>> #FFCCBC
  BorderColor<<kern-exit>>  #E64A19
  BackgroundColor<<ustack>> #FFF9C4
  BorderColor<<ustack>>  #F9A825
}
' === 产生源 ===
rectangle "发送方\nkill() / tkill()\n内核异常 / 内核事件" <<user>> as SENDER
' === 内核投递路径 ===
rectangle "send_signal()\n→ __send_signal()\n  ① 置位 pending set\n  ② RT 信号: 分配 sigqueue 入队列\n  ③ 不屏蔽 → complete_signal()" <<kern-send>> as SEND
rectangle "complete_signal()\n  · 选目标线程(遍历线程组)\n  · set_tsk_thread_flag(\n      TIF_SIGPENDING)\n  · signal_wake_up() →\n     wake_up_state(TASK_INTERRUPTIBLE)" <<kern-send>> as COMPLETE
' === 目标线程 ===
rectangle "目标线程\n(正在运行或睡眠)" <<user>> as TARGET
rectangle "TIF_SIGPENDING\n(per-task flag)\n钉在目标线程上\n\"你有信号待处理\"" <<kern-signal>> as TIF
' === 内核退出路径 ===
rectangle "exit_to_user_mode_loop()\n内核→用户态的必经之路\n检查 TIF_SIGPENDING\n→ arch_do_signal_or_restart()\n  → do_signal()\n    → get_signal() 取出信号" <<kern-exit>> as EXIT
rectangle "handle_signal()\n→ setup_rt_frame()\n  ① 在用户栈分配 rt_sigframe\n  ② 保存 pt_regs → uc_mcontext\n  ③ 设置返回地址 → VDSO sigreturn\n  ④ 重写 pt_regs:\n     ip = handler 地址\n     sp = 用户栈 sigframe 顶部" <<kern-exit>> as HANDLE
' === 用户态 ===
rectangle "用户栈\n┌─────────────┐\n│ rt_sigframe  │\n│ ├─ pretcode  │ → VDSO __vdso_rt_sigreturn\n│ ├─ siginfo_t │\n│ ├─ ucontext_t│ → 保存的寄存器\n│ └─ fpstate   │\n└─────────────┘" <<ustack>> as USTACK
rectangle "信号处理函数\nvoid handler(int sig,\n  siginfo_t *info,\n  void *ucontext)" <<user>> as HANDLER_FN
rectangle "rt_sigreturn 系统调用\n恢复保存的 pt_regs\n→ iret/sysret 回到\n  原始被中断的用户代码" <<kern-exit>> as SIGRETURN
rectangle "原始用户代码\n(信号到来前正在\n 执行的指令位置)" <<user>> as ORIGINAL
' === 数据流箭头 ===
SENDER -down-> SEND : ① 产生信号\n(kill/tkill/异常/内核事件)
SEND -down-> COMPLETE : ② 信号未屏蔽\n选目标 + 唤醒
COMPLETE -[#E64A19,dashed,thickness=2]down-> TIF : ③ 设 TIF_SIGPENDING
TIF -right-> TARGET : 钉在目标线程上
COMPLETE -[#E64A19]down-> EXIT : ④ 唤醒后(或下次 syscall 返回时)\n检查 TIF_SIGPENDING
EXIT -down-> HANDLE : ⑤ get_signal() 取信号\n进入 handle_signal()
HANDLE -right-> USTACK : ⑥ setup_rt_frame()\n在用户栈构建 sigframe
note right of USTACK : ★ pt_regs 完整寄存器\n被保存在 uc_mcontext 中\n返回地址指向 VDSO sigreturn
HANDLE -down-> HANDLER_FN : ⑦ 修改 pt_regs.ip = handler\n返回用户态即进入 handler
HANDLER_FN -down-> SIGRETURN : ⑧ handler 返回\npretcode → VDSO trampoline\n→ sys_rt_sigreturn
SIGRETURN -down-> ORIGINAL : ⑨ 恢复 pt_regs\niret/sysret 回到\n原始用户代码
@enduml
```

六个阶段映射到内核函数调用链：

| 阶段 | 内核函数 | 做的事情 |
|------|---------|---------|
| ① 产生 | `sys_kill → kill_pid_info → group_send_sig_info → send_signal` | 将信号置入目标进程/线程的 pending set |
| ② 选目标 | `complete_signal → signal_wake_up` | 遍历线程组选一个未屏蔽的线程，设 TIF_SIGPENDING，若睡眠则唤醒 |
| ③ 退出检查 | `exit_to_user_mode_loop → arch_do_signal_or_restart → do_signal` | 内核返回用户态前检查 thread flag，发现 TIF_SIGPENDING → 进入信号处理 |
| ④ 伪造栈帧 | `handle_signal → setup_rt_frame` | 在用户栈上构建 rt_sigframe，保存 pt_regs，设置返回地址为 VDSO sigreturn |
| ⑤ 跳转执行 | `setup_rt_frame` 修改 `pt_regs` → iret/sysret | CPU "误以为"返回目标是 handler 函数，实际跳转到 signal handler |
| ⑥ 恢复现场 | `handler ret → __vdso_rt_sigreturn → sys_rt_sigreturn` | 将保存的 pt_regs 写回，CPU 回到原始被中断的代码位置 |

> **一句话**：信号的处理**不发生在信号产生的时刻**——它发生在目标线程**下一次从内核态返回用户态的那一刻**。这是理解信号"异步但非抢占"特性的关键。内核通过**伪造用户栈帧 + 修改 pt_regs + sigreturn 恢复**这套机制，让用户 handler 看起来像是被"插入"到正常执行流中。

**关于"伪造栈帧"的澄清**——handler 本身**在用户态执行**，内核不跑 handler。但内核必须**写入用户栈**来架设跳板：`setup_rt_frame` 使用 `put_user()` / `__put_user()` 将 `rt_sigframe`（含保存的寄存器、siginfo、返回地址）写到用户栈上，然后改写 `pt_regs->sp` 和 `pt_regs->ip`。这样 iret/sysret 返回用户态时，CPU 自然跳入 handler。唯一的例外是设置了 `SA_ONSTACK` + `sigaltstack()`——此时 sigframe 写在**备用信号栈**上，不碰主线程栈，但本质还是写入用户态内存。

---

### 1.2 阶段详解①：信号如何到达目标进程 —— send_signal → complete_signal

信号的三类来源最终都汇入同一个函数 `send_signal()`：

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor     #1976D2
  ArrowColor                 #37474F
  LifeLineBackgroundColor    #FAFAFA
}
title send_signal —— 三类来源归一
actor "用户态进程" as U
participant "sys_kill(pid, sig)" as KILL
participant "sys_tkill(tid, sig)" as TKILL
participant "内核异常处理\nforce_sig(SIGSEGV)" as EXC
participant "内核事件\nsend_sig(SIGCHLD)" as EVT
participant "__send_signal()\n★ 核心：操作 pending set" as SS
participant "目标 pending set\n(sigset_t 位掩码)" as PEND
participant "complete_signal()\n★ 核心：选目标 + 设标志" as CS
participant "target thread_struct\nTIF_SIGPENDING flag" as TIF
participant "目标线程" as TGT
== 路径 A：用户态 kill() ==
U -> KILL : kill(pid, SIGUSR1)
KILL -> KILL : kill_something_info()
KILL -> KILL : kill_pid_info()
KILL -> SS : group_send_sig_info(SIGUSR1)
== 路径 B：用户态 tkill() / tgkill() ==
U -> TKILL : tkill(tid, SIGUSR2)
TKILL -> TKILL : do_tkill()
TKILL -> SS : specific_send_sig_info(SIGUSR2,\n  task=特定线程)
== 路径 C：内核异常 ==
EXC -> EXC : do_page_fault → ...\nforce_sig(SIGSEGV)
EXC -> SS : force_sig(SIGSEGV)
== 路径 D：内核事件 ==
EVT -> EVT : do_exit → do_notify_parent(SIGCHLD)
EVT -> SS : send_sig(SIGCHLD, parent)
== __send_signal 核心逻辑 ==
SS -> SS : ① 检查：信号是否被忽略(SIG_IGN)?\n② 检查：是否来自同一用户(权限)?
SS -> PEND : ③ 置位 pending set\n普通信号: sigaddset(&pending->signal, sig)\n实时信号: 分配 sigqueue, list_add_tail
SS -> CS : ④ 信号未被屏蔽 → 调 complete_signal()
== complete_signal 核心逻辑 ==
note over CS : 发给进程: 遍历线程组\n找第一个未屏蔽的线程\n\n发给线程: 直接就是那个线程
CS -> TIF : ⑤ set_tsk_thread_flag(\n  t, TIF_SIGPENDING)
CS -> TGT : ⑥ signal_wake_up(t, ...)\n若线程睡眠(TASK_INTERRUPTIBLE)\n→ wake_up_state() 唤醒\n若正在运行 → 靠 TIF_SIGPENDING\n  下次返回用户态时被检查到
note over TIF : TIF_SIGPENDING 只是一比特 flag\n内核在返回用户态时检查它\n详见 §1.3
@enduml
```

**关键细节——`__send_signal()` 做了什么**：

1. **权限检查**：`si_fromuser` 为真时，检查发送方是否有权限给对方进程发信号（跨用户时，只有 `SIGKILL`/`SIGCONT` 等有限信号能通过）。
2. **忽略信号**：若目标对此信号处置为 `SIG_IGN` 且不是 `SIGKILL`/`SIGSTOP`——直接返回，不放入 pending。
3. **置位 pending**：
   - **普通信号 (1~31)**：未决集是 `sigset_t` 位掩码——`sigaddset(&pending->signal, sig)` 置一位。**同种信号来 N 次也只记一位**（不排队）。
   - **实时信号 (SIGRTMIN~SIGRTMAX)**：分配 `struct sigqueue`，链入 pending 链表。**每个都排队**，可附带 `sigqueue` 传来的 int/pointer。
4. **是否立即投递**：若信号被目标线程的 blocked 屏蔽字挡住 → 留在 pending，不调 `complete_signal`。等目标 `sigprocmask` 解除屏蔽时再补投。若未屏蔽 → 调 `complete_signal`。

**`complete_signal()` 选目标线程的规则**：

- **发给整个进程 (`group_send_sig_info`)**：遍历线程组链表 → 找**第一个**满足 `!(signal 被 blocked) && (线程可被唤醒)` 的线程。如果所有线程都屏蔽了 → 留在 shared_pending，等解除屏蔽。遍历逻辑展开见 [`signal-multithread.md`](/concepts/process/signal-multithread.md)。
- **发给特定线程 (`specific_send_sig_info`)**：直接操作该线程的 private pending，略过选线程步骤。
- **同步异常 (SIGSEGV/SIGFPE 等)**：刚产生时就已绑定当前线程（`force_sig` 直接操作 `current`），不存在"选"的问题。

**唤醒机制**：

```bash
signal_wake_up(t, ...)
  → 若 task 处于 TASK_INTERRUPTIBLE 睡眠
      → wake_up_state(t, TASK_INTERRUPTIBLE)
          → try_to_wake_up(t)
              → 把 t 移到运行队列
              → 如果 t 优先级高于 current → 设 NEED_RESCHED
  → 若 task 正在运行
      → 什么也不做——TIF_SIGPENDING 已在 complete_signal 阶段
        设好，目标线程下一次退出内核时会自动检查到
```

> **关键理解**：`kill()` 返回成功 ≠ 信号已被目标处理。kill() 只是在 pending set 中置了一位——目标可能正在内核态执行，信号会等到它返回用户态时才被处理（§1.3）。甚至目标阻塞了该信号，信号会一直卡在 pending 里。

---

### 1.3 阶段详解②③：信号的"检查窗口"——只有从内核返回用户态时才处理

信号传递最反直觉的一点：**即使目标线程正在 CPU 上运行用户代码，信号也不会立刻打断它**。信号处理只在以下窗口发生：

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E8F5E9
  ParticipantBorderColor     #388E3C
  ArrowColor                 #37474F
}
title 信号处理的唯一窗口：内核态 → 用户态的切换点
participant "用户态" as USER
participant "内核态" as KERN
participant "exit_to_user_mode_loop" as EXIT
participant "do_signal / get_signal" as DS
participant "handle_signal" as HS
== 场景 1：系统调用返回时 ==
USER -> KERN : syscall 陷入内核
KERN -> KERN : 执行系统调用逻辑
KERN -> EXIT : 准备返回用户态
EXIT -> EXIT : if (ti_work & _TIF_SIGPENDING)\n  → arch_do_signal_or_restart()
note right : ★ 就在这个 if 里发现信号
EXIT -> DS : do_signal(regs)
DS -> DS : get_signal() → 从 pending 中取信号\n检查 & ~blocked, 检查 SIG_DFL/SIG_IGN
DS -> HS : handle_signal(sig, ...)
HS -> USER : 跳转到 handler 执行 (详见 §1.4-1.6)
USER -> USER : handler 执行...
USER -> KERN : ret → VDSO trampoline\n→ sys_rt_sigreturn
KERN -> USER : 恢复现场, 回到原始代码
== 场景 2：中断/异常返回时 ==
USER -> KERN : 硬件中断 / 缺页异常
KERN -> KERN : 处理中断/异常
KERN -> EXIT : 准备返回用户态
note right : 与 syscall 返回走完全相同的\nexit_to_user_mode_loop 路径
== 场景 3：被唤醒时 ==
KERN -> KERN : 目标线程处于睡眠 (TASK_INTERRUPTIBLE)
note over KERN : signal_wake_up 唤醒目标线程
KERN -> EXIT : 调度器选中 → 返回用户态前
note right : 同样是 exit_to_user_mode_loop 路径
== 场景 4：被抢占后重新调度时 ==
KERN -> KERN : 时间片用完 / 更高优先级线程就绪
note over KERN : 内核抢占 → schedule()
KERN -> EXIT : 再次被调度 → 返回用户态前
note right : 如果 TIF_SIGPENDING 在此期间被设上\n在返回用户态前会被检查到
@enduml
```

**四个窗口的核心结论**：

| 窗口 | 触发条件 | TIF_SIGPENDING 何时被检查 |
|------|---------|--------------------------|
| 系统调用返回 | 目标线程主动调用任意 syscall (`read`, `write`, `nanosleep`, ...) | syscall 执行完毕后，`exit_to_user_mode_loop` |
| 中断返回 | 硬件中断/异常将线程"拉入"内核 | 中断/异常处理完成后 |
| 被唤醒 | 目标线程正以 `TASK_INTERRUPTIBLE` 睡眠，`signal_wake_up` 唤醒它 | 调度器将其重新投入 CPU 后，返回用户态前 |
| 重新调度 | 时间片用完后被重新调度 | 同"中断返回"路径 |

**为什么不在运行用户代码时立刻打断？**

信号不是硬件中断——它不走 IDT、不触发中断门、没有硬件级的上下文保存。如果把信号做成抢占式的（任何时刻都能打断用户代码），需要硬件在每条指令后检查信号位，这在 x86 上不可行。内核的选择是：**只在用户→内核→用户这个必经的瓶颈点检查 TIF_SIGPENDING flag**，代价小、实现简单、和中断返回路径复用同一套检查逻辑。

> **一句话**：说信号是"异步"的，不是说它能随时打断任何代码——而是说**它的产生和它的处理不在同一个时刻**。产生由发送方触发（可能在任何上下文），处理被推迟到目标线程退出内核态的那一刻。

---

### 1.4 阶段详解④⑤：内核如何"跳"到用户态的 handler —— handle_signal → setup_rt_frame

这是信号机制中最精妙的部分。内核并不直接"调用"用户态函数——它在用户栈上伪造一个栈帧，然后修改 CPU 寄存器让返回用户态时"恰好"跳转到 handler。

**前提：当前的寄存器现场 (pt_regs)**

当线程因系统调用/中断/异常进入内核时，硬件和软件在内核栈上保存了完整的寄存器快照 `struct pt_regs`（x86-64 上约 23 个寄存器：R15..R8, RDI, RSI, RBP, RBX, RDX, RCX, RAX, RSP, SS, RFLAGS, CS, RIP, ...）。**内核要返回用户态时，会从这个 `pt_regs` 恢复寄存器，然后 `iretq`/`sysret` 跳转到 `pt_regs->ip` 指定的地址**。思路很简单：**改 `pt_regs->ip`，就能控制 CPU 返回用户态后去哪**。

```bash
正常返回路径：
  pt_regs->ip = syscall 下一条指令 (用户代码)
  pt_regs->sp = 原始用户栈指针
  → iretq/sysret 回到原来的位置继续执行
信号投递路径：
  pt_regs->ip = handler 函数地址          ← 改写！
  pt_regs->sp = 新栈顶 (sigframe 之后)     ← 改写！
  → iretq/sysret "跳到" handler 函数
```

**`setup_rt_frame` 在用户栈上的工作**（逐步骤拆解）：

```bash
用户栈布局变化：
BEFORE (信号到来前)          AFTER setup_rt_frame
┌──────────────────┐ ← 高地址    ┌──────────────────┐ ← 高地址
│  原始栈帧         │             │  原始栈帧         │
│  (局部变量/参数)   │             │  (局部变量/参数)   │
├──────────────────┤             ├──────────────────┤
│  原始 RSP ──────→ │             │  rt_sigframe      │
│  (指向某栈帧)      │             │  ┌──────────────┐ │
│                   │             │  │ uc_mcontext   │ │ ← 保存的 pt_regs
│                   │             │  │  (完整寄存器)  │ │    (RIP, RSP, RAX, ...)
│                   │             │  ├──────────────┤ │
│                   │             │  │ siginfo_t     │ │ ← 信号信息
│                   │             │  ├──────────────┤ │
│                   │             │  │ pretcode ────→┼─┼→ VDSO __vdso_rt_sigreturn
│                   │             │  └──────────────┘ │    (handler 返回地址!)
│                   │  ← 新 RSP   │                    │
└──────────────────┘ ← 低地址    └────────────────────┘ ← 低地址
关键设计：
  · uc_mcontext 中保存的 RIP = 信号到来时用户代码被中断的位置
  · uc_mcontext 中保存的 RSP = 原始用户栈指针
  · pretcode (栈顶位置) = VDSO __vdso_rt_sigreturn 的地址
    → handler 执行 ret 指令时, CPU 从栈上 pop 出这个地址并跳转
    → 等于自动进入了 sigreturn 系统调用!
```

**伪代码还原内核行为**：

```c
// kernel/signal.c
static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
    // 1. 检查是否有 SA_SIGINFO 标志
    // 2. 调用 setup_rt_frame 或 setup_frame
    if (failed)
        force_sigsegv(ksig->sig);  // 栈不够 → SIGSEGV
    // 3. 解除该信号的屏蔽（除非 SA_NODEFER）
    signal_setup_done(failed, ksig, ...);
}
// arch/x86/kernel/signal.c (简化)
static int setup_rt_frame(struct ksignal *ksig, struct pt_regs *regs)
{
    struct rt_sigframe __user *frame;
    // ① 在用户栈上分配空间（RSP 向下生长）
    frame = get_sigframe(ksig, regs, sizeof(*frame), ...);
    // ② 将当前 pt_regs 写入 sigframe.uc.uc_mcontext
    if (!__put_user(regs, &frame->uc.uc_mcontext))
        return -EFAULT;
    // ③ 复制 siginfo_t
    if (copy_siginfo_to_user(&frame->info, &ksig->info))
        return -EFAULT;
    // ④ ★ 最关键：设置返回地址 = VDSO __vdso_rt_sigreturn
    if (__put_user(vdso_rt_sigreturn_addr, &frame->pretcode))
        return -EFAULT;
    // ⑤ ★ 改写 pt_regs, 让 CPU "以为"返回目标就是 handler
    regs->ip = (unsigned long)ksig->ka.sa.sa_handler;  // handler 地址
    regs->sp = (unsigned long)frame;                     // 新栈顶
    regs->di = ksig->sig;                                // 第1参数: signum
    regs->si = (unsigned long)&frame->info;              // 第2参数: siginfo*
    regs->dx = (unsigned long)&frame->uc;                // 第3参数: ucontext*
    regs->cs = __USER_CS;  // 用户态代码段
    regs->ss = __USER_DS;  // 用户态数据段
    return 0;
}
```

> **问题 1：内核到底改不改用户栈？**
> **改。** `setup_rt_frame()` 通过 `put_user()` / `__put_user()` 直接把 `rt_sigframe` 写到用户态栈上。sigframe 包含：保存的全部寄存器 (uc_mcontext)、siginfo、FPU 状态、以及返回地址 (pretcode)。这些数据内核不是"暗示"或"通知"，是实实在在的写入用户栈内存。唯一不碰主线程栈的例外是设置了 `SA_ONSTACK` + `sigaltstack()`——此时写入**备用信号栈**，但本质仍然是写用户态内存。
> **问题 2：sigframe 已经在用户栈上了，handler 为什么不直接 `ret` 回原始代码，非要 sigreturn 再进一次内核？**
> 这个问题的关键在于：**sigframe 只是"备份"，handler 在用户态 (Ring 3) 没有能力把备份"还原"到硬件和内核数据结构中**。具体有三个障碍：
> **① 在 IRETQ 执行的那一刻，CPU 恢复寄存器靠的是返回栈帧（R15...RIP），而 handler 不能改写它**。——这个返回栈帧是用户态保存的，还没有被恢复用户态回来，handler 并不会被执行，因此handler  是没有能力改写内核态栈帧的。handler 是在内核设置完 pt_regs 后，执行 iretq 返回用户态时才开始运行的。此时原来的 pt_regs 已经耗尽，被加载到硬件寄存器里了。
> handler 无法直接返回到被切换点不是因 为权限不够，而是 handler 根本没有那个必要的返回栈帧！CPU 上下文在 iretq 切换到 handler 时已经一并被加载到硬件寄存器里了，内核态原来的 pt_regs 结构在 iretq 执行后就失效了，也就是栈帧已经破坏了，handler 无法使用它了！handler 要想回到原来的执行点，就必须重新进入内核，让内核重新从 sigframe（存在用户栈上的备份）中构造一个新的 pt_regs，用这个新的 pt_regs 来再次执行一个新 iretq，回到被中断的原代码。
> **② FPU/SIMD 寄存器无法在用户态恢复。** XRSTOR 等恢复指令在用户态无法操作完整的 FPU 上下文；即使能，cr4 控制位也需要内核态设置。
> **③ 信号屏蔽字是对应线程 task_struct 里 blocked 的内核数据，要在内核态修改。** 即使 handler 调用 `sigprocmask()`，那也是一个需要进内核的系统调用。另外 `TIF_SIGPENDING` 标志、`altstack` 状态只在 task_struct 里——必须是内核来清理并恢复。
> 总结：=sigreturn 不是因为**没地方放备份**，而是因为 handler 在用户态**没有能力把备份还原到寄存器和内核状态**中。内核写 sigframe 解决了"备份"问题，sigreturn 解决了"还原"问题——这是内核和用户态之间、不可缩减的能力边界。

---

### 1.5 阶段详解⑥：handler 返回如何恢复现场 —— rt_sigreturn

handler 执行完毕后执行 `ret` 指令。栈顶的返回地址是 VDSO 中 `__vdso_rt_sigreturn` 的地址——这不是普通函数，而是一个 trampoline（跳板），它立刻执行 `sys_rt_sigreturn` 系统调用：

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E8F5E9
  ParticipantBorderColor     #388E3C
  ArrowColor                 #37474F
  LifeLineBackgroundColor    #FAFAFA
}
title handler 返回与 sigreturn 恢复现场
== 用户态分段 ==
participant "原始用户代码\n(被信号打断的位置)" as ORIG_USER
participant "信号 handler" as HANDLER
participant "VDSO\n__vdso_rt_sigreturn" as VDSO
== 内核态 ==
participant "sys_rt_sigreturn" as SIGRET
participant "restore_sigcontext()" as RESTORE
HANDLER -> HANDLER : handler 函数体执行\n用户可以 sigaction 注册任意函数
HANDLER -> VDSO : ① handler 执行 ret 指令\n栈顶 pretcode = VDSO __vdso_rt_sigreturn
note right : ★ 用户没有显式调用 sigreturn\n而是靠 setup_rt_frame 预设的\n返回地址自动跳进 VDSO
VDSO -> SIGRET : ② mov $SYS_rt_sigreturn, %rax\nsyscall (陷入内核)
SIGRET -> SIGRET : ③ 找到 sigframe\n从用户栈读回所有数据
SIGRET -> RESTORE : ④ restore_sigcontext()\n把 sigframe.uc_mcontext 中保存的\n完整 pt_regs 写回当前的 pt_regs
note over RESTORE : ★ 内核没有任何"记住"\n之前被修改过什么寄存器\n它只是机械地把 sigframe\n中的内容写回到 pt_regs
SIGRET -> SIGRET : ⑤ restore_altstack() 恢复备用栈
SIGRET -> SIGRET : ⑥ 恢复信号屏蔽字 (sigmask)
SIGRET -> ORIG_USER : ⑦ iretq/sysret 返回\npt_regs 已恢复为原始值\n→ 回到被信号打断前的代码
note right of ORIG_USER : CPU 看到的现场和\n信号到来前一模一样\n仿佛什么都没发生
@enduml
```

**`sys_rt_sigreturn` 的关键行为**：

```bash
sys_rt_sigreturn()
  → 从当前 RSP (指向 sigframe) 读取：
      1. uc.uc_mcontext → 写回 current->pt_regs  ★ 恢复全部寄存器
      2. uc.uc_sigmask  → 恢复信号屏蔽字
      3. 备用栈信息      → restore_altstack()
  写回 pt_regs 后，内核的 iretq/sysret 就会回到：
    · RIP = 信号到来时的用户代码地址 (被中断位置)
    · RSP = 信号到来时的用户栈指针
    · RAX, RBX, ... = 信号到来时的值 (handler 的副作用全部丢弃)
```

**为什么不是 handler 直接 longjmp 回去？**

- handler 拿不到内核栈上的 `pt_regs`——那些寄存器在 Ring 0 内核栈上，用户态不可见。
- 只有内核有权操作 `pt_regs`（寄存器状态），所以恢复现场**必须是内核行为**。
- VDSO trampoline 是用户态可执行的唯一入口点——它把"需要内核操作的恢复"包装成一个普通系统调用。

---

### 1.6 完整时序图：从 kill() 到 handler 返回的全路径

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E8F5E9
  ParticipantBorderColor     #388E3C
  ArrowColor                 #37474F
  LifeLineBackgroundColor    #FAFAFA
}
title 信号传递完整时序：kill() → handler 执行 → sigreturn 恢复
actor "进程 A\n(sender)" as SA
actor "进程 B\n(目标, 正在运行)" as SB
== 内核态 ==
participant "sys_kill" as KILL
participant "send_signal\n__send_signal" as SS
participant "complete_signal" as CS
participant "目标 pending set" as PEND
participant "目标 thread_info\nTIF_SIGPENDING" as TI
participant "目标 pt_regs\n(内核栈上)" as REGS
== B 的内核返回路径 ==
participant "exit_to_user_mode_loop\narch_do_signal_or_restart" as EXIT
participant "do_signal\nget_signal" as DS
participant "handle_signal\nsetup_rt_frame" as HS
== 用户态 ==
participant "B 的用户栈" as USTACK
participant "B 的 handler\nmy_handler()" as HANDLER
participant "VDSO\n__vdso_rt_sigreturn" as VDSO
participant "sys_rt_sigreturn\nrestore_sigcontext" as SIGRET
== 阶段一：发送信号 (A 的内核路径) ==
SA -> KILL : kill(B_pid, SIGUSR1)
KILL -> SS : kill_pid_info → group_send_sig_info
SS -> PEND : sigaddset(B->shared_pending, SIGUSR1)\n置位未决集
SS -> CS : 信号未屏蔽, 调 complete_signal
CS -> CS : 遍历 B 的线程组, 选目标线程 T
CS -> TI : set_tsk_thread_flag(T, TIF_SIGPENDING)
note right
B 仍在用户态运行,\n尚未感知到信号
end note
KILL --> SA : kill() 返回 0
note right of SA
★ kill() 返回 != 信号已被处理!\n只是表示信号"已送达未决集"
end note
== 阶段二：B 进入内核再返回 (TIF_SIGPENDING 被检查) ==
SB -> SB : ... 继续运行用户代码 ...
SB -> REGS : B 调用任意 syscall\n(如 read/write/nanosleep)\n陷入内核, pt_regs 保存到内核栈
group B 的内核返回路径
REGS -> EXIT : 系统调用执行完毕,\n准备返回用户态\n→ exit_to_user_mode_loop
EXIT -> EXIT : while (ti_work & _TIF_WORK_MASK)
EXIT -> EXIT : if (ti_work & _TIF_SIGPENDING)\n  → arch_do_signal_or_restart()
EXIT -> DS : do_signal(regs)
DS -> DS : get_signal() 遍历 pending\n找到 SIGUSR1
DS -> DS : 检查处置 ≠ SIG_IGN\n检查信号未被 blocked 屏蔽
DS -> HS : handle_signal(&ksig, regs)
== 阶段三：伪造栈帧 ==
HS -> USTACK : ① 在用户栈上分配 rt_sigframe\n(SP -= sizeof(rt_sigframe))
HS -> USTACK : ② 保存 pt_regs → uc_mcontext
HS -> USTACK : ③ 保存 siginfo_t
HS -> HS : ④ 设置返回地址 = VDSO __vdso_rt_sigreturn\n→ 写入 sigframe.pretcode
HS -> REGS : ⑤ ★ 修改 pt_regs:\n  regs->ip = handler 地址\n  regs->sp = sigframe 顶部\n  regs->di = sig (参数1)\n  regs->si = &siginfo (参数2)\n  regs->dx = &ucontext (参数3)
== 阶段四：进入 handler ==
REGS -> HANDLER : ⑥ iretq/sysret 从 pt_regs 恢复\nCPU 跳转到 handler!
note right
★ 神奇的时刻:\nCPU 以为自己返回了用户代码\n实际上进入了信号处理函数
end note
HANDLER -> HANDLER : my_handler(sig, info, ctx) 执行
== 阶段五：handler 返回 + sigreturn ==
HANDLER -> VDSO : ⑦ handler 执行 ret\n栈顶返回地址 = VDSO sigreturn
VDSO -> SIGRET : ⑧ mov $SYS_rt_sigreturn, rax\nsyscall
SIGRET -> USTACK : ⑨ 从用户栈 sigframe 读回\n保存的 pt_regs
SIGRET -> REGS : ⑩ ★ restore_sigcontext()\n把保存的 pt_regs\n全部写回当前 pt_regs
note right
写回后 regs->ip = 原始用户代码\nregs->sp = 原始用户栈
end note
REGS -> SB : ⑪ iretq/sysret 回到\n原始被中断的用户代码
note over SB
B 继续执行,\n就像信号从未来过
end note
@enduml
```

---

### 1.7 内核态 ↔ 用户态切换全景

一次完整的信号处理经历了 **4 次特权级切换**：

```bash
时间轴 →
用户态  ──┐         ┌──────────────┐         ┌──────────┐
          │         │ handler 执行  │         │ 原始代码  │
          │         │              │         │ 继续执行  │
内核态  ──┼─────────┼──────────────┼─────────┼──────────┼──
          │         │              │         │          │
     ① 陷入   ② iretq/sysret  ③ handler ret  ④ iretq/sysret
     (syscall   跳到 handler     → VDSO        回到原始
      或中断)                   → sys_rt       用户代码
                                sigreturn
切换     ①           ②              ③→④
说明
──┼─────────────────────────────────────────
① │ 用户→内核: syscall / 中断 / 异常陷入, 硬件+软件保存 pt_regs
② │ 内核→用户: setup_rt_frame 改 pt_regs, iretq/sysret 跳到 handler
③ │ 用户→内核: handler ret → VDSO trampoline → sys_rt_sigreturn
④ │ 内核→用户: restore_sigcontext 恢复 pt_regs, iretq/sysret 回到原始代码
```

**每一步的特权级和关键数据结构**：

| 步骤 | 方向 | 触发 | 内核关键操作 | pt_regs 的变化 |
|------|------|------|-------------|--------------|
| ① 陷入 | 用户→内核 | syscall / 中断 | 硬件压 SS/RSP/RFLAGS/CS/RIP，软件压剩余寄存器 → 构建 `pt_regs` | 创建：包含当前全部寄存器 |
| ② 跳到 handler | 内核→用户 | iretq/sysret | `setup_rt_frame` 在用户栈建 sigframe，改写 `regs->ip/sp/di/si/dx` | 修改：IP→handler, SP→sigframe, RDI/RSI/RDX→参数 |
| ③ sigreturn | 用户→内核 | handler ret → VDSO → syscall | `sys_rt_sigreturn` → `restore_sigcontext` 读 sigframe 写回 `pt_regs` | 恢复：全部寄存器回到信号到来前的值 |
| ④ 回到原始代码 | 内核→用户 | iretq/sysret | 从恢复后的 `pt_regs` 恢复寄存器 | 执行：RIP 指向原始被中断处 |

---

### 1.8 特殊场景

#### 系统调用被信号中断：重启 vs 不重启

若目标线程正在执行慢系统调用（如 `read` 从 socket 等待数据），信号可能在 `TASK_INTERRUPTIBLE` 休眠时到来：

1. `signal_wake_up` 唤醒线程
2. `schedule()` 返回后，检查 `signal_pending(current)` → 返回 `-ERESTARTSYS`
3. 内核返回用户态的 `exit_to_user_mode_loop` 中检测到 `TIF_SIGPENDING` → 进入信号处理
4. handler 执行完毕后，sigreturn 恢复了 pt_regs（包括原始 syscall 的返回地址 RAX）
5. **关键分歧**：
   - **`SA_RESTART` 的 handler**：glibc 在 `sigreturn` 后检查 RAX == `-ERESTARTSYS` → 重新执行原系统调用（`read`/`write`/`wait` 等适用）
   - **无 `SA_RESTART`**：syscall 直接返回 `EINTR`，用户代码需要手动处理中断

```bash
read(fd, buf, n)  ← 阻塞中
  → nanosleep (TASK_INTERRUPTIBLE)
    → 信号到达 → 唤醒
    → 返回 -ERESTARTSYS
→ do_signal → handler → sigreturn
  → SA_RESTART?
      YES: 重新执行 read(fd, buf, n)  (内核自动重启)
      NO:  返回 -EINTR 给用户代码
```

#### 嵌套信号

##### 嵌套的基本概念

handler 正在用户态执行期间，另一个（或同一个）信号又来了。处理结果取决于两个因素：

| 因素 | 说明 |
|------|------|
| **新信号是否被屏蔽** | 由 `task->blocked` 决定——handler A 执行期间 `blocked` = 原始屏蔽字 ∪ `sa_mask` ∪ 信号A自身（如果没设 `SA_NODEFER`） |
| **新信号的信号号** | 跟当前 handler 的信号是否相同决定了默认行为 |

**两种嵌套类型：**

```bash
┌──────────────────────────────────────────────────────────────┐
│  类型 1：不同种信号嵌套（默认允许）                            │
│                                                              │
│  handler_SIGINT() 执行中 → SIGTERM 到达                      │
│  → SIGTERM 不在 blocked 里 → 内核嵌套处理 SIGTERM             │
│  → handler_SIGINT 被中断 → handler_SIGTERM 入栈执行           │
│  → handler_SIGTERM 返回 → handler_SIGINT 继续                │
│                                                              │
│  类型 2：同种信号嵌套（默认禁止，SA_NODEFER 开启）              │
│                                                              │
│  handler_SIGINT() 执行中 → 又一个 SIGINT 到达                 │
│  → 默认：SIGINT 在 blocked 里 → 留在 pending，不嵌套           │
│  → SA_NODEFER：SIGINT 不在 blocked → 嵌套处理 → 递归！         │
└──────────────────────────────────────────────────────────────┘
```

##### 内核如何实现自动屏蔽

这个自动屏蔽不是在发送信号时做的，而是在 **handler 开始执行前的 `handle_signal()` 中**：

```c
// kernel/signal.c: handle_signal()
static void handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
    sigset_t *oldset = sigmask_to_save();
    int ret;
    // ① 当前 blocked 保存到 sigframe 里（sigreturn 时恢复需要它）
    // ② ★ 把 handler 的 sa_mask 和当前信号本身加入 blocked
    //    block_action() 等价于:
    //    sigorsets(&blocked, &current->blocked, &ka->sa.sa_mask);
    //    if (!(ka->sa.sa_flags & SA_NODEFER))
    //        sigaddset(&blocked, ksig->sig);
    //    sigprocmask(SIG_SETMASK, &blocked);
    // ③ 设置 sigframe，伪造返回地址
    ret = setup_rt_frame(ksig, oldset, regs);
    // ④ SA_ONESHOT (SA_RESETHAND): handler 执行一次后自动重置为 SIG_DFL
    if (ret == 0 && (ksig->ka->sa.sa_flags & SA_RESETHAND))
        ksig->ka->sa.sa_handler = SIG_DFL;
    // ⑤ signal_delivered: 从 pending 中清除该信号
    signal_delivered(ksig->sig, &ksig->info, &ksig->ka, regs, 0);
}
```

> **关键时序**：自动屏蔽发生在 `setup_rt_frame` **之前**——也就是说，当 `iretq` 进入 handler 的那一刻，当前信号的屏蔽已经生效。handler 从头到尾都不会被同种信号打断（除非设了 `SA_NODEFER`）。

##### 嵌套的完整时序：Handler A 执行中被信号 B 打断

以下是最经典的嵌套场景——handler_SIGUSR1 执行时 SIGUSR2 到达：

```plantuml
@startuml
skinparam shadowing false
title 嵌套信号完整时序（不同信号）
participant "原始代码\n(Ring 3)" as MAIN
participant "内核\n(Ring 0)" as KERN
participant "handler_USR1\n(Ring 3)" as H1
participant "handler_USR2\n(Ring 3)" as H2
== SIGUSR1 到达 ==
MAIN -> KERN : ① 正在运行中...\nSIGUSR1 从另一进程发来
KERN -> KERN : send_signal(SIGUSR1)\n→ TIF_SIGPENDING\n→ exit_to_user_mode_loop
KERN -> KERN : do_signal() → get_signal()
note right
  handle_signal():
  blocked = 原blocked ∪ sa_mask ∪ SIGUSR1
  → SIGUSR1 被屏蔽
  → 建 sigframe_USR1
end note
KERN -> H1 : ② iretq 进入 handler_USR1
== handler_USR1 执行中，SIGUSR2 到达 ==
H1 -> H1 : ③ handler_USR1 正在执行...
KERN <<-- KERN : ④ SIGUSR2 从另一进程发来\n(CPU 在 Ring 3, 但内核通过 IPI\n或下次 syscall 检测到)
== handler_USR1 内调用 write() ==
H1 -> KERN : ⑤ handler_USR1 调用 write() → syscall
KERN -> KERN : sys_write() 执行完毕
KERN -> KERN : exit_to_user_mode_loop\n→ 检测 TIF_SIGPENDING(SIGUSR2)
note right
  SIGUSR2 不在 blocked 里
  → 允许嵌套！
  handle_signal():
  blocked = 当前blocked ∪ sa_mask_USR2 ∪ SIGUSR2
  → 建 sigframe_USR2
  → sigframe_USR2 在 sigframe_USR1 的下方
end note
KERN -> H2 : ⑥ iretq 进入 handler_USR2
== handler_USR2 执行 ==
H2 -> H2 : ⑦ handler_USR2 执行...
H2 -> KERN : ⑧ handler_USR2 返回\n→ ret → VDSO trampoline\n→ sys_rt_sigreturn
KERN -> KERN : restore_sigcontext(sigframe_USR2)
note right
  恢复 blocked 为 handler_USR1 期间的屏蔽字
  (SIGUSR1 仍在 block 中, SIGUSR2 解除)
end note
KERN -> H1 : ⑨ iretq 回到 handler_USR1\n(从 write() 返回处继续)
== handler_USR1 继续 ==
H1 -> H1 : ⑩ handler_USR1 继续执行
H1 -> KERN : ⑪ handler_USR1 返回\n→ ret → VDSO trampoline\n→ sys_rt_sigreturn
KERN -> KERN : restore_sigcontext(sigframe_USR1)
note right
  恢复 blocked 为原始屏蔽字
  (全部解除)
end note
KERN -> MAIN : ⑫ iretq 回到原始代码
@enduml
```

##### 用户栈的 LIFO 结构

每次嵌套都在用户栈上压入一个新的 sigframe，形成严格的 LIFO 链（`uc_link` 指针串起）：

```bash
        高地址（栈顶，往低增长 ↓）
        ┌─────────────────────┐
        │  原始代码栈帧        │  ← 信号到来前的 RSP
        ├─────────────────────┤
        │  rt_sigframe_USR1   │  ← uc_link = NULL（第一层）
        │  · pretcode         │
        │  · uc_mcontext      │     保存的是：原始代码的寄存器
        │  · uc_sigmask       │     保存的是：原始屏蔽字
        ├─────────────────────┤
        │  rt_sigframe_USR2   │  ← uc_link = NULL（第二层；实际上各层互相独立）
        │  · pretcode         │
        │  · uc_mcontext      │     保存的是：handler_USR1 的寄存器（当时 RIP 指向 write 调用后的下一条指令）
        │  · uc_sigmask       │     保存的是：handler_USR1 期间的屏蔽字（SIGUSR1 blocked）
        ├─────────────────────┤  ← 当前 RSP（handler_USR2 执行时）
        │  handler_USR2 栈帧   │
        └─────────────────────┘
        低地址
```

**sigreturn 的展开顺序（正好是压入的逆序）**：

```bash
handler_USR2 sigreturn
  → restore_sigcontext(sigframe_USR2)
  → 恢复到 handler_USR1（从 write() 后的下一条指令继续）
  → blocked 恢复到 handler_USR1 期间的值
handler_USR1 sigreturn
  → restore_sigcontext(sigframe_USR1)
  → 恢复到原始代码（信号到来时的下一条指令）
  → blocked 恢复到原始值
```

> **核心理解**：嵌套不是"handler B 插入到 handler A 中间执行"那么简单。每次嵌套都是一次完整的"切换上下文 → 执行 handler → sigreturn 切回来"循环。sigframe 在用户栈上的排列顺序保证了 LIFO 的正确返回。

##### SA_NODEFER：打开同种信号递归

```c
// 默认（无 SA_NODEFER）：handler 执行期间同种信号被屏蔽
struct sigaction sa;
sa.sa_handler = handler_SIGINT;
sa.sa_flags = 0;               // ← 默认：SIGINT 自动屏蔽
sigaction(SIGINT, &sa, NULL);
// handler_SIGINT() 执行时，另一个 SIGINT 留在 pending
// → handler 返回后，sigreturn 恢复 blocked，SIGINT 再次投递
// SA_NODEFER：允许同种信号递归
sa.sa_flags = SA_NODEFER;      // ← SIGINT 不自动屏蔽
sigaction(SIGINT, &sa, NULL);
// handler_SIGINT() 执行时，另一个 SIGINT 可以嵌套
// → 如果 SIGINT 持续高速到达，用户栈无限增长 → 栈溢出
```

**SA_NODEFER 的危险性**：每次嵌套至少消耗 ~2KB（一个 `rt_sigframe` 的大小，含 FPU 状态）。Linux 默认线程栈 8MB，理论上约 4000 次嵌套后栈溢出。实际应用中，几个快速到来的同种信号就可能导致溢出——因为 handler 本身的局部变量也在消耗栈。

> **最佳实践**：除非 handler 代码极度简单且可重入（如仅设置 `volatile sig_atomic_t` 标志位后立即返回），否则不要使用 `SA_NODEFER`。需要对同种信号的快速响应改用 `signalfd` + epoll 模型。

##### SA_NODEFER 与 SA_RESETHAND 的组合

两个标志组合使用时行为：

```bash
SA_NODEFER | SA_RESETHAND:
  handler 执行一次后重置为 SIG_DFL
  + handler 执行期间同种信号可以嵌套（不屏蔽）
  → 第二次同种信号到达时，handler 已重置为 SIG_DFL（SA_RESETHAND 已生效）
  → 所以第二次走默认动作，不会真的递归
  → 这是传统 System V 信号语义的残余
```

```c
// System V 经典行为（BSD/Linux 不推荐但兼容）
sa.sa_flags = SA_NODEFER | SA_RESETHAND;
// handler 执行 1 次，同种信号在 handler 内不被屏蔽，
// 但 handler 返回后自动变回 SIG_DFL，
// 所以第二次同种信号不会递归进入 handler，而是走默认动作
```

##### 嵌套与 SA_ONSTACK 的关系

如果 handler A 和 handler B 都指定了 `SA_ONSTACK`：

```bash
普通栈                       备用信号栈 (sigaltstack)
────────                    ─────────────────────────
┌──────────────┐            ┌──────────────────────┐
│ 原始帧        │            │  sigframe_USR1       │  ← handler A 入口
├──────────────┤            ├──────────────────────┤
│              │            │  handler_USR1 栈帧    │
│  (空，未使用)  │            ├──────────────────────┤
│              │            │  sigframe_USR2       │  ← handler B 入口
│              │            ├──────────────────────┤
│              │            │  handler_USR2 栈帧    │
└──────────────┘            └──────────────────────┘
```

内核判断逻辑（`handle_signal()` 中）：
- 进入 handler 时：如果是该信号第一次进入（`regs->sp` 不在备用栈范围内）→ 切到备用栈
- 退出 handler（sigreturn）时：`regs->sp` 恢复到 sigframe 中的原始 RSP

> **嵌套 + SA_ONSTACK** 的要点：如果 handler A 已经在备用栈上了，handler B 也在备用栈上（备用栈会容纳两层嵌套）。所有 signal delivery 的内核代码检查 `on_sig_stack(regs->sp)` 来判断：如果当前 RSP 已经在备用栈内，说明是在 handler 内部被再次中断——此时不会切换栈，而是直接在备用栈上继续压 sigframe。

---

## 二、内核数据结构与接口全景

> §一从流程角度把信号一次投递走完了。本节回到"积木"本身——把那条链路涉及的所有数据结构、所有内核接口**逐一定义、逐字段注释、逐函数说明**。先看懂积木长什么样，再回头看流程就通了。

### 2.1 数据结构关系全景

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<percpu>> #C8E6C9
  BorderColor<<percpu>> #388E3C
  BackgroundColor<<pershare>> #BBDEFB
  BorderColor<<pershare>> #1976D2
  BackgroundColor<<perproc>> #E1BEE7
  BorderColor<<perproc>> #7B1FA2
  BackgroundColor<<ustack>> #FFF9C4
  BorderColor<<ustack>> #F9A825
}
rectangle "task_struct\n(current)" <<percpu>> as TASK
rectangle "sighand_struct\n(进程共享, refcount)\n├─ count (引用计数)\n├─ siglock (自旋锁)\n└─ action[64]\n    └─ struct k_sigaction []" <<perproc>> as SH
rectangle "signal_struct\n(进程共享)\n├─ shared_pending\n│   └─ struct sigpending\n├─ group_exit_code\n├─ group_stop_count\n└─ rlim[RLIM_NLIMITS]" <<perproc>> as SIG
rectangle "每个线程(task)私有\n├─ pending (struct sigpending)\n│   ├─ signal (sigset_t 位掩码)\n│   └─ list (sigqueue 链表, RT信号排队)\n└─ blocked (sigset_t 屏蔽字)" <<percpu>> as PRIV
rectangle "struct sigpending\n├─ signal (sigset_t)\n│   每个bit=一种信号是否未决\n└─ list (struct sigqueue 链表)\n    每项=一个排队的RT信号" as SP
rectangle "struct sigqueue\n├─ list (链表节点)\n├─ info (siginfo_t: 谁发的/什么原因)\n├─ flags (SIGQUEUE_PREALLOC?)\n└─ user (struct user_struct*)" as SQ
rectangle "struct k_sigaction\n├─ sa (struct sigaction 用户态拷贝)\n│   ├─ sa_handler / sa_sigaction\n│   ├─ sa_mask\n│   ├─ sa_flags\n│   └─ sa_restorer\n└─ flags (内核内部标志)" as KA
rectangle "struct ksignal\n(临时, 栈上分配)\n├─ sig (信号号)\n├─ info (siginfo_t)\n└─ ka (k_sigaction* 指向动作)" as KS
rectangle "用户栈 rt_sigframe\n(x86-64)\n├─ pretcode (→ VDSO sigreturn)\n├─ info (siginfo_t 副本)\n├─ uc (ucontext_t)\n│   ├─ uc_flags\n│   ├─ uc_link\n│   ├─ uc_stack (备用栈)\n│   ├─ uc_sigmask\n│   └─ uc_mcontext ★\n│       └─ 完整寄存器 (R15..RIP)\n└─ fpstate (浮点/SIMD状态)" <<ustack>> as USF
TASK --> SH : task->sighand
TASK --> SIG : task->signal
TASK --> PRIV : task->pending\ntask->blocked
SIG --> SP : shared_pending
PRIV --> SP : pending
SP --> SQ : list 链表\n(RT信号才分配)
SH --> KA : action[64] 数组
KS --> KA : ka 指针
KS --> USF : setup_rt_frame\n构造 sigframe
@enduml
```

**一句话**：进程级一对 `sighand_struct`+`signal_struct`、每线程一对 `pending`+`blocked`、RT 信号分配 `sigqueue` 排队、投递时在用户栈上临时构建 `rt_sigframe`。

---

### 2.2 struct sighand_struct —— 信号处理动作表（进程共享）

sighand_struct 是进程级唯一的信号处理动作表，所有线程共享同一份实例。

**字段说明：**

| 字段 | 类型 | 含义 |
|------|------|------|
| `count` | `refcount_t` | 引用计数。CLONE_SIGHAND（线程创建）时 +1，task 退出时 -1，归零后 kfree |
| `action[_NSIG]` | `struct k_sigaction` | 64 个信号的处置动作数组，按信号编号索引（1~64）。全进程共享——所有线程看到同一套 handler |
| `siglock` | `spinlock_t` | 保护所有信号操作的自旋锁（send_signal、do_sigaction、get_signal），是信号序列化的根，不能长持 |
| `signalfd_wqh` | `wait_queue_head_t` | signalfd 等待队列，用于 poll()/select() 等待。新信号到达时 wake_up |
| `rcu` | `struct rcu_head` | RCU 延迟释放。信号处理路径可能在 RCU 读锁下访问 sighand，free 时需走 call_rcu |

```c
// include/linux/sched/signal.h (简化, 核心字段)
struct sighand_struct {
    refcount_t              count;          // 引用计数
    struct k_sigaction      action[_NSIG];  // 动作数组, _NSIG=64
    spinlock_t              siglock;        // 保护全部信号状态的锁
    wait_queue_head_t       signalfd_wqh;   // signalfd 等待队列
    struct rcu_head         rcu;            // RCU 延迟释放
};
```

> **锁的语义**：`siglock` 保护 `sighand_struct.action[]`、`signal_struct.shared_pending`、以及当前线程的 `private pending` 和 `blocked`。几乎所有信号函数都先 `spin_lock_irq(&sighand->siglock)`——这是信号被序列化的原因。

---

### 2.3 struct signal_struct —— 线程组共享信号状态（进程共享）

signal_struct 是线程组共享的信号状态容器。各线程通过 `task_struct->signal` 指针指向同一份实例。

**字段说明：**

| 字段 | 类型 | 含义 |
|------|------|------|
| `sigcnt` | `refcount_t` | 引用计数。线程创建 +1，退出 -1，归零释放 |
| `shared_pending` | `struct sigpending` | ★ 进程级未决信号集。kill(pid) 发来的信号落在这里，complete_signal() 从中取信号分发给某条线程 |
| `group_exit_code` | `int` | 组退出码。收到 SIGKILL 时设置，记录退出原因（信号编号的低 8 位） |
| `group_stop_count` | `int` | 正在执行组停止的线程数 |
| `flags` | `unsigned int` | SIGNAL_STOP_STOPPED 等状态标志 |
| `rlim[RLIM_NLIMITS]` | `struct rlimit` | 进程级资源限制（RLIMIT_CPU、RLIMIT_FSIZE…）。SIGXCPU / SIGXFSZ 的触发依赖这里 |
| `utime, stime` | `u64` | 进程累计用户/内核 CPU 时间。SIGPROF（ITIMER_PROF）和 SIGVTALRM（ITIMER_VIRTUAL）依赖这里判断是否超时 |
| `cutime, cstime` | `u64` | 已回收子进程的累计时间 |
| `posix_timers` | `struct hlist_head` | POSIX 定时器链表。timer_create() 创建的定时器挂在这里，超时时由 alarm 软中断 → posix_timer_fn → send_sigqueue 投递信号 |
| `thread_head` | `struct list_head` | 线程组链表头。complete_signal() 遍历它来找到合适的投递目标 |

```c
// include/linux/sched/signal.h (简化, 核心字段)
struct signal_struct {
    refcount_t              sigcnt;             // 引用计数
    struct sigpending       shared_pending;     // 发给整个进程的未决信号
    int                     group_exit_code;    // 组退出码
    int                     group_stop_count;   // 组停止中的线程数
    unsigned int            flags;              // 状态标志
    struct rlimit           rlim[RLIM_NLIMITS]; // 资源限制
    u64                     utime, stime;       // 累计 CPU 时间
    u64                     cutime, cstime;     // 已回收子进程时间
    struct hlist_head       posix_timers;       // POSIX 定时器链表
    struct list_head        thread_head;        // 线程组链表头
};
```

---

### 2.4 struct sigpending —— 未决信号集（每线程私有 + 进程共享各一份）

sigpending 是未决信号的容器，出现在两个地方：每线程的 task->pending 和进程级的 signal->shared_pending。

**字段说明：**

- **`struct list_head list`**：sigqueue 链表头。RT 信号的排队节点链在这里。遍历 pending 时先遍历 list（RT 信号，可能多个），再检查 signal 位掩码（普通信号，每种只记一次）。
- **`sigset_t signal`**：位掩码，_NSIG=64 个 bit，每位对应一个信号号。sigaddset 置位标记"有未决"，sigdelset 清除"已投递"。普通信号只靠位掩码，不分配 sigqueue；RT 信号位掩码 + list 中排队。

```c
// include/linux/signal.h
struct sigpending {
    struct list_head        list;           // sigqueue 链表头
    sigset_t                signal;         // 未决信号位掩码
};
```

---

### 2.5 struct sigqueue —— RT 信号的排队节点

每个 RT 信号实例都分配一个 sigqueue 并链入 pending.list 尾部。来的 N 次各分配一个，依次投递——这就是 RT 信号"排队"的实现。

**字段说明：**

- **`struct list_head list`**：链表节点，挂入 sigpending.list。
- **`kernel_siginfo_t info`**：信号详细信息——谁发出的（si_pid / si_uid）、什么原因（si_code: SI_USER / SI_QUEUE / SI_TKILL / ...）、附带数据（si_int / si_ptr，sigqueue() 传来的）。
- **`int flags`**：标志位。SIGQUEUE_PREALLOC 表示预分配的 sigqueue（per-task 缓存，不分配新内存）；用户态 sigqueue() 发送时从 kmem_cache 分配。
- **`struct user_struct *user`**：指向发送进程的 user_struct，用于审计和资源统计。每个 user 有挂起信号数限制（RLIMIT_SIGPENDING）。

```c
// include/linux/signal_types.h (简化)
struct sigqueue {
    struct list_head        list;           // pending.list 的节点
    kernel_siginfo_t        info;           // siginfo_t 的内容
    int                     flags;          // 标志
    struct user_struct     *user;           // 发送方 user 资源追踪
};
```

---

### 2.6 struct k_sigaction —— 内核视角的信号处置配置

k_sigaction 是内核存储信号处置动作的结构，存于 sighand_struct.action[] 数组中。**每个信号号对应一个独立的 k_sigaction**——SIGINT(2) 和 SIGTERM(15) 可以注册完全不同的 handler。

#### k_sigaction vs 用户态 struct sigaction —— 核心区别

```bash
用户态 (glibc)                         内核态
═══════════════                       ═════════
┌─────────────────────┐     ┌─────────────────────────────┐
│ struct sigaction    │     │ struct k_sigaction          │
│  .sa_handler        │────→│  .sa         ← 原样内嵌     │
│  .sa_mask           │     │   (struct sigaction)        │
│  .sa_flags          │     │                             │
│  .sa_restorer       │     │  .flags      ← 内核独有     │
│                     │     │   SA_IMMUTABLE              │
│ (用户传递进来的)     │     │   SA_UNSUPPORTED            │
│                     │     │                             │
│ ★ 按信号号独立存储   │     │ ★ 按信号号索引             │
│   (每个信号一个)     │     │   action[sig-1]             │
│                     │     │ ★ 全进程共享一份            │
│                     │     │   (所有线程同一套 handler)   │
└─────────────────────┘     └─────────────────────────────┘
```

| 维度 | 用户态 `struct sigaction` | 内核 `struct k_sigaction` |
|------|--------------------------|--------------------------|
| **定义位置** | glibc (`bits/sigaction.h`)，架构相关 | `include/linux/signal_types.h`，架构无关 |
| **嵌套关系** | 独立结构体 | **内嵌** `struct sigaction sa` + 内核专属 `flags` |
| **sa_handler 含义** | 用户态函数指针 | 原样存储，内核**不做任何验证**——只是拿来拷贝进 pt_regs->ip |
| **sa_flags** | 用户设置的值 | 包含用户 sa_flags，内核**额外读写**其中部分位（如自动设置 SA_IMMUTABLE） |
| **sa_restorer** | 已废弃，glibc 不再设置，填 NULL | 同样废弃，setup_rt_frame 用 VDSO 地址替代 |
| **flags** | 无此字段 | 内核私有标志：`SA_IMMUTABLE`（SIGKILL/SIGSTOP 不可改）、`SA_UNSUPPORTED` |
| **生命周期** | 栈/堆上临时，sigaction() 调用后即释放 | `sighand_struct.action[]` 中，随进程存在，直到被覆盖或进程退出 |
| **共享性** | 调用线程私有 | 进程全局——所有线程共享同一份 action 数组 |
| **索引方式** | 按信号号传给 sigaction() | `action[sig-1]`，按信号号索引 |
| **SA_IMMUTABLE** | 用户看不到、设不了 | 内核通过 `force_sig()` 给 SIGKILL/SIGSTOP 自动设置 |

#### sigaction() 系统调用 → 内核做了什么

```c
// kernel/signal.c: do_sigaction()
int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact)
{
    struct k_sigaction *k = &current->sighand->action[sig - 1];
    // ① 如果用户想获取当前处置，直接拷贝出去
    if (oact)
        *oact = *k;
    if (act) {
        // ② ★ SIGKILL 和 SIGSTOP 禁止修改
        if (sig == SIGKILL || sig == SIGSTOP)
            return -EINVAL;
        // ③ ★ 已有 SA_IMMUTABLE 标志的也不能改
        if (k->flags & SA_IMMUTABLE)
            return -EINVAL;
        // ④ 拷贝用户传入的 sigaction 到内核结构
        *k = *act;
        // ⑤ 如果 handler 是 SIG_IGN 且信号默认动作为 Stop/Cont
        //    则清除 pending 中该信号，防止"忽略但已 pending"的僵局
        if (sig_handler_ignored(sig_handler(current, sig), sig)) {
            sigdelset(&current->pending.signal, sig);
            // ...
        }
    }
    return 0;
}
```

> **关键洞察**：内核几乎不做验证，只是原样拷贝用户传来的 `struct sigaction`。`sa_handler` 指向的地址是否有效、是否可以执行——内核不管。如果用户传了一个垃圾地址，第一次信号投递时 `iretq` 跳过去，CPU 会立刻触发 SIGSEGV（又一个同步信号）。

#### flags 字段的细节

```c
// include/linux/signal.h
#define SA_IMMUTABLE     0x00800000U   // 处置不可修改 (SIGKILL/SIGSTOP)
#define SA_UNSUPPORTED   0x00000400U   // 架构不支持该信号的某些特性
```

- **`SA_IMMUTABLE`**：内核通过 `force_sig()` 设置 SIGKILL(9) 和 SIGSTOP(19) 的初始 handler 时加上此标志。此后 `do_sigaction()` 拒绝任何修改。注意这不是用户态 `sa_flags` 里的位（0x00800000 在用户态 `SA_` 命名空间中未分配），所以用户看不到、设不了。
- **`SA_UNSUPPORTED`** 与 `sa_flags` 中的 `SA_UNSUPPORTED`（0x00000400）**共享同一个位**——如果架构不支持某信号特性，该位可以同时出现在 `sa_flags` 和 `flags` 中。
- **用户态 `sa_flags` 子集**：`SA_SIGINFO`、`SA_RESTART`、`SA_NODEFER`、`SA_ONSTACK`、`SA_RESETHAND` 等标志位**只在** `k_sigaction.sa.sa_flags` 中，不在 `k_sigaction.flags` 中。内核读取 `sa_flags` 来决定如何投递信号（是否传 siginfo、是否用备用栈、是否自动屏蔽等）。

---

### 2.7 struct rt_sigframe —— x86-64 用户栈上的信号帧

rt_sigframe 是内核在用户栈上构建的临时结构，handler 执行完毕后 sigreturn 从这里恢复全部寄存器。

**字段说明：**

- **`char __user *pretcode`**（★关键）：信号返回地址。setup_rt_frame 将 `__vdso_rt_sigreturn` 的地址写在这里——handler 执行 ret 时 CPU 从此处 pop 出返回地址跳转，自动进入 VDSO → sys_rt_sigreturn，无需用户显式调用 sigreturn。
- **`struct ucontext uc`**：包含完整上下文。其中 uc_mcontext 保存完整的 CPU 寄存器快照（sigcontext，含 R8~R15、RDI、RSI、RBP、RBX、RDX、RAX、RCX、RSP、RIP、EFLAGS 等 23+ 个寄存器）；uc_sigmask 是 handler 执行期间的 blocked mask；uc_link 指向上一个 ucontext（嵌套信号链）；uc_stack 记录 SA_ONSTACK 时的备用栈信息。
- **`siginfo_t info`**：信号详情（si_signo、si_code、si_pid、附带数据），handler 的第二个参数指向这里。
- **`struct _fpstate fpstate`**：浮点/SIMD 状态（FPU、MMX、SSE、AVX），默认 layout 为 fxregs_state + xstate_header。

```c
// arch/x86/include/asm/sigframe.h (简化 x86-64)
struct rt_sigframe {
    char __user            *pretcode;       // 返回地址 → VDSO sigreturn
    struct ucontext         uc;             // 用户上下文 (含寄存器)
    siginfo_t               info;           // 信号详情
    struct _fpstate         fpstate;        // FPU/SIMD 状态
};
// uc_mcontext 内部 (sigcontext, x86-64):
// struct sigcontext {
//     __u64 r8, r9, r10, r11, r12, r13, r14, r15;  // 被调用者保存
//     __u64 rdi, rsi, rbp, rbx, rdx, rax, rcx, rsp; // 调用约定寄存器
//     __u64 rip;                     // ★ 被中断的指令地址
//     __u64 eflags;                  // CPU 标志寄存器
//     __u64 cs, gs, fs, ss;          // 段选择子
//     __u64 err, trapno, oldmask, cr2;// 异常上下文
//     struct _fpstate *fpstate;      // 指向浮点状态
//     __u64 reserved1[8];
// };
```

> **为什么定义在用户栈上**：`uc_mcontext` 保存的是信号到来那一刻的全部寄存器——这是 sigreturn 能"恢复现场"的物质基础。内核不保存"旧 pt_regs"的指针，而是把它完整拷贝到用户栈上。恢复时再从用户栈拷贝回 `pt_regs`。

---

### 2.8 struct ksignal —— 信号投递的临时上下文

ksignal 是 get_signal() → handle_signal() 之间的临时传递结构，在 get_signal 内部栈上分配。

**字段说明：**

- **`struct k_sigaction *ka`**：指向 sighand_struct.action[sig-1] 中的处置动作。get_signal() 找到目标信号后，把对应的 k_sigaction 指针放在这里，handle_signal() 用这个来调用 setup_rt_frame。
- **`kernel_siginfo_t info`**：待投递信号的详细信息（si_signo、si_code、si_pid 等）。来源是 sigqueue.info 拷贝或由 get_signal 填充。
- **`int sig`**：信号编号（1~64），等于 info.si_signo。

```c
// include/linux/signal_types.h (简化)
struct ksignal {
    struct k_sigaction     *ka;            // 信号处置 (handler / flags / mask)
    kernel_siginfo_t        info;          // 信号详情
    int                     sig;           // 信号号
};
```

---

### 2.9 sigset_t —— 信号集位掩码

sigset_t 是 64 位的位掩码（x86-64 上用一个 u64 搞定），每位对应一个信号号。_NSIG_WORDS=1 表示一个 unsigned long 即可覆盖全部 64 个信号。

操作 sigset_t 必须使用标准宏，不可直接位运算（不同平台 sizeof 可能不同）：sigemptyset（全清零）、sigfillset（全置一）、sigaddset（置位）、sigdelset（清除）、sigismember（测试）。

```c
// include/uapi/asm-generic/signal.h (简化)
typedef struct {
    unsigned long sig[_NSIG_WORDS];        // 位掩码, x86-64 上一个 u64
} sigset_t;
// 操作宏:
//   sigemptyset(set)     → set = 0
//   sigfillset(set)      → set = ~0UL
//   sigaddset(set, sig)  → set |= (1UL << (sig-1))
//   sigdelset(set, sig)  → set &= ~(1UL << (sig-1))
//   sigismember(set, sig)→ (set >> (sig-1)) & 1
// 用法示例:
//   sigset_t mask;
//   sigemptyset(&mask);
//   sigaddset(&mask, SIGINT);             // 把 SIGINT(2) 加入
//   sigprocmask(SIG_BLOCK, &mask, NULL);  // 屏蔽 SIGINT
```

---

### 2.10 核心接口一览

#### 2.10.1 发送侧 —— 让信号"出发"

##### sys_kill —— 给整个进程发信号

- **参数**：`pid` 为目标进程的 tgid（用户态 PID），`sig` 为信号编号。
- **调用链**：`sys_kill → kill_something_info → kill_pid_info → group_send_sig_info → do_send_sig_info → send_signal(SEND_SIG_PRIV)`
- **pid 的特殊值**：
  - pid > 0 → 发给 tgid==pid 的进程
  - pid == 0 → 发给同进程组（同 PGID）的所有进程
  - pid == -1 → 发给有权限的所有进程（除了 init 和自身）
  - pid < -1 → 发给进程组 PGID==-pid 的所有进程
- **返回**：0 成功，-1 失败（ESRCH: 无此进程, EPERM: 无权限）

```c
int sys_kill(pid_t pid, int sig);
```

##### sys_tkill —— 给特定线程发信号

- **参数**：`tid` 为目标线程的 pid（内核 TID，用户态 gettid() 的返回值），`sig` 为信号编号。
- 直接操作目标线程的 private pending，略过选线程步骤。

```c
int sys_tkill(pid_t tid, int sig);
```

##### sys_tgkill —— 给特定线程组的特定线程发信号

- **参数**：`tgid` 为目标进程的 tgid，`tid` 为目标线程的 pid，`sig` 为信号编号。
- 比 tkill 多一层校验：保证 tid 确实属于 tgid 线程组，防止 race（线程退出后 tid 被复用）。

```c
int sys_tgkill(pid_t tgid, pid_t tid, int sig);
```

##### sys_rt_sigqueueinfo —— 带附加数据给特定进程发 RT 信号

- **参数**：`pid` 为目标 tgid，`sig` 必须是 RT 信号（SIGRTMIN..SIGRTMAX），`uinfo` 为用户提供的 siginfo_t（含 si_int/si_ptr 附加数据）。
- 用户态接口为 `sigqueue(pid, sig, value)`，其中 value 是 `union sigval { int sival_int; void *sival_ptr; }`。
- 内核将 value 包装成 sigqueue 节点挂入 pending.list，目标 handler 通过 `info->si_value` 拿到。

```c
int sys_rt_sigqueueinfo(pid_t pid, int sig, siginfo_t __user *uinfo);
```

#### 2.10.2 发送侧的内核内部函数

##### send_signal —— 信号投递的核心入口（四条路径归一）

- **参数**：`sig` 为信号编号，`info` 为信号详情，`t` 为目标 task_struct，`type` 为投递类型。
  - PIDTYPE_PID → 发给特定线程（private pending）
  - PIDTYPE_TGID → 发给整个进程（shared pending）
  - PIDTYPE_PGID → 发给进程组
  - PIDTYPE_SID → 发给会话
- **返回**：0 成功，-1 失败（权限拒绝/信号忽略/不能发 SIGKILL 给 init）。
- **内部调用 __send_signal()**：
  1. 权限检查（prepare_signal）
  2. 准备 struct sigqueue（RT 信号分配，普通信号用预分配 per-task 节点）
  3. `sigaddset(&pending->signal, sig)` 置位 pending
  4. RT 信号：`list_add_tail(&q->list, &pending->list)` 链入队列
  5. 如果 `!sigismember(&t->blocked, sig)` → `complete_signal(sig, t, type)`

```c
int send_signal(int sig, struct kernel_siginfo *info,
                struct task_struct *t, enum pid_type type);
```

##### complete_signal —— 选目标线程 + 设 TIF_SIGPENDING + 唤醒

- **参数**：`sig` 为信号编号，`t` 为目标 task_struct，`type` 为投递类型。
- **核心逻辑**：
  - 如果 type == PIDTYPE_PID（发给特定线程）：直接对 t 操作（设 TIF_SIGPENDING，必要时唤醒）。
  - 否则（发给整个进程）：遍历 signal->thread_head 链表，找 `!(sig blocked) && 线程可唤醒` 的目标；全部被屏蔽则留在 shared_pending 等解除屏蔽。通过 __wants_signal() 检查：`!(sig blocked) && (在线程组中) && !(exiting)`。
- 选中目标后调用 signal_wake_up() 唤醒。

```c
void complete_signal(int sig, struct task_struct *t, enum pid_type type);
```

##### signal_wake_up —— 唤醒接收线程

- **参数**：`t` 为目标 task_struct，`resume` 表示是否强行设置 TIF_SIGPENDING（即使线程在 exit 中）。
- **动作**：
  1. `set_tsk_thread_flag(t, TIF_SIGPENDING)` 设标志位。
  2. `wake_up_state(t, TASK_INTERRUPTIBLE)` 唤醒睡眠中的线程。
  3. 如果线程在内核态运行（正在执行 syscall），不做任何事——TIF_SIGPENDING 会在返回用户态时被 exit_to_user_mode_loop 检查到。

```c
void signal_wake_up(struct task_struct *t, int resume);
```

#### 2.10.3 注册/配置侧 —— 设置信号的处置方式

##### sys_rt_sigaction —— 设置/查询信号处置

- **参数**：`sig` 为信号编号（不能是 SIGKILL/SIGSTOP），`act` 为新处置（NULL=只查询），`oact` 为旧处置输出（NULL=不关心），`sigsetsize` 通常是 sizeof(sigset_t)。
- **调用链**：`sys_rt_sigaction → do_sigaction`
- **do_sigaction() 关键行为**：
  1. 校验 sig 范围（1.._NSIG）且不是 SIGKILL/SIGSTOP
  2. 如果 k_sigaction.flags & SA_IMMUTABLE → 拒绝修改
  3. `spin_lock_irq(&sighand->siglock)`
  4. 取出旧值 → oact
  5. 从用户态拷贝 act → sighand->action[sig-1]
  6. 如果 handler == SIG_IGN 且有 pending 信号 → 清理 pending
  7. `spin_unlock_irq(&sighand->siglock)`
- **返回**：0 成功，-1 失败（EINVAL: 信号号无效 / EFAULT: 用户指针无效）

```c
int sys_rt_sigaction(int sig,
                     const struct sigaction __user *act,
                     struct sigaction __user *oact,
                     size_t sigsetsize);
```

##### sys_rt_sigprocmask —— 检查/修改当前线程的信号屏蔽字

- **参数**：`how` 为操作类型（SIG_BLOCK 加入屏蔽 / SIG_UNBLOCK 解除屏蔽 / SIG_SETMASK 直接覆盖）。`set` 为新屏蔽字（NULL=只查询），`oset` 为旧屏蔽字输出（NULL=不关心）。
- **调用链**：`sys_rt_sigprocmask → sigprocmask → __set_current_blocked`
- **__set_current_blocked 关键行为**：
  1. `spin_lock_irq(&sighand->siglock)`
  2. `current->blocked = newset`（原子替换）
  3. `recalc_sigpending()` 重新检查：是否有被解除屏蔽的未决信号？
  4. 如果有 → `signal_wake_up(current, 0)`
  5. `spin_unlock_irq(&sighand->siglock)`
- sigprocmask 是线程级操作——只改 current->blocked。多线程环境下应使用 pthread_sigmask，它与 sigprocmask 在 Linux 上走同一条系统调用 sys_rt_sigprocmask。

```c
int sys_rt_sigprocmask(int how, sigset_t __user *set,
                       sigset_t __user *oset, size_t sigsetsize);
```

#### 2.10.4 投递/检查侧 —— 返回用户态时的信号处理

##### exit_to_user_mode_loop —— 内核→用户态的出口（架构无关层）

- **调用栈**：`syscall_exit_to_user_mode / interrupt_exit_to_user_mode → exit_to_user_mode_loop(regs, ti_work)`
- **关键逻辑**（简化）：
```bash
  while (ti_work & _TIF_WORK_MASK) {
      local_irq_enable();
      if (ti_work & _TIF_NEED_RESCHED) schedule();      // 先调度
      if (ti_work & _TIF_SIGPENDING)                     // ★ 再处理信号
          arch_do_signal_or_restart(regs);
      if (ti_work & _TIF_NOTIFY_RESUME) ...              // 最后通知
  }
  ```
- 这是信号被处理的**唯一入口**——无论 syscall/中断/异常返回，最终都走到这个循环。

```c
void exit_to_user_mode_loop(struct pt_regs *regs, unsigned long ti_work);
```

##### arch_do_signal_or_restart / do_signal

1. 调用 get_signal() 从 pending 中取出一个可投递的信号。
2. 根据返回的 ksig 决定：handle_signal / 默认动作 / 重启系统调用。

```c
void arch_do_signal_or_restart(struct pt_regs *regs, bool has_signal);
```

##### get_signal —— 从 pending 中取出下一个待处理信号

- **参数**：`ksig` 为输出参数，填充取到的信号和处置。
- **返回**：true = 找到可投递信号，false = 没有（已全部处理或仍在屏蔽中）。
- **核心逻辑**：
  1. `spin_lock_irq(&sighand->siglock)`
  2. 检查是否有 group_stop（SIGSTOP/SIGTSTP 等）
  3. dequeue_signal 遍历 pending：
     a. 先遍历 current->pending.list（RT 信号队列）
     b. 再遍历 shared_pending.list
     c. 再检查 pending.signal 位掩码（普通信号）
     d. 对每个候选检查：不是 SIG_IGN、不被 blocked、不在退出中
  4. 找到后：`ksig->ka = &sighand->action[sig-1]`
  5. `spin_unlock_irq(&sighand->siglock)`
- **动作分支**：SIG_DFL+终止 → do_group_exit / SIG_DFL+停止 → do_signal_stop / SIG_IGN → continue / 有handler → return true

```c
bool get_signal(struct ksignal *ksig);
```

#### 2.10.5 处理侧 —— handler 跳转的准备

##### handle_signal —— 架设"跳板"：在用户栈伪造 sigframe，改写 pt_regs

handler 本身在**用户态**执行，但内核必须提前往用户栈写入 sigframe 来架设跳板。`setup_rt_frame` 通过 `put_user()` / `__put_user()` 直接写用户态内存——如果写入失败（栈不可写或空间不够）则 `force_sigsegv`。

- **参数**：`ksig` 为信号+处置信息，`regs` 为当前 pt_regs（将被修改）。
- **核心逻辑**：
  1. 检查 SA_SIGINFO → 决定用 setup_rt_frame（有 siginfo）还是 setup_frame（无）。
  2. 调用 setup_rt_frame(ksig, regs)：
     - get_sigframe() 在用户栈计算 rt_sigframe 位置：正常栈 `(regs->sp - 128) & ~0x3F`（16 字节对齐，留出红色区）；若设置了 `SA_ONSTACK` + `sigaltstack()`，则用备用栈 `altstack.ss_sp + altstack.ss_size - sizeof(*frame)`
     - `__put_user()` 将 regs 拷贝到 frame->uc.uc_mcontext（★内核写用户栈）
     - `copy_siginfo_to_user()` 将 ksig->info 拷贝到 frame->info（★内核写用户栈）
     - `put_user(vdso_addr, &frame->pretcode)` 设置返回地址 = VDSO sigreturn（★内核写用户栈）
     - ★ 改写 regs：`regs->ip = handler地址`, `regs->sp = frame`, `regs->di = sig`（参数1）, `regs->si = &frame->info`（参数2）, `regs->dx = &frame->uc`（参数3）
  3. iretq/sysret 返回用户态，CPU 自然进入 handler。
- 如果 setup_rt_frame 失败（用户栈不可写/空间不够）→ force_sigsegv(sig)。

```c
void handle_signal(struct ksignal *ksig, struct pt_regs *regs);
```

#### 2.10.6 恢复侧 —— handler 返回后的现场恢复

##### sys_rt_sigreturn —— 从用户栈 sigframe 恢复完整 pt_regs

- **触发**：handler 执行 ret → 跳到 VDSO `__vdso_rt_sigreturn` → `mov $SYS_rt_sigreturn, %rax` → syscall。
- **核心逻辑**（arch/x86/kernel/signal.c）：
  1. 从 `current->regs->sp` 定位到用户栈上的 rt_sigframe。
  2. restore_sigcontext()：从 frame->uc.uc_mcontext 读取所有寄存器值，逐一写回 current->pt_regs（regs->ip = sigcontext->rip, regs->sp = sigcontext->rsp, ...，全部 23+ 个寄存器）。
  3. restore_altstack()：如果 SA_ONSTACK，恢复原备用栈的状态。
  4. __set_current_blocked()：恢复信号屏蔽字为 sigframe.uc.uc_sigmask。
  5. iretq/sysret 返回——此时 pt_regs 已完全恢复为信号到来前的值。
- 这是内核唯一能"撤销" setup_rt_frame 修改的地方——没有 sigreturn，handler 永远回不到原始代码。
- **安全机制**：内核不信任用户栈上的 sigframe 数据，恢复前有严格校验（uc_flags 必须为 0、段寄存器合法、浮点状态格式正确），任何一项不通过 → 直接 SIGSEGV。

```c
asmlinkage long sys_rt_sigreturn(void);
```

---

## 三、信号的默认动作与三种处置

进程对每个信号可以选三种处置(存在 `sighand_struct` 的动作表里)：

| 处置 | 含义 |
|------|------|
| **SIG_DFL** | 默认动作(Term 终止 / Core 终止+转储 / Ign 忽略 / Stop 暂停) |
| **SIG_IGN** | 忽略该信号 |
| **自定义 handler** | 执行你 `sigaction` 注册的函数 |

- 默认动作的四类(Term/Core/Ign/Stop)及哪些信号产 core,见 [../../crash/signals.md](/crash/signals.md)。
- **`SIGKILL`(9) 和 `SIGSTOP`(19) 无法被捕获/忽略/屏蔽**——内核硬保证,这样 `kill -9` 总能杀掉进程(D 状态不可中断睡眠除外,见 [../scheduling.md](/concepts/process/scheduling.md))。

## 四、一个信号的生命周期：产生 → 未决 → 投递 → 处理

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor     #1976D2
  ArrowColor                 #37474F
}
participant "发送方\nkill()/异常/内核" as S
participant "未决集 pending\n(进程共享 or 线程私有)" as P
participant "目标线程\n(返回用户态时检查)" as T
S -> P : ① 产生:置位到未决集\n(pending 是**位集合**,同种普通信号不排队)
note over P : ② 信号在此"未决(pending)"\n直到被投递
T -> P : ③ 线程从内核态返回用户态时\n检查:有未决 && 未被 blocked 屏蔽?
P -> T : ④ 投递(deliver):\n执行处置(默认动作/handler)
note over T : 若被屏蔽(blocked)→ 继续留在未决集,\n等解除屏蔽再投递
@enduml
```

- **产生(generate)**:`kill`/`tgkill`、CPU 异常(缺页无法满足→SIGSEGV,见 [../../crash/signals.md](/crash/signals.md))、内核事件(子进程退出→SIGCHLD)。信号被**置位到未决集**。
- **未决(pending)**:普通信号的未决集是**位集合**——同一种信号来多次只记一位,**不排队**(可能"丢")。
- **投递(deliver)**:目标线程**从内核态返回用户态的那一刻**检查未决且未屏蔽的信号,执行处置。所以信号不是"立刻打断",而是"下次回用户态时处理"。
- **屏蔽(block)**:`sigprocmask`/`pthread_sigmask` 设置的屏蔽字里被挡住的信号,留在未决集,解除屏蔽后才投递。

## 五、多线程：信号送给哪个线程？

这是信号在多线程下最容易错的点：

| 发送方式 | 送到 | 谁处理 |
|---------|------|--------|
| `kill(pid)` / 终端 Ctrl-C(SIGINT) | 进程(shared pending) | **任选一个没屏蔽该信号的线程**处理 |
| `pthread_kill(tid)` / `tgkill` | 特定线程(private pending) | **那个指定线程**处理 |
| 同步异常(SIGSEGV/SIGFPE) | 触发异常的**那个线程** | 该线程自己 |

- **进程级信号任选线程**:所以多线程程序常**用一个专门线程处理信号**(其余线程屏蔽它),避免不确定性——`sigwait` + 专职线程是常见模式。
- fork 只带走调用线程、继承信号 handler 但清空未决集的坑,见 [../fork-and-threads.md](/concepts/process/fork-and-threads.md)。

> **深入阅读**：[../signal-multithread.md](/concepts/process/signal-multithread.md) —— 把本节拆开讲透：`complete_signal()` 怎么遍历线程组选线程、`TIF_SIGPENDING` 标志到 `setup_rt_frame` 伪造返回路径的完整时序（含内核↔用户态切换每一帧）、sigreturn 恢复现场机制、三种编程模式（sigwait/指定线程/默认）。

## 六、实时信号：会排队的信号

- **普通信号(1–31)**:未决集是位集合,**不排队**——屏蔽期间同种信号来 100 次,解除后只投递 1 次。
- **实时信号(SIGRTMIN–SIGRTMAX,32–64)**:**会排队**,每个都投递,还能带一个数据值(`sigqueue`)。用于不能丢事件的场景。

## 七、信号的性能问题

> 信号是"低速通道"——它不适合高频事件。每投递一次信号，代价不仅仅是 handler 本身，还包括内核态↔用户态的**两次额外穿越**、siglock 全局锁争用、以及 FPU 上下文的完整保存/恢复。

### 7.1 信号的开销模型：一次投递到底花了什么

```bash
一次信号投递的完整内核路径:
═══════════════════════════════════════════════════════════════
                    │ 开销来源                                   │  近似成本
────────────────────┼───────────────────────────────────────────┼─────────
 内核入口 (send)    │ spin_lock_irq(&sighand->siglock)          │  锁争用
                    │ __send_signal()                           │
                    │   → 分配 sigqueue (RT) 或忽略 (标准重复)    │  kmem 分配
                    ├───────────────────────────────────────────┼─────────
 选目标 + 唤醒      │ complete_signal()                         │
                    │   → 遍历 thread_head 选线程                │  链表遍历
                    │   → signal_wake_up()                      │
                    │     → set TIF_SIGPENDING                  │  置标志
                    │     → wake_up_state() 唤醒目标线程          │  调度延迟
────────────────────┼───────────────────────────────────────────┼─────────
 返回用户态入口     │ exit_to_user_mode_loop()                   │
  (deliver)         │   → 检查 TIF_SIGPENDING                   │  一次测试
                    │   → do_signal() → get_signal()            │
                    │     → spin_lock_irq(&sighand->siglock)  ← │  第二次锁!
                    │     → dequeue_signal() 遍历 pending        │  队列扫描
                    ├───────────────────────────────────────────┼─────────
 架跳板             │ handle_signal() → setup_rt_frame()        │
                    │   → put_user() 写 sigframe 到用户栈         │  uaccess 写入
                    │   → fpu__save() 保存完整 FPU/SIMD 状态      │  ★最大开销
                    │     (512~2688 字节, 取决于是否 AVX-512)     │
                    │   → 修改 pt_regs (sp/ip/di/si/dx)         │  寄存器操作
────────────────────┼───────────────────────────────────────────┼─────────
 handler 执行完毕   │ ret → VDSO __vdso_rt_sigreturn             │
  (return)          │   → sys_rt_sigreturn                      │  再进内核!
                    │     → restore_sigcontext()                 │  恢复寄存器
                    │     → fpu__restore() 恢复 FPU/SIMD         │  ★第二大开销
                    │     → __set_current_blocked()              │  恢复屏蔽字
                    │     → iretq/sysret 返回原始代码             │
═══════════════════════════════════════════════════════════════
```

**核心结论**：一次信号投递 = **3 次内核穿越**（send 在内核、deliver 时出→进→出），其中 FPU 保存/恢复是最大开销来源，siglock 被拿了两次。

### 7.2 各开销项的详细分析

#### 1) siglock 全局争用 —— 信号路径的序列化瓶颈

```bash
所有信号操作共争同一把锁:
  send_signal()     }── spin_lock_irq(&sighand->siglock)
  do_sigaction()    }
  sigprocmask()     }
  get_signal()      }
高频场景:
  ┌─────────────────────────────────────────┐
  │ 线程A: sigprocmask(SIG_UNBLOCK)         │  ← 等 siglock
  │ 线程B: complete_signal()                │  ← 等 siglock
  │ 线程C: get_signal() 正在持有 siglock     │  ← 当前持有者
  └─────────────────────────────────────────┘
```

**影响**：
- 多线程频繁收信号时，所有信号操作被串行化——同一时刻整个线程组至多一个线程在执行信号路径
- `sigprocmask()` 高频调用（如每个循环迭代 block/unblock）会直接和信号投递竞争这把锁
- `spin_lock_irq` 还会关本地 CPU 中断，在高频中断的核上代价额外放大

**缓解**：
```c
// 坏做法: 每次进入临界区都改信号屏蔽
for (int i = 0; i < 1000000; i++) {
    sigprocmask(SIG_BLOCK, &set, NULL);  // ← 拿 siglock
    do_critical_work();
    sigprocmask(SIG_UNBLOCK, &set, NULL);// ← 再拿 siglock
}
// 好做法: 用其他同步方式（互斥锁/原子变量）
```

#### 2) FPU/SIMD 状态保存是最大开销

```c
// arch/x86/kernel/fpu/core.c
// setup_rt_frame → fpregs_lock → fpstate_save
// FPU 上下文大小:
//   FXSAVE  (x87+SSE):     512 字节
//   XSAVE   (AVX):         896~1024 字节
//   XSAVE   (AVX-512):     ~2688 字节  ← Zen4/ICX 上特别大
```

`setup_rt_frame` 和 `sigreturn` 各做一次完整的 FPU 保存/恢复。如果 handler 本身不碰浮点，这个代价是纯浪费——内核不知道 handler 会用不用浮点，必须保守保存。

> **AVX-512 延迟**：保存/恢复 2688 字节的 XSAVE 区域，配合 XSAVEOPT/XSAVES（只保存脏状态）能缓解，但不是所有 CPU 都支持。

#### 3) 用户栈写入 + 两次内核穿越

```bash
用户态                    内核态
  │                         │
  ├──── 正常执行 ─────────→ │ ① send_signal (内核态, 不管发送方在哪里)
  │                         │
  │ (目标线程还在用户态/睡眠) │
  │                         │
  ├──── 下次进入内核态 ────→ │ (syscall/中断/调度)
  │                         │
  │                         │ ② exit_to_user_mode_loop 检查到 TIF_SIGPENDING
  │                         │    → do_signal → handle_signal → setup_rt_frame
  │                         │    → 写 sigframe 到用户栈 (put_user × N)
  │                         │
  ├←──── iretq 回用户态 ──── │ ③ ★ 第二次穿越: 进 handler
  │  handler 执行            │
  │  ... ret                 │
  │                         │
  │ ──── VDSO sigreturn ──→ │ ④ ★ 第三次穿越: sigreturn syscall
  │                         │    → restore_sigcontext
  │                         │    → restore_altstack
  │                         │
  ├←──── iretq 回用户态 ──── │ ⑤ ★ 回到原始代码
  │  继续原始执行             │
```

普通 syscall 穿越 2 次（进→出），信号投递额外多 2 次（handler→sigreturn 进→出），总共 **4 次内核穿越**。

#### 4) RT 信号的 sigqueue 分配开销

```c
// kernel/signal.c: __send_signal()
q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit, 0);
// 优先从 per-task 预分配缓存取 (1 个)
// 不够 → kmem_cache_alloc(sigqueue_cachep, GFP_ATOMIC)  ← 内存分配!
```

标准信号复用同一个预分配的 sigqueue（`task->sigqueue_cache`），不分配内存。RT 信号**每次**都分配新 sigqueue——高频 RT 信号本质是在对 `sigqueue_cachep` 做高频 `kmem_cache_alloc`/`kfree`。

**RLIMIT_SIGPENDING** 是硬限制：每个 `user_struct` 的 `sigpending` 计数达到上限后，`__sigqueue_alloc` 返回 NULL → 信号被丢弃。对于高频 RT 信号场景，这个限制是实际的吞吐瓶颈。

#### 5) 信号数量 X 投递频率 = 延迟放大

```bash
一次信号投递延迟 = siglock × 2 + FPU save + uaccess write + FPU restore + sigreturn
                 ≈ 数微秒 ~ 数十微秒 (取决于 FPU 大小和 cache 命中)
高频场景 (每秒 10 万次信号):
  10万 × 10μs = 1秒 CPU 时间被信号开销吃掉
  如果目标是单核，相当于 100% CPU 被信号机制独占
```

### 7.3 多线程信号的性能陷阱

**问题 1**：`complete_signal()` 遍历 `thread_head` 链表找没屏蔽的线程（O(n_threads)）。线程数多时这个遍历本身也持着 siglock。

**问题 2**：信号投递总是发生在"返回用户态"那一刻，不是立刻。如果目标线程在密集计算（不停留在内核态），信号会一直 pending 到下一次 syscall/中断/时间片耗尽。

**问题 3**：所有线程共享一个 sighand → 一把 siglock。线程 A 在 handler 中调用 `sigprocmask`（拿 siglock），会阻塞线程 B 的信号投递。

### 7.4 高性能替代方案：什么场景不该用信号

> 📌 完整论述见独立文档：[signals-alternatives.md](/concepts/process/task-resources/signals-alternatives.md) —— **重点分析每个替代方案为什么快**，从内核代码路径逐项对比传统信号的开销，含性能量化表和选择决策树。

一句话总结替代方案的核心思路：**把信号从"异步中断模型"变成"fd 同步事件模型"**——事件到达时挂到 epoll 等待队列，用户代码在正常上下文中同步读取，从而避开了 FPU 保存/恢复、sigframe 构造/销毁、sigreturn、async-signal-safe 限制等全部信号专有开销。

| 场景 | 不该用信号 | 应该用 | 快多少 |
|------|-----------|--------|--------|
| 高频事件通知（每秒万次以上） | `kill/sigqueue` 发信号 | `eventfd` + epoll / `signalfd` + epoll | eventfd 快 ~10×（无 siglock、无 sigqueue、无 FPU） |
| 进程间数据传递 | RT 信号带 `si_value`（最多 8 字节） | 共享内存 + `eventfd` 通知 | 数据量无限、零拷贝、通知快 ~5× |
| 线程间同步 | `pthread_kill` | futex / 条件变量 | 无争用时不进内核（0 syscall！） |
| 定时器 | `setitimer` → SIGALRM | `timerfd` + epoll | 回调只做 `ctx->ticks++` + wake_up（~30 cycles vs ~500 cycles） |
| I/O 就绪通知 | SIGIO | epoll / io_uring | 同样的 epoll，省去整个信号路径 |
| 子进程回收 | SIGCHLD handler | `signalfd(SIGCHLD)` + epoll / `waitpid(-1, WNOHANG)` 轮询 | 接收端零额外开销（合入 epoll 事件循环） |

### 7.5 性能观测与排查

```bash
# 1. 看信号吞吐——进程在狂收信号吗？
perf stat -e 'signal:signal_generate' -p <pid> -- sleep 10
# 高频信号 → signal_generate 计数飙升
# 2. 看 siglock 争用
perf record -e 'sched:sched_wakeup' -e 'signal:*' -p <pid> -- sleep 5
perf script | grep signal
# 3. /proc 直接看 pending
watch -n 0.5 'cat /proc/<pid>/status | grep -E "SigPnd|ShdPnd"'
# 非零超过几秒 → 信号被阻塞或处理不过来
# 4. 进程 CPU 时间分布——信号路径占了多少？
cat /proc/<pid>/status | grep -E "sig|cpu"
# 如果 voluntary_ctxt_switches 异常高 → 信号频繁打断
```

**判定信号是否是性能瓶颈的简单规则**：如果 `perf top` 中以下函数排名靠前，说明信号路径在吃 CPU：
- `__send_signal` / `complete_signal`
- `do_signal` / `get_signal` / `dequeue_signal`
- `setup_rt_frame` / `__setup_rt_frame`
- `fpu__save` / `fpu__restore` / `copy_fpstate_to_sigframe`

## 八、观测

```bash
cat /proc/<pid>/status | grep -E 'Sig|Shd'
#   SigBlk: 屏蔽字(blocked)   SigIgn: 忽略集   SigCgt: 已捕获(有 handler)集   SigPnd/ShdPnd: 未决集
#   十六进制位掩码,每一位对应一个信号号
kill -l                     # 列出所有信号号↔名字
```

- `SigCgt`(caught)的位 = 装了自定义 handler 的信号;`SigBlk` = 当前屏蔽的。排查"信号为什么没反应"先看这两个。
- `ShdPnd`(进程共享未决)/`SigPnd`(本线程未决)不为 0 = 有信号卡在未决(可能被屏蔽了)。

## 九、和其它文档的关系

- **父篇**:[../task-struct.md](/concepts/process/task-struct.md)(signal/sighand 是 task 的资源对象)。
- **姊妹篇**:[signals-user.md](/concepts/process/task-resources/signals-user.md) —— 用户态编程指南:API 速查 + 十大关键坑 + 5 个实战 Demo。本篇讲内核机制,那篇讲"怎么写信号代码"。
- **性能替代篇**:[signals-alternatives.md](/concepts/process/task-resources/signals-alternatives.md) —— 为什么不直接用信号？signalfd / eventfd / timerfd / 共享内存+eventfd / futex 各自的性能来源（逐项对比内核代码路径），含量化表和选择决策树。
- **崩溃排查视角**:[../../crash/signals.md](/crash/signals.md)(哪些信号杀进程、产 core、反推 bug)——本篇讲机制,那篇讲排查。
- **多线程 fork 的信号坑**:[../fork-and-threads.md](/concepts/process/fork-and-threads.md)。
- **投递时机**:[../scheduling.md](/concepts/process/scheduling.md)(返回用户态时检查)、[../interrupts.md](/concepts/process/interrupts.md)(信号是"软件中断",与硬件中断对照)、[../../code/syscall.md](/concepts/process/syscall.md)(内核态返回时处理)。
- **不可中断睡眠**:[../scheduling.md](/concepts/process/scheduling.md)(D 状态连 SIGKILL 都不理)。

## 十、一句话总结

> **信号的内核状态分两半:处理动作表 `sighand_struct`(全进程共享一套 handler,sigaction 改它)+ 未决信号(发给进程的进共享集、发给线程的进私有集)+ 每线程独立的屏蔽字。一个信号经历产生→置位未决→(线程返回用户态时)检查未屏蔽则投递→执行处置(默认/忽略/handler);普通信号未决集是位集合不排队,实时信号会排队。多线程下 kill(pid) 任选一个没屏蔽的线程处理(故常用专职线程 sigwait),pthread_kill 定向,同步异常给触发线程。SIGKILL/SIGSTOP 不可捕获。透过 `/proc/<pid>/status` 的 SigBlk/SigCgt/SigPnd 观测。**

