﻿# 信号：用户态编程指南 —— API 速查 + 关键坑 + Demo

> 这是 [signals-kernel.md](/concepts/process/task-resources/signals-kernel.md) 的姊妹篇。内核篇讲"信号在内核里怎么走"（数据结构 + 调用链 + 时序），本篇讲"用户态怎么写信号代码才不出事"（API + 坑 + 实战 Demo）。先读哪篇取决于你的目的——要排查内核级信号问题看那篇，要用好信号写用户态程序看这篇。

## 零、一句话认知：信号编程的核心矛盾 ——"异步"与"受限"

信号的难点不在于 API 多复杂（`sigaction` 就一个结构体），而在于**信号 handler 在任意时刻"插入"你的正常执行流**——它中断你的代码、在你的栈上执行、和你竞争一切共享资源。这个"异步"带来了三大约束：

| 约束 | 后果 | 应对 |
|------|------|------|
| **handler 中只能用 async-signal-safe 函数** | `printf`/`malloc`/`pthread_mutex_lock` 全不能用——可能死锁 | handler 只做最少的事：设 flag、写 pipe、`sem_post` |
| **handler 和主程序共享全局变量** | 数据竞争——`int x = x + 1` 可能在 handler 中被中断，读到一半的 x | `volatile sig_atomic_t`（单次读写原子），或阻塞信号保护临界区 |
| **系统调用被中断返回 EINTR** | `read`/`write`/`select` 突然返回 -1，`errno=EINTR`——必须处理 | 循环重试，或用 `SA_RESTART`（但不覆盖所有 syscall） |

> 一句话：**handler 里只做"通知"，不处理业务。** 真正的处理放到主循环（通过 flag / pipe / eventfd / signalfd + epoll 把异步信号转化为同步事件）。

---

## 一、基础 API 速查

### 1.1 `signal()` —— 历史遗留，避免使用

```c
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
// signum: 信号编号
// handler: SIG_DFL / SIG_IGN / 函数指针
// 返回值: 旧 handler 地址, 失败返回 SIG_ERR
```

**不要用 `signal()`！** 它已被 POSIX 列为 obsolete，原因有四：

1. **行为不可移植** —— 不同 Unix 变体对 `SA_RESTART`、`SA_NODEFER`、信号屏蔽语义的实现不同。
2. **System V：handler 执行一次后自动恢复为 `SIG_DFL`** —— 你注册的 handler 失效了！
3. **BSD/Linux：不会重置 handler**，但阻塞语义仍与 System V 不同。
4. **无法设 `SA_SIGINFO`** 获取额外信号信息（如发送者 PID、故障地址）。

请用 `sigaction()`。`signal()` 唯一可能出现的场景是你看十年前的代码。

### 1.2 `sigaction()` —— 标准信号注册方式

`sigaction()` 是注册信号 handler 的**唯一推荐方式**。签名：

```c
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
// signum: 信号编号 (不能是 SIGKILL=9 或 SIGSTOP=19)
// act:    NULL = 只查询, 非空 = 设置新处置
// oldact: NULL = 不关心旧值, 非空 = 输出旧处置
// 返回值: 0 成功, -1 失败 (errno: EINVAL/EINTR/EFAULT)
```

`struct sigaction` 关键字段：

| 字段 | 含义 | 注意事项 |
|------|------|---------|
| `sa_handler` | 简单 handler：`void (*)(int)` | 不需要 `siginfo` 时用 |
| `sa_sigaction` | 扩展 handler：`void (*)(int, siginfo_t*, void*)` | 需要 `si_pid`/`si_addr`/`si_value` 时用 |
| `sa_mask` | handler 执行期间自动加入屏蔽字的额外信号集 | 当前信号已自动屏蔽（若无 `SA_NODEFER`），这里是**追加** |
| `sa_flags` | 行为标志位 | 见 §1.10 速查表 |
| `sa_restorer` | 已废弃，不要填 | — |

**什么场景用 `sa_sigaction`（而不是 `sa_handler`）？**

`sa_handler` 只有一个 `int sig` 参数——你只知道"来了个信号"，其他一概不知。`sa_sigaction` 额外给你 `siginfo_t *info`（谁发的、怎么来的、带了什么）和 `void *ucontext`（寄存器现场），下面四个是实际用得上的场景：

| 场景 | 用 `info` 里的什么 | 为什么需要 |
|------|-------------------|-----------|
| **定位 SIGSEGV 崩溃地址** | `info->si_addr` | handler 里拿到引发段错误的访存地址，可以判断是空指针还是野指针、打印日志后再 `_exit()`。JDK/JVM 的 `hs_err_pid.log` 里 `siginfo: si_addr: 0x...` 就是这个字段 |
| **sigqueue 带内数据传递** | `info->si_value`（`union sigval`，4-8 字节） | 两个进程之间用 `sigqueue(pid, sig, value)` 发信号时，payload 直接跟在信号里，handler 不用再走共享内存查数据。适合**非常轻量**的一对一通知（如"缓冲区就绪，地址是 0x..."） |
| **区分信号来源做权限判断** | `info->si_code` + `info->si_pid` | `SI_USER` = 其他进程 `kill` 来的（可能恶意），`SI_QUEUE` = `sigqueue`（可信内部通道），`SI_KERNEL` = 内核生成（如 SIGIO）——handler 可以根据来源做不同响应或忽略不明来源信号 |
| **获取崩溃时的完整寄存器现场** | `ucontext` → `uc_mcontext.gregs` | `SIGSEGV`/`SIGBUS` handler 中拿到 `RIP`（哪条指令崩的）、`RSP`、所有通用寄存器，可以**自己写 mini-coredump** 或做精细的故障诊断——Google Breakpad/Chromium 的信号 handler 就这么干的 |

**什么场景用 `sa_mask`？**

`sa_mask` 是 handler 执行期间**追加**屏蔽的信号集——当前信号本身已经自动屏蔽（`SA_NODEFER` 不开时），`sa_mask` 是"再额外屏蔽这些"：

| 场景 | 屏蔽什么 | 为什么 |
|------|---------|--------|
| **优雅退出流程不可被打断** | SIGTERM handler 里 `sa_mask` 加上 SIGINT、SIGHUP | 退出流程在写 checkpoint、刷缓冲区、通知下游——如果此时被 SIGINT handler 嵌套打断，可能导致只写了一部分数据就挂了 |
| **保护 handler 操作的全局状态** | 所有会操作同一块共享数据的信号的 handler，互相加 `sa_mask` | 两个 handler 都改同一个 `volatile` 变量，嵌套会导致竞态。用 `sa_mask` 让它们串行执行 |
| **防止信号风暴递归** | 与 SIGUSR1 协作的外部信号，全加入 SIGUSR1 的 `sa_mask` | 某些 workflow 中 SIGUSR1 handler 要一次性处理队列，处理期间不希望被其他协作信号搅乱状态机 |
| **确保 handler 原子性** | handler 中操作非重入函数（如 `malloc` 内部状态）时，屏蔽所有可能触发 handler 的信号 | 虽然严格不应在 handler 里调 `malloc`，但如果实在避不开（如仅打印一条日志），至少通过 `sa_mask` 把罪魁信号都挡掉，降低崩溃概率 |

> `sa_handler` 和 `sa_sigaction` 是 **union**，通过 `sa_flags & SA_SIGINFO` 区分用哪一个。设了 `SA_SIGINFO` 就必须填 `sa_sigaction`，反之填 `sa_handler`。

```c
// 基础用法: 注册 SIGTERM handler
void demo_sigaction_basic(void)
{
    struct sigaction sa;
    sa.sa_handler = my_term_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sigaction(SIGTERM, &sa, NULL);
}
// 扩展用法: 注册带 siginfo 的 handler (能拿到谁发的/附带数据)
void demo_sigaction_siginfo(void)
{
    struct sigaction sa;
    sa.sa_sigaction = my_siginfo_handler;     // ★ 注意字段名是 sa_sigaction
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO | SA_RESTART;    // ★ 必须设 SA_SIGINFO!
    sigaction(SIGUSR1, &sa, NULL);
}
```

`sa_sigaction` handler 的参数（三参数版 `void handler(int sig, siginfo_t *info, void *ucontext)`）：

| 参数 | 典型读取内容 |
|------|-------------|
| `info->si_signo` | 信号编号（同 `sig`） |
| `info->si_code` | 信号来源：`SI_USER`（kill）、`SI_QUEUE`（sigqueue）、`SI_TKILL`、`SI_KERNEL` 等 |
| `info->si_pid` | 发送者 PID |
| `info->si_uid` | 发送者 UID |
| `info->si_value` | `sigqueue()` 传来的附带数据（`union sigval`） |
| `info->si_addr` | `SIGSEGV`/`SIGBUS` 的故障地址 |
| `ucontext` | 完整寄存器状态（同内核 `pt_regs` 的用户态副本） |

#### 1.2.1 `sigaction()` 背后发生了什么 —— 系统调用 + 内核注册流程

`sigaction()` 不是纯用户态函数，它是一次**完整的系统调用**。glibc 只是"薄封装"，真正的注册工作全部在内核态完成。

**调用链（用户态 → 内核态）**

```bash
用户态                                   内核态
───────                                  ──────
sigaction(sig, act, oldact)
  │
  └─ glibc: __sigaction()
       │
       └─ INLINE_SYSCALL(rt_sigaction, ...)
            │
            syscall 指令 ──────────────┐
                                       ▼
                              sys_rt_sigaction()
                                │
                                ├─ copy_from_user()        ← 拷入用户态 sa
                                ├─ do_sigaction()
                                │    ├─ spin_lock(&siglock) ← 拿自旋锁
                                │    ├─ 读 action[sig-1]    ← 旧值
                                │    ├─ 校验 SA_IMMUTABLE   ← SIGKILL/SIGSTOP 不可改
                                │    ├─ 写 action[sig-1]    ← 新 handler+flags+mask
                                │    └─ spin_unlock
                                └─ copy_to_user()          ← 拷出旧值到 oldact
                              iret/sysret ───────────────→ 返回用户态
```

1. **glibc 包装层**：`sigaction()` 在 glibc 中实际调用的是 `__sigaction()`，它把 POSIX 的 `struct sigaction` 转成内核的 `struct kernel_sigaction`（主要是处理 `sa_restorer` 字段差异，该字段已废弃），然后走 `syscall(__NR_rt_sigaction, ...)`。
2. **`syscall` 指令**：触发 CPU 从 ring3（用户态）切换到 ring0（内核态），这是进入内核的唯一入口。之后 CPU 执行内核的 `entry_SYSCALL_64` → 查系统调用表 → 跳转到 `__x64_sys_rt_sigaction`。
3. **`copy_from_user()`**：内核**不能直接解引用用户态指针**（安全原因：用户可能传非法地址；缺页原因：用户页可能被换出）。必须通过 `copy_from_user` 把 `act` 安全地拷到内核栈上。
4. **`do_sigaction()` —— 核心逻辑**：

| 步骤 | 操作 | 说明 |
|------|------|------|
| 拿锁 | `spin_lock(&sighand->siglock)` | 这是信号系统的"根锁"——发信号(`send_signal`)、改 handler(`do_sigaction`)、取信号(`get_signal`) 全部先拿这把锁。同一时刻一个进程组最多一个线程在执行信号路径 |
| 读旧值 | `k = &sighand->action[sig - 1]` | 取出旧 handler，后面通过 `copy_to_user` 写回 `oldact` |
| 校验 | `k->sa.sa_flags & SA_IMMUTABLE` | `SIGKILL`(9) 和 `SIGSTOP`(19) 在内核启动时就被标为 `SA_IMMUTABLE`——任何修改它们的尝试直接返回 `-EINVAL`。这就是为什么你永远不能捕获 SIGKILL |
| 写新值 | 写入 `handler` 函数指针 + `sa_flags` + `sa_mask` | 如果 `act == SIG_IGN`(1) 或 `SIG_DFL`(0)，写入内核特殊标记值（非真实函数指针）。否则写入用户态传进来的 handler 地址 |
| 特殊处理 | 若 `SIGCHLD` 且设了 `SA_NOCLDWAIT` | 内核标记为自动回收僵尸子进程，不再需要 `waitpid` |
| 放锁 | `spin_unlock(&sighand->siglock)` | — |

5. **返回**：`copy_to_user(oldact, &old, ...)` 把旧 handler 拷回用户态，然后 `iret`/`sysret` 切回 ring3。

**本质：在锁保护下写一个数组元素**

```bash
task_struct                   sighand_struct (全线程共享，CLONE_SIGHAND)
┌──────────────┐             ┌──────────────────────────────┐
│  sighand ────┼────────────→│ count       (refcount)       │
│  signal  ────┼──→ ...      │ siglock     (spinlock)       │
│  pending     │             │ action[0] : SIG1 的 handler  │
│  blocked     │             │ action[1] : SIG2 的 handler  │
└──────────────┘             │ ...                          │
                             │ action[8] : SIGKILL (锁死!)  │
                             │ action[sig-1] ← sigaction()  │
                             │             写入的就是这里!   │
                             │ ...                          │
                             │ action[63]                   │
                             └──────────────────────────────┘
```

- 全进程所有线程**共享同一个 `sighand_struct`**（`CLONE_SIGHAND`），所以线程 A 调用 `sigaction()` 改了 handler，线程 B 收信号也走同一个 handler。不存在"每线程独立 handler"。
- 操作非常轻量——本质上就是在锁保护下写一个数组元素，没有遍历、没有内存分配、没有复杂逻辑。**真正的开销发生在"信号被投递时"**（内核需要压 `sigframe` 到用户栈、设置返回地址为 `__kernel_rt_sigreturn`），但那不属于 `sigaction()` 调用的范围。

> **总结**：`sigaction()` = 系统调用 → 进内核 → 拿锁 → 写 `action[]` 数组 → 放锁 → 返回。全程在内核态完成，用户态只负责传递参数结构体。

### 1.3 `kill()` / `raise()` / `killpg()`

```c
#include <signal.h>
int kill(pid_t pid, int sig);
int raise(int sig);
int killpg(pid_t pgrp, int sig);
```

**`kill()`** —— 给进程/进程组发信号。

`pid` 参数的语义：

| `pid` 值 | 目标 |
|----------|------|
| `> 0` | 发给 `tgid == pid` 的那个进程 |
| `== 0` | 发给与调用者**同进程组**的所有进程 |
| `== -1` | 发给有权限的所有进程（除 init 和自身） |
| `< -1` | 发给进程组 `PGID == -pid` 的所有进程 |

`sig == 0` 时**不发送信号**，只检查进程是否存在（权限检查仍生效）。返回值：0 成功，-1 失败（`ESRCH` 目标不存在、`EPERM` 无权限、`EINVAL` 信号号无效）。

**`raise(sig)`** 等价于 `kill(getpid(), sig)`，给调用进程自身发信号（实际发给当前线程）。

**`killpg(pgrp, sig)`** 等价于 `kill(-pgrp, sig)`。

### 1.4 `sigprocmask()` / `pthread_sigmask()`

```c
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
// 返回值: 0 成功, -1 失败
```

- `how` 操作类型：`SIG_BLOCK`（追加屏蔽 `set \|= mask`）、`SIG_UNBLOCK`（解除屏蔽 `set &= ~mask`）、`SIG_SETMASK`（直接覆盖 `set = mask`）。
- `set`：新的屏蔽字，传 `NULL` 表示只查询不修改。
- `oldset`：输出旧屏蔽字，传 `NULL` 表示不关心。

这两个函数**只影响当前线程的 `blocked` 屏蔽字**，不影响其他线程。在 Linux 上它们走同一条系统调用 `sys_rt_sigprocmask`，但 POSIX 规定多线程程序必须用 `pthread_sigmask`。

```c
// 示例: 临界区保护——临时屏蔽 SIGINT 和 SIGTERM
void critical_section_with_signal_block(void)
{
    sigset_t block_set, old_set;
    sigemptyset(&block_set);
    sigaddset(&block_set, SIGINT);
    sigaddset(&block_set, SIGTERM);
    pthread_sigmask(SIG_BLOCK, &block_set, &old_set);
    // ... 临界区代码 (不会被 SIGINT/SIGTERM 打断) ...
    pthread_sigmask(SIG_SETMASK, &old_set, NULL);  // 恢复
}
```

### 1.5 `sigwait()` / `sigwaitinfo()` / `sigtimedwait()`

```c
#include <signal.h>
int sigwait(const sigset_t *set, int *sig);
// 返回值: 0 成功, >0 失败 (errno: EINVAL/EINTR)
int sigwaitinfo(const sigset_t *set, siginfo_t *info);
int sigtimedwait(const sigset_t *set, siginfo_t *info,
                 const struct timespec *timeout);
// 返回值: >0 = 信号编号, -1 = 出错/超时 (errno: EINTR/EAGAIN/EINVAL)
// timeout: NULL = 无限等待
```

**这是多线程下处理信号最干净的方式。** 不像 handler 那样"异步打断"——`sigwait` 是**同步阻塞**等待：线程正常调用 `sigwait()`，阻塞直到信号到达，返回后可以调用任何函数（没有 async-signal-safe 限制）。

使用步骤：
1. 主线程在创建其他线程之前，`pthread_sigmask(SIG_BLOCK)` 屏蔽目标信号。
2. 子线程继承屏蔽字（或显式屏蔽）。
3. 专用线程执行 `sigwait(set, &sig)`，同步等待。
4. 信号到达后 `sigwait` 返回，线程根据信号执行业务逻辑。

> **关键前提**：目标信号必须在**所有线程**中被屏蔽，否则信号会走 handler 而非 `sigwait`。

`sigwaitinfo()` 比 `sigwait` 多了 `siginfo_t` 输出（能拿到 `si_pid`/`si_value` 等）。`sigtimedwait()` 再增加了超时支持。

### 1.6 `signalfd()` —— 把信号变成 fd（epoll 集成）

```c
#include <sys/signalfd.h>
int signalfd(int fd, const sigset_t *mask, int flags);
// fd:    -1 = 创建新 fd, 已有 fd = 修改监听的信号集
// mask:  要监听的信号集 (这些信号必须被 block, 否则走 handler)
// flags: SFD_CLOEXEC / SFD_NONBLOCK / 0
// 返回值: >=0 = 新 fd, -1 = 失败
```

**为什么用它**：信号变为普通 fd 可读 → epoll 唤醒 → `read()` 读出信号 → 在主循环里同步处理。没有 handler、没有 EINTR 重试、没有 async-signal-safe 限制、和 IO 事件统一调度。

`read(fd, &fdsi, sizeof(fdsi))` 读出 `struct signalfd_siginfo`，其中的 `ssi_signo`（信号编号）、`ssi_pid`（发送者 PID）、`ssi_uid`（发送者 UID）、`ssi_code`（来源）等价于 `siginfo_t` 的对应字段。

### 1.7 `sigqueue()` —— 带数据发送 RT 信号

```c
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
// pid: 目标进程 tgid
// sig: 信号编号 —— ★ 必须是 SIGRTMIN..SIGRTMAX 之一
// value: 附带数据 (sival_int 或 sival_ptr)
// 返回值: 0 成功, -1 失败
```

接收方 handler 通过 `info->si_value.sival_int` / `info->si_value.sival_ptr` 拿到附带数据。

和 `kill()` 的关键区别：

| | `kill(SIGUSR1)` | `sigqueue(SIGRTMIN+0, value)` |
|--|----------------|-------------------------------|
| 排队 | 不排队——连续 3 次可能只投递 1 次 | RT 信号排队——连续 3 次依次投递 |
| 附带数据 | 无 (`si_value` 为空) | 携带 `union sigval` |
| 容量限制 | 无（只计数一次） | 受 `RLIMIT_SIGPENDING` 限制（每个 user 最大挂起信号数） |

### 1.8 `sigaltstack()` —— 备用信号栈（防止栈溢出时无法处理 SIGSEGV）

```c
#include <signal.h>
int sigaltstack(const stack_t *ss, stack_t *oss);
// ss: 新栈配置 (NULL=只查询), oss: 旧栈配置输出 (NULL=不关心)
// 返回值: 0 成功, -1 失败
```

`stack_t` 结构：`ss_sp`（栈基址）、`ss_size`（大小，推荐 `SIGSTKSZ`=8KB）、`ss_flags`（`0`/`SS_DISABLE` 禁用/`SS_ONSTACK` 当前在用）。

**什么时候需要**：程序可能栈溢出 → `SIGSEGV`，默认栈不够容纳 `sigframe + handler`；或者想在 handler 中安全打印 backtrace。设置备用栈后，`sigaction` 必须加 `SA_ONSTACK` flag。

```c
// 使用示例
void setup_altstack(void)
{
    static char altstack[SIGSTKSZ] __attribute__((aligned(16)));
    stack_t ss = { .ss_sp = altstack, .ss_size = SIGSTKSZ, .ss_flags = 0 };
    sigaltstack(&ss, NULL);
}
void register_segv_with_altstack(void)
{
    struct sigaction sa;
    sa.sa_sigaction = segv_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO | SA_ONSTACK;  // ★ SA_ONSTACK!
    sigaction(SIGSEGV, &sa, NULL);
}
```

### 1.9 `sigsuspend()` / `pause()` —— 原子"解除屏蔽 + 等待"

```c
#include <signal.h>
int sigsuspend(const sigset_t *mask);
int pause(void);
// sigsuspend 返回: 永远返回 -1, errno=EINTR (这是正常行为!)
```

`sigsuspend(mask)` 原子地完成三件事：① 用 `mask` 替换当前线程的屏蔽字 → ② 挂起直到收到未被 `mask` 屏蔽的信号 → ③ handler 返回后恢复原来的屏蔽字。

这解决了经典的 **TOCTOU 竞态**：如果分两步做——先 `sigprocmask(SIG_UNBLOCK)` 再 `pause()`——信号可能在两者之间到达并永远丢失。`sigsuspend` 把"解除屏蔽 + 等待"合为原子操作，消除窗口。

### 1.10 `sa_flags` 速查表（设错了坑很多）

| flag | 含义 | 何时设 | 不设的后果 |
|------|------|--------|-----------|
| `SA_SIGINFO` | handler 用 `sa_sigaction`（三参数版） | 需要 `info->si_pid`/`si_addr`/`si_value` 时 | handler 只能用 `sa_handler`（单参数），丢失来源信息 |
| `SA_RESTART` | 可中断系统调用被信号打断后自动重启 | 不希望 `read`/`write` 因为信号返回 `EINTR` 时 | `read(fd,...)` 可能返回 -1, `errno=EINTR`，需手动重试 |
| `SA_NODEFER` | handler 执行中**不自动屏蔽**当前信号 | handler 需要可重入（当前信号可以再次进入） | 当前信号自动屏蔽，同种信号排队等 handler 执行完 |
| `SA_NOCLDSTOP` | SIGCHLD 只在子进程**终止**时产生，暂停/恢复时不产生 | 不关心子进程 STOP/CONT 状态 | SIGCHLD 在子进程 STOP/CONT 时也触发，干扰 waitpid |
| `SA_NOCLDWAIT` | 子进程终止后**不变成僵尸**（自动回收） | 不想 waitpid 且不关心子进程退出状态 | 子进程变僵尸，需显式 waitpid |
| `SA_ONSTACK` | handler 在 `sigaltstack` 设置的备用栈上执行 | 主栈可能溢出（SIGSEGV handler/深递归栈） | handler 使用主栈——主栈若已满则 handler 无法执行 |
| `SA_RESETHAND` | handler 执行一次后恢复为 SIG_DFL（System V 兼容） | 兼容老代码 | handler 不会被清除，每次信号都调你的 handler |
| `SA_NODEFER` | handler 执行中屏蔽字不追加当前信号 | 同种信号可嵌套 | 同种信号 handler 期间被阻塞，防止栈无限嵌套 |

---

## 二、十大关键点与常见坑

### 2.1 坑一：在 handler 里调用非 async-signal-safe 函数（最常见）

```c
// WRONG —— 可能死锁/崩溃!
void bad_handler(int sig)
{
    printf("Got signal %d\n", sig);  // ★ printf 不是 async-signal-safe!
    malloc(1024);                      // ★ malloc 不是 async-signal-safe!
    pthread_mutex_lock(&g_lock);       // ★ 死锁! 如果被中断的线程正持有 g_lock
    // 想象: 主线程 print("hello") → 拿到 stdout 内部锁 → 信号来了
    //       → handler → printf("signal") → 试图拿 stdout 锁 → 死锁!
}
// CORRECT —— handler 只做"通知"
volatile sig_atomic_t g_got_signal = 0;
int sig_pipefd[2];
void good_handler(int sig)
{
    g_got_signal = sig;                 // OK: sig_atomic_t 的写入是原子的
    char c = sig;
    write(sig_pipefd[1], &c, 1);        // OK: write() 是 async-signal-safe
}
```

#### async-signal-safe 的正式定义

POSIX.1 (IEEE Std 1003.1-2001, §2.4.3 "Signal Actions") 给出了严格的原文定义：

> "The behavior is undefined if the signal handler refers to any object other than errno with static storage duration other than by assigning a value to an object declared as volatile sig_atomic_t, or if the signal handler calls any function defined in this standard other than one of the functions listed in the following table."

译：信号 handler 中，如果访问了除 `errno` 以外的任何静态存储期对象（除非只对 `volatile sig_atomic_t` 类型的变量做赋值），或者调用了标准规定的函数列表之外的任何函数 → **行为未定义**。

---

#### 这条规则的本质——"为什么某些函数安全、某些不安全"

| 不安全的原因 | 根本机制 |
|-------------|---------|
| **① 不可重入**（non-reentrant） | 函数内部使用了全局/静态数据。如 `malloc` 操作全局 free list——主线程 `malloc` 到一半被中断 → handler 再 `malloc` → free list 处于不一致状态 → 堆损坏/UB。 |
| **② 持锁死锁** | 函数内部持有互斥锁。如 `printf` 持有 `stdout` 的 `FILE*` 内部锁——主线程 `printf` 获得锁 → 信号中断 → handler `printf` 试图获得同一把锁 → 死锁。 |
| **③ 同信号自嵌套**（self-deadlock） | handler 执行中同种信号再次到达（除非设 `SA_NODEFER`）→ handler 嵌套 → 重入同一函数 → 内部状态破坏。 |
| **④ errno 被覆盖** | handler 调用会设置 `errno` 的函数 → 主线程正在检查的 `errno` 被篡改。所以 handler 入口应 `save/restore errno`（见 §2.10）。 |

**async-signal-safe 函数通过以下方式规避这些风险**：
- 无状态的纯计算（`getpid`、`getuid`……）
- 直接系统调用，内核保证原子性（`write`、`read`、`close`……）
- 内部不依赖用户态锁、不维护跨调用状态（`open`、`stat`……）

> **一句话记忆**：凡是"内部有锁、有 `malloc` 缓冲、有跨调用全局状态"的函数，都不是 async-signal-safe。handler 里只能用"内核直接代理"的系统调用包装函数。

---

##### 白名单（POSIX.1-2001 §2.4.3 完整列表，常用子集）

```bash
_exit()    abort()    accept()    access()    alarm()
bind()     chdir()    chmod()     chown()     close()
connect()  creat()    dup()       dup2()      execve()
fchmod()   fchown()   fcntl()     fdatasync() fork()
fstat()    ftruncate() getegid()   geteuid()   getgid()
getgroups() getpeername() getpgrp() getpid()   getppid()
getsockname() getsockopt() getuid() kill()    link()
listen()   lseek()    lstat()     mkdir()     open()
pipe()     poll()     pselect()   _Exit()     read()
recv()     recvfrom() recvmsg()   rename()    rmdir()
select()   sem_post() send()      sendmsg()   sendto()
setgid()   setuid()   shutdown()  sigaction() sigaddset()
sigdelset() sigemptyset() sigfillset() sigismember()
signal()   sigpending() sigprocmask() sigsuspend()
sleep()    socket()   socketpair() stat()     symlink()
time()     times()    umask()     uname()     unlink()
utime()    wait()     waitpid()   write()
```

##### ★ 不是 async-signal-safe 但大家常用的（会出问题！）

`printf` / `fprintf` / `sprintf`、`malloc` / `free` / `realloc`、`pthread_mutex_lock`、`pthread_cond_signal`、`std::string`、`std::vector`、`new` / `delete`、`syslog()`（内部有 `malloc`）

### 2.2 坑二：EINTR —— 系统调用被信号中断

```c
// WRONG —— 信号导致的 EINTR, 程序以为 IO 出错
ssize_t bad_read(int fd, void *buf, size_t count)
{
    ssize_t n = read(fd, buf, count);
    if (n < 0) {
        perror("read failed");  // 可能打印 "read failed: Interrupted system call"
        return -1;              // 但这不是真正的错误!
    }
    return n;
}
// CORRECT: 循环重试
ssize_t safe_read(int fd, void *buf, size_t count)
{
    ssize_t n;
    do {
        n = read(fd, buf, count);
    } while (n == -1 && errno == EINTR);
    return n;
}
// 或者用 glibc 提供的宏:
//   ssize_t n = TEMP_FAILURE_RETRY(read(fd, buf, count));
```

**受 EINTR 影响的系统调用**（常见，不全）：

`read` / `write` / `recv` / `send`（slow devices：终端/socket/pipe）、`select` / `poll` / `epoll_wait`、`sleep` / `nanosleep` / `usleep`、`wait` / `waitpid` / `waitid`、`accept` / `connect`、`flock` / `fcntl(F_SETLKW)`、`sem_wait` / `sem_timedwait`、`sigwait` / `sigsuspend` / `pause`、`msgrcv` / `msgsnd` / `semop`。

**不受 EINTR 影响**的：磁盘 IO 的 `read`/`write`（块设备，非 slow device），以及设 `SA_RESTART` 后的大部分 syscall（但**不完全**！见 §2.5）。

### 2.3 坑三：handler 与主程序共享变量 —— volatile sig_atomic_t 不是万能药

`volatile sig_atomic_t` 只保证**单次读写**不被信号打断。任何涉及"读-改-写"的操作（如 `g_counter++` 对应 load + inc + store 三条指令）仍然不是原子的——信号可能在任意两条指令之间到达，导致计数丢失。

```c
// WRONG —— g_counter++ 不是原子操作!
volatile sig_atomic_t g_counter = 0;
void counter_handler(int sig) { g_counter++; }  // load+inc+store, 可能丢计数
// CORRECT 方案 1: sig_atomic_t 只用于"设标志" (单次赋值)
volatile sig_atomic_t g_flag = 0;
void flag_handler(int sig) { g_flag = 1; }  // OK: 单次赋值是原子的
```

**方案 2：阻塞信号保护临界区**。如果主线程需要安全地读写共享变量，可以在读取前临时屏蔽信号——这时甚至不需要 `volatile`：

```c
int g_count = 0;  // 不需要 volatile sig_atomic_t
int get_count_safe(void)
{
    sigset_t block_set, old_set;
    sigemptyset(&block_set);
    sigaddset(&block_set, SIGUSR1);
    pthread_sigmask(SIG_BLOCK, &block_set, &old_set);  // 信号不会在此时打断
    int ret = g_count;
    pthread_sigmask(SIG_SETMASK, &old_set, NULL);
    return ret;
}
```

**`sig_atomic_t` 的类型限制**：它保证是"单次读写原子"的整数类型，通常是 `int`（`sizeof(int)` 以内）。在 32-bit 平台上 `long long` 的 load/store 不是原子的——不能用作 `sig_atomic_t`。`struct`/packed 类型也不行。

### 2.4 坑四：SA_NODEFER 与 handler 重入的栈爆炸

设了 `SA_NODEFER` 后，handler 执行期间**不会自动屏蔽当前信号**——同种信号可以再次进入 handler。如果 handler 执行时间超过信号的到达间隔，会导致无限嵌套：

```bash
信号到达 → handler 开始 → 同一信号又到达 → 又一次进入 handler
→ 栈上又多压一层 sigframe → 又触发 → 又进入 → ... → 栈溢出 → SIGSEGV
```

```c
// WRONG —— SA_NODEFER + 慢 handler + 高频信号 = 栈爆炸
sa.sa_flags = SA_NODEFER;
sigaction(SIGALRM, &sa, NULL);
void alarm_handler(int sig)
{
    alarm(1);           // 1秒后又触发
    do_slow_work();     // 超过1秒 → handler 重新进入!
}
```

**默认行为（不设 `SA_NODEFER`）更安全**：同种信号在 handler 期间自动被屏蔽，排队等待，handler 返回后才投递下一个。

### 2.5 坑五：SA_RESTART 不是所有系统调用都生效

设了 `SA_RESTART` 不代表可以忽略 `EINTR`。它只能自动重启**部分**系统调用：

| `SA_RESTART` 能自动重启 | `SA_RESTART` **不能**重启（仍返回 `EINTR`！） |
|------------------------|------------------------------------------|
| `read()`/`write()`（slow device） | `poll()`、`ppoll()`、`select()`、`pselect()` |
| `wait()` 系列 | `epoll_wait()`、`epoll_pwait()` |
| `ioctl()` 等 | `sleep()`、`nanosleep()` |
| | `connect()`（已发起连接的） |
| | `recvfrom()`、`recvmsg()`、`sendto()`、`sendmsg()` |

**结论**：网络编程里尤其注意——`epoll_wait` 即使设了 `SA_RESTART` 也会返回 `EINTR`。

```c
while (running) {
    int n = epoll_wait(epoll_fd, events, MAX_EVENTS, timeout);
    if (n == -1 && errno == EINTR)
        continue;  // 即使有 SA_RESTART, epoll_wait 也会返回 EINTR!
    // ... 处理事件
}
```

### 2.6 坑六：多线程信号处理模式选择

多线程下有四种处理信号的模式，按推荐度从高到低排列：

---

#### 模式 1（★★★★★ 最推荐）：sigwait + 专用线程

完全同步——没有 handler、没有 async-signal-safe 限制、没有 EINTR。专用线程阻塞在 `sigwait()`，信号到达后正常返回，可以调用任何函数。

```c
#include <signal.h>
#include <pthread.h>
static void *signal_thread(void *arg)
{
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGTERM);
    sigaddset(&set, SIGINT);
    int sig;
    while (1) {
        if (sigwait(&set, &sig) != 0) continue;
        switch (sig) {
        case SIGTERM: case SIGINT:
            printf("Shutting down...\n");  // 可以 printf!
            cleanup_and_exit();
            break;
        }
    }
    return NULL;
}
void setup_signal_thread(void)
{
    // 关键: 在主线程创建其他线程之前屏蔽目标信号——子线程自动继承屏蔽字
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGTERM);
    sigaddset(&set, SIGINT);
    pthread_sigmask(SIG_BLOCK, &set, NULL);
    pthread_t tid;
    pthread_create(&tid, NULL, signal_thread, NULL);
    // 然后启动业务线程...
}
```

---

#### 模式 2（★★★★★ 适合已有事件循环）：signalfd + epoll

信号变为 fd 可读事件，和业务 IO 统一调度。同样无 handler、无 async-signal-safe 限制。

```c
void setup_signalfd_epoll(void)
{
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGTERM);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGCHLD);
    pthread_sigmask(SIG_BLOCK, &mask, NULL);  // 必须 block
    int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
    struct epoll_event ev = { .events = EPOLLIN, .data.fd = sfd };
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sfd, &ev);
    // 主循环中 epoll_wait 返回 sfd 可读 → read(sfd, ...) → 同步处理
}
```

---

#### 模式 3（★★★ 简单场景）：handler 设 flag

最简单，但局限大——只能通知、不能传数据，只能在主循环中周期性轮询检查。

```c
volatile sig_atomic_t g_shutdown = 0;
void shutdown_handler(int sig) { g_shutdown = 1; }
// 主循环:
while (!g_shutdown) {
    // ... 业务逻辑，周期性检查 g_shutdown ...
}
```

---

#### 模式 4（★★☆ 需要快速响应）：handler 写 pipe

handler 中 `write()` 一个字节到 pipe，主循环把 pipe 读端加入 epoll——比轮询 flag 更快响应。

```c
int sig_pipe[2];
void pipe_handler(int sig)
{
    char c = (char)sig;
    write(sig_pipe[1], &c, 1);  // write 是 async-signal-safe
}
// 主循环把 sig_pipe[0] 加入 epoll → 收到信号立即唤醒 → 同步处理
```

### 2.7 坑七：fork() + 信号 = 灾难合集

多线程程序中使用 `fork()` + 信号有三个经典问题：

**问题 1 —— handler 遗传但上下文丢失**：子进程继承了父进程的所有 `sigaction` handler，但子进程只有一条线程（调用 `fork()` 的那条）。如果父进程是 sigwait 模式，子进程没有 signal_thread——信号可能走到不被期望的路径上。

> **解决**：`execve` 后 handler 全部重置为 `SIG_DFL`（已加载新程序），这就是为什么 `fork + exec` 相对安全。非 exec 路径需要在 `fork()` 后手动重置关键 handler。

**问题 2 —— 多线程 fork 锁死**（async-signal-safe 灾难）：另一线程 `fork()` → 子进程只克隆了调用 `fork()` 的那条线程 → 如果被蒸发的那条线程正持有一把锁 `L` → 子进程中 `L` 永远处于 locked 状态但无人解锁 → 子进程任何 `lock(&L)` 操作**永久死锁**。

> ★ **绝对不要在信号 handler 中 `fork()`！**

**问题 3 —— SIGCHLD 竞态**：`fork()` 后立即注册 `SIGCHLD` handler 时，子进程可能已经退出了。

> **解决**：`fork()` 前 `block SIGCHLD` → `fork()` → 注册 handler → 再 `unblock`（见 §2.8 的完整正确写法）。

### 2.8 坑八：SIGCHLD + waitpid 的竞态条件

经典 race condition：如果先 `fork()` 再注册 `SIGCHLD` handler，子进程可能在 handler 注册完成前就退出了——SIGCHLD 丢失。

```c
// WRONG 1: 顺序反了
signal(SIGCHLD, sigchld_handler);
pid_t pid = fork();    // 如果子进程在 signal() 和 fork() 之间退出了?
// WRONG 2: 另一种竞态
pid_t pid = fork();
if (pid == 0) { exit(0); }
signal(SIGCHLD, sigchld_handler);  // 子进程可能已经退出了!
```

**正确做法**：先 block `SIGCHLD` → `fork` → 注册 handler → 再 unblock。在屏蔽期间子进程退出也不会丢失信号——`SIGCHLD` 留在 pending 中，解除屏蔽后立即投递。

```c
void safe_fork_and_collect(void)
{
    sigset_t block_mask, old_mask;
    sigemptyset(&block_mask);
    sigaddset(&block_mask, SIGCHLD);
    sigprocmask(SIG_BLOCK, &block_mask, &old_mask);  // ① 先屏蔽
    struct sigaction sa;
    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
    sigaction(SIGCHLD, &sa, NULL);             // ② 注册 handler
    pid_t pid = fork();                         // ③ fork
    if (pid == 0) {
        sigprocmask(SIG_SETMASK, &old_mask, NULL);
        /* child logic */
        _exit(0);
    }
    sigprocmask(SIG_SETMASK, &old_mask, NULL);  // ④ unblock (此时 handler 已就位)
}
// sigchld_handler 中安全回收所有退出的子进程:
void sigchld_handler(int sig)
{
    int saved_errno = errno;  // ★ 保存 errno! (见 2.10)
    pid_t pid;
    int status;
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        // 记录 pid 的退出状态, 或通过其他机制通知主循环
        // 不能在 handler 中调用 malloc/printf!
    }
    errno = saved_errno;      // ★ 恢复 errno
}
```

### 2.9 坑九：信号栈溢出与 SA_ONSTACK

**场景**：程序因 bug 导致栈无限递归 → 栈撞到 guard page → `SIGSEGV`。**问题**：此时正常栈已满，内核尝试在栈上再压一个 sigframe → 失败 → 进程直接被杀，没有任何机会记录"栈溢出了"的诊断信息。

**解决**：设置备用栈 + `SA_ONSTACK`。handler 在备用栈上执行，即使主栈已满也能安全记录诊断信息再退出（`_exit`，不要 `return`——栈已毁）。

```c
#include <signal.h>
static uint8_t altstack_buf[SIGSTKSZ] __attribute__((aligned(16)));
void segv_handler(int sig, siginfo_t *info, void *ctx)
{
    ucontext_t *uc = (ucontext_t *)ctx;
    void *fault_addr = info->si_addr;
    char msg[256];
    int len = snprintf(msg, sizeof(msg),
                       "SIGSEGV at %p, IP=%p, SP=%p\n",
                       fault_addr,
                       (void*)uc->uc_mcontext.gregs[REG_RIP],
                       (void*)uc->uc_mcontext.gregs[REG_RSP]);
    write(STDERR_FILENO, msg, len);  // write 是 async-signal-safe
    _exit(128 + sig);                // 不要 return! 栈已毁
}
void setup_segv_altstack(void)
{
    stack_t ss = { .ss_sp = altstack_buf, .ss_size = SIGSTKSZ, .ss_flags = 0 };
    sigaltstack(&ss, NULL);
    struct sigaction sa;
    sa.sa_sigaction = segv_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGBUS, &sa, NULL);
}
```

### 2.10 坑十：信号 handler 中 errno 的保存与恢复

handler 中调用的函数（如 `waitpid`）可能修改 `errno`。如果被中断的主线程正在检查 `errno`（例如刚执行过 `read` 返回 -1 后读 `errno`），handler 会悄无声息地篡改它——这是非常隐蔽的 bug。

```c
// WRONG —— waitpid 可能把 errno 改成 ECHILD, 破坏被中断代码的 errno
void bad_sigchld(int sig)
{
    pid_t pid;
    while ((pid = waitpid(-1, NULL, WNOHANG)) > 0);
}
// CORRECT: 进入 handler 时保存 errno, 退出前恢复
void good_sigchld(int sig)
{
    int saved_errno = errno;  // ★ 第一时间保存
    pid_t pid;
    while ((pid = waitpid(-1, NULL, WNOHANG)) > 0);
    errno = saved_errno;      // ★ 退出前恢复
}
```

> **经验法则**：所有信号 handler 第一行 = 保存 `errno`，最后一行 = 恢复 `errno`。

---

## 三、典型场景 Demo

### 3.1 优雅退出 —— SIGTERM/SIGINT 信号处理

**场景**：服务器收到 `SIGTERM` 或 Ctrl-C（`SIGINT`）时：① 停止接受新连接 → ② 等待当前请求完成 → ③ 清理资源 → ④ 退出。

**策略**：`signalfd` + epoll —— 优雅退出 = 同步流程，不用 handler。

```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/signalfd.h>
#include <sys/epoll.h>
#include <errno.h>
#define MAX_EVENTS 64
static volatile sig_atomic_t g_running = 1;
static void setup_signal_fd(int epoll_fd)
{
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGTERM);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGHUP);   // reload 配置
    /* ★ 阻塞信号: 不让信号走默认动作或 handler */
    if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) {
        perror("sigprocmask");
        exit(1);
    }
    /* 创建 signalfd */
    int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
    if (sfd == -1) {
        perror("signalfd");
        exit(1);
    }
    /* 加入 epoll */
    struct epoll_event ev = {
        .events = EPOLLIN,
        .data = { .fd = sfd },
    };
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sfd, &ev) == -1) {
        perror("epoll_ctl signalfd");
        exit(1);
    }
}
static void handle_signal(int sfd)
{
    struct signalfd_siginfo fdsi;
    ssize_t n = read(sfd, &fdsi, sizeof(fdsi));
    if (n != sizeof(fdsi)) {
        if (errno == EAGAIN) return;  // 没有更多信号
        perror("read signalfd");
        return;
    }
    switch (fdsi.ssi_signo) {
    case SIGTERM:
    case SIGINT:
        printf("Received signal %d, shutting down gracefully...\n",
               fdsi.ssi_signo);
        g_running = 0;
        break;
    case SIGHUP:
        printf("Received SIGHUP, reloading config...\n");
        /* reload_config(); ← 同步调用, 没有 handler 限制! */
        break;
    }
}
int main(void)
{
    int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
    if (epoll_fd == -1) { perror("epoll_create1"); return 1; }
    setup_signal_fd(epoll_fd);
    /* 业务 fd 也加入 epoll... */
    struct epoll_event events[MAX_EVENTS];
    while (g_running) {
        int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (n == -1) {
            if (errno == EINTR) continue;  /* ★ 处理 EINTR */
            perror("epoll_wait");
            break;
        }
        for (int i = 0; i < n; i++) {
            /* 通过 data.fd 区分是 signalfd 还是业务 fd */
            handle_signal(events[i].data.fd);
        }
    }
    printf("Shutdown complete.\n");
    close(epoll_fd);
    return 0;
}
```

### 3.2 超时机制 —— alarm + 信号

**场景**：读取用户输入，5 秒超时。

**策略**：`alarm(5)` + `SIGALRM` handler 设 flag + `read()` 返回 `EINTR`。适用于简单场景；复杂场景请用 epoll + timerfd。

```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
static volatile sig_atomic_t g_timeout = 0;
static void alarm_handler(int sig)
{
    g_timeout = 1;  // 只设 flag, 不做 IO
}
ssize_t timed_read(int fd, void *buf, size_t count, unsigned int timeout_sec)
{
    struct sigaction sa, old_sa;
    /* 设置 SIGALRM handler */
    sa.sa_handler = alarm_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;  // 不设 SA_RESTART: 让 read 返回 EINTR
    sigaction(SIGALRM, &sa, &old_sa);
    g_timeout = 0;
    alarm(timeout_sec);  // 设置闹钟
    ssize_t n = read(fd, buf, count);
    alarm(0);                    // 取消闹钟
    sigaction(SIGALRM, &old_sa, NULL);  // 恢复旧 handler
    if (n == -1 && errno == EINTR && g_timeout)
        return 0;  // 超时: 返回 0
    return n;  // 正常读取
}
// Demo
int main(void)
{
    char buf[256];
    printf("Enter something (5 sec timeout): ");
    fflush(stdout);
    ssize_t n = timed_read(STDIN_FILENO, buf, sizeof(buf) - 1, 5);
    if (n == 0) {
        printf("\nTimeout!\n");
    } else if (n > 0) {
        buf[n] = '\0';
        printf("You entered: %s\n", buf);
    } else {
        perror("read");
    }
    return 0;
}
```

### 3.3 多线程：sigwait 专用信号线程

**场景**：多线程服务器，用一个专用线程处理所有外部信号。

**优点**：信号处理完全同步（可以做任何事：`printf`、`malloc`、加锁）、不打断业务线程（业务线程甚至不知道信号存在）、线程安全（没有 handler 竞争）。

```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#include <errno.h>
#define NUM_WORKERS 4
static volatile sig_atomic_t g_running = 1;
static void *worker_thread(void *arg)
{
    int id = *(int *)arg;
    while (g_running) {
        printf("[Worker %d] doing work...\n", id);
        sleep(2);
    }
    printf("[Worker %d] stopping.\n", id);
    return NULL;
}
static void *signal_thread(void *arg)
{
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGTERM);
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGUSR1);
    int sig;
    while (1) {
        int ret = sigwait(&set, &sig);
        if (ret != 0) {
            fprintf(stderr, "sigwait error: %s\n", strerror(ret));
            continue;
        }
        switch (sig) {
        case SIGTERM:
        case SIGINT:
            printf("[Signal thread] Received %d, shutting down...\n", sig);
            g_running = 0;
            return NULL;  // 退出信号线程, main 等待 worker 结束
        case SIGUSR1:
            printf("[Signal thread] Received SIGUSR1, printing stats...\n");
            /* 可以调用任何函数: malloc/printf/加锁/访问全局数据 */
            /* 不需要 async-signal-safe! */
            break;
        }
    }
}
int main(void)
{
    pthread_t sig_tid, workers[NUM_WORKERS];
    int ids[NUM_WORKERS];
    /* ★ 步骤 1: 在主线程中屏蔽目标信号 */
    sigset_t block_set;
    sigemptyset(&block_set);
    sigaddset(&block_set, SIGTERM);
    sigaddset(&block_set, SIGINT);
    sigaddset(&block_set, SIGUSR1);
    pthread_sigmask(SIG_BLOCK, &block_set, NULL);
    /* ★ 步骤 2: 创建信号线程 (继承屏蔽字——依然 block) */
    pthread_create(&sig_tid, NULL, signal_thread, NULL);
    /* ★ 步骤 3: 创建工作线程 (继承屏蔽字——所有线程都 block 这些信号) */
    for (int i = 0; i < NUM_WORKERS; i++) {
        ids[i] = i;
        pthread_create(&workers[i], NULL, worker_thread, &ids[i]);
    }
    /* 等待信号线程结束 (收到 SIGTERM/SIGINT 时) */
    pthread_join(sig_tid, NULL);
    /* 等待所有工作线程结束 */
    for (int i = 0; i < NUM_WORKERS; i++) {
        pthread_join(workers[i], NULL);
    }
    printf("All threads stopped. Goodbye.\n");
    return 0;
}
```

### 3.4 signalfd + epoll 完整集成

**场景**：已有 epoll 主循环的服务器，把信号当做"一种 fd 事件"统一处理。

**和 §3.1 的区别**：这里展示 `signalfd` 收到信号后读出信号详情，以及用 `SIGCHLD` 回收已退出的子进程。

```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/signalfd.h>
#include <sys/epoll.h>
#include <sys/wait.h>
#include <errno.h>
static volatile sig_atomic_t g_running = 1;
static int g_signal_fd = -1;
int setup_signals(int epoll_fd)
{
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGTERM);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGCHLD);  // 子进程退出
    sigaddset(&mask, SIGHUP);   // reload
    sigprocmask(SIG_BLOCK, &mask, NULL);
    g_signal_fd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
    if (g_signal_fd == -1) {
        perror("signalfd");
        return -1;
    }
    struct epoll_event ev = {
        .events  = EPOLLIN,
        .data.fd = g_signal_fd,
    };
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, g_signal_fd, &ev) == -1) {
        perror("epoll_ctl signalfd");
        return -1;
    }
    return 0;
}
void process_signal_event(void)
{
    struct signalfd_siginfo fdsi;
    ssize_t n;
    for (;;) {
        n = read(g_signal_fd, &fdsi, sizeof(fdsi));
        if (n == -1 && errno == EAGAIN)
            break;  // 没有更多信号了
        if (n != sizeof(fdsi)) {
            perror("read signalfd");
            return;
        }
        switch (fdsi.ssi_signo) {
        case SIGTERM:
        case SIGINT:
            printf("[main] signal %d from pid=%d, shutting down\n",
                   fdsi.ssi_signo, fdsi.ssi_pid);
            g_running = 0;
            break;
        case SIGCHLD: {
            /* ★ 在 epoll 回调里安全 waitpid——没有 async-signal-safe 限制! */
            int status;
            pid_t pid;
            while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
                if (WIFEXITED(status))
                    printf("[main] child %d exited with %d\n",
                           pid, WEXITSTATUS(status));
                else if (WIFSIGNALED(status))
                    printf("[main] child %d killed by signal %d\n",
                           pid, WTERMSIG(status));
            }
            break;
        }
        case SIGHUP:
            printf("[main] SIGHUP, reload config...\n");
            /* reload_config_file(); */
            break;
        }
    }
}
int main(void)
{
    int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
    setup_signals(epoll_fd);
    /* 模拟: fork 一个子进程来做背景工作 */
    pid_t pid = fork();
    if (pid == 0) {
        sleep(2);
        printf("[child] exiting\n");
        _exit(42);
    }
    struct epoll_event events[16];
    while (g_running) {
        int n = epoll_wait(epoll_fd, events, 16, 5000);
        if (n == -1) {
            if (errno == EINTR) continue;
            perror("epoll_wait");
            break;
        }
        for (int i = 0; i < n; i++) {
            if (events[i].data.fd == g_signal_fd)
                process_signal_event();
            else
                /* 处理业务 fd 事件 */;
        }
    }
    close(g_signal_fd);
    close(epoll_fd);
    return 0;
}
```

### 3.5 SIGSEGV 崩溃时打印 backtrace（备用栈 + RBP 链遍历）

**场景**：程序 crash（`SIGSEGV`/`SIGBUS`/`SIGABRT`）时，在退出前输出调用栈，帮助定位问题。

**约束**：handler 必须在备用栈上执行（主栈可能已满）；只能调用 async-signal-safe 函数；`backtrace()` 是 GNU 扩展且内部用 `malloc`——不安全。这里用 `write()` + 手动 RBP 链遍历。生产环境可用 libunwind 的 `_Ux86_64_step()`（不分配内存）。

```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
/* 备用栈 */
static uint8_t g_altstack[SIGSTKSZ] __attribute__((aligned(16)));
static void crash_handler(int sig, siginfo_t *info, void *ucontext)
{
    ucontext_t *uc = (ucontext_t *)ucontext;
#define REG(R) uc->uc_mcontext.gregs[REG_##R]
    /* write() 直接写到 stderr, async-signal-safe */
    char buf[512];
    int len = snprintf(buf, sizeof(buf),
        "\n"
        "========================================\n"
        "FATAL: signal %d (%s)\n"
        "========================================\n"
        "  fault addr:  %p\n"
        "  RIP:         %p\n"
        "  RSP:         %p\n"
        "  RBP:         %p\n"
        "  RAX:         0x%016lx\n"
        "  errno:       %d\n"
        "========================================\n",
        sig, strsignal(sig),
        info->si_addr,
        (void*)REG(RIP),
        (void*)REG(RSP),
        (void*)REG(RBP),
        REG(RAX),
        errno);
    write(STDERR_FILENO, buf, len);
    /* ★ 简单调用栈回溯: 从 RBP 链手动遍历 */
    void **rbp = (void **)REG(RBP);
    int frames = 0;
    while (rbp && frames < 20) {
        void *ret_addr = rbp[1];  // x86-64: [RBP+8] = return address
        if (!ret_addr) break;
        char line[128];
        int l = snprintf(line, sizeof(line),
                         "  [%2d] %p\n", frames, ret_addr);
        write(STDERR_FILENO, line, l);
        void **next_rbp = (void **)rbp[0];
        if (next_rbp <= rbp) break;  // 栈只向下生长
        rbp = next_rbp;
        frames++;
    }
    write(STDERR_FILENO, "========================================\n", 43);
#undef REG
    _exit(128 + sig);  // 不要 return——栈状态不确定
}
void install_crash_handler(void)
{
    /* 设置备用栈 */
    stack_t ss = {
        .ss_sp    = g_altstack,
        .ss_size  = SIGSTKSZ,
        .ss_flags = 0,
    };
    if (sigaltstack(&ss, NULL) == -1) {
        perror("sigaltstack");
        return;
    }
    struct sigaction sa = {
        .sa_sigaction = crash_handler,
        .sa_flags     = SA_SIGINFO | SA_ONSTACK,
    };
    sigemptyset(&sa.sa_mask);
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGBUS,  &sa, NULL);
    sigaction(SIGABRT, &sa, NULL);
    sigaction(SIGFPE,  &sa, NULL);
    sigaction(SIGILL,  &sa, NULL);
}
/* Demo: 触发一个 SIGSEGV */
int main(void)
{
    install_crash_handler();
    printf("About to crash...\n");
    fflush(stdout);
    /* 写 NULL 指针 — 触发 SIGSEGV */
    *(volatile int *)NULL = 42;
    printf("This should never print.\n");
    return 0;
}
```

运行输出示例：

```bash
About to crash...
========================================
FATAL: signal 11 (Segmentation fault)
========================================
  fault addr:  (nil)
  RIP:         0x401320
  RSP:         0x7ffe12340000
  ...
========================================
```

然后可用 `addr2line` 定位：`$ addr2line -e ./a.out 0x401320` → 输出源码行号。

---

## 四、和本仓库其他文档的关系

| 相关文档 | 内容 | 与本文的关系 |
|---------|------|-------------|
| [signals-kernel.md](/concepts/process/task-resources/signals-kernel.md) | 信号内核机制：数据结构 + 调用链 + 完整传递路径 6 阶段 | 姊妹篇——内核篇讲"怎么走"，本篇讲"怎么写"。写用户态代码前先看内核篇理解底层时序，遇到坑回来看这篇 |
| [signals-alternatives.md](/concepts/process/task-resources/signals-alternatives.md) | 信号性能替代方案：signalfd / eventfd / timerfd / futex 为什么比信号快 ~10× | 补充篇——本篇 §1.6 提到了 signalfd，那篇彻底讲清为什么 signalfd/eventfd 比传统信号高性能（逐项对比内核路径） |
| [signal-multithread.md](/concepts/process/signal-multithread.md) | 多线程信号投递：complete_signal 选线程算法、内核↔用户态切换每一帧 | 本篇 §2.6 的 sigwait 模式在这篇有完整的内核时序解释 |
| [fork-and-threads.md](/concepts/process/fork-and-threads.md) | 多线程 fork 的陷阱 | 本篇 §2.7 的 fork + 信号问题在这篇有详细展开 |
| [../../crash/signals.md](/crash/signals.md) | 崩溃排查：哪些信号杀进程、产 core、反推 bug | 本篇 §3.5 的 crash handler 是那篇的补充——那篇讲"crash 后怎么办"，本篇 §3.5 讲"crash 前怎么捕获" |

---

## 五、一句话总结

> **信号编程的核心原则：handler 只做"通知"（设 `volatile sig_atomic_t` flag / `write` pipe / `sem_post`），真正的处理放到主循环中——通过 flag 轮询 / pipe epoll / signalfd + epoll / sigwait 专用线程 把异步信号转化为同步事件。记住三句话：① handler 里只能用 async-signal-safe 函数（`printf`/`malloc`/加锁 全不能用）；② 所有可能被信号打断的系统调用都要处理 `EINTR`（即使设了 `SA_RESTART`，`epoll_wait`/`poll`/`select` 仍会返回 `EINTR`）；③ 多线程下最快放弃 handler，用 `sigwait` 专用线程或 `signalfd` + epoll 同步处理。`sigaction` 永远比 `signal` 好，`signalfd` 永远比 handler 安全。**

