瀏覽代碼

ingest: 02-进程线程与IPC(5篇)

OpenCode 14 小時之前
父節點
當前提交
b09f45ee1f

+ 943 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/02-进程线程与IPC/01-信号机制.md

@@ -0,0 +1,943 @@
+---
+title: 信号机制
+tags: [嵌入式Linux, Linux应用编程, 进程, 信号, signal, sigaction, 信号集, 阻塞, 实时信号, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第八章 信号:基础"
+---
+
+# 信号机制
+
+> 💡 **关联知识**:[[02-进程线程与IPC/02-进程管理]]、[[02-进程线程与IPC/03-进程间通信]]、[[02-进程线程与IPC/04-线程与线程同步]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/6. 信号机制]]
+
+信号是 Linux 应用编程里处理**异步事件**的核心手段:进程正常跑着,某个事件(按键、除零、定时器超时、子进程退出)突然发生,内核用一个"编号"打断进程当前流程,通知它去应对。本篇从概念、分类、常见信号、默认行为一路讲到 `signal` / `sigaction`、发送信号、信号集、阻塞与等待,最后是实战测试、跨平台对比与面试题。
+
+本章篇幅较长,因为信号"概念简单、细节很多":同一个信号在不同状态下行为不同,阻塞与未决(pending)是两个概念,实时信号又能排队。把这些厘清,才算真正会用信号。
+
+---
+
+## 1. 基本概念
+
+### 1.1 信号是什么
+
+信号是**事件发生时对进程的通知机制**,也可以称为**软件中断**。它与硬件中断的相似之处在于:能够打断程序当前执行的正常流程,是软件层次上对中断机制的一种模拟。大多数情况下无法预测信号到达的准确时间,所以信号提供了一种处理**异步事件**的方法。
+
+几个关键定性:
+
+- **信号是用来通信的**:一个具有合适权限的进程能向另一个进程发送信号,这可以作为一种同步技术,甚至是进程间通信(IPC)的原始形式。
+- **信号由谁处理、怎么处理**:信号通常发送给对应进程,进程收到后执行以下操作之一——**忽略信号**、**捕获信号**(执行预先绑定的处理函数)、**执行系统默认操作**。
+- **信号是异步的**:产生信号的事件对进程而言是随机出现的,进程不能靠"测试一个变量"或简单系统调用来判断是否产生了信号,只能被打断后被动进入处理流程。
+- **信号本质上是 int 类型数字编号**:内核为每个信号定义了唯一整数编号,从 1 开始顺序展开;每个信号都有对应名字(宏),程序里应使用符号名而非编号,因为编号可能因系统而异。
+
+> 两种信号**绝不能被忽略**:`SIGKILL` 和 `SIGSTOP`。原因是它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号,进程的运行行为是未定义的。
+
+### 1.2 信号从哪里来
+
+| 来源 | 说明 | 例子 |
+| ---- | ---- | ---- |
+| 硬件异常 | 硬件检测到错误条件通知内核,内核再发给相关进程 | 除数为 0、数组越界引用无法访问的内存 |
+| 终端特殊字符 | 终端下输入产生信号的字符 | `Ctrl+C` → `SIGINT`;`Ctrl+\` → `SIGQUIT`;`Ctrl+Z` → `SIGTSTP` |
+| `kill()` 系统调用 | 进程把任意信号发给另一个进程或进程组 | 受限:收发双方所有者相同,或发送者是 root |
+| `kill` 命令 | 用户在终端向其它进程发信号,内部就是 `kill()` | `kill -9 xxx` 杀死 PID 为 xxx 的进程 |
+| 软件事件 | 检测到某种软件条件已发生 | 定时器超时、CPU 时间超限、子进程退出 |
+| 进程自身 | 进程可以给自己发信号 | `raise()`、`abort()` |
+
+绝大多数到达进程的信号来自内核。
+
+### 1.3 信号处理流程图
+
+```mermaid
+flowchart TD
+    A[事件发生] --> B{信号来源}
+    B -->|硬件异常| C[内核]
+    B -->|终端 Ctrl+C / Ctrl+Z| C
+    B -->|kill 系统调用 / kill 命令| C
+    B -->|软件事件: 定时器 / 子进程退出| C
+    C --> D[内核向目标进程投递信号]
+    D --> E{信号是否在进程信号掩码中?}
+    E -->|是: 阻塞| F[信号置为未决 pending]
+    F --> G[解除阻塞后重新投递]
+    G --> H{进程设置的处理方式}
+    E -->|否| H
+    H -->|SIG_IGN 忽略| I[丢弃, 无任何影响]
+    H -->|捕获: 自定义处理函数| J[执行信号处理函数]
+    H -->|SIG_DFL 系统默认| K[执行默认操作: term/ core/ ignore/ stop/ cont]
+    J --> L[返回被中断处继续执行]
+```
+
+### 1.4 程序启动与进程创建时的处理方式
+
+- **程序启动**:应用刚启动(或代码还没执行到 `signal()`)时,进程对所有信号的处理方式都是**系统默认操作**。所以不调用 `signal()` 的程序,`Ctrl+C` 会按默认操作终止进程。
+- **进程创建**:父进程调用 `fork()` 创建子进程时,子进程**继承父进程的信号处理方式**(子进程复制父进程内存映像,信号捕获函数地址在子进程中有意义)。
+
+---
+
+## 2. 信号的分类
+
+Linux 下从两个角度分类:**可靠性**(可靠信号 / 不可靠信号)与**实时性**(实时信号 / 非实时信号)。
+
+### 2.1 可靠信号与不可靠信号
+
+早期 UNIX 信号机制简单原始,问题主要有两个:
+
+- 进程每次处理信号后,就把对该信号的响应设置为系统默认操作,用户若不想这样,就得在处理函数结尾再调用一次 `signal()` 重新绑定。
+- 于是"不可靠信号"主要指的是:进程可能对信号做出错误反应,以及**信号可能丢失**(处理信号时又来了新信号,导致丢失)。
+
+Linux 支持不可靠信号,但做了改进:**调用完信号处理函数后不必重新调用 `signal()`**。因此 Linux 下的不可靠信号问题主要就是"**信号可能丢失**"。
+
+- 信号值 **小于 SIGRTMIN(34)** 的(即 1~31)都是不可靠信号。
+- 新增的 `SIGRTMIN~SIGRTMAX`(34~64)被定义为**可靠信号**:支持排队、不会丢失;发送函数为 `sigqueue()`,绑定函数为 `sigaction()`。
+
+### 2.2 实时信号与非实时信号
+
+实时性分类与可靠性分类相互对应:
+
+- **非实时信号**都不支持排队,都是不可靠信号;一般也称为**标准信号**。
+- **实时信号**都支持排队,都是可靠信号;它保证了发送的多个信号都能被接收,是 POSIX 标准的一部分。
+
+```mermaid
+flowchart LR
+    subgraph STD["标准信号 1~31"]
+        S1[不可靠]
+        S2[不支持排队]
+        S3[信号可能丢失]
+    end
+    subgraph RT["实时信号 34~64"]
+        R1[可靠]
+        R2[支持排队, 多次发送多次传递]
+        R3[可携带伴随数据]
+    end
+```
+
+使用 `kill -l` 可以查看系统所有信号:编号 1~31 是不可靠信号,34~64 是可靠信号,可靠信号没有具体名字,用 `SIGRTMIN+N` / `SIGRTMAX-N` 表示。
+
+---
+
+## 3. 常见信号与默认行为
+
+标准信号(1~31)中,每个信号都有确定的用途、含义以及系统默认操作。
+
+| 信号名称 | 编号 | 描述 | 系统默认操作 |
+| -------- | ---- | ---- | ------------ |
+| SIGINT | 2 | 终端中断符(`Ctrl+C`) | term |
+| SIGQUIT | 3 | 终端退出符(`Ctrl+\`) | term + core |
+| SIGILL | 4 | 非法硬件指令 | term + core |
+| SIGABRT | 6 | 异常终止(`abort()`) | term + core |
+| SIGBUS | 7 | 内存访问错误 | term + core |
+| SIGFPE | 8 | 算术异常 | term + core |
+| SIGKILL | 9 | 终极终止信号(不可阻塞/忽略/捕获) | term |
+| SIGUSR1 | 10 | 用户自定义信号 1 | term |
+| SIGSEGV | 11 | 无效的内存引用 | term + core |
+| SIGUSR2 | 12 | 用户自定义信号 2 | term |
+| SIGPIPE | 13 | 管道关闭 | term |
+| SIGALRM | 14 | 定时器超时(alarm) | term |
+| SIGTERM | 15 | 终止进程(`kill` 默认信号) | term |
+| SIGCHLD / SIGCLD | 17 | 子进程终止或停止 | ignore |
+| SIGCONT | 18 | 使停止状态的进程继续运行 | cont |
+| SIGSTOP | 19 | 停止进程(不可阻塞/忽略/捕获) | stop |
+| SIGTSTP | 20 | 终端停止符(`Ctrl+Z`) | stop |
+| SIGXCPU | 24 | 超过 CPU 限制 | term + core |
+| SIGVTALRM | 26 | 虚拟定时器超时 | term |
+| SIGWINCH | 28 | 终端窗口尺寸发生变化 | ignore |
+| SIGPOLL / SIGIO | 29 | 异步 I/O | term / ignore |
+| SIGSYS | 31 | 无效系统调用 | term + core |
+
+> **默认操作缩写**:`term` 终止进程;`core` 生成核心转储文件;`ignore` 忽略信号;`cont` 继续运行进程;`stop` 停止进程(注意停止 ≠ 终止,而是暂停)。
+
+### 3.1 重点信号逐条说明
+
+- **SIGINT**:用户在终端按下中断字符(通常 `Ctrl+C`),内核发给前台进程组中每个进程。默认终止进程——这就是平时 `Ctrl+C` 能杀掉前台进程的原因。
+- **SIGQUIT**:按下退出字符(通常 `Ctrl+\`),内核发给前台进程组中每个进程。默认终止进程并生成核心转储文件。进程陷入无限循环、不再响应时用它比较合适。
+- **SIGKILL**:**必杀信号**,无法被阻塞、忽略或捕获,总能终止进程。当 `SIGINT` / `SIGQUIT` 都杀不掉时才动它,`kill -9 xxx` 的 `-9` 就是它。
+- **SIGSEGV**:无效内存引用时发送,常见原因是解引用了含错误地址的指针(如未初始化指针)或传了无效参数。默认终止进程。
+- **SIGPIPE**:向已关闭的管道、FIFO 或套接字写入时产生。默认终止进程。
+- **SIGALRM**:与 `alarm()` 或 `setitimer()` 有关,定时器到时内核发它给应用。
+- **SIGTERM**:**终止进程的标准信号**,也是 `kill` 命令的默认信号。精心设计的应用应捕获 `SIGTERM` 并绑定处理函数,在函数里清理临时文件、释放资源再退出。直接 `kill -9` 会跳过这些处理,是暴力方式,应作为最后手段。
+- **SIGCHLD**:父进程的某个子进程终止时内核发给父进程;子进程因信号停止或恢复时也可能发送(停止指暂停,不是终止)。默认忽略;父进程若想被告知子进程状态改变,应捕获它。
+- **SIGCONT**:发给已停止的进程使其恢复运行。进程不在停止态时默认忽略;在停止态时默认使其继续运行。
+- **SIGSTOP**:**必停信号**,无法忽略或捕获,总能停止进程(停止只是暂停,进程没有终止)。
+- **SIGTSTP**:终端停止字符(通常 `Ctrl+Z`),内核发给前台进程组中每个进程使其停止。
+- **SIGUSR1 / SIGUSR2**:供程序员自定义使用,内核绝不会主动为进程产生它们,可用于进程间通知事件或同步。
+- **SIGHUP**:用户准备退出会话时,系统向会话发 `SIGHUP`,会话再发给所有子进程,子进程收到后默认终止。详见「守护进程」相关章节。
+
+---
+
+## 4. 设置信号处理方式
+
+Linux 提供 `signal()` 与 `sigaction()` 两个接口设置处理方式,推荐使用后者。
+
+### 4.1 signal()
+
+```c
+#include <signal.h>
+
+typedef void (*sig_t)(int);
+sig_t signal(int signum, sig_t handler);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `signum` | 需要设置的信号,可使用信号名(宏)或数字编号,建议用信号名 |
+| `handler` | `sig_t` 类型函数指针:自定义处理函数(捕获)、`SIG_IGN`(忽略)、`SIG_DFL`(系统默认) |
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| 成功 | 返回指向此前信号处理函数的指针 |
+| 失败 | 返回 `SIG_ERR`,并设置 `errno` |
+
+`SIG_IGN`、`SIG_DFL` 的定义:
+
+```c
+/* Fake signal functions. */
+#define SIG_ERR ((sig_t) -1)   /* Error return. */
+#define SIG_DFL ((sig_t) 0)    /* Default action. */
+#define SIG_IGN ((sig_t) 1)    /* Ignore signal. */
+```
+
+处理函数的 `int` 参数就是当前触发的信号,用于把多个信号绑到同一个处理函数时区分来源。
+
+### 4.2 sigaction()
+
+```c
+#include <signal.h>
+
+int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `signum` | 需要设置的信号(`SIGKILL`、`SIGSTOP` 除外) |
+| `act` | 指向 `struct sigaction`,描述新处理方式;为 `NULL` 表示不改变当前处理方式 |
+| `oldact` | 不为 `NULL` 时,返回信号之前的处理方式;不关心可传 `NULL` |
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| 成功 | 返回 0 |
+| 失败 | 返回 -1,并设置 `errno` |
+
+`sigaction()` 允许单独获取处理函数而不设置,还可以通过标志精细控制调用处理函数时的行为,因此灵活性与可移植性更好。
+
+### 4.3 struct sigaction
+
+```c
+struct sigaction {
+    void     (*sa_handler)(int);
+    void     (*sa_sigaction)(int, siginfo_t *, void *);
+    sigset_t   sa_mask;
+    int        sa_flags;
+    void     (*sa_restorer)(void);
+};
+```
+
+| 成员 | 含义 |
+| ---- | ---- |
+| `sa_handler` | 指定信号处理函数,与 `signal()` 的 `handler` 相同 |
+| `sa_sigaction` | 替代处理函数,能获取更多信息(通过 `siginfo_t`)。与 `sa_handler` 互斥,通过 `SA_SIGINFO` 标志选择 |
+| `sa_mask` | 执行处理函数前,把这组信号加入进程信号掩码;处理函数返回后恢复。进入处理函数时,当前处理的信号会被**自动**加入掩码,保证同种信号不重入 |
+| `sa_restorer` | 已过时,不要使用 |
+| `sa_flags` | 一组控制标志,多个用 `\|` 组合 |
+
+`sa_flags` 可用标志:
+
+| 标志 | 含义 |
+| ---- | ---- |
+| `SA_NOCLDSTOP` | 若 `signum` 为 `SIGCHLD`,子进程停止(收到 `SIGSTOP`/`SIGTSTP`/`SIGTTIN`/`SIGTTOU`)或恢复(收到 `SIGCONT`)时不收 `SIGCHLD` |
+| `SA_NOCLDWAIT` | 若 `signum` 是 `SIGCHLD`,子进程终止时不转为僵尸进程 |
+| `SA_NODEFER` | 不阻塞从自身处理函数中接收的此信号(默认会阻塞同种信号以避免竞态) |
+| `SA_RESETHAND` | 执行完处理函数后,把处理方式重置为系统默认操作 |
+| `SA_RESTART` | 被信号中断的系统调用,在信号处理完成后自动重新发起 |
+| `SA_SIGINFO` | 使用 `sa_sigaction` 而非 `sa_handler` |
+
+### 4.4 siginfo_t
+
+```c
+siginfo_t {
+    int       si_signo;     /* Signal number */
+    int       si_errno;     /* An errno value */
+    int       si_code;      /* Signal code */
+    int       si_trapno;    /* Trap number that caused hardware-generated signal */
+    pid_t     si_pid;       /* Sending process ID */
+    uid_t     si_uid;       /* Real user ID of sending process */
+    int       si_status;    /* Exit value or signal */
+    clock_t   si_utime;     /* User time consumed */
+    clock_t   si_stime;     /* System time consumed */
+    sigval_t  si_value;     /* Signal value */
+    int       si_int;       /* POSIX.1b signal */
+    void     *si_ptr;       /* POSIX.1b signal */
+    int       si_overrun;   /* Timer overrun count; POSIX.1b timers */
+    int       si_timerid;   /* Timer ID; POSIX.1b timers */
+    void     *si_addr;      /* Memory location which caused fault */
+    long      si_band;      /* Band event */
+    int       si_fd;        /* File descriptor */
+    short     si_addr_lsb;  /* Least significant bit of address */
+    void     *si_call_addr; /* Address of system call instruction */
+    int       si_syscall;   /* Number of attempted system call */
+    unsigned int si_arch;   /* Architecture of attempted system call */
+}
+```
+
+实际使用时用 `man sigaction` 查阅即可,重点字段是 `si_signo`、`si_code`、`si_pid`、`si_value`。
+
+### 4.5 信号处理函数说明
+
+一般而言,**信号处理函数设计得越简单越好**,道理和中断处理函数一样:越快越好,不要做大量消耗 CPU 时间的事情。重要原因是:越简单,引发信号竞争条件(竞态)的风险越低。
+
+---
+
+## 5. 向进程发送信号
+
+### 5.1 kill()
+
+```c
+#include <sys/types.h>
+#include <signal.h>
+
+int kill(pid_t pid, int sig);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `pid` | 目标进程/进程组,取值见下表 |
+| `sig` | 需要发送的信号;为 0 时不发送信号,但仍执行错误检查,可用于判断进程是否存在 |
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| 成功 | 返回 0 |
+| 失败 | 返回 -1,并设置 `errno`(如 `ESRCH` 表示进程不存在) |
+
+`pid` 不同取值含义:
+
+| pid 取值 | 含义 |
+| -------- | ---- |
+| `pid > 0` | 发送给 pid 指定的进程 |
+| `pid == 0` | 发送给当前进程所在进程组中的每个进程 |
+| `pid == -1` | 发送给当前进程有权发送信号的每个进程,但进程 1(init)除外 |
+| `pid < -1` | 发送给 ID 为 `-pid` 的进程组中的每个进程 |
+
+权限规则:root 进程可以把信号发给任何进程;普通用户进程的基本规则是**发送者进程的实际/有效用户 ID 必须等于接收者进程的实际/有效用户 ID**。
+
+`killpg()` 也可用于向进程组发送信号;`raise()` 则是向自身发送信号。
+
+### 5.2 raise()
+
+```c
+#include <signal.h>
+
+int raise(int sig);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `sig` | 需要发送的信号 |
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| 成功 | 返回 0 |
+| 失败 | 返回非零值 |
+
+`raise()` 等价于 `kill(getpid(), sig);`,其中 `getpid()` 用于获取进程自身 PID。
+
+### 5.3 sigqueue()(实时信号)
+
+```c
+#include <signal.h>
+
+int sigqueue(pid_t pid, int sig, const union sigval value);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `pid` | 接收信号的进程 PID |
+| `sig` | 需要发送的信号;为 0 时用于检查进程是否存在 |
+| `value` | 信号的伴随数据,`union sigval` 类型 |
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| 成功 | 返回 0 |
+| 失败 | 返回 -1,并设置 `errno` |
+
+```c
+typedef union sigval {
+    int sival_int;
+    void *sival_ptr;
+} sigval_t;
+```
+
+伴随数据既可以是整型,也可以是指针。
+
+---
+
+## 6. alarm() 与 pause()
+
+### 6.1 alarm()
+
+```c
+#include <unistd.h>
+
+unsigned int alarm(unsigned int seconds);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `seconds` | 定时秒数;为 0 表示取消之前设置的 alarm 闹钟 |
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| 成功 | 若之前已设置闹钟且未超时,返回旧闹钟的剩余秒数;否则返回 0 |
+
+要点:
+
+- 定时时间到时,内核向进程发送 `SIGALRM` 信号。
+- **每个进程只能设置一个 alarm 闹钟**,新闹钟会替代旧闹钟。
+- alarm 闹钟**只能触发一次**,不能循环;要实现循环触发,可在 `SIGALRM` 处理函数中再次调用 `alarm()`。
+- `SIGALRM` 默认操作是终止进程,但大多数使用闹钟的进程都会捕获它。
+
+### 6.2 pause()
+
+```c
+#include <unistd.h>
+
+int pause(void);
+```
+
+`pause()` 使进程暂停运行、进入休眠,直到进程捕获到一个信号为止;只有执行了信号处理函数并从其返回时 `pause()` 才返回。此时返回 -1,并把 `errno` 设置为 `EINTR`。
+
+用 `alarm()` + `pause()` 可以模拟一个 `sleep`:设置闹钟后挂起,闹钟到点触发 `SIGALRM`,处理函数执行完返回,`pause()` 随之中断。
+
+---
+
+## 7. 信号集
+
+信号集(signal set)是能表示多个信号的数据类型,很多系统调用用它传参,如 `sigaction()`、`sigprocmask()`、`sigpending()`。它其实就是 `sigset_t`:
+
+```c
+#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
+typedef struct {
+    unsigned long int __val[_SIGSET_NWORDS];
+} sigset_t;
+```
+
+### 7.1 操作 API
+
+```c
+#include <signal.h>
+
+int sigemptyset(sigset_t *set);                  /* 初始化为不含任何信号 */
+int sigfillset(sigset_t *set);                   /* 初始化为包含所有信号(含实时信号) */
+int sigaddset(sigset_t *set, int signum);        /* 添加一个信号 */
+int sigdelset(sigset_t *set, int signum);        /* 删除一个信号 */
+int sigismember(const sigset_t *set, int signum);/* 测试信号是否在集合中 */
+```
+
+| 函数 | 参数 | 返回值 |
+| ---- | ---- | ------ |
+| `sigemptyset` / `sigfillset` | `set` 指向待初始化信号集 | 成功 0;失败 -1 并设 `errno` |
+| `sigaddset` / `sigdelset` | `set` 信号集;`signum` 目标信号 | 成功 0;失败 -1 并设 `errno` |
+| `sigismember` | `set` 信号集;`signum` 待测信号 | 在集合中返回 1;不在返回 0;失败 -1 并设 `errno` |
+
+典型用法:
+
+```c
+sigset_t sig_set;
+
+sigemptyset(&sig_set);       /* 清空 */
+sigaddset(&sig_set, SIGINT); /* 加入 SIGINT */
+
+if (1 == sigismember(&sig_set, SIGINT))
+    puts("信号集中包含 SIGINT 信号");
+```
+
+---
+
+## 8. 获取信号的描述信息
+
+Linux 下每个信号都有一串对应的字符串描述信息,存放在 `sys_siglist` 数组中,可直接用 `sys_siglist[SIGINT]` 获取。更推荐使用 `strsignal()`。
+
+```c
+#include <string.h>
+
+char *strsignal(int sig);
+```
+
+返回 `sig` 对应描述信息字符串的指针;若 `sig` 无效,返回 `"Unknown signal"`。
+
+```c
+#include <signal.h>
+
+void psignal(int sig, const char *s);
+```
+
+`psignal()` 把信号的描述信息输出到标准错误,并允许调用者附加信息:整个输出由字符串 `s`、冒号、空格、描述信号编号的字符串和换行符组成。
+
+---
+
+## 9. 信号掩码(阻塞信号传递)
+
+内核为每个进程维护一个**信号掩码**(就是一个信号集)。当进程收到属于掩码中的信号时,该信号被阻塞、无法投递给进程处理,内核会将它保持为**未决(pending)**状态,直到该信号从掩码中移除,才投递给进程。
+
+向信号掩码中添加信号的方式:
+
+1. 调用 `signal()` 或 `sigaction()` 为某信号设置处理方式时,进程**自动**把该信号加入掩码,保证处理期间同种信号被阻塞(`sigaction()` 是否如此取决于 `SA_NODEFER`);处理函数返回后自动移除。
+2. `sigaction()` 的 `sa_mask` 额外指定一组信号,进入处理函数时加入、返回后移除。
+3. 使用 `sigprocmask()` 显式添加/移除。
+
+### 9.1 sigprocmask()
+
+```c
+#include <signal.h>
+
+int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `how` | 操作方式,见下表 |
+| `set` | 要加入/移除的信号集;为 `NULL` 表示不改动当前掩码 |
+| `oldset` | 不为 `NULL` 时,在修改前获取当前信号掩码存入其中 |
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| 成功 | 返回 0 |
+| 失败 | 返回 -1,并设置 `errno` |
+
+`how` 取值:
+
+| 宏 | 含义 |
+| -- | ---- |
+| `SIG_BLOCK` | 把 `set` 中所有信号加入信号掩码(当前值 ∪ set) |
+| `SIG_UNBLOCK` | 把 `set` 中所有信号从信号掩码移除 |
+| `SIG_SETMASK` | 信号掩码直接设置为 `set` |
+
+```c
+sigset_t sig_set;
+
+sigemptyset(&sig_set);              /* 初始化为空 */
+sigaddset(&sig_set, SIGINT);        /* 加入 SIGINT */
+sigprocmask(SIG_BLOCK, &sig_set, NULL);   /* 阻塞 SIGINT */
+/* ... 关键代码 ... */
+sigprocmask(SIG_UNBLOCK, &sig_set, NULL); /* 解除阻塞 */
+```
+
+---
+
+## 10. 阻塞等待信号:sigsuspend()
+
+考虑这样一个场景:执行受保护的关键代码时不希望被 `SIGINT` 打断,于是执行前把 `SIGINT` 加入掩码,执行完恢复掩码,然后调用 `pause()` 等待信号唤醒。
+
+问题在于:如果信号的传递恰好发生在**恢复信号掩码之后、`pause()` 之前**,信号处理函数会先执行,返回后主程序再进入 `pause()` 被阻塞,直到下一次信号才能唤醒——这有违本意。虽然概率不大,但确实是缺陷。
+
+解决方案是把"恢复信号掩码"和"挂起进程"封装成一个**原子操作**,这正是 `sigsuspend()` 的目的:
+
+```c
+#include <signal.h>
+
+int sigsuspend(const sigset_t *mask);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `mask` | 指向一个信号集 |
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| 总是返回 -1 | 设置 `errno` 指示错误,通常为 `EINTR`(被信号中断);调用失败时 `errno` 设为 `EFAULT` |
+
+`sigsuspend()` 把进程信号掩码替换为 `mask`,然后挂起进程,直到捕获到信号被唤醒(如果捕获的信号本身是 `mask` 集合成员,则不会唤醒、继续挂起),并从信号处理函数返回;一旦返回,`sigsuspend()` 会把信号掩码恢复为调用前的值。
+
+它等价于以不可中断(原子)方式执行:
+
+```c
+sigprocmask(SIG_SETMASK, &mask, &old_mask);
+pause();
+sigprocmask(SIG_SETMASK, &old_mask, NULL);
+```
+
+---
+
+## 11. 实时信号与 sigpending()
+
+### 11.1 sigpending()
+
+如果进程正在执行信号处理函数,期间又收到信号,且该信号在信号掩码中,内核会阻塞它,把它加入进程的**等待信号集**(未决信号集)。`sigpending()` 用于获取当前处于等待状态的信号:
+
+```c
+#include <signal.h>
+
+int sigpending(sigset_t *set);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `set` | 处于等待状态的信号会存放到它指向的信号集中 |
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| 成功 | 返回 0 |
+| 失败 | 返回 -1,并设置 `errno` |
+
+```c
+sigset_t sig_set;
+
+sigemptyset(&sig_set);
+sigpending(&sig_set);
+if (1 == sigismember(&sig_set, SIGINT))
+    puts("SIGINT 信号处于等待状态");
+```
+
+### 11.2 实时信号的优势
+
+等待信号集只是一个掩码,仅表明信号是否发生,不能表示发生次数:同一个信号在阻塞状态下产生多次,只会被记录一次,之后仅传递一次——这是标准信号的缺点之一。
+
+实时信号相比标准信号的优势:
+
+- 信号范围扩大,可应用于应用自定义目的(标准信号只有 `SIGUSR1`、`SIGUSR2` 两个可自定义)。
+- 内核采用**队列化管理**:同一实时信号多次发送会多次传递。
+- 发送实时信号时可指定**伴随数据**(整型或指针值),供接收进程在处理函数中获取。
+- **不同实时信号的传递顺序得到保障**:多个实时信号等待时,先传编号最小的(编号越小优先级越高);同类型多个信号排队时,传递顺序与发送顺序一致。
+
+Linux 内核定义了 31 个实时信号,编号范围 34~64;`SIGRTMIN` 表示编号最小的实时信号,`SIGRTMAX` 表示最大的。
+
+应用使用实时信号需要两点:
+
+1. 发送进程用 `sigqueue()` 发送实时信号及伴随数据。
+2. 接收进程用 `sigaction()` 为该信号建立处理函数,并加入 `SA_SIGINFO`,用 `sa_sigaction` 指向的处理函数,这样才能收到伴随数据(用 `sa_handler` 则收不到伴随数据)。
+
+---
+
+## 12. 异常退出:abort()
+
+```c
+#include <stdlib.h>
+
+void abort(void);
+```
+
+`abort()` 通常产生 `SIGABRT` 信号来终止调用它的进程。`SIGABRT` 默认操作是终止进程并生成核心转储文件。即使在程序中捕获了 `SIGABRT`,程序依然会终止:无论阻塞还是忽略 `SIGABRT`,`abort()` 调用都不受影响,总会成功终止进程。
+
+对比:`exit()`、`_exit()`、`_Exit()` 用于**正常退出**,`abort()` 用于**异常退出**。
+
+---
+
+## 13. 信号处理函数编写注意事项(扩展)
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+### 13.1 为什么处理函数里不能随便调用库函数
+
+信号处理函数可能在任意时刻打断主流程,如果主流程正持有锁、正在修改某个全局数据结构(如 `malloc` 的内部堆管理结构、`stdio` 的缓冲区),而处理函数又调用了同样的非可重入函数,就会破坏内部状态。
+
+- **可重入(reentrant)**:函数在执行过程中被打断、再次进入后仍能正确工作。多数库函数不可重入(内部使用静态/全局数据)。
+- **异步信号安全(async-signal-safe)**:POSIX 规定了信号处理函数中可以安全调用的函数集合,如 `write()`、`read()`、`_exit()`、`open()`、`close()`、`kill()`、`sigaction()`、`signal()`、`waitpid()` 等。
+
+**`printf()`、`malloc()`、`free()`、`strtok()`、`exit()` 不属于异步信号安全函数**。所以处理函数里通常用 `write(STDOUT_FILENO, ...)` 而不是 `printf()`。
+
+### 13.2 处理函数通用原则
+
+- 尽量只设置一个 `volatile sig_atomic_t` 标志位,把实际工作留给主循环。
+- 使用 `volatile sig_atomic_t` 类型的全局标志,保证读写是原子的且不被编译器优化掉。
+- 避免在信号处理函数中做耗时、加锁、分配内存的操作。
+- 需要处理同种信号嵌套时,用 `sigaction()` 的 `sa_mask` 明确阻塞哪些信号。
+- 需要让被中断的系统调用自动重启时,加 `SA_RESTART` 标志。
+
+### 13.3 sigwait():同步等待信号(扩展)
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+`sigwait()` 提供"同步"等待信号的方式:先阻塞目标信号,再调用 `sigwait()` 挂起等待,它返回时把信号编号写入 `*sig`。
+
+```c
+#include <signal.h>
+
+int sigwait(const sigset_t *set, int *sig);
+```
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| 成功 | 返回 0 |
+| 失败 | 返回错误编号(不设置 `errno`) |
+
+与 `sigsuspend()` 的区别:`sigsuspend()` 仍会调用在 `sigaction` 中注册的处理函数,返回 -1 和 `EINTR`;`sigwait()` 则**不调用**处理函数,直接把信号取走。多线程程序里常用 `sigwait()` 让某个线程专职处理信号。
+
+---
+
+## 14. 完整可复制源码
+
+以下程序覆盖 signal、sigaction、kill、raise、alarm、pause、信号集、sigprocmask、sigsuspend、sigqueue 的主干用法,可直接编译运行。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <signal.h>
+#include <unistd.h>
+#include <sys/types.h>
+
+static volatile sig_atomic_t g_sig_flag = 0;
+
+/* 基础信号处理函数:最简实现,只打印信号编号 */
+static void sig_handler(int sig)
+{
+    printf("Received signal: %d\n", sig);
+}
+
+/* 支持伴随数据的处理函数,需配合 SA_SIGINFO 使用 */
+static void sig_rt_handler(int sig, siginfo_t *info, void *context)
+{
+    (void)context;
+    printf("接收到实时信号: %d, 伴随数据: %d, 来自进程: %d\n",
+           sig, info->si_value.sival_int, info->si_pid);
+}
+
+/* 通过全局标志位通知主循环(推荐做法) */
+static void sig_flag_handler(int sig)
+{
+    g_sig_flag = sig;
+}
+
+int main(int argc, char *argv[])
+{
+    if (argc < 2) {
+        fprintf(stderr, "用法: %s <demo>\n", argv[0]);
+        fprintf(stderr, "  signal    - signal() 捕获 SIGINT\n");
+        fprintf(stderr, "  sigaction - sigaction() 捕获 SIGINT\n");
+        fprintf(stderr, "  alarm     - alarm() + pause() 模拟 sleep\n");
+        fprintf(stderr, "  block     - sigprocmask() 阻塞 SIGINT\n");
+        fprintf(stderr, "  suspend   - sigsuspend() 原子等待信号\n");
+        fprintf(stderr, "  sigqueue  - 向自身发送实时信号及其伴随数据\n");
+        exit(-1);
+    }
+
+    /* ------------------------------------------------------------------
+     * 1. signal():捕获 Ctrl+C,不再终止进程
+     * ------------------------------------------------------------------ */
+    if (0 == strcmp(argv[1], "signal")) {
+        sig_t ret = signal(SIGINT, (sig_t)sig_handler);
+        if (SIG_ERR == ret) {
+            perror("signal error");
+            exit(-1);
+        }
+        printf("已捕获 SIGINT,按 Ctrl+C 试试(用 kill -9 才能退出)\n");
+        for ( ; ; )
+            pause();
+
+    /* ------------------------------------------------------------------
+     * 2. sigaction():功能等价,但控制力更强(推荐)
+     * ------------------------------------------------------------------ */
+    } else if (0 == strcmp(argv[1], "sigaction")) {
+        struct sigaction sig = {0};
+
+        sig.sa_handler = sig_handler;
+        sig.sa_flags = 0;
+        if (-1 == sigaction(SIGINT, &sig, NULL)) {
+            perror("sigaction error");
+            exit(-1);
+        }
+        printf("已捕获 SIGINT(sigaction 版本),按 Ctrl+C 试试\n");
+        for ( ; ; )
+            pause();
+
+    /* ------------------------------------------------------------------
+     * 3. alarm() + pause():模拟 sleep
+     * ------------------------------------------------------------------ */
+    } else if (0 == strcmp(argv[1], "alarm")) {
+        struct sigaction sig = {0};
+
+        sig.sa_handler = sig_handler;
+        sig.sa_flags = 0;
+        if (-1 == sigaction(SIGALRM, &sig, NULL)) {
+            perror("sigaction error");
+            exit(-1);
+        }
+        printf("启动 3 秒闹钟\n");
+        alarm(3);
+        pause();
+        puts("休眠结束");
+
+    /* ------------------------------------------------------------------
+     * 4. sigprocmask():信号掩码阻塞,验证 raise() 后不会立即处理
+     * ------------------------------------------------------------------ */
+    } else if (0 == strcmp(argv[1], "block")) {
+        struct sigaction sig = {0};
+        sigset_t sig_set;
+
+        sig.sa_handler = sig_handler;
+        sig.sa_flags = 0;
+        if (-1 == sigaction(SIGINT, &sig, NULL))
+            exit(-1);
+
+        sigemptyset(&sig_set);
+        sigaddset(&sig_set, SIGINT);
+        sigprocmask(SIG_BLOCK, &sig_set, NULL);   /* 阻塞 SIGINT */
+
+        raise(SIGINT);                            /* 立即发送,但被阻塞 */
+        printf("信号已发送,但被阻塞,先休眠 2 秒\n");
+        sleep(2);
+        printf("休眠结束,即将解除阻塞\n");
+
+        sigprocmask(SIG_UNBLOCK, &sig_set, NULL); /* 解除阻塞后立即处理 */
+
+    /* ------------------------------------------------------------------
+     * 5. sigsuspend():原子地恢复掩码并挂起
+     * ------------------------------------------------------------------ */
+    } else if (0 == strcmp(argv[1], "suspend")) {
+        struct sigaction sig = {0};
+        sigset_t new_mask, old_mask, wait_mask;
+
+        sigemptyset(&new_mask);
+        sigaddset(&new_mask, SIGINT);
+        sigemptyset(&wait_mask);
+
+        sig.sa_handler = sig_handler;
+        sig.sa_flags = 0;
+        if (-1 == sigaction(SIGINT, &sig, NULL))
+            exit(-1);
+
+        sigprocmask(SIG_BLOCK, &new_mask, &old_mask);
+        puts("执行保护代码段");
+        /* ... 受保护代码段 ... */
+
+        /* 原子地恢复掩码并挂起,避免"恢复后、pause 前"的信号丢失窗口 */
+        if (-1 != sigsuspend(&wait_mask))
+            exit(-1);
+
+        sigprocmask(SIG_SETMASK, &old_mask, NULL);
+
+    /* ------------------------------------------------------------------
+     * 6. sigqueue():发送实时信号及伴随数据
+     * ------------------------------------------------------------------ */
+    } else if (0 == strcmp(argv[1], "sigqueue")) {
+        struct sigaction sig = {0};
+        sigset_t sig_set;
+        union sigval sig_val;
+
+        sig.sa_sigaction = sig_rt_handler;
+        sig.sa_flags = SA_SIGINFO;    /* 必须,才能收到伴随数据 */
+        sigemptyset(&sig.sa_mask);
+        if (-1 == sigaction(SIGRTMIN, &sig, NULL)) {
+            perror("sigaction error");
+            exit(-1);
+        }
+
+        /* 解除 SIGRTMIN 的阻塞,确保实时信号能投递 */
+        sigemptyset(&sig_set);
+        sigaddset(&sig_set, SIGRTMIN);
+        sigprocmask(SIG_UNBLOCK, &sig_set, NULL);
+
+        sig_val.sival_int = 10;
+        if (-1 == sigqueue(getpid(), SIGRTMIN, sig_val)) {
+            perror("sigqueue error");
+            exit(-1);
+        }
+        puts("实时信号发送成功");
+        sleep(1);
+
+    } else {
+        fprintf(stderr, "未知参数: %s\n", argv[1]);
+        exit(-1);
+    }
+
+    exit(0);
+}
+```
+
+### 14.1 源码逐段解释
+
+- **`volatile sig_atomic_t g_sig_flag`**:信号处理函数与主流程共享数据时的标准类型,保证读写原子且不被优化。
+- **`signal()` 分支**:演示最简捕获写法。捕获后 `Ctrl+C` 不再终止进程,需另开终端 `kill -9`。
+- **`sigaction()` 分支**:与 `signal()` 等价功能,但设置 `sa_flags`、`sa_mask` 更灵活,是推荐写法。
+- **`alarm()` 分支**:`alarm(3)` 设置 3 秒闹钟,`pause()` 挂起;到点触发 `SIGALRM` 执行处理函数,返回后 `pause()` 返回。
+- **`block` 分支**:先 `SIG_BLOCK` 阻塞 `SIGINT`,然后 `raise(SIGINT)`;由于被阻塞,处理函数不会立刻执行,直到 `SIG_UNBLOCK` 解除后才执行。
+- **`suspend` 分支**:`sigsuspend(&wait_mask)` 用空集替换掩码(即不阻塞任何信号)并挂起,等价于原子地"恢复掩码 + pause"。
+- **`sigqueue` 分支**:`SA_SIGINFO` + `sa_sigaction` 才能取到 `si_value`;`SIGRTMIN` 属于可靠信号,支持伴随数据。
+
+---
+
+## 15. 实验步骤与调试方法
+
+### 15.1 编译与运行
+
+```bash
+# 本地 Ubuntu 上编译
+gcc -Wall -o sig_demo sig_demo.c
+
+./sig_demo signal      # 按 Ctrl+C,观察自定义处理函数被调用
+```
+
+### 15.2 常用调试命令
+
+| 命令 | 用途 |
+| ---- | ---- |
+| `kill -l` | 列出系统所有信号及其编号 |
+| `kill -9 <pid>` | 发送 SIGKILL,强制终止进程 |
+| `kill -SIGTERM <pid>` | 发送 SIGTERM(kill 默认信号) |
+| `ps -aux` \| `ps -ef` | 查看进程 PID 与状态 |
+| `top` | 动态查看进程、CPU 占用,可对进程发信号 |
+| `strace -e trace=signal ./app` | 跟踪进程收到的信号 |
+| `ulimit -c unlimited` | 允许生成核心转储文件 |
+
+### 15.3 实验一:捕获 SIGINT
+
+```bash
+./sig_demo signal &
+# 记录后台进程 pid
+kill -SIGINT <pid>     # 观察到 "Received signal: 2"
+kill -9 <pid>          # 只有 SIGKILL 才能杀死它
+```
+
+结论:进程捕获 `SIGINT` 后,`Ctrl+C` 与 `kill -SIGINT` 都无法终止它;`kill -9`(SIGKILL)可以。
+
+### 15.4 实验二:前台与后台的差异
+
+把捕获了 `SIGINT` 的程序放到后台运行,再按 `Ctrl+C`,会发现进程收不到信号——因为内核只把 `SIGINT` 发给**前台进程组**。需用 `kill` 命令手动发送。
+
+### 15.5 实验三:验证信号掩码
+
+运行 `./sig_demo block`:预期先打印"信号已发送,但被阻塞",2 秒后才打印"Received signal: 2"。说明阻塞期间信号未决、解除后才投递。
+
+### 15.6 实验四:验证实时信号伴随数据
+
+运行 `./sig_demo sigqueue`:预期打印"接收到实时信号: 34, 伴随数据: 10"。若去掉 `SA_SIGINFO`,则收不到伴随数据。
+
+---
+
+## 16. 跨平台对比:IMX6ULL vs STM32 vs RK3568
+
+| 维度 | I.MX6ULL(本教程) | STM32(裸机/HAL) | RK3568(Linux) |
+| ---- | ---------------- | ----------------- | --------------- |
+| 信号机制 | 完整 POSIX 信号,1~64 | 无进程信号;有硬件中断 NVIC | 完整 POSIX 信号 |
+| 异步事件载体 | signal / sigaction | 中断服务函数(ISR) | signal / sigaction |
+| 处理函数约束 | 异步信号安全、尽量短 | ISR 中不宜耗时、注意 `volatile` | 同 I.MX6ULL |
+| 定时事件 | alarm / setitimer + SIGALRM | SysTick / TIM 中断 | POSIX timer / alarm |
+| 进程模型 | 多进程,可 fork/vfork/exec | 通常单裸机主循环 + 中断 | 多进程、多线程 |
+| 实时信号 | 支持排队、可带伴随数据 | 无对应概念 | 支持 |
+| 调试工具 | `kill -l`、`strace`、`ps/top` | 调试器断点、逻辑分析仪 | 同 I.MX6ULL |
+
+要点:**STM32 裸机没有"信号"这一层**,它用 NVIC 硬件中断直接封装异步事件;而 I.MX6ULL 与 RK3568 都运行 Linux,本篇所有 API 通用。信号更适合"用户态异步通知",硬件事件仍由驱动 + 中断处理。
+
+---
+
+## 17. 面试精选(5 题)
+
+### Q1:信号的本质是什么?为什么说它是"软件中断"?什么是不可靠信号与可靠信号?
+
+**答案**:信号本质是内核定义的 `int` 类型数字编号(1~64),用于在事件发生时通知进程。它像硬件中断一样能打断程序正常流程,属于软件层面对中断机制的模拟,且到达时间不可预测,因此是处理异步事件的手段。按可靠性分:信号值 1~31(小于 `SIGRTMIN`,即 34)的是**不可靠信号**,不支持排队,阻塞期间多次到达只记一次、可能丢失;34~64 是**可靠信号(实时信号)**,支持排队,多次发送多次传递,还可携带伴随数据,且不同实时信号按编号从小到大传递。可靠信号用 `sigqueue()` 发送、`sigaction()` 绑定。
+
+### Q2:`signal()` 和 `sigaction()` 有什么区别?为什么推荐 `sigaction()`?
+
+**答案**:`signal()` 只接受"信号 + 处理函数指针"两个参数,用法简单,但无法设置 `sa_mask`(处理期间额外阻塞哪些信号)、`sa_flags`(如 `SA_SIGINFO`、`SA_RESTART`、`SA_NODEFER`),也无法单独获取旧处理方式而不修改,且不同实现语义有差异,可移植性差。`sigaction()` 通过 `struct sigaction` 提供完整控制:可指定替代处理函数 `sa_sigaction` 获取 `siginfo_t` 附加信息、可设置处理期间的信号掩码避免竞态、可用标志精确控制系统调用重启与同种信号是否阻塞,并能通过 `oldact` 取回旧设置。因此工程上推荐 `sigaction()`。此外两者都不能设置 `SIGKILL`、`SIGSTOP`。
+
+### Q3:信号被"阻塞"和"忽略"有什么区别?"未决(pending)"又是什么?
+
+**答案**:**忽略**是处理动作——用 `SIG_IGN` 设置后,信号到达时被直接丢弃,进程永远不会处理它;**阻塞**是投递控制——信号进入进程信号掩码,内核暂时不投递给进程,但它被记在"等待信号集(未决信号集)"中,一旦解除阻塞就立即投递并处理。所以阻塞不是丢弃,而是延后。用 `sigprocmask()` 增删掩码,用 `sigpending()` 查询当前有哪些信号处于未决状态。注意:等待信号集只是掩码,对标准信号无法表示"发生了几次",这也是实时信号支持排队的价值所在。
+
+### Q4:什么是 fork 之后的竞争条件?如何用信号保证父/子进程的执行顺序?
+
+**答案**:`fork()` 之后父子进程都在 `fork()` 返回处继续,谁先被 CPU 调度是不确定的(多核甚至可能同时运行),因此不能假设父先或子先。若程序正确性依赖特定顺序,就构成竞争条件。可以用信号做同步:希望**子进程先运行**时,父进程先 `sigaction` 注册好 `SIGUSR1` 处理函数,然后父进程调用 `sigsuspend(&wait_mask)` 挂起;子进程完成工作后 `kill(getppid(), SIGUSR1)` 唤醒父进程。这样父进程必然等到子进程发信号后才继续,顺序被强制确定。
+
+### Q5:信号处理函数里为什么不能用 `printf`?应遵循哪些原则?
+
+**答案**:信号处理函数可能在任意时刻打断主流程,若主流程此刻正在 `printf`(操作 `stdio` 缓冲区)或 `malloc`(操作堆管理结构),处理函数再次调用同类**非可重入**函数,会破坏其内部状态导致崩溃或数据错乱。`printf`、`malloc`、`exit`、`strtok` 等都不是异步信号安全函数。应遵循:只调用 POSIX 规定的异步信号安全函数(如 `write`、`read`、`_exit`、`kill`、`waitpid`);用 `volatile sig_atomic_t` 标志位记录事件、把真正工作交给主循环;用 `sa_mask` 控制处理期间的信号阻塞以避免竞态;处理函数尽量短小,降低竞争风险。
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第八章 信号:基础

+ 1178 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/02-进程线程与IPC/02-进程管理.md

@@ -0,0 +1,1178 @@
+---
+title: 进程管理
+tags: [嵌入式Linux, Linux应用编程, 进程, fork, exec, 环境变量, 虚拟地址空间, 僵尸进程, 守护进程, 单例模式, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第九章 进程"
+---
+
+# 进程管理
+
+> 💡 **关联知识**:[[02-进程线程与IPC/01-信号机制]]、[[02-进程线程与IPC/03-进程间通信]]、[[02-进程线程与IPC/04-线程与线程同步]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/5. 进程创建与管理]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/7. 进程控制]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/8. 守护进程]]
+
+进程是 Linux 应用编程的核心对象:程序是磁盘上的静态文件,进程是程序被加载运行后的动态实例。本篇覆盖进程全生命周期——从哪里开始(`main` 由引导代码调用)、运行在什么内存与地址空间里、如何派生(`fork`/`vfork`)、如何替换(`exec`)、如何收尾(`wait`/僵尸/孤儿)、进程之间是什么关系(进程组/会话),最后落地两个工程必备技能:**守护进程**与**单例模式运行**。
+
+---
+
+## 1. 进程与程序
+
+### 1.1 main() 函数由谁调用
+
+C 语言程序总是从 `main` 开始执行:
+
+```c
+int main(void)
+int main(int argc, char *argv[])
+```
+
+需要向应用传参时选第二种。但 `main()` 并不是被"凭空"调用的:操作系统下的应用程序在运行 `main()` 之前,需要先执行一段**引导代码**,由引导代码去调用 `main()`。编写应用时不用管引导代码,链接时会由链接器把它链接进可执行文件。
+
+当执行应用程序时,Linux 下输入可执行文件的相对路径或绝对路径即可运行(如 `./app`、`/home/dt/app`),也可在后面附加参数(`./app arg1 arg2`)。程序运行需要经过**加载器**:加载器负责把应用程序加载到内存中执行。
+
+参数传递链路:
+
+```mermaid
+sequenceDiagram
+    participant Shell as shell 进程
+    participant Loader as 加载器
+    participant Start as 引导代码
+    participant Main as main()
+    Shell->>Loader: 解析命令行参数并传递 (arg1, arg2)
+    Loader->>Start: 加载程序并把参数交给引导代码
+    Start->>Main: 调用 main(argc, argv)
+```
+
+即:shell 进程逐一解析命令行参数 → 传给加载器 → 加载器在加载应用时传给引导代码 → 引导代码调用 `main()` 时最终传递给 `main()`。
+
+### 1.2 程序如何结束
+
+进程终止分正常终止与异常终止:
+
+| 类别 | 方式 |
+| ---- | ---- |
+| 正常终止 | `main()` 中 `return` 返回;调用 `exit()`;调用 `_exit()` 或 `_Exit()` |
+| 异常终止 | 调用 `abort()`;进程接收到某个信号(如 `SIGKILL`) |
+
+### 1.3 注册进程终止处理函数 atexit()
+
+```c
+#include <stdlib.h>
+
+int atexit(void (*function)(void));
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `function` | 函数指针,指向注册的函数;该函数无参数、无返回值 |
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| 成功 | 返回 0 |
+| 失败 | 返回非 0 |
+
+注意:如果程序使用 `_exit()` 或 `_Exit()` 终止进程而不是 `exit()`,则**不会执行**注册的终止处理函数。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+
+static void bye(void)
+{
+    puts("Goodbye!");
+}
+
+int main(int argc, char *argv[])
+{
+    if (atexit(bye)) {
+        fprintf(stderr, "cannot set exit function\n");
+        exit(-1);
+    }
+    exit(0);    /* 退出时会调用 bye() */
+}
+```
+
+### 1.4 何为进程、进程号
+
+- **程序**:一个可执行文件,存放在磁盘中,是静态概念。
+- **进程**:可执行程序的实例,即文件被运行;是动态过程,从被加载运行开始,到运行结束终止,这就是进程的生命周期。
+
+Linux 下每个进程都有一个**进程号(PID)**,是正数,用于唯一标识系统中某个进程。`ps` 命令可查看进程号;`kill()` 等系统调用用 PID 标识目标进程。
+
+```c
+#include <sys/types.h>
+#include <unistd.h>
+
+pid_t getpid(void);   /* 获取本进程 PID */
+pid_t getppid(void);  /* 获取父进程 PID */
+```
+
+两个函数都无需参数,返回对应进程号(`pid_t` 类型)。在用 `fork()` 区分父子进程、给父进程发信号、排查进程关系时都会用到。
+
+---
+
+## 2. 进程的环境变量
+
+每个进程都有一组环境变量,以字符串形式存储在字符串数组(环境列表)中,每个字符串是 `name=value` 形式,即"名称-值"成对集合。shell 下用 `env` 查看,用 `export` 添加/删除。
+
+```bash
+export LINUX_APP=123456   # 添加环境变量
+export -n LINUX_APP       # 删除环境变量
+```
+
+进程的环境变量从父进程继承而来:在 shell 下执行应用,该进程的环境变量就是从 shell 进程继承来的。新进程在创建之前,会继承父进程的环境变量副本。
+
+### 2.1 访问环境变量:environ
+
+环境变量存放在字符串数组中,全局变量 `environ` 指向它,声明即可使用:
+
+```c
+extern char **environ;
+
+int main(void)
+{
+    int i;
+    for (i = 0; NULL != environ[i]; i++)
+        puts(environ[i]);
+    return 0;
+}
+```
+
+通过元素是否为 `NULL` 判断数组是否到末尾。
+
+### 2.2 getenv()
+
+```c
+#include <stdlib.h>
+
+char *getenv(const char *name);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `name` | 要获取的环境变量名称 |
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| 存在 | 返回该环境变量值字符串的指针 |
+| 不存在 | 返回 `NULL` |
+
+注意:不应该修改 `getenv()` 返回的字符串,修改它意味着修改环境变量的值。需要修改应使用下面提供的函数。
+
+### 2.3 putenv() / setenv()
+
+```c
+#include <stdlib.h>
+
+int putenv(char *string);
+int setenv(const char *name, const char *value, int overwrite);
+```
+
+`putenv()`:参数 `string` 是 `name=value` 形式的字符串;成功返回 0,失败返回非 0 并设置 `errno`。
+
+> **重要细节**:`putenv()` 会让 `environ` 中某元素**直接指向该字符串**,而不是副本。因此不能随意修改 `string` 指向的内容,`string` 也不应是栈上分配的自动变量。
+
+`setenv()`:`name` 为变量名,`value` 为值,`overwrite` 决定变量已存在时是否覆盖。
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `name` | 需要添加或修改的环境变量名称 |
+| `value` | 环境变量的值 |
+| `overwrite` | 环境变量已存在时:为 0 不改变现有值(本次调用无影响);非 0 则覆盖;不存在时都表示添加 |
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| 成功 | 返回 0 |
+| 失败 | 返回 -1,并设置 `errno` |
+
+`setenv()` 与 `putenv()` 的两个区别:
+
+1. `putenv()` 不为 `name=value` 字符串分配内存;`setenv()` 会分配缓冲区并复制。
+2. `setenv()` 可通过 `overwrite` 控制"仅添加不覆盖",`putenv()` 无法控制。
+
+因此**推荐使用 `setenv()`**,用自动变量作参数也不会有问题。
+
+### 2.4 unsetenv() / clearenv()
+
+```c
+#include <stdlib.h>
+
+int unsetenv(const char *name);   /* 从环境变量表移除 name */
+int clearenv(void);               /* 清空所有环境变量 */
+```
+
+也可以直接把全局变量赋值为 `NULL` 来清空:
+
+```c
+environ = NULL;
+```
+
+`clearenv()` 内部的做法其实就是把 `environ` 赋值为 `NULL`。
+
+> **内存泄漏提醒**:`setenv()` 会为环境变量分配内存缓冲区;`clearenv()` 并不知晓这些缓冲区的存在,无法释放它们。反复调用这两个函数的程序会不断产生内存泄漏。
+
+### 2.5 更简单的添加方式
+
+执行程序时可在路径前直接以 `name=value` 形式添加环境变量,多个之间用空格分隔:
+
+```bash
+NAME=value ./app
+```
+
+### 2.6 环境变量的作用
+
+环境变量常见用途之一是在 shell 中:`HOME` 表示用户家目录,`USER` 表示当前用户名,`SHELL` 表示 shell 解析器名称,`PWD` 表示当前所在目录。自己的应用程序同样可以使用进程环境变量(比如读取配置文件路径、日志级别等)。
+
+---
+
+## 3. 进程的内存布局
+
+历史沿袭至今,C 语言程序一直由以下几部分组成:
+
+| 段 | 名称 | 内容 | 特性 |
+| -- | ---- | ---- | ---- |
+| 正文段 | 代码段 | CPU 执行的机器语言指令 | 只读,防止意外修改;可被多个进程共享 |
+| 初始化数据段 | 数据段 | 显式初始化的全局变量和静态变量 | 加载时从可执行文件读取值 |
+| 未初始化数据段 | bss 段 | 未显式初始化的全局变量和静态变量 | 程序执行前系统将其全部初始化为 0;可执行文件只记录位置与大小 |
+| 栈 | stack | 函数内局部变量、函数调用保存的信息、实参、返回值 | 动态增长收缩,由栈帧组成 |
+| 堆 | heap | 运行时动态分配的内存(如 `malloc`) | 可在运行时动态分配 |
+
+> **bss** 一词来源于早期汇编操作符,意思是"由符号开始的块"(block started by symbol)。
+
+`size` 命令可查看二进制可执行文件的文本段、数据段、bss 段大小:
+
+```bash
+size app
+```
+
+### 3.1 典型内存布局
+
+```mermaid
+flowchart TB
+    subgraph HIGH["高地址"]
+        STACK["栈 stack<br/>局部变量 / 返回地址 / 实参<br/>向低地址增长"]
+        GAP1["(空闲)"]
+        HEAP["堆 heap<br/>malloc 分配<br/>向高地址增长"]
+        BSS[".bss<br/>未初始化全局/静态变量"]
+        DATA[".data<br/>已初始化全局/静态变量"]
+        TEXT[".text 正文段(只读)<br/>机器指令"]
+    end
+    STACK --- GAP1 --- HEAP --- BSS --- DATA --- TEXT
+```
+
+这只是便于说明的典型方式,并不要求具体实现一定如此安排。
+
+---
+
+## 4. 进程的虚拟地址空间
+
+Linux 采用**虚拟内存管理技术**:每个进程都在自己独立的地址空间中运行。32 位系统中,每个进程的逻辑地址空间均为 4GB,按 3:1 比例分配:
+
+- **用户进程享有 3GB**;
+- **内核独自享有剩下的 1GB**。
+
+虚拟地址通过硬件 **MMU(内存管理单元)** 映射到实际物理地址。建立映射后,对虚拟地址的读写就是对物理地址的读写。因此应用程序中读写的 `0x80800000` 并不对应硬件物理地址 `0x80800000`。
+
+### 4.1 为什么需要虚拟地址
+
+若没有虚拟地址机制,所有应用直接访问物理地址,会出现:
+
+- 多个程序运行时,必须保证内存总量小于实际物理内存;
+- 内存使用效率低(空间不足时把程序在硬盘与内存间大量搬入搬出);
+- 进程地址空间不隔离(进程可修改其它进程甚至内核的数据,不安全);
+- 无法确定程序的链接地址(代码加载地址由系统随机分配,编译时无法确认)。
+
+引入虚拟地址后带来的优点:
+
+- **进程与进程、进程与内核相互隔离**:一个进程不能读取或修改另一个进程或内核的内存数据,提高安全性与稳定性。
+- **可共享内存**:不同进程的虚拟地址空间可映射到相同的物理地址空间,这也是共享内存实现 IPC 的基础。
+- **便于实现内存保护机制**:同一块共享内存,不同进程可采取不同保护措施(只读 / 可读可写)。
+- **编译应用时无需关心链接地址**:不再要求链接地址与运行地址一致。
+
+---
+
+## 5. fork() 创建子进程
+
+```c
+#include <unistd.h>
+
+pid_t fork(void);
+```
+
+一个现有进程调用 `fork()` 创建新进程:调用者称为**父进程**,被创建者称为**子进程**。
+
+理解 `fork()` 的关键:调用完成后存在两个进程,各自从 `fork()` 的返回处继续执行,因此 **`fork()` 会返回两次**——子进程返回一个值,父进程返回一个值。
+
+| 返回位置 | 返回值 | 含义 |
+| -------- | ------ | ---- |
+| 父进程 | 子进程的 PID | 调用成功 |
+| 子进程 | 0 | 调用成功 |
+| 父进程 | -1(并设 `errno`) | 调用失败,不创建子进程 |
+
+### 5.1 子进程拷贝了什么
+
+`fork()` 成功后,子进程是父进程的一个副本:
+
+- 拷贝父进程的**数据段、堆、栈**;
+- 继承父进程打开的**文件描述符**;
+- 父子进程**不共享**这些存储空间,是子进程对父进程相应部分的完全复制;`fork()` 后各进程均可修改自己的栈数据与堆变量,互不影响;
+- **代码段(文本段)是共享的**:只读,内存中只存在一份。
+
+```mermaid
+flowchart TD
+    P["父进程<br/>代码段(只读,共享)<br/>数据段/堆/栈(副本)"] -->|fork| C["子进程<br/>独立 PCB / 独立 PID<br/>数据段/堆/栈副本"]
+    P -->|fork返回子进程PID| R1[继续执行 fork 之后的代码]
+    C -->|fork返回0| R2[继续执行 fork 之后的代码]
+```
+
+子进程被创建后便是一个独立进程:拥有自己的进程空间、系统内唯一的 PID、自己独立的 PCB(进程控制块),会被内核同等调度执行,参与到系统进程调度中。
+
+### 5.2 父子进程间的文件共享
+
+调用 `fork()` 后,子进程获得父进程所有文件描述符的**副本**(创建方式类似 `dup()`),意味着父子进程对应的文件描述符指向**相同的文件表**,进而指向磁盘中相同的文件。因此:
+
+- 若子进程更新了文件偏移量,这个改变也会影响父进程相应文件描述符的偏移量;
+- 父子进程都写同一个文件时,效果类似使用了 `O_APPEND`:每次写入都从文件末尾接续。
+
+对应地,如果父进程 `fork()` **之后**父子进程**各自 `open()`** 同一个文件,则两个文件描述符指向**不同的文件表**,各自有独立的偏移量,一方修改偏移量不影响另一方,写入数据会出现**覆盖**。
+
+| 场景 | 文件描述符 | 文件表 | 偏移量 | 写入效果 |
+| ---- | ---------- | ------ | ------ | -------- |
+| `open` 后 `fork` | 子进程继承副本 | 同一份 | 共享 | 接续写 |
+| `fork` 后各自 `open` | 各进程独立 | 各一份 | 独立 | 可能覆盖 |
+
+### 5.3 fork() 的使用场景
+
+1. **父进程希望子进程复制自己**,使父子进程同时执行不同的代码段。网络服务进程常用:父进程等待客户端请求,收到请求后 `fork()` 创建子进程处理请求,父进程继续等待下一个请求。
+2. **一个进程要执行不同的程序**:如 app1 中 `fork()` 创建子进程,子进程立即调用 `exec` 族函数去执行 app2,从 app2 的 `main()` 开始运行。
+
+---
+
+## 6. vfork()
+
+```c
+#include <sys/types.h>
+#include <unistd.h>
+
+pid_t vfork(void);
+```
+
+`fork()` 会复制父进程数据段、堆、栈的大量内容,代价较大。若子进程 `fork()` 后立刻 `exec`,父进程的数据根本用不上,浪费时间和效率。`vfork()` 就是为"子进程立即执行 `exec`"这一场景专门设计的,效率高于 `fork()`。
+
+`vfork()` 与 `fork()` 的两个主要区别:
+
+1. **不复制父进程地址空间**:子进程在调用 `exec` 或 `_exit` 之前,在父进程的空间中运行、共享父进程内存。若子进程修改了父进程数据(除 `vfork` 返回值变量)、进行函数调用,或没有调用 `exec`/`_exit` 就返回,可能带来未知结果。
+2. **保证子进程先运行**:子进程调用 `exec` 之后父进程才可能被调度运行。
+
+> **实践建议**:`vfork()` 可能导致难以察觉的 bug,应尽量避免使用。现代 Linux 内核的 `fork()` 已采用**写时复制(copy-on-write)**技术,效率比早期实现高很多,除非速度绝对重要,否则应舍弃 `vfork()` 而使用 `fork()`。
+
+正式使用场合,一般应在子进程中立即调用 `exec`;如果 `exec` 失败,子进程应调用 `_exit()` 退出(`vfork()` 产生的子进程不应调用 `exit`,因为会导致对父进程 stdio 缓冲区的刷新和关闭)。
+
+> ⚠️ **来源说明**:以下"写时复制细节"不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+**写时复制(COW)**:`fork()` 后父子进程的页表指向同一批物理页,这些页被标记为只读;任一进程尝试写入时触发缺页异常,内核才为该进程复制出独立的一份物理页。这样"只读共享、写时才复制",避免了 `fork` 时的大面积内存复制开销。`vfork` 则更极端——完全不复制,直接共享父进程地址空间,因此父进程会被挂起直到子进程 `exec` 或退出。
+
+---
+
+## 7. fork() 之后的竞争条件
+
+`fork()` 后子进程成为独立进程,可被系统调度运行,父进程也继续被调度。**无法确定父子谁先访问 CPU**(多核上甚至可能同时各自访问一个 CPU),因此谁先运行、谁后运行是不确定的。
+
+对于执行顺序有要求的程序,可能因竞争条件导致结果错误。解决办法是采用同步技术,例如用信号:
+
+- 希望**子进程先运行**:父进程在 `fork()` 前注册好信号处理函数,`fork()` 后父进程调用 `sigsuspend(&wait_mask)` 挂起阻塞,子进程完成工作后 `kill(getppid(), SIGUSR1)` 唤醒父进程。
+
+```mermaid
+sequenceDiagram
+    participant F as 父进程
+    participant C as 子进程
+    F->>F: sigaction 注册 SIGUSR1 处理函数
+    F->>C: fork()
+    F->>F: sigsuspend() 挂起
+    C->>C: 先执行子进程工作
+    C->>F: kill(getppid(), SIGUSR1)
+    F->>F: 被唤醒, 继续执行父进程工作
+```
+
+---
+
+## 8. 进程的诞生与终止
+
+### 8.1 进程的诞生
+
+进程可以通过 `fork()` 或 `vfork()` 创建子进程。Linux 下所有进程都由其父进程创建:在 shell 终端执行 `./app`,app 进程就是 shell 终端进程创建出来的。
+
+最原始的父进程是 **init 进程**(PID 为 1):它是系统启动后运行的第一个进程,由内核启动,理论上没有父进程,管理着系统上所有其它进程。
+
+### 8.2 进程的终止
+
+进程的正常终止有多种方式(`return`、`exit()`、`_exit()`、`_Exit()`),异常终止也有多种(`abort()`、收到信号)。
+
+`_exit()` / `exit()` 的 `status` 参数定义**终止状态**,父进程可用 `wait()` 获取。虽然 `status` 是 `int`,但**仅有低 8 位表示终止状态**。一般终止状态为 0 表示成功终止,非 0 表示执行中出现错误(如文件打开失败、读写失败)。
+
+程序里一般使用 `exit()` 库函数而非 `_exit()` 系统调用。`exit()` 最终也会通过 `_exit()` 终止进程,但在此之前会完成:
+
+1. 调用程序中注册的进程终止处理函数(如 `atexit()` 注册的);
+2. 刷新 stdio 流缓冲区;
+3. 执行 `_exit()` 系统调用。
+
+### 8.3 exit() 与 _exit() 的 stdio 缓冲差异
+
+父子进程不应都使用 `exit()` 终止,只能一个用 `exit()`、另一个用 `_exit()`(一般**子进程用 `_exit()`、父进程用 `exit()`**)。原因是 `exit()` 会刷新 stdio 缓冲区。
+
+```c
+printf("Hello World!");    /* 注意:没有 \n */
+fork();
+/* 父进程和子进程都 exit(0) */
+```
+
+结果 `"Hello World!"` 被打印了**两次**。原因:
+
+- 进程用户空间内存中维护了 stdio 缓冲区,`fork()` 创建子进程时会复制这些缓冲区;
+- 标准输出默认使用**行缓冲**,遇到换行符才立即显示。上面字符串没有换行符,`printf()` 时不会立即刷新;
+- `fork()` 后子进程拷贝了含数据的缓冲区,父子调用 `exit()` 时都刷新各自缓冲区,于是打印两次。
+
+避免重复输出的方法:
+
+1. 对行缓冲设备加上换行符(`printf` 加 `\n`,或用会自动加换行符的 `puts()`);
+2. `fork()` 前用 `fflush()` 刷新 stdio 缓冲区,或用 `setvbuf()` / `setbuf()` 关闭缓冲;
+3. 子进程调用 `_exit()` 退出而非 `exit()`,退出时不刷新 stdio 缓冲区。
+
+---
+
+## 9. 监视子进程
+
+很多应用设计中,父进程需要知道子进程何时终止及终止状态,并回收其资源。
+
+### 9.1 wait()
+
+```c
+#include <sys/types.h>
+#include <sys/wait.h>
+
+pid_t wait(int *status);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `status` | 存放子进程终止状态信息;可为 `NULL`,表示不接收 |
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| 成功 | 返回终止的子进程 PID |
+| 失败 | 返回 -1 |
+
+`wait()` 执行的动作:
+
+- 若所有子进程都还在运行,`wait()` 一直阻塞,直到某个子进程终止;
+- 若进程没有子进程,`wait()` 返回 -1,`errno` 设为 `ECHILD`;
+- 若调用前已有子进程终止,则不会阻塞,立即为其"收尸"、处理后事(**一次 `wait()` 只处理一次**)。
+
+`status` 检查宏:
+
+| 宏 | 含义 |
+| -- | ---- |
+| `WIFEXITED(status)` | 子进程正常终止时返回 true |
+| `WEXITSTATUS(status)` | 返回子进程退出状态(`exit`/`_exit` 指定的值) |
+| `WIFSIGNALED(status)` | 子进程被信号终止时返回 true |
+| `WTERMSIG(status)` | 返回导致子进程终止的信号编号 |
+| `WCOREDUMP(status)` | 子进程终止时产生核心转储文件时返回 true |
+
+获取到的 `status` 并不是 `exit()` 时指定的状态,需要通过 `WEXITSTATUS` 宏转换。
+
+### 9.2 waitpid()
+
+`wait()` 的限制:
+
+- 无法等待某个**特定**子进程完成,只能按顺序等待下一个终止的子进程;
+- 子进程没终止时总是阻塞,无法非阻塞等待;
+- 只能发现被终止的子进程,对子进程因信号(如 `SIGSTOP`)而停止、或已停止的子进程收到 `SIGCONT` 后恢复的情况无能为力。
+
+```c
+#include <sys/types.h>
+#include <sys/wait.h>
+
+pid_t waitpid(pid_t pid, int *status, int options);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `pid` | 取值含义见下表 |
+| `status` | 与 `wait()` 的 `status` 意义相同 |
+| `options` | 位掩码,见下表 |
+
+`pid` 取值:
+
+| pid 取值 | 含义 |
+| -------- | ---- |
+| `pid > 0` | 等待进程号为 pid 的子进程 |
+| `pid == 0` | 等待与调用进程同一进程组的所有子进程 |
+| `pid < -1` | 等待进程组标识符与 `\|pid\|` 相等的所有子进程 |
+| `pid == -1` | 等待任意子进程;`wait(&status)` 与 `waitpid(-1, &status, 0)` 等价 |
+
+`options` 标志:
+
+| 标志 | 含义 |
+| ---- | ---- |
+| `WNOHANG` | 子进程没有状态改变时立即返回(非阻塞等待,可轮询);返回 0 表示没有发生改变 |
+| `WUNTRACED` | 除返回终止子进程状态外,还返回因信号而停止的子进程状态 |
+| `WCONTINUED` | 返回因收到 `SIGCONT` 而恢复运行的子进程状态 |
+
+`waitid()` 与 `waitpid()` 类似,提供更多扩展功能,可查阅 man 手册。
+
+### 9.3 僵尸进程与孤儿进程
+
+父进程与子进程生命周期往往不同,由此产生两种特殊进程。
+
+**孤儿进程**:父进程先于子进程结束,子进程成为"孤儿"。Linux 中所有孤儿进程都自动成为 **init 进程(PID 1)**的子进程,`getppid()` 返回 1(图形界面环境下可能被 `upstart` 等进程收养,返回其它 PID;字符界面下为 init)。
+
+**僵尸进程**:子进程先于父进程结束,而父进程**还未来得及收尸**,子进程就变成僵尸进程。父进程调用 `wait()`(或其变体)后,僵尸进程被内核彻底删除。如果父进程没有调用 `wait()` 就退出,init 进程会接管其子进程并自动调用 `wait()`,从而移除僵尸进程。
+
+要点:
+
+- 僵尸进程**无法通过信号杀死**,连 `SIGKILL` 也不行;
+- 只能杀死僵尸进程的父进程(或等待其父进程终止),让 init 接管并清理;
+- 若系统存在大量僵尸进程,会填满内核进程表,阻碍新进程创建;
+- 程序设计一定要监视子进程状态变化,子进程终止就调用 `wait()` 回收。
+
+### 9.4 SIGCHLD 信号
+
+当发生以下情况时,父进程会收到 `SIGCHLD` 信号:
+
+- 父进程的某个子进程终止;
+- 父进程的某个子进程因收到信号而停止(暂停运行)或恢复。
+
+子进程终止是**异步事件**,父进程不能一直 `wait()` 阻塞或轮询,否则无法处理自己的事。`SIGCHLD` 默认处理方式是**忽略**,因此要捕获它、绑定处理函数,在处理函数中调用 `wait()` 收回子进程。
+
+**关键技巧**:调用信号处理函数时会暂时把引发调用的信号加入进程信号掩码(除非 `SA_NODEFER`)。若在处理一个终止子进程时相继有两个子进程终止,即使产生两次 `SIGCHLD`,父进程也只能捕获一次,处理函数每次只调用一次 `wait()` 就会有"漏网之鱼"。
+
+因此 `SIGCHLD` 处理函数的惯用写法是**循环以非阻塞方式调用 `waitpid()`**,直到再无终止的子进程:
+
+```c
+while (waitpid(-1, NULL, WNOHANG) > 0)
+    continue;
+```
+
+返回 0 表示再无僵尸进程,返回 -1 表示有错误发生。并且应在创建任何子进程之前为 `SIGCHLD` 绑定处理函数。
+
+### 9.5 进程状态转换图
+
+```mermaid
+stateDiagram-v2
+    [*] --> 就绪态: 创建进程
+    就绪态 --> 运行态: 被 CPU 调度
+    运行态 --> 就绪态: 时间片到
+    运行态 --> 等待态: 等待资源/IO (浅度/深度睡眠)
+    等待态 --> 就绪态: 条件成立
+    运行态 --> 暂停态: SIGSTOP / SIGTSTP
+    暂停态 --> 就绪态: SIGCONT
+    运行态 --> 僵尸态: 进程结束但父进程未收尸
+    僵尸态 --> [*]: 父进程 wait 回收
+```
+
+---
+
+## 10. 执行新程序:exec 族
+
+当子进程的工作不是运行父进程的代码段,而是运行另一个新程序时,用 `exec` 函数。
+
+### 10.1 execve()
+
+```c
+#include <unistd.h>
+
+int execve(const char *filename, char *const argv[], char *const envp[]);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `filename` | 需要载入当前进程空间的新程序路径名,绝对路径或相对路径均可 |
+| `argv` | 传给新程序的命令行参数,字符串指针数组,以 `NULL` 结束,对应 `main` 的 `argv`;`argv[0]` 是新程序自身路径名 |
+| `envp` | 新程序的环境变量列表,字符串指针数组,以 `NULL` 结束,字符串格式为 `name=value`;对应新程序的 `environ` |
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| 成功 | **永不返回** |
+| 失败 | 返回 -1,并设置 `errno` |
+
+`execve()` 把新程序加载到进程内存空间,用新程序替换旧程序:栈、数据、堆会被新程序的相应部件替换,然后从新程序的 `main()` 开始执行。对它的成功调用永不返回,也无需检查返回值——一旦返回就表明发生错误。
+
+`execve()` 是系统调用,也是 exec 族中的一员。基于它还提供了一系列库函数:`execl()`、`execlp()`、`execle()`、`execv()`、`execvp()`、`execvpe()`。通常把用这些函数加载外部新程序的过程称为 **exec 操作**。
+
+### 10.2 exec 库函数
+
+```c
+#include <unistd.h>
+
+extern char **environ;
+
+int execl(const char *path, const char *arg, ... /* (char *) NULL */);
+int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
+int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */);
+int execv(const char *path, char *const argv[]);
+int execvp(const char *file, char *const argv[]);
+int execvpe(const char *file, char *const argv[], char *const envp[]);
+```
+
+区别归纳:
+
+| 函数 | 参数形式 | 是否查 PATH | 是否自定义环境变量 |
+| ---- | -------- | ----------- | ------------------ |
+| `execl` | 可变参数列表,以 `NULL` 结尾 | 否(需路径) | 否 |
+| `execv` | `argv` 数组 | 否(需路径) | 否 |
+| `execlp` | 可变参数列表 | **是**(可只给文件名) | 否 |
+| `execvp` | `argv` 数组 | **是** | 否 |
+| `execle` | 可变参数列表 + `envp` | 否 | **是** |
+| `execvpe` | `argv` 数组 + `envp` | **是** | **是** |
+
+- **`l` 表示 list(参数列表),`v` 表示 vector(数组)**:决定参数以可变参数还是字符串数组传递。
+- **`p` 表示 PATH**:允许只提供文件名,系统在 `PATH` 环境变量指定的目录中查找;也兼容相对/绝对路径。
+- **`e` 表示 environment**:可以指定自定义环境变量列表给新程序。
+
+调用示例:
+
+```c
+/* execv 传参 */
+char *arg_arr[5];
+arg_arr[0] = "./newApp";
+arg_arr[1] = "Hello";
+arg_arr[2] = "World";
+arg_arr[3] = NULL;
+execv("./newApp", arg_arr);
+
+/* execl 传参 */
+execl("./newApp", "./newApp", "Hello", "World", NULL);
+
+/* execvpe 传参 */
+char *env_arr[5] = {"NAME=app", "AGE=25", "SEX=man", NULL};
+execvpe("./newApp", arg_arr, env_arr);
+
+/* execle 传参 */
+execle("./newApp", "./newApp", "Hello", "World", NULL, env_arr);
+```
+
+### 10.3 为什么要在子进程中 exec
+
+可以直接在子进程分支写代码,但不够灵活、扩展性不够好。把子进程要运行的程序单独做成一个可执行文件,由 `fork()` + `exec()` 组合运行,结构更清晰。这也是 `fork` 的第二种使用场景:子进程从 `fork()` 返回后立即调用 `exec` 族函数。
+
+### 10.4 system()
+
+```c
+#include <stdlib.h>
+
+int system(const char *command);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `command` | 需要执行的 shell 命令字符串,如 `"ls -al"`、`"echo HelloWorld"` |
+
+`system()` 内部通过 `fork()`、`execl()`、`waitpid()` 实现:先 `fork()` 创建子进程运行 shell,再由 shell 执行 `command` 指定的命令。
+
+返回值:
+
+- `command` 为 `NULL`:shell 可用返回非 0,不可用返回 0;
+- 无法创建子进程或无法获取子进程终止状态:返回 -1;
+- 子进程不能执行 shell:相当于子进程通过 `_exit(127)` 终止;
+- 全部系统调用成功:返回执行 `command` 的 shell 进程的终止状态。
+
+优点是用起来方便,无需自己处理 `fork`、`exec`、`waitpid`、`exit` 等细节;代价是**效率低**:至少要创建两个进程(一个跑 shell,一个或多个跑解析出的命令),每条命令都会调用一次 `exec`。对效率有要求的程序不建议直接使用。
+
+---
+
+## 11. 进程状态与进程关系
+
+### 11.1 进程的六种状态
+
+| 状态 | 含义 |
+| ---- | ---- |
+| 就绪态(Ready) | 满足被 CPU 调度的所有条件但尚未被调度执行,得到 CPU 就能直接运行 |
+| 运行态 | 当前正在被 CPU 调度运行 |
+| 僵尸态 | 进程已结束,但父进程还未给它"收尸"(即僵尸进程) |
+| 可中断睡眠状态 | 浅度睡眠,可被信号唤醒 |
+| 不可中断睡眠状态 | 深度睡眠,无法被信号唤醒,只能等条件成立 |
+| 暂停态 | 进程暂停运行(不是终止),一般由信号暂停(如 `SIGSTOP`),可用 `SIGCONT` 恢复 |
+
+浅度睡眠与深度睡眠统称为**等待态(阻塞态)**:表示进程在等待某种条件成立后进入就绪态,处于等待态的进程无法参与系统调度。新创建的进程处于就绪态。
+
+### 11.2 进程关系
+
+每个进程都有唯一的 PID,也有自己的生命周期与父进程,于是形成以 init 为根的**进程家族树**。子进程终止时父进程会得到通知并取得退出状态。进程间关系主要包括:无关系(相互独立)、父子进程关系、进程组、会话。
+
+### 11.3 进程组
+
+每个进程除了 PID、父进程 PID 外,还有**进程组 ID**(标识属于哪个进程组)。进程组是一个或多个进程的集合,这些进程彼此可能有父子、兄弟关系,或在功能上有联系。
+
+进程组的意义是**方便管理**:若为了完成一个任务并发运行 100 个进程,没有进程组就要逐个终止;有了进程组,把这些进程设为同一组,终止整个进程组即可。
+
+关于进程组:
+
+- 每个进程必定属于某一个进程组,且只能属于一个进程组;
+- 每个进程组有一个**组长进程**,组长进程的 ID 等于进程组 ID;
+- 在组长进程 ID 前加负号即可操作整个进程组;
+- 组长进程不能再创建新的进程组;
+- 只要进程组中还存在一个进程,进程组就存在,与组长进程是否终止无关;
+- 进程组的生命周期从被创建开始,到所有进程终止或离开该组;
+- 默认情况下,新进程继承父进程的进程组 ID。
+
+```c
+#include <unistd.h>
+
+pid_t getpgid(pid_t pid);   /* 获取指定进程的进程组 ID, pid 为 0 表示调用者 */
+pid_t getpgrp(void);        /* 获取调用者进程组 ID, 等价于 getpgid(0) */
+
+int setpgid(pid_t pid, pid_t pgid);  /* 加入现有进程组或创建新进程组 */
+int setpgrp(void);                   /* 等价于 setpgid(0, 0) */
+```
+
+`setpgid()` 把 pid 指定进程的进程组 ID 设为 pgid:若 `pid == pgid`,则该进程成为组长并创建新进程组;`pid == 0` 使用调用者 PID;`pgid == 0` 创建新进程组,由 pid 指定进程当组长。一个进程只能为自己或子进程设置进程组 ID,子进程调用 `exec` 后就不能再更改其进程组 ID。
+
+### 11.4 会话
+
+**会话是一个或多个进程组的集合**:
+
+- 一个会话可包含一个或多个进程组,但只能有一个**前台进程组**,其它是后台进程组;
+- 每个会话有一个**会话首领(leader)**,即创建会话的进程;
+- 会话可以有控制终端,也可以没有;有控制终端时也只能连接一个(通常是登录终端设备或伪终端设备);
+- 会话首领进程连接终端后,该终端成为会话的**控制终端**,首领进程称为**控制进程**;
+- 终端上的输入和信号发送给会话的**前台进程组**中的所有进程,如 `Ctrl+C`(`SIGINT`)、`Ctrl+Z`(`SIGTSTP`)、`Ctrl+\`(`SIGQUIT`)。
+
+用户登录时一个新会话就开始了;打开多个终端窗口就是创建了多个终端会话。会话首领进程的进程组 ID 作为**会话 ID(sid)**,默认情况下新进程继承父进程的会话 ID。
+
+```c
+#include <unistd.h>
+
+pid_t getsid(pid_t pid);   /* pid 为 0 返回调用者会话 ID */
+pid_t setsid(void);        /* 创建新会话 */
+```
+
+若调用者不是进程组组长,`setsid()` 会创建一个新会话:调用者成为新会话首领,同时成为新进程组组长,且新会话没有控制终端。成功返回新会话 ID,失败返回 -1。
+
+`setsid()` 是守护进程的关键一步。
+
+---
+
+## 12. 守护进程
+
+### 12.1 何为守护进程
+
+守护进程(Daemon,也称精灵进程)是运行在后台的特殊进程,独立于控制终端,周期性地执行任务或等待处理事件。两个主要特点:
+
+- **长期运行**:一般在系统启动时开始运行,除非强行终止,直到系统关机都保持运行;不受用户登录注销影响。
+- **与控制终端脱离**:普通进程从终端开始运行,依附于该终端(会话的控制终端);终端关闭时会话退出、由该终端运行的所有进程被终止。守护进程脱离终端并在后台运行,避免信息在终端显示,也不会被终端产生的信息打断。
+
+Linux 中大多数服务器就是用守护进程实现的(如 `inetd`、`httpd`),作业规划进程 `crond` 也是。守护进程名通常以 `d` 结尾。守护进程与终端无关联,**自成进程组、自成会话,即 `pid = gid = sid`**。
+
+`ps -ajx` 查看:`TTY` 一栏为 `?` 表示没有控制终端;`COMMAND` 用中括号 `[]` 括起来的是**内核线程**(在内核里创建,没有用户空间代码,通常以 `k` 开头表示 Kernel)。
+
+### 12.2 编写守护进程的步骤
+
+```mermaid
+flowchart TD
+    A[1. 创建子进程, 父进程 exit] --> B[2. 子进程 setsid 创建新会话]
+    B --> C[3. chdir 将工作目录改为 /]
+    C --> D[4. umask 0 重设文件权限掩码]
+    D --> E[5. 关闭所有不再需要的文件描述符]
+    E --> F[6. 把 0/1/2 重定向到 /dev/null]
+    F --> G[7. 忽略 SIGCHLD 信号]
+    G --> H[守护进程主体循环]
+```
+
+1. **创建子进程、终止父进程**:父进程 `fork()` 后 `exit()`。这样做让 shell 认为命令已执行完毕;同时子进程有自己独立的 PID,**保证子进程不是进程组组长**,这是调用 `setsid()` 的先决条件。
+2. **子进程调用 setsid() 创建会话**:因为子进程不是组长,`setsid()` 使其创建新会话、成为会话首领与新进程组组长,且新会话没有控制终端。这一步有三个作用:摆脱原会话控制、摆脱原进程组控制、摆脱原控制终端控制。
+3. **将工作目录更改为根目录**:子进程继承父进程的当前工作目录,而运行中当前目录所在的文件系统不能被卸载。通常让 `/` 作为守护进程当前目录(也可指定其它目录)。
+4. **重设文件权限掩码 umask**:子进程继承父进程的文件权限掩码,会给使用文件带来麻烦;把掩码设为 0,确保子进程有最大操作权限。用法:`umask(0)`。
+5. **关闭不再需要的文件描述符**:子进程继承的所有文件描述符都消耗系统资源,且可能导致所在文件系统无法卸载。关闭它们,使守护进程不再持有从父进程继承的文件描述符。
+6. **把文件描述符 0、1、2 定位到 /dev/null**:使守护进程的输出无处显示,也无法从交互式用户处接收输入。
+7. **其它:忽略 SIGCHLD 信号**:不是必须,但对并发服务器进程特别重要。服务器收到客户端请求时创建子进程处理,若父进程不 `wait()` 回收,子进程会变僵尸;若 `wait()` 又会增加负担、影响并发性能。把 `SIGCHLD` 处理方式设为 `SIG_IGN`,可让内核把僵尸进程转交 init 处理,既不产生僵尸进程、又省去回收开销。
+
+> ⚠️ **来源说明**:以下"double-fork 双 fork"不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+标准做法中常再 `fork()` 一次(**double fork**):第一次 fork 后父进程退出,子进程 `setsid()` 成为会话首领;再 fork 一次并让第一个子进程退出,使最终的工作进程既不是会话首领、也不是进程组组长,从而**永远无法重新获得控制终端**(`setsid()` 要求调用者不是组长,因此工作进程无法再调用 `setsid` 获取终端)。教材采用单次 fork + `setsid` 的方式,对常规场景已经足够。
+
+### 12.3 SIGHUP 信号
+
+用户准备退出会话时,系统向该会话发 `SIGHUP`;会话把 `SIGHUP` 发给所有子进程,子进程收到后默认终止;当会话中所有进程都退出时,会话终止。
+
+如果程序忽略 `SIGHUP`(`signal(SIGHUP, SIG_IGN)`),则不会随终端退出而退出——此时它已成为守护进程。因为控制终端只是会话中的一个进程,只有会话中所有进程退出,会话才结束。
+
+---
+
+## 13. 单例模式运行
+
+有些程序不允许同时运行多个实例(如服务器、守护进程),这就是**单例模式运行**。常用两种实现思路。
+
+### 13.1 通过文件存在与否判断(不推荐)
+
+程序正式运行前先判断特定文件是否存在:存在则说明进程已在运行,立即退出;不存在则创建文件,程序结束时删除。以 `O_RDONLY | O_CREAT | O_EXCL` 打开文件,并用 `atexit()` 注册删除函数。
+
+```c
+int fd = open("./testApp.lock", O_RDONLY | O_CREAT | O_EXCL, 0666);
+if (-1 == fd) {
+    fputs("不能重复执行该程序!\n", stderr);
+    exit(-1);
+}
+atexit(delete_file);   /* remove(LOCK_FILE) */
+```
+
+三个明显问题:
+
+1. 程序用 `_exit()` 退出时无法执行删除函数,文件残留;
+2. 程序异常退出同样无法执行删除函数,文件残留;
+3. 计算机掉电关机,文件残留,重启后程序无法执行。
+
+补救(`exit()` 代替 `_exit()`、把文件放 `/tmp` 使其随重启销毁、注册信号处理函数删文件)都不彻底——`SIGKILL`/`SIGSTOP` 无法被忽略或捕获。所以这种方法**并不靠谱**。
+
+### 13.2 使用文件锁(推荐)
+
+程序启动后先打开特定文件(一般用 `O_WRONLY | O_CREAT`,不存在则创建),然后**尝试获取文件锁**:
+
+- 获取成功:把 PID 写入文件,**不关闭文件、不解锁**,保证进程一直持有该锁;
+- 获取失败:说明程序已在运行,退出本次启动。
+
+> 提示:当程序退出或文件关闭后,文件锁会自动解锁。`flock()` 产生的是**咨询锁(建议性锁)**,不是强制性锁;`fcntl()`、`lockf()` 也可实现上锁。
+
+```c
+fd = open("./testApp.pid", O_WRONLY | O_CREAT, 0666);
+if (-1 == fd) { perror("open error"); exit(-1); }
+
+if (-1 == flock(fd, LOCK_EX | LOCK_NB)) {   /* 非阻塞互斥锁 */
+    fputs("不能重复执行该程序!\n", stderr);
+    close(fd);
+    exit(-1);
+}
+
+ftruncate(fd, 0);            /* 文件截断为 0 */
+sprintf(str, "%d\n", getpid());
+write(fd, str, strlen(str)); /* 写入当前 PID */
+```
+
+这种机制在服务器程序中很常见。Linux 的 `/var/run/` 目录下有很多 `.pid` 后缀文件,就是为保证程序单例运行而设计的;命名方式通常是"程序名 + `.pid`"(如 `acpid.pid`)。实现单例守护进程时,也应把该文件放到 `/var/run/` 下并命名为 `name.pid`。
+
+其它方法(启动时用 `ps` 判断进程是否存在等)也可用,但最常用的还是**文件锁**。
+
+---
+
+## 14. 完整可复制源码
+
+### 14.1 进程全生命周期演示
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <errno.h>
+
+/* 进程终止处理函数,验证 atexit 只对 exit() 生效 */
+static void bye(void)
+{
+    puts("bye: 进程正常终止处理函数被调用");
+}
+
+int main(int argc, char *argv[])
+{
+    if (argc < 2) {
+        fprintf(stderr, "用法: %s <fork|exec|env|za|daemon>\n", argv[0]);
+        exit(-1);
+    }
+
+    /* ------------------------------------------------------------------
+     * 1. fork:区分父子进程,父进程 wait 回收子进程
+     * ------------------------------------------------------------------ */
+    if (0 == strcmp(argv[1], "fork")) {
+        pid_t pid;
+        int status;
+
+        atexit(bye);
+
+        pid = fork();
+        if (-1 == pid) {
+            perror("fork error");
+            exit(-1);
+        }
+
+        if (0 == pid) {
+            /* 子进程:使用 _exit() 避免刷新父进程 stdio 缓冲区 */
+            printf("这是子进程打印信息<pid: %d, 父进程 pid: %d>\n",
+                   getpid(), getppid());
+            sleep(1);
+            _exit(7);
+        } else {
+            /* 父进程:阻塞等待子进程结束并回收 */
+            printf("这是父进程打印信息<pid: %d, 子进程 pid: %d>\n",
+                   getpid(), pid);
+
+            if (waitpid(pid, &status, 0) == -1) {
+                perror("waitpid error");
+                exit(-1);
+            }
+            if (WIFEXITED(status))
+                printf("子进程正常退出,退出状态: %d\n", WEXITSTATUS(status));
+            else if (WIFSIGNALED(status))
+                printf("子进程被信号终止,信号: %d\n", WTERMSIG(status));
+        }
+
+    /* ------------------------------------------------------------------
+     * 2. exec:用 execlp 执行系统命令
+     * ------------------------------------------------------------------ */
+    } else if (0 == strcmp(argv[1], "exec")) {
+        puts("即将 exec 执行 ls -al(成功则不返回)");
+        execlp("ls", "ls", "-a", "-l", NULL);
+        perror("execlp error");   /* 能执行到这里说明 exec 失败 */
+        exit(-1);
+
+    /* ------------------------------------------------------------------
+     * 3. 环境变量:读取、添加、删除
+     * ------------------------------------------------------------------ */
+    } else if (0 == strcmp(argv[1], "env")) {
+        extern char **environ;
+        int i;
+        const char *val;
+
+        for (i = 0; NULL != environ[i]; i++)
+            puts(environ[i]);
+
+        val = getenv("HOME");
+        printf("HOME = %s\n", val ? val : "(不存在)");
+
+        if (setenv("MY_APP", "hello", 1) != 0) {
+            perror("setenv error");
+            exit(-1);
+        }
+        printf("MY_APP = %s\n", getenv("MY_APP"));
+
+        if (unsetenv("MY_APP") != 0) {
+            perror("unsetenv error");
+            exit(-1);
+        }
+        printf("删除后 MY_APP = %s\n", getenv("MY_APP") ? "存在" : "(不存在)");
+
+    /* ------------------------------------------------------------------
+     * 4. 僵尸进程:子进程退出、父进程不收尸
+     * ------------------------------------------------------------------ */
+    } else if (0 == strcmp(argv[1], "za")) {
+        pid_t pid = fork();
+
+        if (-1 == pid) { perror("fork error"); exit(-1); }
+        if (0 == pid) {
+            printf("子进程<%d>即将退出,父进程不收尸将成为僵尸\n", getpid());
+            sleep(1);
+            _exit(0);
+        }
+
+        printf("父进程<%d>不调用 wait,子进程将变僵尸,用 'ps -aux' 查看 Z 状态\n",
+               getpid());
+        for ( ; ; )
+            sleep(1);
+
+    /* ------------------------------------------------------------------
+     * 5. 守护进程:完整 7 步实现
+     * ------------------------------------------------------------------ */
+    } else if (0 == strcmp(argv[1], "daemon")) {
+        pid_t pid;
+        int i;
+
+        /* 1. 创建子进程、父进程退出 */
+        pid = fork();
+        if (0 > pid) { perror("fork error"); exit(-1); }
+        else if (0 < pid) exit(0);
+
+        /* 2. 创建新会话、脱离控制终端 */
+        if (0 > setsid()) { perror("setsid error"); exit(-1); }
+
+        /* 3. 工作目录改为根目录 */
+        if (0 > chdir("/")) { perror("chdir error"); exit(-1); }
+
+        /* 4. 重设文件权限掩码 */
+        umask(0);
+
+        /* 5. 关闭所有文件描述符 */
+        for (i = 0; i < sysconf(_SC_OPEN_MAX); i++)
+            close(i);
+
+        /* 6. 0/1/2 重定向到 /dev/null */
+        open("/dev/null", O_RDWR);
+        dup(0);
+        dup(0);
+
+        /* 7. 忽略 SIGCHLD 信号 */
+        signal(SIGCHLD, SIG_IGN);
+
+        /* 守护进程主体 */
+        for ( ; ; ) {
+            sleep(1);
+            /* 实际守护进程在此处理周期性任务 */
+        }
+
+    } else {
+        fprintf(stderr, "未知参数: %s\n", argv[1]);
+        exit(-1);
+    }
+
+    exit(0);
+}
+```
+
+> 编译 `daemon` 分支需要 `#include <fcntl.h>`(`open`、`O_RDWR`)。完整编译:
+> `gcc -Wall -o proc_demo proc_demo.c`
+
+### 14.2 源码逐段解释
+
+- **`fork` 分支**:`atexit()` 注册的函数只在父进程 `exit()` 时被调用(子进程用 `_exit` 绕过),可对照观察。父进程用 `waitpid()` 回收并解析退出状态。
+- **`exec` 分支**:`execlp()` 执行 `ls -al`,`p` 表示查 `PATH`,所以无需写 `/bin/ls`。exec 成功则不返回。
+- **`env` 分支**:遍历 `environ`、`getenv` 读取、`setenv` 添加、`unsetenv` 删除,覆盖环境变量主要 API。
+- **`za` 分支**:子进程先退出、父进程死循环不收尸,`ps -aux` 可看到子进程状态为 `Z`(zombie)。
+- **`daemon` 分支**:严格按 7 步实现守护进程;运行后没有任何输出(已重定向到 `/dev/null`),用 `ps -ajx` 查看 `TTY` 为 `?`。
+
+### 14.3 单例模式运行(文件锁)
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/file.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#define LOCK_FILE "/var/run/proc_demo.pid"
+
+int main(void)
+{
+    char str[20] = {0};
+    int fd;
+
+    /* 打开锁文件,不存在则创建 */
+    fd = open(LOCK_FILE, O_WRONLY | O_CREAT, 0666);
+    if (-1 == fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 非阻塞方式获取互斥文件锁 */
+    if (-1 == flock(fd, LOCK_EX | LOCK_NB)) {
+        fputs("不能重复执行该程序!\n", stderr);
+        close(fd);
+        exit(-1);
+    }
+
+    puts("程序运行中...");
+
+    ftruncate(fd, 0);                 /* 截断旧内容 */
+    sprintf(str, "%d\n", getpid());
+    write(fd, str, strlen(str));      /* 写入 PID */
+
+    for ( ; ; )
+        sleep(1);
+
+    exit(0);
+}
+```
+
+要点:进程退出或关闭 fd 后文件锁自动释放;`flock` 是建议性锁,需所有实例都遵守才有效。普通用户对 `/var/run/` 可能无写权限,测试时可改用当前目录。
+
+---
+
+## 15. 实验步骤与调试方法
+
+### 15.1 编译运行
+
+```bash
+gcc -Wall -o proc_demo proc_demo.c
+
+./proc_demo fork
+./proc_demo exec
+./proc_demo env
+```
+
+### 15.2 常用调试命令
+
+| 命令 | 用途 |
+| ---- | ---- |
+| `ps -aux` / `ps -ef` | 查看进程信息与 PID |
+| `ps -ajx` | 查看进程组、会话、TTY、状态(守护进程 TTY 为 `?`) |
+| `top` | 动态查看进程、CPU/内存占用 |
+| `pstree -p` | 以树状显示进程父子关系 |
+| `kill -9 <pid>` | 强制终止进程 |
+| `size <可执行文件>` | 查看 text / data / bss 段大小 |
+| `env` | 查看当前 shell 环境变量 |
+| `strace ./app` | 跟踪系统调用(fork/exec/wait 等) |
+
+### 15.3 实验一:验证 fork 返回值与文件共享
+
+运行 `./proc_demo fork`:子进程打印 `fork` 返回 0、父进程打印子进程 PID。再写两个程序分别测试"open 后 fork"(接续写)与"fork 后各自 open"(覆盖写),对照第 5.2 节的结论。
+
+### 15.4 实验二:观察僵尸进程
+
+```bash
+./proc_demo za &
+ps -aux | grep proc_demo
+```
+
+预期看到子进程 `STAT` / 状态列为 `Z`(zombie)。它无法被 `kill -9` 杀死;`kill` 掉其父进程后,僵尸进程被 init 接管清理。
+
+### 15.5 实验三:验证孤儿进程
+
+父进程先退出(休眠较短),子进程后退出(休眠较长)并在退出前打印 `getppid()`,观察其变为 init(字符界面下为 1)或图形界面下的收养进程。
+
+### 15.6 实验四:验证守护进程脱离终端
+
+```bash
+./proc_demo daemon
+ps -ajx | grep proc_demo     # TTY 应为 ?
+# 关闭当前终端后重新打开,进程依然存在
+```
+
+对比:普通前台进程在终端关闭后会随会话收到 `SIGHUP` 而终止。
+
+---
+
+## 16. 跨平台对比:IMX6ULL vs STM32 vs RK3568
+
+| 维度 | I.MX6ULL(本教程) | STM32(裸机/HAL) | RK3568(Linux) |
+| ---- | ---------------- | ----------------- | --------------- |
+| 进程模型 | 多进程,完整 fork/exec/wait | 无进程概念,单主循环 + 中断 | 同 I.MX6ULL |
+| 并发方式 | 进程 + 线程 | 中断、状态机、RTOS 任务 | 进程 + 线程 |
+| 内存模型 | 虚拟地址空间 + MMU(3:1) | 直接物理地址,无 MMU(部分带 MPU) | 虚拟地址空间 + MMU |
+| 环境变量 | `environ`/`getenv`/`setenv` | 无(编译期宏配置) | 同 I.MX6ULL |
+| 可执行文件格式 | ELF | .hex/.bin(直接烧写 Flash) | ELF |
+| 守护进程 | 标配,`setsid` + `/var/run/*.pid` | 无对应概念(RTOS 常驻任务) | 同 I.MX6ULL |
+| 回收子进程 | `wait`/`waitpid`,防僵尸 | 无 | 同 I.MX6ULL |
+| 调试工具 | `ps`/`top`/`pstree`/`strace`/`size` | IDE 调试 + 逻辑分析仪 | 同 I.MX6ULL |
+
+要点:
+
+- **STM32 裸机没有进程、虚拟内存、环境变量与 exec**,它用 RTOS 任务或中断模拟"并发",资源由链接脚本静态规划。I.MX6ULL 与 RK3568 都是 Linux,本篇 API 通用。
+- 嵌入式设备上写守护进程,要注意 `/var/run/` 的读写权限与根文件系统是否为只读;只读根文件系统时应改到可写分区(如 `/tmp`、`/run`)或 tmpfs。
+- I.MX6ULL 这类板子资源有限(内存 512MB 级别),滥用 `fork()` 复制大量内存会显著增加内存压力,必要时应考虑线程方案(见下一篇线程同步)。
+
+---
+
+## 17. 面试精选(5 题)
+
+### Q1:`fork()` 为什么"返回两次"?父子进程返回值分别是什么?
+
+**答案**:`fork()` 会创建父进程的一个副本,调用完成后系统中存在两个进程,且都从 `fork()` 返回处继续执行,所以返回两次。**父进程返回子进程的 PID(正数)**,用于标识它创建了哪个子进程;**子进程返回 0**,因为它不需要自己的 PID(可用 `getpid()` 获取);**失败时仅在父进程返回 -1** 并设置 `errno`,不创建子进程。程序用返回值区分父子:`case 0` 是子进程分支,`default` 是父进程分支。子进程拷贝父进程的数据段、堆、栈并继承文件描述符,但共享只读代码段。
+
+### Q2:`fork()` 和 `vfork()` 有什么区别?为什么现在不推荐 `vfork()`?
+
+**答案**:两者都创建子进程、返回值相同,区别有两点:① `vfork()` 不复制父进程地址空间,子进程在 `exec`/`_exit` 前直接在父进程空间中运行并共享内存;`fork()` 复制(现代内核用写时复制优化)。② `vfork()` 保证子进程先运行,子进程 `exec` 后父进程才可能被调度;`fork()` 父子执行顺序不确定,存在竞争条件。不推荐 `vfork()` 是因为:它共享地址空间,子进程若修改父进程数据、调用函数或未 `exec`/`_exit` 就返回,行为未定义,容易产生难以察觉的 bug;而现代 `fork()` 用写时复制后效率已大幅提高,除非速度绝对重要,否则应用 `fork()`。
+
+### Q3:什么是僵尸进程和孤儿进程?怎么处理?
+
+**答案**:**僵尸进程**是子进程已终止、但父进程尚未调用 `wait()` 收尸的进程,它已释放大部分资源,只剩退出状态等信息留在内核进程表中;无法被任何信号(包括 `SIGKILL`)杀死,只能由父进程 `wait()` 回收,或父进程退出后由 init 接管回收;大量僵尸会填满内核进程表阻碍新进程创建。**孤儿进程**是父进程先于子进程退出,子进程被 init(PID 1)或图形界面下的收养进程接管,`getppid()` 返回 1(或收养者 PID)。处理方案:父进程必须监视子进程,用 `wait()`/`waitpid()` 回收;更优雅的是捕获 `SIGCHLD`,在处理函数中循环 `while (waitpid(-1, NULL, WNOHANG) > 0) continue;` 一次清理所有已终止子进程。
+
+### Q4:`exit()` 和 `_exit()` 有什么区别?为什么子进程一般用 `_exit()` 退出?
+
+**答案**:`exit()` 是 C 库函数,`_exit()` 是系统调用。`exit()` 最终也会通过 `_exit()` 终止进程,但在此之前会:① 调用 `atexit()` 注册的终止处理函数;② 刷新 stdio 流缓冲区。`_exit()` 则直接终止,不做这些。子进程一般用 `_exit()` 的原因是:`fork()` 会复制父进程的 stdio 缓冲区,若子进程调用 `exit()` 刷新缓冲区,会把从父进程复制来的、尚未刷新的数据重复输出(典型现象:`printf("Hello World!")` 不带换行符时被打印两次);而且 `vfork` 产生的子进程调用 `exit()` 还会刷新并关闭父进程的 stdio 缓冲区。因此约定子进程用 `_exit()`、父进程用 `exit()`。
+
+### Q5:如何编写一个守护进程?为什么 `setsid()` 前必须先 `fork()`?
+
+**答案**:标准步骤:① `fork()` 子进程并让父进程 `exit()`;② 子进程调用 `setsid()` 创建新会话、脱离控制终端;③ `chdir("/")` 避免占用可卸载文件系统;④ `umask(0)` 获得最大文件权限;⑤ 关闭所有继承的文件描述符;⑥ 把 0/1/2 重定向到 `/dev/null`;⑦ 将 `SIGCHLD` 设为 `SIG_IGN`,免去回收子进程的负担。之所以 `setsid()` 前要 `fork()`,是因为 **`setsid()` 要求调用者不是进程组组长**(否则返回失败);刚 `fork()` 出来的子进程继承了父进程的进程组 ID,但拥有自己独立的 PID,因此它不是组长,满足 `setsid()` 的调用条件。`setsid()` 后子进程成为新会话首领、新进程组组长,`pid == gid == sid`,且新会话没有控制终端。工程上还常用 double-fork:再 fork 一次确保最终进程不是会话首领,从此无法重新获得控制终端。
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第九章 进程

+ 832 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/02-进程线程与IPC/03-进程间通信.md

@@ -0,0 +1,832 @@
+---
+title: 进程间通信
+tags: [嵌入式Linux, Linux应用编程, IPC, 管道, FIFO, 消息队列, 信号量, 共享内存, Socket, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十章 进程间通信简介"
+---
+
+# 进程间通信
+
+> 💡 **关联知识**:[[02-进程线程与IPC/01-信号机制]]、[[02-进程线程与IPC/02-进程管理]]、[[02-进程线程与IPC/04-线程与线程同步]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/19. 管道]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/20. 共享内存与mmap]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/21. 消息队列]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/22. 信号量]]
+
+所谓进程间通信(interprocess communication,IPC),指的是系统中两个进程之间的通信。不同进程处于各自独立、隔离的地址空间中,因此相互通信比较难,Linux 内核提供了多种 IPC 机制。
+
+> **本篇定位说明**:教材第十章以**了解为主**,系统介绍了 IPC 的分类与各种机制的概念,未展开各机制的 API 细节。因此本篇结构为:**第 1~3 章与第 5 章为教材内容**(IPC 概念、机制分类、机制总览);**具体的 pipe/mkfifo/msgget/semget/shmget 等 API 与完整源码为扩展知识**,各扩展小节均带来源说明,便于你真正上手查询与使用。
+
+---
+
+## 1. 进程间通信简介
+
+系统的每一个进程都有各自的地址空间,并且相互独立、隔离,每个进程都处于自己的地址空间中。所以:
+
+- **同一进程内部的模块(譬如不同函数)之间通信很简单**,用全局变量即可;
+- **两个不同进程之间通信通常比较难**,因为它们处于不同的地址空间中。
+
+大多数程序是单进程程序(可以有多线程),不需要考虑进程间通信;复杂、大型的应用程序(如 GUI、服务器应用)则会根据实际需要设计成多进程程序,这时才需要 IPC。
+
+---
+
+## 2. 内核提供了哪些 IPC 机制
+
+Linux 内核提供的多种 IPC 机制基本从 UNIX 系统继承而来。对 UNIX 发展做出重大贡献的两大主力——AT&T 贝尔实验室与 BSD(加州大学伯克利分校的伯克利软件发布中心)——在进程间通信方面侧重点不同:
+
+- **前者**对早期 UNIX 的进程间通信手段进行了系统的改进和扩充,形成 **System V IPC**,通信进程局限在单个计算机内;
+- **后者**跳过了该限制,形成了基于**套接字(Socket,即网络)**的进程间通信机制。
+
+Linux 把两者都继承了下来:
+
+```mermaid
+flowchart TD
+    root["Linux IPC 机制"]
+    root --> u["早期 UNIX IPC"]
+    root --> sv["System V IPC"]
+    root --> posix["POSIX IPC"]
+    root --> sock["Socket IPC"]
+
+    u --> u1["管道 pipe"]
+    u --> u2["FIFO 有名管道"]
+    u --> u3["信号 signal"]
+
+    sv --> s1["System V 信号量"]
+    sv --> s2["System V 消息队列"]
+    sv --> s3["System V 共享内存"]
+
+    posix --> p1["POSIX 信号量"]
+    posix --> p2["POSIX 消息队列"]
+    posix --> p3["POSIX 共享内存"]
+
+    sock --> k1["基于 Socket 的进程间通信"]
+```
+
+总结:
+
+- **UNIX IPC**:管道、FIFO、信号;
+- **System V IPC**:信号量、消息队列、共享内存;
+- **POSIX IPC**:信号量、消息队列、共享内存;
+- **Socket IPC**:基于 Socket 的进程间通信。
+
+> 较早的 System V IPC 存在一些不足之处,而 POSIX IPC 是在 System V IPC 的基础上改进形成的,弥补了它的一些不足。
+
+### 2.1 机制总览对比
+
+> ⚠️ **来源说明**:下表"适用场景与限制"的细节为扩展知识,机制分类本身来自教材。
+
+| 机制 | 类别 | 通信方向 | 能否跨非亲缘进程 | 数据形式 | 典型适用场景 | 主要限制 |
+| ---- | ---- | -------- | ---------------- | -------- | ------------ | -------- |
+| 管道 pipe | UNIX IPC | 单工 | 否,仅父子/兄弟 | 无格式字节流 | shell 管道、父子进程简单数据流 | 只能亲缘进程、单向 |
+| FIFO 有名管道 | UNIX IPC | 半双工/双向 | 是 | 无格式字节流 | 无亲缘关系进程间简单数据流 | 字节流无消息边界 |
+| 信号 signal | UNIX IPC | 单向通知 | 是 | 编号 + 少量伴随数据 | 异步事件通知、进程同步 | 传递信息少 |
+| System V / POSIX 消息队列 | System V/POSIX | 双向 | 是 | 有格式的消息 | 需要消息边界、异步收发 | 内核中受系统限制 |
+| System V / POSIX 信号量 | System V/POSIX | 不传数据 | 是 | 计数器 | 访问共享资源的同步/互斥 | 只做同步,不传数据 |
+| System V / POSIX 共享内存 | System V/POSIX | 双向 | 是 | 裸内存 | 大数据量、高性能共享 | 需自行配合同步机制 |
+| Socket | Socket IPC | 全双工 | 是(可跨主机) | 字节流/数据报 | 网络通信、跨主机、跨语言 | 协议栈开销 |
+
+---
+
+## 3. 管道和 FIFO
+
+管道是 UNIX 系统上最古老的 IPC 方法,它在 20 世纪 70 年代早期 UNIX 的第三个版本上就出现了。**把一个进程连接到另一个进程的数据流称为管道**,管道被抽象成一个文件(pipe 文件类型)。
+
+管道包括三种:
+
+| 类型 | 单工/双工 | 适用范围 |
+| ---- | --------- | -------- |
+| 普通管道 pipe | 单工,数据只能单向传输 | 只能在父子或兄弟进程间使用 |
+| 流管道 s_pipe | 半双工,可以双向传输 | 只能在父子或兄弟进程间使用 |
+| 有名管道 name_pipe(FIFO) | 双向 | 允许在不相关(非父子/兄弟)的进程间通信 |
+
+- **普通管道**:用于具有亲缘关系的进程间通信,数据只能单向传输;要实现双向传输必须使用**两个管道**。
+- **流管道**:去除了普通管道的单向限制,以半双工方式双向传输,但仍只能在亲缘进程间通信。
+- **有名管道 FIFO**:同时突破普通管道的两种限制,既可双向传输,又能在非亲缘关系进程间通信。
+
+### 3.1 管道(pipe)读写细节
+
+> ⚠️ **来源说明**:本节 API 原型与阻塞规则不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+```c
+#include <unistd.h>
+
+int pipe(int pipefd[2]);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `pipefd` | 输出参数,`pipefd[0]` 为读端,`pipefd[1]` 为写端 |
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| 成功 | 返回 0 |
+| 失败 | 返回 -1,并设置 `errno` |
+
+典型用法:父进程 `pipe()` 后 `fork()`,子进程继承两个 fd;关闭不用的一端后即可单向通信(父写 `pipefd[1]`,子读 `pipefd[0]`,或反之)。
+
+```mermaid
+flowchart LR
+    P["父进程"] -->|"write(pipefd[1], buf, n)"| PIPE[("管道<br/>内核缓冲区")]
+    PIPE -->|"read(pipefd[0], buf, n)"| C["子进程"]
+```
+
+**读写阻塞规则**:
+
+| 情况 | 行为 |
+| ---- | ---- |
+| 管道为空,仍有写端打开 | `read()` 阻塞,直到有数据写入 |
+| 所有写端都已关闭 | `read()` 不再阻塞,返回 0(读到 EOF) |
+| 管道缓冲区已满 | `write()` 阻塞,直到有空间 |
+| 所有读端都已关闭 | `write()` 触发 `SIGPIPE` 信号,默认终止进程(`write` 返回 -1,`errno` 为 `EPIPE`) |
+| 写入量 ≤ `PIPE_BUF`(Linux 默认 4096) | 写入是原子的,不会与其它进程的写交错 |
+
+要点:管道是**无格式字节流**,没有消息边界;读端关闭后写端继续写会收到 `SIGPIPE`,因此健壮的程序应处理或忽略 `SIGPIPE`。
+
+### 3.2 有名管道 FIFO
+
+> ⚠️ **来源说明**:本节 API 原型与打开规则不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+FIFO 在文件系统中以一个**文件**存在(文件类型为 `p`),因此无亲缘关系的进程只要知道路径就能打开它通信。
+
+```c
+#include <sys/types.h>
+#include <sys/stat.h>
+
+int mkfifo(const char *pathname, mode_t mode);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `pathname` | FIFO 文件路径 |
+| `mode` | 权限位(会与 `umask` 作用) |
+
+| 返回值 | 含义 |
+| ------ | ---- |
+| 成功 | 返回 0 |
+| 失败 | 返回 -1,并设置 `errno`(如 `EEXIST`) |
+
+创建后,用 `open()`、`read()`、`write()`、`close()` 像普通文件一样操作,规则与 pipe 一致。**打开阻塞规则**:
+
+| 打开方式 | 行为 |
+| -------- | ---- |
+| `open(path, O_RDONLY)` | 阻塞,直到有进程以写方式打开该 FIFO |
+| `open(path, O_WRONLY)` | 阻塞,直到有进程以读方式打开该 FIFO |
+| 加 `O_NONBLOCK` 只读打开 | 立即成功返回 |
+| 加 `O_NONBLOCK` 只写打开 | 若无读者,返回 -1,`errno` 为 `ENXIO` |
+
+### 3.3 shell 中的管道
+
+shell 的 `|` 就是 pipe 的经典应用:`ls -l | grep .c` 中,shell 创建管道并让 `ls` 的标准输出接到管道写端、`grep` 的标准输入接到读端。
+
+---
+
+## 4. 信号
+
+关于信号的内容在第八章已经介绍过:信号用于通知接收信号的进程有某种事件发生,因此**可用于进程间通信**;除了用于进程间通信之外,进程还可以发送信号给进程本身。
+
+信号作为 IPC 手段的特点:
+
+- 传递的信息量少(一个信号编号,加实时信号的少量伴随数据);
+- 主要作为**异步事件通知**与进程同步手段,而不是数据传输手段;
+- 可靠信号支持排队与伴随数据。
+
+详细用法见 [[02-进程线程与IPC/01-信号机制]]。
+
+---
+
+## 5. 消息队列
+
+消息队列是**消息的链表**,存放在内核中并由**消息队列标识符**标识。它克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺陷。消息队列包括 POSIX 消息队列和 System V 消息队列。
+
+消息队列是 UNIX 下不同进程之间实现共享资源的一种机制:UNIX 允许不同进程将格式化的数据流以消息队列形式发送给任意进程;**有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息**。
+
+### 5.1 System V 消息队列 API
+
+> ⚠️ **来源说明**:本节 API 原型、结构体与示例不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+```c
+#include <sys/types.h>
+#include <sys/ipc.h>
+#include <sys/msg.h>
+
+key_t ftok(const char *pathname, int proj_id);              /* 生成 IPC key */
+
+int msgget(key_t key, int msgflg);                          /* 创建/获取消息队列 */
+int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);   /* 发送消息 */
+ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); /* 接收 */
+int msgctl(int msqid, int cmd, struct msqid_ds *buf);       /* 控制 */
+```
+
+| 函数 | 参数 | 返回值 |
+| ---- | ---- | ------ |
+| `msgget` | `key` IPC 键;`msgflg` 如 `IPC_CREAT \| 0666` | 成功返回消息队列 ID;失败 -1 并设 `errno` |
+| `msgsnd` | `msqid` 队列 ID;`msgp` 消息指针;`msgsz` 消息正文长度(不含 `mtype`);`msgflg` 如 `IPC_NOWAIT` | 成功 0;失败 -1 并设 `errno` |
+| `msgrcv` | `msqid`;`msgp` 接收缓冲区;`msgsz` 缓冲区正文容量;`msgtyp` 指定接收哪类消息;`msgflg` | 成功返回实际接收字节数;失败 -1 并设 `errno` |
+| `msgctl` | `msqid`;`cmd` 如 `IPC_RMID`(删除队列)、`IPC_STAT`、`IPC_SET`;`buf` | 成功 0;失败 -1 并设 `errno` |
+
+消息结构(用户自定义,`mtype` 必须为正):
+
+```c
+struct msgbuf {
+    long mtype;       /* 消息类型,必须 > 0 */
+    char mtext[1];    /* 消息正文,长度由 msgsz 指定 */
+};
+```
+
+`msgtyp` 取值规则:
+
+| msgtyp | 含义 |
+| ------ | ---- |
+| `0` | 接收队列中的第一条消息 |
+| `> 0` | 接收第一条类型为 `msgtyp` 的消息 |
+| `< 0` | 接收类型值 ≤ `|msgtyp|` 的消息中类型最小的第一条 |
+
+`msgsnd` / `msgrcv` 在队列满 / 为空时默认阻塞,加 `IPC_NOWAIT` 则非阻塞返回(`errno` 为 `EAGAIN` / `ENOMSG`)。
+
+---
+
+## 6. 信号量
+
+信号量是一个**计数器**,与其它 IPC 方式不同:它主要用于控制多个进程间或一个进程内多个线程间对**共享资源**的访问,相当于内存中的标志。进程可根据它判定是否能够访问某些共享资源,同时也可以修改该标志。除了用于共享资源的访问控制外,还可用于**进程同步**。
+
+它常作为一种**锁机制**,防止某进程访问资源时其它进程也访问该资源,因此主要作为进程间以及同一进程内不同线程之间的同步手段。Linux 提供了一组接口来操作信号量,声明在头文件 `sys/sem.h` 中。
+
+### 6.1 System V 信号量 API 与 PV 操作
+
+> ⚠️ **来源说明**:本节 API 原型、结构体与示例不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+```c
+#include <sys/types.h>
+#include <sys/ipc.h>
+#include <sys/sem.h>
+
+int semget(key_t key, int nsems, int semflg);                  /* 创建/获取信号量集 */
+int semop(int semid, struct sembuf *sops, size_t nsops);       /* PV 操作 */
+int semctl(int semid, int semnum, int cmd, ...);               /* 控制 */
+```
+
+| 函数 | 参数 | 返回值 |
+| ---- | ---- | ------ |
+| `semget` | `key` IPC 键;`nsems` 信号量个数;`semflg` 如 `IPC_CREAT \| 0666` | 成功返回信号量集 ID;失败 -1 并设 `errno` |
+| `semop` | `semid`;`sops` 操作数组;`nsops` 操作个数 | 成功 0;失败 -1 并设 `errno` |
+| `semctl` | `semid`;`semnum` 第几个信号量;`cmd` 如 `SETVAL`/`GETVAL`/`IPC_RMID`;第四个参数为 `union semun` | 依 `cmd` 而定 |
+
+```c
+struct sembuf {
+    unsigned short sem_num;   /* 信号量在集合中的下标 */
+    short          sem_op;    /* 操作值:>0 表示 V 操作(释放);<0 表示 P 操作(申请);0 表示等待为 0 */
+    short          sem_flg;   /* 如 IPC_NOWAIT、SEM_UNDO */
+};
+```
+
+**P / V 语义**(扩展):
+
+- **P 操作(申请资源)**:`sem_op = -1`,信号量减 1;若当前值为 0,则阻塞等待,直到有进程释放。
+- **V 操作(释放资源)**:`sem_op = +1`,信号量加 1,唤醒等待者。
+- **等待为 0**:`sem_op = 0`,阻塞直到信号量值为 0,用于同步等待。
+
+`semctl` 的常用 `cmd`:
+
+| cmd | 含义 |
+| --- | ---- |
+| `SETVAL` | 设置某个信号量的初值(需传 `union semun.val`) |
+| `GETVAL` | 获取某个信号量的当前值 |
+| `IPC_RMID` | 删除整个信号量集 |
+| `IPC_STAT` / `IPC_SET` | 获取/设置状态信息 |
+
+`union semun` 需由用户自行定义(部分系统在头文件中提供):
+
+```c
+union semun {
+    int              val;
+    struct semid_ds *buf;
+    unsigned short  *array;
+};
+```
+
+---
+
+## 7. 共享内存
+
+共享内存就是映射一段能被其它进程所访问的内存:这段共享内存由一个进程创建,但其它多个进程都可以访问,使得多个进程可以访问**同一块内存空间**。
+
+共享内存是**最快的 IPC 方式**,它是针对其它进程间通信方式运行效率低而专门设计的。它往往与其它通信机制(譬如结合信号量)来使用,以实现进程间的同步和通信——因为共享内存本身不提供互斥,多个进程同时写会产生竞态。
+
+### 7.1 System V 共享内存 API
+
+> ⚠️ **来源说明**:本节 API 原型与示例不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+```c
+#include <sys/ipc.h>
+#include <sys/shm.h>
+
+int shmget(key_t key, size_t size, int shmflg);                        /* 创建/获取 */
+void *shmat(int shmid, const void *shmaddr, int shmflg);              /* 挂载到进程地址空间 */
+int shmdt(const void *shmaddr);                                       /* 脱离 */
+int shmctl(int shmid, int cmd, struct shmid_ds *buf);                 /* 控制 */
+```
+
+| 函数 | 参数 | 返回值 |
+| ---- | ---- | ------ |
+| `shmget` | `key` IPC 键;`size` 共享内存大小;`shmflg` 如 `IPC_CREAT \| 0666` | 成功返回共享内存 ID;失败 -1 并设 `errno` |
+| `shmat` | `shmid`;`shmaddr` 一般为 `NULL`(由内核选择地址);`shmflg` 如 `0` / `SHM_RDONLY` | 成功返回映射后的虚拟地址;失败返回 `(void *)-1` |
+| `shmdt` | `shmaddr` 由 `shmat` 返回的地址 | 成功 0;失败 -1 并设 `errno` |
+| `shmctl` | `shmid`;`cmd` 如 `IPC_RMID`、`IPC_STAT`;`buf` | 成功 0;失败 -1 并设 `errno` |
+
+使用流程:
+
+```mermaid
+flowchart LR
+    A["进程A: shmget 创建"] --> B["shmat 挂载"]
+    B --> C["读写共享内存"]
+    D["进程B: shmget 以同一 key 获取"] --> E["shmat 挂载"]
+    E --> C
+    C --> F["shmdt 脱离"]
+    F --> G["shmctl IPC_RMID 删除"]
+```
+
+### 7.2 与 mmap 对比
+
+> ⚠️ **来源说明**:本节为扩展知识。
+
+`mmap()` 也能实现内存映射,常用于**共享内存**与文件映射。两者定位不同:
+
+| 维度 | System V 共享内存 | mmap |
+| ---- | ----------------- | ---- |
+| 载体 | 匿名内核共享内存段 | 文件或匿名映射(`MAP_SHARED`) |
+| 持久化 | 直到 `IPC_RMID`,不随进程退出消失 | 文件映射随文件持久化;匿名映射随进程组消失 |
+| 生命周期管理 | `shmctl(IPC_RMID)` | `unlink` 文件 / 自动回收 |
+| 接口 | `shmget/shmat/shmdt/shmctl` | `mmap/munmap/msync` |
+| 与文件 IO 关系 | 无文件对象 | 可与文件内容直接关联,读写即文件读写 |
+| 常见用途 | 纯进程间大块数据共享 | 文件映射、设备映射(如 FrameBuffer)、匿名共享内存 |
+
+二者都需要自行配合同步机制(信号量、互斥锁、信号)使用。
+
+---
+
+## 8. 套接字(Socket)
+
+Socket 是一种 IPC 方法,是基于**网络**的 IPC 方法,允许位于**同一主机**或使用网络连接起来的**不同主机**上的应用程序之间交换数据,说白了就是网络通信。
+
+在一个典型的客户端/服务器场景中,应用使用 socket 通信的方式如下:
+
+- 各个应用程序创建一个 socket。socket 是一个允许通信的"设备",两个应用程序都需要用到它;
+- 服务器将自己的 socket 绑定到一个众所周知的地址(名称)上,使得客户端能够定位到它的位置。
+
+Socket 的具体内容在网络编程章节介绍。其最大优势是**全双工、可跨主机、语言无关**;代价是协议栈开销。
+
+---
+
+## 9. 完整可复制源码
+
+### 9.1 管道:父子进程通信
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+int main(void)
+{
+    int pipefd[2];
+    pid_t pid;
+    char buf[128] = {0};
+
+    if (-1 == pipe(pipefd)) {
+        perror("pipe error");
+        exit(-1);
+    }
+
+    pid = fork();
+    if (-1 == pid) {
+        perror("fork error");
+        exit(-1);
+    }
+
+    if (0 == pid) {
+        /* 子进程:只读,关闭写端 */
+        close(pipefd[1]);
+        ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1);
+        if (n > 0) {
+            buf[n] = '\0';
+            printf("子进程读到: %s\n", buf);
+        }
+        close(pipefd[0]);
+        _exit(0);
+    }
+
+    /* 父进程:只写,关闭读端 */
+    close(pipefd[0]);
+    const char *msg = "Hello from parent via pipe";
+    write(pipefd[1], msg, strlen(msg));
+    close(pipefd[1]);
+
+    wait(NULL);
+    exit(0);
+}
+```
+
+**逐段解释**:
+
+- `pipe(pipefd)` 创建管道,`pipefd[0]` 读、`pipefd[1]` 写。
+- `fork()` 后子进程继承两个 fd;子进程关闭写端、父进程关闭读端,形成单向数据流。
+- 父进程 `write` 后关闭写端;子进程 `read` 到数据后打印。若父进程关闭写端时子进程还没读,`read` 仍能读到缓冲数据,读完再次 `read` 会返回 0(EOF)。
+
+### 9.2 FIFO:两个独立进程通信
+
+写端 `fifo_write.c`:
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#define FIFO_PATH "./my_fifo"
+
+int main(void)
+{
+    int fd;
+    const char *msg = "Hello from FIFO writer";
+
+    /* 不存在则创建,EEXIST 忽略 */
+    if (-1 == mkfifo(FIFO_PATH, 0666) && errno != EEXIST) {
+        perror("mkfifo error");
+        exit(-1);
+    }
+
+    /* 写方式打开会阻塞,直到有进程以读方式打开 */
+    fd = open(FIFO_PATH, O_WRONLY);
+    if (-1 == fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    write(fd, msg, strlen(msg));
+    close(fd);
+
+    return 0;
+}
+```
+
+读端 `fifo_read.c`:
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#define FIFO_PATH "./my_fifo"
+
+int main(void)
+{
+    int fd;
+    char buf[128] = {0};
+
+    if (-1 == mkfifo(FIFO_PATH, 0666) && errno != EEXIST) {
+        perror("mkfifo error");
+        exit(-1);
+    }
+
+    fd = open(FIFO_PATH, O_RDONLY);
+    if (-1 == fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    ssize_t n = read(fd, buf, sizeof(buf) - 1);
+    if (n > 0) {
+        buf[n] = '\0';
+        printf("FIFO 读端收到: %s\n", buf);
+    }
+    close(fd);
+
+    return 0;
+}
+```
+
+编译与运行(需两个终端,先运行读端再运行写端):
+
+```bash
+gcc -Wall -o fifo_write fifo_write.c
+gcc -Wall -o fifo_read  fifo_read.c
+# 终端1
+./fifo_read
+# 终端2
+./fifo_write
+```
+
+### 9.3 System V 消息队列:发送与接收
+
+发送端 `msg_send.c`:
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/ipc.h>
+#include <sys/msg.h>
+
+#define MSG_KEY  0x1234
+
+struct msgbuf {
+    long mtype;
+    char mtext[128];
+};
+
+int main(void)
+{
+    int msqid;
+    struct msgbuf msg;
+
+    /* 打开或创建消息队列 */
+    msqid = msgget(MSG_KEY, IPC_CREAT | 0666);
+    if (-1 == msqid) {
+        perror("msgget error");
+        exit(-1);
+    }
+
+    msg.mtype = 1;
+    strcpy(msg.mtext, "Hello System V message queue");
+
+    if (-1 == msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0)) {
+        perror("msgsnd error");
+        exit(-1);
+    }
+
+    puts("消息发送成功");
+    return 0;
+}
+```
+
+接收端 `msg_recv.c`:
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/ipc.h>
+#include <sys/msg.h>
+
+#define MSG_KEY  0x1234
+
+struct msgbuf {
+    long mtype;
+    char mtext[128];
+};
+
+int main(void)
+{
+    int msqid;
+    struct msgbuf msg;
+
+    msqid = msgget(MSG_KEY, IPC_CREAT | 0666);
+    if (-1 == msqid) {
+        perror("msgget error");
+        exit(-1);
+    }
+
+    /* msgtyp = 0:接收第一条消息 */
+    if (-1 == msgrcv(msqid, &msg, sizeof(msg.mtext), 0, 0)) {
+        perror("msgrcv error");
+        exit(-1);
+    }
+
+    printf("收到消息(type=%ld): %s\n", msg.mtype, msg.mtext);
+
+    /* 删除消息队列 */
+    msgctl(msqid, IPC_RMID, NULL);
+    return 0;
+}
+```
+
+**逐段解释**:
+
+- `msgget(MSG_KEY, IPC_CREAT | 0666)`:以固定 key 创建/获取队列,双方用同一 key 找到同一队列。
+- `msgsnd` 的 `msgsz` 是**正文长度**,不含 `mtype`;`mtype` 必须大于 0。
+- `msgrcv` 的 `msgtyp = 0` 表示取第一条消息;`msgsz` 是接收缓冲区正文容量。
+- 接收端 `msgctl(msqid, IPC_RMID, NULL)` 删除队列,释放内核资源。
+
+### 9.4 System V 信号量:PV 互斥
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/ipc.h>
+#include <sys/sem.h>
+#include <unistd.h>
+
+#define SEM_KEY 0x5678
+
+union semun {
+    int              val;
+    struct semid_ds *buf;
+    unsigned short  *array;
+};
+
+/* P 操作:申请资源,信号量 -1 */
+static int sem_p(int semid)
+{
+    struct sembuf op = { .sem_num = 0, .sem_op = -1, .sem_flg = 0 };
+    return semop(semid, &op, 1);
+}
+
+/* V 操作:释放资源,信号量 +1 */
+static int sem_v(int semid)
+{
+    struct sembuf op = { .sem_num = 0, .sem_op = 1, .sem_flg = 0 };
+    return semop(semid, &op, 1);
+}
+
+int main(void)
+{
+    int semid;
+    union semun arg;
+
+    /* 创建含 1 个信号量的集合 */
+    semid = semget(SEM_KEY, 1, IPC_CREAT | 0666);
+    if (-1 == semid) {
+        perror("semget error");
+        exit(-1);
+    }
+
+    /* 初值设为 1(当作互斥锁) */
+    arg.val = 1;
+    if (-1 == semctl(semid, 0, SETVAL, arg)) {
+        perror("semctl error");
+        exit(-1);
+    }
+
+    sem_p(semid);       /* 进入临界区 */
+    puts("进入临界区,持有信号量...");
+    sleep(2);
+    sem_v(semid);       /* 退出临界区 */
+
+    puts("退出临界区");
+    semctl(semid, 0, IPC_RMID);   /* 删除信号量集 */
+    return 0;
+}
+```
+
+### 9.5 System V 共享内存
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/ipc.h>
+#include <sys/shm.h>
+
+#define SHM_KEY 0x9abc
+#define SHM_SIZE 4096
+
+int main(void)
+{
+    int shmid;
+    char *addr;
+
+    /* 创建共享内存段 */
+    shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);
+    if (-1 == shmid) {
+        perror("shmget error");
+        exit(-1);
+    }
+
+    /* 挂载到当前进程地址空间(shmaddr 为 NULL) */
+    addr = shmat(shmid, NULL, 0);
+    if ((void *)-1 == addr) {
+        perror("shmat error");
+        exit(-1);
+    }
+
+    /* 直接像普通内存一样读写 */
+    strcpy(addr, "Hello shared memory");
+    printf("写入共享内存: %s\n", addr);
+
+    /* 脱离 */
+    if (-1 == shmdt(addr)) {
+        perror("shmdt error");
+        exit(-1);
+    }
+
+    /* 删除共享内存段 */
+    shmctl(shmid, IPC_RMID, NULL);
+    return 0;
+}
+```
+
+**逐段解释**:
+
+- `shmget` 以 key 创建共享内存,其它进程用同一 key + `shmget` 即可获取同一段内存。
+- `shmat` 返回映射后的虚拟地址,之后直接当普通内存读写(无需 `read`/`write`),这是共享内存高效的原因。
+- `shmdt` 脱离映射;`shmctl(IPC_RMID)` 删除共享内存段。注意删除后已挂载的进程仍可继续访问,直到最后脱离。
+
+---
+
+## 10. 实验步骤与调试方法
+
+### 10.1 编译运行
+
+```bash
+gcc -Wall -o pipe_demo  pipe_demo.c
+gcc -Wall -o msg_send   msg_send.c
+gcc -Wall -o msg_recv   msg_recv.c
+gcc -Wall -o sem_demo   sem_demo.c
+gcc -Wall -o shm_demo   shm_demo.c
+
+./pipe_demo
+./msg_send && ./msg_recv
+./sem_demo
+./shm_demo
+```
+
+### 10.2 常用调试命令
+
+| 命令 | 用途 |
+| ---- | ---- |
+| `ls -l <fifo>` | 查看 FIFO 文件,类型为 `p` |
+| `ipcs` | 查看所有 System V IPC 对象(共享内存/消息队列/信号量) |
+| `ipcs -q` | 只看消息队列 |
+| `ipcs -m` | 只看共享内存 |
+| `ipcs -s` | 只看信号量 |
+| `ipcrm -q <msqid>` | 删除消息队列 |
+| `ipcrm -m <shmid>` | 删除共享内存 |
+| `ipcrm -s <semid>` | 删除信号量集 |
+| `strace ./app` | 跟踪 `pipe`/`read`/`write`/`msgget` 等系统调用 |
+
+### 10.3 实验一:验证管道 EOF 与 SIGPIPE
+
+- 父进程写完立即 `close(pipefd[1])`,子进程 `read` 完数据后再次 `read` 会返回 0,说明读到 EOF;
+- 反过来,让读端提前关闭,写端继续 `write`,进程会收到 `SIGPIPE` 默认终止。用 `signal(SIGPIPE, SIG_IGN)` 忽略后,`write` 返回 -1、`errno` 为 `EPIPE`。
+
+### 10.4 实验二:验证消息队列持久化
+
+先运行 `./msg_send`,再在另一个时刻运行 `./msg_recv`:消息仍能收到,因为**消息队列存在于内核中**,不随发送进程退出而消失。用 `ipcs -q` 可看到队列,`ipcrm -q <id>` 清理。
+
+### 10.5 实验三:观察 FIFO 打开阻塞
+
+先运行 `./fifo_read`(只读打开阻塞等待写者),此时进程挂起;再运行 `./fifo_write`,两端同时被唤醒完成通信。用 `ps` 可看到读端处于睡眠状态。
+
+### 10.6 实验四:共享内存 + 信号量配合
+
+共享内存本身不互斥。可写一个程序模拟"多个进程同时写同一块共享内存",无信号量保护时观察数据错乱;再用信号量 P/V 包住临界区,数据保持一致。
+
+---
+
+## 11. 跨平台对比:IMX6ULL vs STM32 vs RK3568
+
+| 维度 | I.MX6ULL(本教程) | STM32(裸机/RTOS) | RK3568(Linux) |
+| ---- | ---------------- | ------------------ | --------------- |
+| 进程隔离 | 完整 MMU 隔离,需 IPC | 通常单地址空间,全局变量即可 | 同 I.MX6ULL |
+| 管道/FIFO | 支持 | 无(可用环形缓冲区替代) | 支持 |
+| 消息队列 | System V / POSIX | RTOS 消息队列(如 FreeRTOS `xQueueSend`) | 同 I.MX6ULL |
+| 信号量 | System V / POSIX | RTOS 信号量/互斥量 | 同 I.MX6ULL |
+| 共享内存 | `shmget` 系列、`mmap` | 直接访问全局内存/双缓冲 | 同 I.MX6ULL |
+| 跨主机通信 | Socket | 需外接网络模块 + 协议栈 | Socket |
+| 调试工具 | `ipcs`/`ipcrm`/`ls -l` | 调试器观察内存 | 同 I.MX6ULL |
+
+要点:
+
+- **STM32 裸机上"进程间通信"概念基本不成立**,因为没有进程隔离,全局变量、RTOS 队列/信号量就够用;只有到 Linux 这类带 MMU 的系统,IPC 才是刚需。
+- I.MX6ULL 与 RK3568 都是 Linux,System V IPC 接口通用(RK3568 通常内核配置更全,POSIX IPC、`mq_*`、`sem_*` 都可用)。
+- 嵌入式 Linux 上使用 System V IPC 要注意:**内核资源有限**(`msgmni`、`shmmax`、`semmni` 等参数),对象用完要及时 `IPC_RMID`,否则会泄漏内核资源。
+
+---
+
+## 12. 面试精选(5 题)
+
+### Q1:Linux 有哪些进程间通信机制?各自适合什么场景?
+
+**答案**:分四类。**UNIX IPC**:管道(亲缘进程、单向字节流)、FIFO 有名管道(无亲缘进程、可双向)、信号(异步事件通知、信息量少)。**System V IPC**:消息队列(有格式消息、异步收发、有消息边界)、信号量(计数器,做同步/互斥,不传数据)、共享内存(大块数据、最快的 IPC,需自配同步)。**POSIX IPC**:与 System V 对应,接口更现代(`sem_open`/`mq_open`/`shm_open`)。**Socket IPC**:全双工、可跨主机、跨语言,适合网络与复杂场景。选型原则:小数据同步用信号/信号量;简单字节流用管道/FIFO;需要消息边界用消息队列;大数据量高性能用共享内存 + 信号量;跨主机用 Socket。
+
+### Q2:管道的读写分别在什么情况下阻塞?读端关闭后写端继续写会怎样?
+
+**答案**:`read`:管道为空且仍有写端打开时阻塞;所有写端关闭后不再阻塞,返回 0(EOF)。`write`:管道缓冲区满时阻塞,直到有空间。若**所有读端都已关闭**,写端继续 `write` 会触发 `SIGPIPE` 信号,默认终止进程,`write` 返回 -1 且 `errno = EPIPE`。因此健壮程序应忽略或处理 `SIGPIPE`,例如 `signal(SIGPIPE, SIG_IGN)` 后再判断返回值。此外管道写入量不超过 `PIPE_BUF`(Linux 默认 4096 字节)时是原子写,多进程写不会交错。
+
+### Q3:消息队列相对管道有什么优势?`msgrcv` 的 `msgtyp` 参数怎么用?
+
+**答案**:消息队列存放在**内核中**,有消息边界(保留消息长度),支持按类型选择性接收,还能突破管道"缓冲区大小受限"和"只能承载无格式字节流"的缺陷;且不要求收发进程有亲缘关系,发送方发完即可退出(消息保留在内核)。`msgrcv` 的 `msgtyp`:等于 0 接收队列中第一条消息;大于 0 接收第一条类型等于 `msgtyp` 的消息;小于 0 接收类型值 ≤ `|msgtyp|` 的消息中类型最小的那一条。配合 `IPC_NOWAIT` 可非阻塞读取。
+
+### Q4:共享内存为什么是最快的 IPC?为什么它通常要和信号量配合使用?
+
+**答案**:共享内存把同一块物理内存映射到多个进程各自的虚拟地址空间,进程读写它就像读写自己的内存——**没有任何内核数据拷贝**。管道、消息队列等都要经历"用户空间→内核缓冲区→用户空间"的两次拷贝,因此共享内存最快。但它本身**不提供任何同步或互斥**:多个进程同时读写会产生竞态、读到不一致数据。所以通常配一个信号量(或互斥锁)来保证同一时刻只有一个进程进入临界区,实现"高效数据传输 + 正确同步"的组合。
+
+### Q5:`fork()` 之后父子进程如何通信?请描述一个基于管道的实现,并说明为什么要关闭不用的文件描述符。
+
+**答案**:先 `pipe(pipefd)` 创建管道,再 `fork()`。由于子进程继承父进程的文件描述符,父子都能访问 `pipefd[0]`(读端)和 `pipefd[1]`(写端)。实现单向通信:若要父写子读,父进程关闭 `pipefd[0]`、子进程关闭 `pipefd[1]`,父进程 `write(pipefd[1], ...)`、子进程 `read(pipefd[0], ...)`。**必须关闭不用的端**,否则会有两个问题:① 无法正确产生 EOF——只有当所有写端都关闭后,读端的 `read` 才返回 0,如果不关闭子进程里继承的写端,父进程关闭了自己的写端,管道仍被视为有写者打开,读端会一直阻塞;② 无谓地占用文件描述符、影响资源回收。这正是"每个进程只保留自己需要的一端"的原因。
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第十章 进程间通信简介

+ 1987 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/02-进程线程与IPC/04-线程与线程同步.md

@@ -0,0 +1,1987 @@
+---
+title: 线程与线程同步
+tags: [嵌入式Linux, Linux应用编程, 多线程, pthread, 线程同步, 互斥锁, 条件变量, 自旋锁, 读写锁, 生产者消费者, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十一章 线程、第十二章 线程同步"
+---
+
+# 线程与线程同步
+
+> 💡 **关联知识**:[[02-进程线程与IPC/01-信号机制]]、[[02-进程线程与IPC/02-进程管理]]、[[02-进程线程与IPC/03-进程间通信]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/9. pthread多线程基础]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/10. 互斥锁]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/11. 条件变量]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/12. 读写锁与自旋锁]];深入 → [[Linux+C+C++技术体系梳理/2. Linux系统编程/13. 线程池模式]]
+
+进程让程序"能同时做多件事",线程则让**同一个程序内部**同时做多件事。进程切换开销大、进程间通信麻烦,而同一进程内的线程共享地址空间,切换便宜、通信容易,因此线程是现代并发程序的主力。但共享也带来代价:多个线程同时读写同一变量会引发**竞态**,必须用互斥锁、条件变量、自旋锁、读写锁等同步机制来保护。本篇把"线程基础"和"线程同步"合并在一起讲完,学完即可独立编写生产级多线程程序。
+
+---
+
+## 1. 线程基础
+
+### 1.1 什么是线程
+
+**线程是参与系统调度的最小单位**。它被包含在进程之中,是进程中的实际运行单位;一个线程是进程中一条单一顺序的控制流(执行路线),一个进程可以创建多个线程,多个线程并发运行、各执行不同任务。
+
+启动一个程序时,操作系统创建一个进程,同时一个线程立刻运行,这个线程叫**主线程**。`main()` 就是主线程的入口函数,`main()` 执行的任务就是主线程的任务。因此:
+
+- 任何一个进程都至少包含一个主线程,只有主线程的进程称为**单线程进程**;
+- 除主线程外还包含其它线程的,称为**多线程进程**,其它线程通常由主线程调用 `pthread_create()` 创建,称为主线程的**子线程**。
+
+主线程的重要性体现在两方面:
+
+- 其它新线程(子线程)由主线程创建;
+- 主线程通常最后结束运行,负责各种清理工作,例如回收子线程。
+
+线程的特点:
+
+- 线程不单独存在,而是包含在进程中;
+- 线程是参与系统调度的基本单位;
+- 可并发执行:同一进程的多个线程并发执行,宏观上表现为同时运行;
+- 共享进程资源:同一进程的各线程具有相同的地址空间,可访问进程已打开的文件、定时器、信号量等。
+
+同时,每个线程又有**私有**的部分:各自的调用栈(线程栈)、寄存器环境(register context)、线程本地存储(thread-local storage)。
+
+```mermaid
+flowchart TB
+    accTitle: 进程与线程的资源关系
+    accDescr: 同一进程内多个线程共享代码段、数据段、堆和打开的文件,但各自拥有独立的线程栈和寄存器环境。
+    subgraph P["进程(共享资源)"]
+        CODE["代码段(共享,只读)"]
+        DATA["数据段 / 堆(共享)"]
+        FD["文件描述符表(共享)"]
+        SIG["信号处理方式(共享)"]
+        subgraph T1["线程 1"]
+            S1["私有线程栈"]
+            R1["私有寄存器环境"]
+            L1["私有 TLS"]
+        end
+        subgraph T2["线程 2"]
+            S2["私有线程栈"]
+            R2["私有寄存器环境"]
+            L2["私有 TLS"]
+        end
+        subgraph T3["线程 3"]
+            S3["私有线程栈"]
+            R3["私有寄存器环境"]
+            L3["私有 TLS"]
+        end
+    end
+```
+
+线程也有自己的生命周期。新创建的线程进入**就绪态**,获得 CPU 调度后进入**运行态**;时间片耗尽或被更高优先级线程抢占时回到就绪态;执行到需要等待的操作(加锁、条件变量等待、`sleep`、阻塞 I/O 等)时进入**阻塞态**,条件满足或被唤醒后重新回到就绪态;执行 `pthread_exit()`、从 start 函数 `return`、或被取消请求在取消点生效时进入**终止态**。
+
+```mermaid
+stateDiagram-v2
+    accTitle: 线程生命周期状态
+    accDescr: 线程从创建到终止的状态迁移,包含就绪、运行、阻塞等待与终止,取消请求需到达取消点才生效。
+    [*] --> 就绪: pthread_create
+    就绪 --> 运行: 获得CPU调度
+    运行 --> 就绪: 时间片耗尽或被抢占
+    运行 --> 阻塞: 加锁失败或wait或sleep或阻塞IO
+    阻塞 --> 就绪: 条件满足或被唤醒
+    运行 --> 终止: pthread_exit或return
+    运行 --> 终止: 取消请求到达取消点
+    终止 --> [*]
+```
+
+### 1.2 线程 vs 进程
+
+创建多个子进程本质上就是多个单线程进程,同样能并发处理多任务。两种模型各有优劣:
+
+**多进程编程的劣势:**
+
+- 进程间切换开销大。微观上仍是轮流切换,进程间切换开销远大于同一进程内多线程切换,对中小型程序不划算。
+- 进程间通信麻烦。每个进程处于各自独立、隔离的地址空间,相互通信较麻烦。
+
+**多线程编程的优势:**
+
+- 同一进程的多个线程间切换开销小;
+- 同一进程的多个线程间通信容易(共享地址空间);
+- 线程创建速度远大于进程创建速度;
+- 多线程在多核处理器上更有优势。
+
+多线程也有缺点:编程难度高,需要考虑线程安全、信号处理等问题,编写与调试比单线程困难。多进程模型通常用于大型应用程序(如网络服务器),中小型程序使用较少。
+
+| 对比项 | 多进程 | 多线程 |
+| ------ | ------ | ------ |
+| 地址空间 | 各自独立、隔离 | 共享同一进程地址空间 |
+| 切换开销 | 大 | 小 |
+| 创建速度 | 慢 | 快 |
+| 通信方式 | 需要 IPC(管道、共享内存等) | 直接读写全局变量即可 |
+| 健壮性 | 一个进程崩溃不影响其它进程 | 一个线程崩溃可能拖垮整个进程 |
+| 资源占用 | 每进程独立资源 | 共享进程资源,额外仅线程栈等 |
+| 典型场景 | 网络服务器、大型系统 | 中小型并发程序、GUI、数据处理 |
+
+### 1.3 并发、并行与串行
+
+- **串行**:一件事、一件事接着做,只有一个执行单元;
+- **并发**:交替做不同的事,同一个执行单元上把时间切成时间片,每个任务执行一段时间后切换,即**时分复用**;
+- **并行**:同时做不同的事,需要多个执行单元(多核)。
+
+生活化比喻:
+
+- 吃饭吃到一半电话来了,吃完才去接——不支持并发也不支持并行,只是串行;
+- 停下吃饭去接电话,接完继续吃——支持并发;
+- 一边打电话一边吃饭——支持并行。
+
+单核处理器(如 I.MX6ULL 的单核 Cortex-A7)只有一个执行单元,只能以**并发**方式运行系统中的线程;多核处理器可并行执行多个线程,且每个执行单元内部仍以并发方式调度多个线程。由于处理器速度极快,交替轮询一次的时间在宏观上几乎可忽略,所以"看起来"所有线程在同时运行。
+
+### 1.4 线程 ID
+
+每个进程有进程 ID(`pid_t`,非负整数),每个线程也有其标识,称为**线程 ID**。进程 ID 在整个系统中唯一,而**线程 ID 只在所属进程上下文中才有意义**。线程 ID 使用 `pthread_t` 数据类型表示。
+
+| 函数 | 原型 | 参数 | 返回值 | 说明 |
+| ---- | ---- | ---- | ------ | ---- |
+| `pthread_self` | `pthread_t pthread_self(void);` | 无 | 当前线程 ID | 总是成功 |
+| `pthread_equal` | `int pthread_equal(pthread_t t1, pthread_t t2);` | 两个线程 ID | 相等返回非 0,否则返回 0 | 跨平台比较线程 ID 的正确方式 |
+
+在 Linux 下 `pthread_t` 实际是 `unsigned long int`,但其它系统不一定,所以应把 `pthread_t` 当作**不透明类型**,用 `pthread_equal()` 比较,而不是直接用 `==`(调试打印时可以临时按其实际类型打印)。
+
+线程 ID 的用途:很多线程函数(`pthread_cancel()`、`pthread_detach()`、`pthread_join()` 等)用线程 ID 标识目标线程;也可用特定线程 ID 作为动态数据结构的标签,标识数据结构的创建者或属主线程。
+
+```c
+#include <pthread.h>
+#include <stdio.h>
+
+int main(void)
+{
+    pthread_t tid = pthread_self();
+    printf("当前线程 ID<%lu>\n", (unsigned long)tid);
+    return 0;
+}
+```
+
+### 1.5 创建线程:pthread_create()
+
+| 项 | 内容 |
+| -- | ---- |
+| 原型 | `int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);` |
+| 头文件 | `<pthread.h>` |
+| 链接 | `-lpthread`(pthread 不在 gcc 默认链接库中) |
+| 参数 `thread` | `pthread_t *`,成功时新线程 ID 保存在此 |
+| 参数 `attr` | `pthread_attr_t *`,为 `NULL` 表示使用默认属性 |
+| 参数 `start_routine` | 函数指针,新线程从此函数开始运行,签名 `void *(*)(void *)` |
+| 参数 `arg` | 传给 `start_routine` 的参数;可为 `NULL` |
+| 返回值 | 成功返回 `0`;失败返回**错误号**(不设置 `errno`) |
+
+关键点:
+
+- `start_routine` 只能接收一个 `void *` 参数,需要传多个参数时应封装成结构体,把结构体地址作为 `arg`;
+- `arg` 一般应指向**全局变量或堆变量**,保证在线程生命周期内该对象一直存在;若指向栈变量,线程访问时可能已失效;
+- `pthread_create()` 失败时返回错误码而**不像系统调用那样设置 `errno`**。每个线程有自己的一份 `errno` 副本(为兼容使用 `errno` 的函数),但线程函数中直接返回错误码更清晰,可把错误范围限制在出错函数内;
+- 线程创建成功后立即加入系统调度队列,获得 CPU 后从 `start_routine()` 开始运行。调用后无法确定系统先调度主线程还是新线程,若程序对执行顺序有强制要求,必须使用同步技术。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <pthread.h>
+#include <string.h>
+#include <unistd.h>
+
+static void *new_thread_start(void *arg)
+{
+    printf("新线程: 进程 ID<%d>  线程 ID<%lu>\n",
+           getpid(), (unsigned long)pthread_self());
+    return (void *)0;
+}
+
+int main(void)
+{
+    pthread_t tid;
+    int ret;
+
+    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
+    if (ret) {
+        fprintf(stderr, "Error: %s\n", strerror(ret));
+        exit(-1);
+    }
+
+    printf("主线程: 进程 ID<%d>  线程 ID<%lu>\n",
+           getpid(), (unsigned long)pthread_self());
+
+    sleep(1);   /* 主线程休眠,避免新线程还没运行进程就结束了 */
+    exit(0);
+}
+```
+
+编译运行:
+
+```bash
+gcc -Wall -o testApp testApp.c -lpthread
+./testApp
+```
+
+两个线程的进程 ID 相同(同属一个进程),线程 ID 不同;Linux 下线程 ID 数值很大,看起来像一个指针。
+
+### 1.6 终止线程
+
+终止线程的方式:
+
+- 线程的 start 函数执行 `return` 语句并返回指定值,返回值就是线程的退出码;
+- 线程调用 `pthread_exit()`;
+- 调用 `pthread_cancel()` 取消线程;
+- **注意**:如果进程中任意线程调用 `exit()`、`_exit()` 或 `_Exit()`,将导致**整个进程**终止。
+
+| 项 | 内容 |
+| -- | ---- |
+| 原型 | `void pthread_exit(void *retval);` |
+| 头文件 | `<pthread.h>` |
+| 参数 `retval` | 线程返回值(退出码),可由 `pthread_join()` 获取 |
+| 返回值 | 无 |
+
+要点:
+
+- `pthread_exit()` 等价于在线程 start 函数中执行 `return`,区别是可在 start 函数所调用的**任意函数**中调用它来终止线程;
+- `retval` 所指向的内容**不应分配在线程栈中**(线程终止后栈内容是否有效不确定),start 函数的返回值同理;
+- 如果主线程调用 `pthread_exit()`,主线程终止,但其它线程仍正常运行,直到所有线程都终止进程才终止。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <pthread.h>
+#include <unistd.h>
+
+static void *new_thread_start(void *arg)
+{
+    printf("新线程 start\n");
+    sleep(1);
+    printf("新线程 end\n");
+    pthread_exit(NULL);
+}
+
+int main(void)
+{
+    pthread_t tid;
+    int ret;
+
+    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
+    if (ret) {
+        fprintf(stderr, "Error: %s\n", strerror(ret));
+        exit(-1);
+    }
+
+    printf("主线程 end\n");
+    pthread_exit(NULL);   /* 主线程退出,但进程不结束,新线程继续运行 */
+    exit(0);
+}
+```
+
+### 1.7 回收线程:pthread_join()
+
+类似父进程用 `wait()/waitpid()` 回收子进程,线程用 `pthread_join()` 阻塞等待线程终止并获取退出码、回收资源。
+
+| 项 | 内容 |
+| -- | ---- |
+| 原型 | `int pthread_join(pthread_t thread, void **retval);` |
+| 头文件 | `<pthread.h>` |
+| 参数 `thread` | 需要等待的线程 ID |
+| 参数 `retval` | 非 `NULL` 时将目标线程退出状态复制到 `*retval`;若线程被取消,则 `*retval` 为 `PTHREAD_CANCELED`;不关心可传 `NULL` |
+| 返回值 | 成功返回 `0`;失败返回错误码 |
+
+要点:
+
+- 调用 `pthread_join()` 会阻塞等待指定线程终止;若该线程已终止则立即返回;
+- 多个线程同时 `pthread_join()` 等待同一线程,结果不确定;
+- 若线程**未分离**,必须用 `pthread_join()` 回收;若线程终止后无人 `join`,该线程成为**僵尸线程**(与僵尸进程类似),浪费系统资源,积累过多将无法创建新线程。进程终止后,僵尸线程会被父进程回收;
+- 与 `waitpid()` 的显著差别:
+  - 线程之间关系对等,任意线程均可 `pthread_join()` 等待另一个线程(进程则是父进程唯一可 `wait()`);
+  - **不能以非阻塞方式调用** `pthread_join()`(`waitpid()` 可 `WNOHANG`)。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <pthread.h>
+#include <string.h>
+#include <unistd.h>
+
+static void *new_thread_start(void *arg)
+{
+    printf("新线程 start\n");
+    sleep(2);
+    printf("新线程 end\n");
+    pthread_exit((void *)10);
+}
+
+int main(void)
+{
+    pthread_t tid;
+    void *tret;
+    int ret;
+
+    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
+    if (ret) {
+        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
+        exit(-1);
+    }
+
+    ret = pthread_join(tid, &tret);
+    if (ret) {
+        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
+        exit(-1);
+    }
+    printf("新线程终止, code=%ld\n", (long)tret);   /* 输出 10 */
+
+    exit(0);
+}
+```
+
+### 1.8 取消线程:pthread_cancel()
+
+有时需要向线程发送请求,要求它立刻退出,称为**取消线程**。
+
+| 项 | 内容 |
+| -- | ---- |
+| 原型 | `int pthread_cancel(pthread_t thread);` |
+| 头文件 | `<pthread.h>` |
+| 参数 | 目标线程 ID |
+| 返回值 | 成功返回 `0`;失败返回错误码 |
+| 说明 | 发出请求后立即返回,不等待目标线程退出 |
+
+默认情况下目标线程也会立刻退出,行为如同调用了参数为 `PTHREAD_CANCELED`(即 `(void *)-1`)的 `pthread_exit()`。但线程可设置自己不被取消或控制如何被取消,因此 `pthread_cancel()` 只是**提出请求**。
+
+#### 取消状态与取消类型
+
+| 函数 | 原型 |
+| ---- | ---- |
+| 设置取消状态 | `int pthread_setcancelstate(int state, int *oldstate);` |
+| 设置取消类型 | `int pthread_setcanceltype(int type, int *oldtype);` |
+
+两者"设置新值 + 取回旧值"都是**原子操作**,`oldstate/oldtype` 可为 `NULL`。成功返回 `0`,失败返回非 0 错误码。
+
+`state` 取值:
+
+- `PTHREAD_CANCEL_ENABLE`:线程可被取消(新建线程与主线程的默认值);
+- `PTHREAD_CANCEL_DISABLE`:线程不可被取消,收到取消请求时将其**挂起**,直到状态变为 ENABLE。
+
+`type` 取值(仅在状态为 ENABLE 时有效):
+
+- `PTHREAD_CANCEL_DEFERRED`:取消请求被挂起,直到线程到达某个**取消点**才响应(默认值);
+- `PTHREAD_CANCEL_ASYNCHRONOUS`:可能在任意时间点取消线程,应用场景很少。
+
+当线程调用 `fork()` 创建子进程时,子进程会继承调用线程的取消状态和类型;调用 `exec` 函数时,新程序主线程的取消状态和类型重置为默认值(ENABLE + DEFERRED)。
+
+#### 取消点
+
+取消点是一系列函数,只有执行到这些函数时才会真正响应取消请求。系统认为未到取消点时代码正在执行关键工作,不应被停止。常见取消点函数包括:
+
+| 类别 | 函数 |
+| ---- | ---- |
+| I/O | `read()`、`write()`、`open()`、`close()`、`pread()`、`pwrite()`、`fcntl()` |
+| 睡眠 | `sleep()`、`nanosleep()`、`clock_nanosleep()`、`pause()`、`sigsuspend()` |
+| 线程/信号 | `pthread_join()`、`pthread_cond_wait()`、`pthread_cond_timedwait()`、`pthread_testcancel()`、`sigwait()` |
+| 等待/多路复用 | `wait()`、`waitpid()`、`select()`、`poll()`、`pselect()`、`accept()`、`connect()` |
+| IPC | `mq_send()`、`mq_receive()`、`sem_wait()`、`msgrcv()`、`msgsnd()` |
+| 同步/文件 | `fsync()`、`fdatasync()`、`msync()`、`system()`、`tcdrain()` |
+
+除这些函数外,不得将任何其它函数视为取消点(调用它们不会招致取消)。可用 `man 7 pthreads` 查看完整列表。
+
+如果一个循环体不含取消点(如空 `for(;;)`),线程将永远无法被取消。此时可用 `void pthread_testcancel(void);` 手动产生一个取消点:若线程已有挂起的取消请求,调用该函数后线程立即终止。
+
+#### 取消示例
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <pthread.h>
+#include <string.h>
+#include <unistd.h>
+
+static void *new_thread_start(void *arg)
+{
+    printf("新线程--running\n");
+    for ( ; ; )
+        sleep(1);          /* sleep 是取消点,可被取消 */
+    return (void *)0;
+}
+
+int main(void)
+{
+    pthread_t tid;
+    void *tret;
+    int ret;
+
+    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
+    if (ret) {
+        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
+        exit(-1);
+    }
+
+    sleep(1);
+
+    ret = pthread_cancel(tid);          /* 发送取消请求 */
+    if (ret) {
+        fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
+        exit(-1);
+    }
+
+    ret = pthread_join(tid, &tret);
+    if (ret) {
+        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
+        exit(-1);
+    }
+    printf("新线程终止, code=%ld\n", (long)tret);   /* 输出 -1,即 PTHREAD_CANCELED */
+
+    exit(0);
+}
+```
+
+把 start 函数改成不含取消点的空循环,主线程将无法取消它;在循环中加 `pthread_testcancel()` 后又能被取消。
+
+### 1.9 分离线程:pthread_detach()
+
+默认线程终止时可由其它线程 `pthread_join()` 获取状态并回收资源。若不关心返回值、希望线程终止时系统**自动回收**,可将其**分离**。
+
+| 项 | 内容 |
+| -- | ---- |
+| 原型 | `int pthread_detach(pthread_t thread);` |
+| 头文件 | `<pthread.h>` |
+| 参数 | 需要分离的线程 ID |
+| 返回值 | 成功返回 `0`;失败返回错误码 |
+
+要点:
+
+- 线程既可以分离别的线程,也可以分离自己:`pthread_detach(pthread_self());`
+- 一旦处于分离状态,就**不能再使用 `pthread_join()`** 获取其终止状态,且此过程不可逆;
+- 处于分离状态的线程终止后,系统自动回收其资源。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <pthread.h>
+#include <string.h>
+#include <unistd.h>
+
+static void *new_thread_start(void *arg)
+{
+    int ret;
+
+    ret = pthread_detach(pthread_self());   /* 自行分离 */
+    if (ret) {
+        fprintf(stderr, "pthread_detach error: %s\n", strerror(ret));
+        return NULL;
+    }
+
+    printf("新线程 start\n");
+    sleep(2);
+    printf("新线程 end\n");
+    pthread_exit(NULL);
+}
+
+int main(void)
+{
+    pthread_t tid;
+    int ret;
+
+    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
+    if (ret) {
+        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
+        exit(-1);
+    }
+
+    sleep(1);
+
+    ret = pthread_join(tid, NULL);          /* 已分离,必然失败 */
+    if (ret)
+        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));  /* Invalid argument */
+
+    pthread_exit(NULL);
+}
+```
+
+### 1.10 清理处理函数:pthread_cleanup_push/pop
+
+类似进程用 `atexit()` 注册终止处理函数,线程退出时也可执行**线程清理函数**。与进程不同,一个线程可以注册多个清理函数,记录在**清理函数栈**中(先进后出),执行顺序与注册顺序相反,全部执行完线程才终止。
+
+| 项 | 内容 |
+| -- | ---- |
+| 原型 | `void pthread_cleanup_push(void (*routine)(void *), void *arg);`<br/>`void pthread_cleanup_pop(int execute);` |
+| 头文件 | `<pthread.h>` |
+| `routine` | 清理函数指针,无返回值,只有一个 `void *` 参数 |
+| `arg` | 调用清理函数 `routine()` 时传给它的参数 |
+| `execute` | 为 `0` 只移除栈顶清理函数不执行;非 `0` 则移除并执行该函数 |
+
+清理函数栈中的函数在以下情况被执行:
+
+- 线程调用 `pthread_exit()` 退出时;
+- 线程响应取消请求时;
+- 用**非 0**参数调用 `pthread_cleanup_pop()` 时。
+
+除此之外,其它终止方式**不会**执行清理函数,例如在线程 start 函数中执行 `return` 退出时不会执行。
+
+注意:`pthread_cleanup_push()` 和 `pthread_cleanup_pop()` 实际是**宏**,会展开为 `{` 和 `}` 包裹的语句序列,因此必须在与线程相同的作用域中**成对匹配**使用,否则编译报错。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <pthread.h>
+#include <string.h>
+#include <unistd.h>
+
+static void cleanup(void *arg)
+{
+    printf("cleanup: %s\n", (char *)arg);
+}
+
+static void *new_thread_start(void *arg)
+{
+    printf("新线程--start run\n");
+    pthread_cleanup_push(cleanup, "第 1 次调用");
+    pthread_cleanup_push(cleanup, "第 2 次调用");
+    pthread_cleanup_push(cleanup, "第 3 次调用");
+
+    sleep(2);
+    pthread_exit((void *)0);        /* 触发清理函数栈 */
+
+    /* 仅为与 pthread_cleanup_push 配对,否则编译不通过 */
+    pthread_cleanup_pop(0);
+    pthread_cleanup_pop(0);
+    pthread_cleanup_pop(0);
+}
+
+int main(void)
+{
+    pthread_t tid;
+    void *tret;
+    int ret;
+
+    ret = pthread_create(&tid, NULL, new_thread_start, NULL);
+    if (ret) {
+        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
+        exit(-1);
+    }
+
+    ret = pthread_join(tid, &tret);
+    if (ret) {
+        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
+        exit(-1);
+    }
+    printf("新线程终止, code=%ld\n", (long)tret);
+
+    exit(0);
+}
+```
+
+输出顺序为"第 3 次调用、第 2 次调用、第 1 次调用",验证了先进后出。若把 `pthread_exit()` 换成 `return`,则不会执行清理函数。
+
+### 1.11 线程属性
+
+创建线程时可通过 `pthread_attr_t` 设置属性;`attr` 为 `NULL` 表示全部使用默认值。若自定义属性,需先用 `pthread_attr_init()` 初始化、用完用 `pthread_attr_destroy()` 销毁。
+
+| 函数 | 原型 |
+| ---- | ---- |
+| 初始化/销毁属性 | `int pthread_attr_init(pthread_attr_t *attr);`<br/>`int pthread_attr_destroy(pthread_attr_t *attr);` |
+| 栈地址与大小 | `int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);`<br/>`int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);` |
+| 单独设置栈大小 | `int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);`<br/>`int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);` |
+| 单独设置栈地址 | `int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);`<br/>`int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr);` |
+| 分离状态 | `int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);`<br/>`int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);` |
+
+`detachstate` 取值:
+
+- `PTHREAD_CREATE_DETACHED`:新建线程一开始便处于分离状态,结束后由系统回收资源,无法被 `pthread_join()`;
+- `PTHREAD_CREATE_JOINABLE`:默认值,正常启动,可被其它线程获取终止状态。
+
+设置栈大小为 4 KB 的示例:
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <pthread.h>
+#include <string.h>
+
+static void *new_thread_start(void *arg)
+{
+    puts("Hello World!");
+    return (void *)0;
+}
+
+int main(int argc, char *argv[])
+{
+    pthread_attr_t attr;
+    pthread_t tid;
+    int ret;
+
+    pthread_attr_init(&attr);               /* 初始化属性对象 */
+    pthread_attr_setstacksize(&attr, 4096); /* 设置栈大小为 4K */
+
+    ret = pthread_create(&tid, &attr, new_thread_start, NULL);
+    if (ret) {
+        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
+        exit(-1);
+    }
+
+    ret = pthread_join(tid, NULL);
+    if (ret) {
+        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
+        exit(-1);
+    }
+
+    pthread_attr_destroy(&attr);            /* 销毁属性对象 */
+    exit(0);
+}
+```
+
+以分离状态启动线程:
+
+```c
+pthread_attr_t attr;
+pthread_attr_init(&attr);
+pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
+pthread_create(&tid, &attr, new_thread_start, NULL);
+/* ... */
+pthread_attr_destroy(&attr);
+```
+
+### 1.12 线程安全与可重入
+
+#### 线程栈
+
+每个线程有自己独立的栈地址空间(线程栈),运行过程中的自动变量(局部变量)都分配在自己的线程栈中,互不干扰。创建线程时可配置栈大小与起始地址,多数情况保持默认即可。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <pthread.h>
+
+static void *new_thread(void *arg)
+{
+    int number = *((int *)arg);
+    unsigned long int tid = pthread_self();
+    printf("当前为<%d>号线程, 线程 ID<%lu>\n", number, tid);
+    return (void *)0;
+}
+
+static int nums[5] = {0, 1, 2, 3, 4};
+
+int main(int argc, char *argv[])
+{
+    pthread_t tid[5];
+    int j;
+
+    for (j = 0; j < 5; j++)
+        pthread_create(&tid[j], NULL, new_thread, &nums[j]);
+
+    for (j = 0; j < 5; j++)
+        pthread_join(tid[j], NULL);
+
+    exit(0);
+}
+```
+
+5 个线程使用同一个 start 函数,但每个线程的栈中各有一份 `number`、`tid`,互不影响。
+
+#### 可重入函数(Reentrant)
+
+形成多条执行流有两种情况:一是多线程;二是**信号处理**——信号异步到来会打断主程序,从而在单线程进程内也形成主程序与信号处理函数两条执行流。
+
+**可重入函数**:如果一个函数被同一进程的多个不同执行流同时调用,每次调用总能产生正确结果,则称为可重入函数。
+
+可重入函数分两类:
+
+- **绝对可重入函数**:无论怎么调用都可重入。特点是:函数内使用的变量均为局部变量(操作的内存地址均为本地栈地址);参数和返回值均为值类型;函数内调用的其它函数也都是绝对可重入函数。这类就是 pure code 可重入,多个副本使用分离的栈,互不干扰。
+- **带条件的可重入函数**:满足某个/某些条件时才可重入。例如函数只读取全局变量而不修改它,可重入的前提是"多执行流调用期间该全局变量绝不被其它地方修改";参数为指针的函数,若每个执行流传入的都是自己本地变量的地址则安全,若传入共享变量地址则不安全。
+
+反例(不可重入):
+
+```c
+static int glob = 0;
+
+static void func(int loops)
+{
+    int local;
+    int j;
+
+    for (j = 0; j < loops; j++) {
+        local = glob;   /* 读全局变量 */
+        local++;
+        glob = local;   /* 写全局变量,多执行流并发将出错 */
+    }
+}
+```
+
+很多 C 库函数都有可重入版本,名称后面加 `_r`,如 `asctime()/asctime_r()`、`ctime()/ctime_r()`、`localtime()/localtime_r()`。可用 `man` 查看函数的 ATTRIBUTES 信息。
+
+#### 线程安全函数
+
+**线程安全函数**:一个函数被多个线程(不包括信号处理函数产生的执行流)同时调用时,总能产生正确结果。
+
+**可重入函数一定是线程安全函数,但线程安全函数不一定是可重入函数**——可重入函数是线程安全函数的真子集。区别在于:可重入只从语言语法角度分析(不涉及具体实现机制),而线程安全函数可以使用线程同步技术实现。例如把上面读写 `glob` 的函数加上互斥锁保护后,它变成线程安全函数,但因为修改了外部全局变量,仍不是可重入函数。
+
+判断方法:用 man 手册查看 ATTRIBUTES,`MT-Safe` 表示线程安全、`MT-Unsafe` 表示线程不安全(MT = multithreaded)。POSIX.1-2001/2008 规定所有函数都必须是线程安全的,但有一批例外(`asctime`、`ctime`、`gmtime`、`localtime`、`rand`、`strerror`、`strtok`、`getenv`、`readdir`、`system` 等)。`man 7 pthreads` 可查看完整列表。
+
+#### 一次性初始化:pthread_once()
+
+多线程环境下,有些初始化代码段只能执行一次。用 `pthread_once()` 保证 `init_routine()` 仅执行一次(由内核调度决定在哪个线程执行)。
+
+| 项 | 内容 |
+| -- | ---- |
+| 原型 | `int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));` |
+| 头文件 | `<pthread.h>` |
+| 初始化 | `pthread_once_t once_control = PTHREAD_ONCE_INIT;` |
+| 返回值 | 成功返回 `0`;失败返回错误编码 |
+
+若一个线程调用 `pthread_once()` 时另一个线程也调用,后者会阻塞等待,直到第一个完成初始化返回。调用成功返回时,可确定所有状态都已初始化完成。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <pthread.h>
+
+static pthread_once_t once = PTHREAD_ONCE_INIT;
+
+static void initialize_once(void)
+{
+    printf("initialize_once 被执行: 线程 ID<%lu>\n", pthread_self());
+}
+
+static void func(void)
+{
+    pthread_once(&once, initialize_once);   /* 只会执行一次 */
+    printf("函数 func 执行完毕.\n");
+}
+
+static void *thread_start(void *arg)
+{
+    printf("线程%d 被创建: 线程 ID<%lu>\n",
+           *((int *)arg), pthread_self());
+    func();
+    pthread_exit(NULL);
+}
+
+static int nums[5] = {0, 1, 2, 3, 4};
+
+int main(void)
+{
+    pthread_t tid[5];
+    int j;
+
+    for (j = 0; j < 5; j++)
+        pthread_create(&tid[j], NULL, thread_start, &nums[j]);
+
+    for (j = 0; j < 5; j++)
+        pthread_join(tid[j], NULL);
+
+    exit(0);
+}
+```
+
+输出中 `initialize_once()` 只会被执行一次。
+
+#### 线程特有数据(TLS)
+
+线程特有数据(线程私有数据)为每个调用线程维护一份变量副本,每个线程通过**特有数据键(key)** 访问时都拿到本线程绑定的副本,从而避免变量成为线程间共享数据。常用于把非线程安全函数改造为线程安全函数。
+
+涉及 4 个函数:
+
+| 函数 | 原型 | 说明 |
+| ---- | ---- | ---- |
+| `pthread_key_create` | `int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));` | 创建 key,可绑定解构函数 |
+| `pthread_setspecific` | `int pthread_setspecific(pthread_key_t key, const void *value);` | 保存线程私有缓冲区指针并与 key、当前线程关联 |
+| `pthread_getspecific` | `void *pthread_getspecific(pthread_key_t key);` | 获取当前线程关联的私有缓冲区,未设置返回 `NULL` |
+| `pthread_key_delete` | `int pthread_key_delete(pthread_key_t key);` | 删除 key(不触发解构函数,需先确保无线程使用) |
+
+`pthread_key_create()` 只需在第一个调用线程中创建一次,因此通常配合 `pthread_once()` 使用。当使用该 key 的线程终止时,绑定的 `destructor()` 会被自动调用以释放私有数据。
+
+典型用法(把返回静态缓冲区的 `strerror` 改造为线程安全版):
+
+```c
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <pthread.h>
+
+#define MAX_ERROR_LEN 256
+
+static pthread_once_t once = PTHREAD_ONCE_INIT;
+static pthread_key_t strerror_key;
+
+static void destructor(void *buf)
+{
+    free(buf);                  /* 释放线程私有缓冲区 */
+}
+
+static void create_key(void)
+{
+    if (pthread_key_create(&strerror_key, destructor))
+        pthread_exit(NULL);
+}
+
+static char *my_strerror(int errnum)
+{
+    char *buf;
+
+    if (pthread_once(&once, create_key))       /* 只创建一次 key */
+        pthread_exit(NULL);
+
+    buf = pthread_getspecific(strerror_key);   /* 取本线程私有缓冲区 */
+    if (NULL == buf) {                          /* 首次调用,需分配 */
+        buf = malloc(MAX_ERROR_LEN);
+        if (NULL == buf)
+            pthread_exit(NULL);
+        if (pthread_setspecific(strerror_key, buf))
+            pthread_exit(NULL);
+    }
+
+    if (errnum < 0 || errnum >= _sys_nerr || NULL == _sys_errlist[errnum])
+        snprintf(buf, MAX_ERROR_LEN, "Unknown error %d", errnum);
+    else {
+        strncpy(buf, _sys_errlist[errnum], MAX_ERROR_LEN - 1);
+        buf[MAX_ERROR_LEN - 1] = '\0';
+    }
+
+    return buf;
+}
+```
+
+> ⚠️ **来源说明**:`_GNU_SOURCE`、`_sys_errlist`、`_sys_nerr` 属于 GNU 扩展,用于演示;实际项目中更推荐使用可重入的 `strerror_r()`。
+
+#### 线程局部存储:__thread
+
+线程局部存储比线程特有数据更简单:在全局或静态变量声明时加 `__thread` 修饰符,每个线程就都拥有一份该变量的拷贝,一直存在到线程终止时自动释放。
+
+```c
+static __thread char buf[512];
+```
+
+注意事项:
+
+- 变量声明中若用了 `static` 或 `extern`,`__thread` 必须紧随其后;
+- 与一般全局/静态变量一样,声明时可设初始值;
+- 可用取值操作符 `&` 获取线程局部变量地址。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <pthread.h>
+
+static __thread char buf[100];      /* 每个线程一份拷贝 */
+
+static void *thread_start(void *arg)
+{
+    strcpy(buf, "Child Thread\n");
+    printf("子线程: buf (%p) = %s", buf, buf);
+    pthread_exit(NULL);
+}
+
+int main(int argc, char *argv[])
+{
+    pthread_t tid;
+    int ret;
+
+    strcpy(buf, "Main Thread\n");
+
+    if (ret = pthread_create(&tid, NULL, thread_start, NULL)) {
+        fprintf(stderr, "pthread_create error: %d\n", ret);
+        exit(-1);
+    }
+
+    if (ret = pthread_join(tid, NULL)) {
+        fprintf(stderr, "pthread_join error: %d\n", ret);
+        exit(-1);
+    }
+
+    printf("主线程: buf (%p) = %s", buf, buf);
+    exit(0);
+}
+```
+
+主线程和子线程打印出的 `buf` 地址不同,证明它们操作的是各自线程的副本。
+
+### 1.13 线程与信号
+
+Linux 信号模型基于进程设计,信号问世远早于线程,因此两者结合使用较为复杂。以下是信号在多线程下的行为映射规则:
+
+- **系统默认行为属于进程层面**:任一线程收到未经处理的信号,都会执行该信号的默认动作(通常停止或终止进程);
+- **信号处理函数属于进程层面**:进程中所有线程共享注册的信号处理函数;
+- **信号发送既可针对整个进程,也可针对特定线程**。以下三种情况信号针对某个线程:
+  - 硬件异常信号(`SIGBUS`、`SIGFPE`、`SIGILL`、`SIGSEGV`)由某个线程执行指令引起,系统将其发送给该线程;
+  - 线程对已断开的管道写操作产生的 `SIGPIPE` 信号;
+  - 由 `pthread_kill()` 或 `pthread_sigqueue()` 发出的信号。
+  - 除此之外(如其它进程 `kill()`/`sigqueue()`、终端 `Ctrl+C` 产生的 `SIGINT` 等)均属于进程层面;
+- **多线程进程收到绑定了处理函数的信号时,内核会任选一个线程来处理**,并非每个线程都收到并处理;
+- **信号掩码属于线程层面**:多线程下不存在作用于整个进程的信号掩码,各线程可独立阻止或放行信号;
+- 内核分别维护"针对整个进程挂起的信号"和"针对每个线程挂起的信号",`sigpending()` 返回两者的并集。
+
+#### 线程的信号掩码
+
+单线程程序用 `sigprocmask()` 设置进程信号掩码;多线程用 `pthread_sigmask()` 设置各线程的信号掩码,用法与 `sigprocmask()` 完全一样。
+
+| 项 | 内容 |
+| -- | ---- |
+| 原型 | `int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);` |
+| 头文件 | `<signal.h>` |
+
+每个新建线程会从其创建者处继承信号掩码,之后可调用 `pthread_sigmask()` 改变自己的掩码。
+
+#### 向线程发送信号
+
+| 函数 | 原型 | 说明 |
+| ---- | ---- | ---- |
+| `pthread_kill` | `int pthread_kill(pthread_t thread, int sig);` | 向同一进程中的指定线程发送信号;`sig` 为 0 时不发送信号但仍做错误检查 |
+| `pthread_sigqueue` | `int pthread_sigqueue(pthread_t thread, int sig, const union sigval value);` | 与 `sigqueue()` 类似,但向指定线程发送并携带伴随数据 |
+
+`kill()`/`sigqueue()` 针对整个进程;`pthread_kill()`/`pthread_sigqueue()` 针对某个线程。
+
+#### 异步信号安全函数
+
+**异步信号安全函数**:可以在信号处理函数中被安全调用的线程安全函数,要求比线程安全函数更严格。**可重入函数一定是异步信号安全函数**,线程安全函数则不一定是。
+
+例如一个用互斥锁保护的线程安全函数,若信号处理函数中再次调用它,而处理信号的正是刚持有锁的线程,就会**死锁**。要将其实现为异步信号安全函数,可在获取锁之前通过设置信号掩码禁止接收该信号(将函数实现为不可被信号中断)。
+
+常见异步信号安全函数(部分):`_exit`、`open`、`close`、`read`、`write`、`fork`、`execve`、`kill`、`wait`、`waitpid`、`sigaction`、`sigprocmask`、`sigqueue`、`pipe`、`socket`、`connect`、`accept`、`pthread_kill`、`pthread_self`、`pthread_sigmask` 等。可用 `man 7 signal` 查看完整列表。
+
+一个安全的信号处理函数应满足:
+
+- 处理函数本身代码可重入,且只调用异步信号安全函数;
+- 主程序执行不安全函数、或操作信号处理函数也会更新的全局数据结构时,阻塞信号传递。
+
+> 注意:本书示例在信号处理函数中调用了非异步信号安全的 `printf()` 仅为了打印方便,实际项目中不应这样用。
+
+---
+
+## 2. 线程同步
+
+### 2.1 为什么需要线程同步
+
+线程同步是为了对**共享资源**的访问进行保护,解决**数据一致性**问题。
+
+- 若每个线程访问的变量都是其它线程不会读写的(如各自局部变量、只有一个线程访问的全局变量),不存在一致性问题;
+- 变量只读时,多线程同时读取也没有问题;
+- 当一个线程可修改的变量,其它线程也能读取或修改时,就存在一致性问题,需要同步。
+
+本质原因是多个线程**并发访问**共享资源形成**竞态(race)**,可能导致读写到无效值。例如线程 A 读变量、再写新值,写操作需两个时钟周期,线程 B 在中间读取,就会读到不一致的值。
+
+用两个线程各递增全局变量 1000 万次来说明:期望结果 2000 万,实际结果往往小于它。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <pthread.h>
+#include <unistd.h>
+#include <string.h>
+
+static int g_count = 0;
+
+static void *new_thread_start(void *arg)
+{
+    int loops = *((int *)arg);
+    int l_count, j;
+
+    for (j = 0; j < loops; j++) {
+        l_count = g_count;      /* 读全局变量到本地 */
+        l_count++;              /* 本地递增 */
+        g_count = l_count;      /* 写回全局变量 */
+    }
+
+    return (void *)0;
+}
+
+static int loops;
+int main(int argc, char *argv[])
+{
+    pthread_t tid1, tid2;
+    int ret;
+
+    if (2 > argc)
+        loops = 10000000;       /* 默认 1000 万次 */
+    else
+        loops = atoi(argv[1]);
+
+    ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
+    if (ret) {
+        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
+        exit(-1);
+    }
+
+    ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
+    if (ret) {
+        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
+        exit(-1);
+    }
+
+    ret = pthread_join(tid1, NULL);
+    if (ret) {
+        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
+        exit(-1);
+    }
+
+    ret = pthread_join(tid2, NULL);
+    if (ret) {
+        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
+        exit(-1);
+    }
+
+    printf("g_count = %d\n", g_count);
+    exit(0);
+}
+```
+
+传参 1000 时结果正确(2000),传默认 1000 万时结果通常小于 2000 万,这就是数据不一致。
+
+```mermaid
+flowchart LR
+    accTitle: 并发访问共享变量导致数据不一致
+    accDescr: 两个线程交错读写同一全局变量,线程B在写操作未完成时读取,得到不一致的值。
+    subgraph A["线程 A"]
+        A1["读 g_count = 5"] --> A2["写 6(需 2 个时钟周期)"]
+    end
+    subgraph B["线程 B"]
+        B1["在写周期中间读 g_count"] --> B2["读到 5,而非 6"]
+    end
+    A2 -.->|"中间被读取"| B1
+```
+
+Linux 提供了多种线程同步机制:互斥锁、条件变量、自旋锁、读写锁等。
+
+### 2.2 互斥锁(Mutex)
+
+互斥锁(mutex,互斥量)本质是一把锁:访问共享资源前上锁,访问完成后解锁。上锁后任何其它试图再次加锁的线程都会被阻塞,直到当前线程释放锁。若有多个线程阻塞等待,解锁后它们会竞争,只有一个能成功上锁,其余继续阻塞。
+
+程序设计的一个前提:**所有访问共享资源的线程都必须遵守相同的加锁规则**。若允许某个线程不拿锁就访问共享资源,即使其它线程都加锁,仍会出现数据不一致。
+
+#### 2.2.1 初始化
+
+互斥锁使用 `pthread_mutex_t` 类型,两种初始化方式:
+
+方式一:宏 `PTHREAD_MUTEX_INITIALIZER`(只适用于定义时直接初始化)。
+
+```c
+pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
+```
+
+方式二:`pthread_mutex_init()`(适用于先定义后初始化、或堆中动态分配)。
+
+| 项 | 内容 |
+| -- | ---- |
+| 原型 | `int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);` |
+| 头文件 | `<pthread.h>` |
+| 参数 `mutex` | 待初始化的互斥锁 |
+| 参数 `attr` | 互斥锁属性;`NULL` 表示默认属性(等价于宏方式,区别是宏方式不做错误检查) |
+| 返回值 | 成功返回 `0`;失败返回非 0 错误码 |
+
+```c
+pthread_mutex_t mutex;
+pthread_mutex_init(&mutex, NULL);
+
+/* 或堆上分配 */
+pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t));
+pthread_mutex_init(mutex, NULL);
+```
+
+#### 2.2.2 加锁与解锁
+
+| 函数 | 原型 | 说明 | 返回值 |
+| ---- | ---- | ---- | ------ |
+| `pthread_mutex_lock` | `int pthread_mutex_lock(pthread_mutex_t *mutex);` | 未锁定则上锁并返回;已被锁定则**阻塞**直到解锁 | 成功 0,失败非 0 |
+| `pthread_mutex_trylock` | `int pthread_mutex_trylock(pthread_mutex_t *mutex);` | 未锁定则上锁返回;已锁定则**不阻塞**,返回 `EBUSY` | 成功 0,失败非 0(`EBUSY`) |
+| `pthread_mutex_unlock` | `int pthread_mutex_unlock(pthread_mutex_t *mutex);` | 解锁 | 成功 0,失败非 0 |
+
+以下行为均属错误:
+
+- 对处于未锁定状态的互斥锁解锁;
+- 解锁由其它线程锁定的互斥锁。
+
+多个线程阻塞等待时,解锁后哪个线程先上锁无法判断。
+
+用互斥锁保护前面的 `g_count` 示例(默认 1000 万次也能得到正确结果):
+
+```c
+static pthread_mutex_t mutex;
+static int g_count = 0;
+
+static void *new_thread_start(void *arg)
+{
+    int loops = *((int *)arg);
+    int l_count, j;
+
+    for (j = 0; j < loops; j++) {
+        pthread_mutex_lock(&mutex);     /* 上锁 */
+
+        l_count = g_count;
+        l_count++;
+        g_count = l_count;
+
+        pthread_mutex_unlock(&mutex);   /* 解锁 */
+    }
+
+    return (void *)0;
+}
+
+/* main 中创建线程前初始化:pthread_mutex_init(&mutex, NULL); */
+```
+
+以非阻塞方式加锁(效果相同,但会忙等):
+
+```c
+while (pthread_mutex_trylock(&mutex));   /* 轮询直到拿到锁 */
+/* 访问共享资源 */
+pthread_mutex_unlock(&mutex);
+```
+
+互斥锁的加锁、临界区、解锁与唤醒竞争关系如下:
+
+```mermaid
+flowchart TD
+    accTitle: 互斥锁加锁解锁流程
+    accDescr: 线程访问共享资源前先加锁,锁空闲则进入临界区并在退出时解锁,锁被占用则阻塞等待,解锁后唤醒等待线程竞争。
+    A["访问共享资源前"] --> B["pthread_mutex_lock"]
+    B --> C{"锁是否空闲"}
+    C -->|是| D["加锁成功,进入临界区"]
+    C -->|否| E["阻塞休眠,等待锁释放"]
+    E --> C
+    D --> F["访问共享资源"]
+    F --> G["pthread_mutex_unlock"]
+    G --> H["唤醒其它等待线程竞争"]
+```
+
+#### 2.2.3 销毁
+
+| 项 | 内容 |
+| -- | ---- |
+| 原型 | `int pthread_mutex_destroy(pthread_mutex_t *mutex);` |
+| 返回值 | 成功返回 `0`;失败返回非 0 错误码 |
+
+- 不能销毁还没有解锁的互斥锁;
+- 没有初始化的互斥锁也不能销毁;
+- 销毁后不能再上锁/解锁,需重新 `pthread_mutex_init()` 后才能使用。
+
+#### 2.2.4 死锁
+
+- 同一线程对同一互斥锁加锁两次,会陷入死锁;
+- 更隐蔽的情况:一个线程需要同时访问两个由不同互斥锁保护的资源,多个线程以不同顺序加锁时可能死锁。
+
+```c
+/* 线程 A */                    /* 线程 B */
+pthread_mutex_lock(mutex1);     pthread_mutex_lock(mutex2);
+pthread_mutex_lock(mutex2);     pthread_mutex_lock(mutex1);
+```
+
+两个线程互相请求对方持有的锁,都无法向前运行。
+
+避免方法:
+
+1. **定义锁的层级关系**,所有线程总是按相同顺序对一组互斥锁加锁(如都先锁 `mutex1` 再锁 `mutex2`);
+2. 当顺序难确定时,先 `pthread_mutex_lock()` 锁定第一个锁,再用 `pthread_mutex_trylock()` 逐个尝试锁定其余锁;任一 `trylock` 失败(返回 `EBUSY`)就释放所有锁,过一段时间重试。这种方式效率较低,因为可能多次循环。
+
+#### 2.2.5 互斥锁属性与类型
+
+`pthread_mutexattr_t` 定义互斥锁属性。使用非默认属性时,参数 `attr` 必须指向已初始化的属性对象。
+
+| 函数 | 原型 |
+| ---- | ---- |
+| 初始化/销毁属性 | `int pthread_mutexattr_init(pthread_mutexattr_t *attr);`<br/>`int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);` |
+| 获取/设置类型 | `int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);`<br/>`int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);` |
+
+4 种类型:
+
+| 类型 | 行为 |
+| ---- | ---- |
+| `PTHREAD_MUTEX_NORMAL` | 标准类型,不做错误检查或死锁检测;同一线程重复加锁会死锁;对未锁定或由其它线程锁定的锁解锁结果不确定 |
+| `PTHREAD_MUTEX_ERRORCHECK` | 提供错误检查:同一线程重复加锁、解锁他人锁定的锁、解锁未锁定的锁都会返回错误;运行较慢,可作调试工具 |
+| `PTHREAD_MUTEX_RECURSIVE` | 递归锁,允许同一线程在解锁前多次加锁并维护加锁次数;解锁次数必须等于加锁次数才真正释放 |
+| `PTHREAD_MUTEX_DEFAULT` | 默认行为;用 `PTHREAD_MUTEX_INITIALIZER` 或 `attr` 为 `NULL` 初始化的锁属于此类,Linux 下行为与 NORMAL 相仿 |
+
+使用方式:
+
+```c
+pthread_mutex_t mutex;
+pthread_mutexattr_t attr;
+
+pthread_mutexattr_init(&attr);
+pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
+pthread_mutex_init(&mutex, &attr);
+
+/* ... 使用 ... */
+
+pthread_mutexattr_destroy(&attr);
+pthread_mutex_destroy(&mutex);
+```
+
+### 2.3 条件变量(Condition Variable)
+
+条件变量用于**自动阻塞线程**,直到某个特定事件发生或条件满足。它通常与互斥锁搭配使用,包含两个动作:
+
+- 一个线程等待某个条件满足而被阻塞;
+- 另一个线程在条件满足时发出"信号"。
+
+> 这里的"信号"指条件变量的通知,与第八章的 Linux 信号不是一回事。
+
+条件变量不保存状态信息,只是传递应用程序状态信息的一种通讯机制。若在没有任何线程等待时发送信号,该信号会不了了之。
+
+条件变量必须与互斥锁配合:因为条件检测通常需要访问共享资源,条件本身由互斥锁保护,线程改变条件状态前必须先锁住互斥锁。
+
+#### 2.3.1 初始化与销毁
+
+| 函数 | 原型 |
+| ---- | ---- |
+| 初始化 | `int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);` |
+| 销毁 | `int pthread_cond_destroy(pthread_cond_t *cond);` |
+
+也可用宏 `PTHREAD_COND_INITIALIZER` 初始化:
+
+```c
+pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
+```
+
+`attr` 为 `NULL` 表示默认属性(等价于宏方式)。成功返回 `0`,失败返回非 0 错误码。
+
+注意事项:
+
+- 使用前必须初始化;
+- 对已初始化的条件变量再次初始化可能导致未定义行为;
+- 对未初始化的条件变量销毁可能导致未定义行为;
+- 仅当没有任何线程等待它时,销毁才是最安全的;
+- 销毁后可以再次 `pthread_cond_init()` 重新初始化。
+
+#### 2.3.2 通知与等待
+
+| 函数 | 原型 | 说明 |
+| ---- | ---- | ---- |
+| `pthread_cond_signal` | `int pthread_cond_signal(pthread_cond_t *cond);` | 至少唤醒**一个**等待线程,更高效 |
+| `pthread_cond_broadcast` | `int pthread_cond_broadcast(pthread_cond_t *cond);` | 唤醒**所有**等待线程,总能得到正确结果 |
+| `pthread_cond_wait` | `int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);` | 阻塞等待,直到收到通知;成功 0,失败非 0 |
+
+要点:
+
+- `pthread_cond_signal()` 和 `pthread_cond_broadcast()` 调用成功返回 `0`,失败返回非 0;
+- `pthread_cond_wait()` 内部会对 `mutex` 操作:调用前线程必须已持有互斥锁;函数**原子地**把线程加入等待列表并解锁互斥锁;被唤醒返回时会**再次锁住互斥锁**;
+- 当 `pthread_cond_broadcast()` 唤醒所有线程时,互斥锁也只能被某一线程锁住,其它线程获取锁失败又会陷入阻塞。
+
+#### 2.3.3 判断条件为什么用 while 而不是 if
+
+调用 `pthread_cond_wait()` 返回后,并不能确定判断条件是真还是假,必须立即重新检查;若条件不满足,继续休眠等待。因此必须用 `while` 循环:
+
+```c
+while (0 >= g_avail)
+    pthread_cond_wait(&cond, &mutex);
+```
+
+原因:
+
+- 多个线程等待同一条件时,任何线程都可能率先醒来获取互斥锁,并可能修改共享变量从而改变条件状态;
+- 可能存在**虚假通知**。
+
+#### 2.3.4 条件变量的属性
+
+条件变量有两个属性:进程共享属性和时钟属性,各有 get/set 方法,本书不深入介绍。
+
+#### 2.3.5 条件变量示例
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <pthread.h>
+#include <unistd.h>
+#include <string.h>
+
+static pthread_mutex_t mutex;   /* 定义互斥锁 */
+static pthread_cond_t cond;     /* 定义条件变量 */
+static int g_avail = 0;         /* 全局共享资源 */
+
+/* 消费者线程 */
+static void *consumer_thread(void *arg)
+{
+    for ( ; ; ) {
+        pthread_mutex_lock(&mutex);         /* 上锁 */
+
+        while (0 >= g_avail)                /* 用 while 重新检查条件 */
+            pthread_cond_wait(&cond, &mutex);
+
+        while (0 < g_avail)
+            g_avail--;                      /* 消费 */
+
+        pthread_mutex_unlock(&mutex);       /* 解锁 */
+    }
+
+    return (void *)0;
+}
+
+/* 主线程(生产者) */
+int main(int argc, char *argv[])
+{
+    pthread_t tid;
+    int ret;
+
+    pthread_mutex_init(&mutex, NULL);
+    pthread_cond_init(&cond, NULL);
+
+    ret = pthread_create(&tid, NULL, consumer_thread, NULL);
+    if (ret) {
+        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
+        exit(-1);
+    }
+
+    for ( ; ; ) {
+        pthread_mutex_lock(&mutex);         /* 上锁 */
+        g_avail++;                          /* 生产 */
+        pthread_mutex_unlock(&mutex);       /* 解锁 */
+        pthread_cond_signal(&cond);         /* 通知消费者 */
+    }
+
+    exit(0);
+}
+```
+
+消费者判断 `g_avail <= 0` 时调用 `pthread_cond_wait()` 阻塞并解锁;生产者 `g_avail++` 后解锁并 `pthread_cond_signal()` 唤醒消费者,消费者被唤醒后自动重新加锁并消费。
+
+```mermaid
+sequenceDiagram
+    accTitle: 互斥锁与条件变量协作流程
+    accDescr: 消费者在条件不满足时等待,生产者生产后发信号唤醒,wait 原子解锁并在返回时重新加锁。
+    participant C as 消费者线程
+    participant M as 互斥锁
+    participant V as 条件变量
+    participant P as 生产者线程
+    C->>M: pthread_mutex_lock
+    C->>V: g_avail<=0 时 pthread_cond_wait
+    Note over C,M: wait 原子地解锁并阻塞
+    P->>M: pthread_mutex_lock
+    P->>P: g_avail++ 生产
+    P->>M: pthread_mutex_unlock
+    P->>V: pthread_cond_signal
+    V-->>C: 唤醒,wait 重新加锁返回
+    C->>C: 重新检查条件并消费
+    C->>M: pthread_mutex_unlock
+```
+
+#### 2.3.6 超时等待:pthread_cond_timedwait()
+
+`pthread_cond_wait()` 会无限期等待。若希望等待一段有上限的时间,可使用 `pthread_cond_timedwait()`,超时后返回错误码 `ETIMEDOUT`。
+
+> ⚠️ **来源说明**:`pthread_cond_timedwait()` 在《I.MX6U嵌入式Linux C应用编程指南》中仅作为"取消点函数"被列出,未展开讲解其原型与用法,以下为 POSIX 标准接口的扩展补充。
+
+| 项 | 内容 |
+| -- | ---- |
+| 原型 | `int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);` |
+| 参数 `abstime` | **绝对时间**(从 Epoch 起的秒 + 纳秒),不是相对时间 |
+| 返回值 | 成功 0;超时返回 `ETIMEDOUT`;其它错误返回相应错误码 |
+
+```c
+#include <time.h>
+#include <errno.h>
+
+struct timespec ts;
+clock_gettime(CLOCK_REALTIME, &ts);   /* 取当前时间 */
+ts.tv_sec += 3;                        /* 绝对超时时间:当前 +3 秒 */
+
+pthread_mutex_lock(&mutex);
+while (0 >= g_avail) {
+    if (ETIMEDOUT == pthread_cond_timedwait(&cond, &mutex, &ts)) {
+        /* 超时,做相应处理 */
+        break;
+    }
+}
+pthread_mutex_unlock(&mutex);
+```
+
+注意 `abstime` 是绝对时间,必须先取当前时间再加上超时时长。
+
+### 2.4 自旋锁(Spinlock)
+
+自旋锁与互斥锁相似,本质也是一把锁。区别在于:
+
+- 互斥锁获取不到时线程**陷入阻塞(休眠)**,直到获取到锁被唤醒;
+- 自旋锁获取不到时在**原地自旋**(循环查看锁的持有者是否释放),一直占用 CPU。
+
+实现上,互斥锁基于自旋锁实现,所以自旋锁更底层。自旋锁的不足是:未获得锁时一直运行、占着 CPU,若不能很快获得锁会降低 CPU 效率。
+
+试图对同一自旋锁加锁两次**必然死锁**;而同一互斥锁加锁两次不一定死锁(如 ERRORCHECK 类型会返回错误)。
+
+自旋锁适用于**需要保护的临界区执行时间很短**的场景:持锁线程很快释放锁,自旋等待的线程只需等待很短时间,效率高。
+
+| 对比项 | 互斥锁 | 自旋锁 |
+| ------ | ------ | ------ |
+| 实现层次 | 基于自旋锁实现,更上层 | 更底层 |
+| 获取失败行为 | 阻塞休眠,唤醒后竞争 | 原地自旋,忙等 |
+| 开销 | 休眠/唤醒开销大 | 忙等占用 CPU,但切换开销小 |
+| 适用场景 | 临界区可能较长 | 临界区极短 |
+| 中断上下文 | 不能用于中断服务函数 | 可用于中断服务函数,内核中会自动禁止抢占 |
+
+#### 2.4.1 初始化与销毁
+
+| 函数 | 原型 |
+| ---- | ---- |
+| 初始化 | `int pthread_spin_init(pthread_spinlock_t *lock, int pshared);` |
+| 销毁 | `int pthread_spin_destroy(pthread_spinlock_t *lock);` |
+
+`pshared` 取值:
+
+- `PTHREAD_PROCESS_SHARED`:共享自旋锁,可在多个进程的线程间共享;
+- `PTHREAD_PROCESS_PRIVATE`:私有自旋锁,只有本进程内的线程可用。
+
+成功返回 `0`,失败返回非 0 错误码。
+
+#### 2.4.2 加锁与解锁
+
+| 函数 | 原型 | 说明 |
+| ---- | ---- | ---- |
+| `pthread_spin_lock` | `int pthread_spin_lock(pthread_spinlock_t *lock);` | 未锁定则上锁;已锁定则自旋等待 |
+| `pthread_spin_trylock` | `int pthread_spin_trylock(pthread_spinlock_t *lock);` | 获取不到立即返回 `EBUSY`,不自旋 |
+| `pthread_spin_unlock` | `int pthread_spin_unlock(pthread_spinlock_t *lock);` | 解锁 |
+
+成功返回 `0`,失败返回非 0 错误码。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <pthread.h>
+#include <unistd.h>
+#include <string.h>
+
+static pthread_spinlock_t spin;     /* 定义自旋锁 */
+static int g_count = 0;
+
+static void *new_thread_start(void *arg)
+{
+    int loops = *((int *)arg);
+    int l_count, j;
+
+    for (j = 0; j < loops; j++) {
+        pthread_spin_lock(&spin);       /* 自旋锁上锁 */
+
+        l_count = g_count;
+        l_count++;
+        g_count = l_count;
+
+        pthread_spin_unlock(&spin);     /* 自旋锁解锁 */
+    }
+
+    return (void *)0;
+}
+
+static int loops;
+int main(int argc, char *argv[])
+{
+    pthread_t tid1, tid2;
+    int ret;
+
+    if (2 > argc)
+        loops = 10000000;
+    else
+        loops = atoi(argv[1]);
+
+    pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);
+
+    ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
+    if (ret) {
+        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
+        exit(-1);
+    }
+
+    ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
+    if (ret) {
+        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
+        exit(-1);
+    }
+
+    pthread_join(tid1, NULL);
+    pthread_join(tid2, NULL);
+
+    printf("g_count = %d\n", g_count);
+    pthread_spin_destroy(&spin);
+    exit(0);
+}
+```
+
+对比可见,替换为自旋锁后程序运行耗时明显变短,但要注意使用场景。
+
+### 2.5 读写锁(Read-Write Lock)
+
+互斥锁和自旋锁只有"加锁/不加锁"两种状态,一次只有一个线程能加锁。读写锁有 3 种状态:
+
+- 读模式下的加锁状态(读加锁);
+- 写模式下的加锁状态(写加锁);
+- 不加锁状态。
+
+**一次只有一个线程可以占有写模式的读写锁,但可以有多个线程同时占有读模式的读写锁**,因此读写锁比互斥锁并行性更高。读写锁也称共享互斥锁:读模式锁住称为共享模式锁住,写模式锁住称为互斥模式锁住。
+
+两条规则:
+
+- 读写锁处于**写加锁**状态时,解锁前所有试图加锁的线程(无论读模式还是写模式)都被阻塞;
+- 读写锁处于**读加锁**状态时,所有以读模式加锁的线程都能成功;任何以写模式加锁的线程都被阻塞,直到所有读模式锁被释放。
+
+因此读写锁非常适合**读的次数远大于写的次数**的场景。
+
+#### 2.5.1 初始化与销毁
+
+| 函数 | 原型 |
+| ---- | ---- |
+| 初始化 | `int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);` |
+| 销毁 | `int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);` |
+
+也可用宏 `PTHREAD_RWLOCK_INITIALIZER`:
+
+```c
+pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
+```
+
+成功返回 `0`,失败返回非 0 错误码。
+
+#### 2.5.2 上锁与解锁
+
+| 函数 | 原型 | 说明 |
+| ---- | ---- | ---- |
+| `pthread_rwlock_rdlock` | `int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);` | 以读模式加锁 |
+| `pthread_rwlock_wrlock` | `int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);` | 以写模式加锁 |
+| `pthread_rwlock_unlock` | `int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);` | 解锁(读/写模式通用) |
+| `pthread_rwlock_tryrdlock` | `int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);` | 非阻塞读加锁,失败返回 `EBUSY` |
+| `pthread_rwlock_trywrlock` | `int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);` | 非阻塞写加锁,失败返回 `EBUSY` |
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <pthread.h>
+#include <unistd.h>
+#include <string.h>
+
+static pthread_rwlock_t rwlock;     /* 定义读写锁 */
+static int g_count = 0;
+
+static void *read_thread(void *arg)
+{
+    int number = *((int *)arg);
+    int j;
+
+    for (j = 0; j < 10; j++) {
+        pthread_rwlock_rdlock(&rwlock);             /* 读模式获取锁 */
+        printf("读线程<%d>, g_count=%d\n", number + 1, g_count);
+        pthread_rwlock_unlock(&rwlock);
+        sleep(1);
+    }
+
+    return (void *)0;
+}
+
+static void *write_thread(void *arg)
+{
+    int number = *((int *)arg);
+    int j;
+
+    for (j = 0; j < 10; j++) {
+        pthread_rwlock_wrlock(&rwlock);             /* 写模式获取锁 */
+        printf("写线程<%d>, g_count=%d\n", number + 1, g_count += 20);
+        pthread_rwlock_unlock(&rwlock);
+        sleep(1);
+    }
+
+    return (void *)0;
+}
+
+static int nums[5] = {0, 1, 2, 3, 4};
+int main(int argc, char *argv[])
+{
+    pthread_t tid[10];
+    int j;
+
+    pthread_rwlock_init(&rwlock, NULL);
+
+    for (j = 0; j < 5; j++)
+        pthread_create(&tid[j], NULL, read_thread, &nums[j]);
+
+    for (j = 0; j < 5; j++)
+        pthread_create(&tid[j + 5], NULL, write_thread, &nums[j]);
+
+    for (j = 0; j < 10; j++)
+        pthread_join(tid[j], NULL);
+
+    pthread_rwlock_destroy(&rwlock);
+    exit(0);
+}
+```
+
+#### 2.5.3 读写锁属性
+
+读写锁只有一个属性——**进程共享属性**,与互斥锁、自旋锁相同。
+
+| 函数 | 原型 |
+| ---- | ---- |
+| 初始化/销毁属性 | `int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);`<br/>`int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);` |
+| 获取/设置共享属性 | `int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);`<br/>`int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);` |
+
+`pshared` 取值:
+
+- `PTHREAD_PROCESS_SHARED`:共享读写锁,可在多个进程的线程间共享;
+- `PTHREAD_PROCESS_PRIVATE`:私有读写锁,只有本进程内线程可用(默认值)。
+
+```c
+pthread_rwlock_t rwlock;
+pthread_rwlockattr_t attr;
+
+pthread_rwlockattr_init(&attr);
+pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_PRIVATE);
+pthread_rwlock_init(&rwlock, &attr);
+
+/* ... 使用 ... */
+
+pthread_rwlock_destroy(&rwlock);
+pthread_rwlockattr_destroy(&attr);
+```
+
+### 2.6 各同步机制对比
+
+| 机制 | 类型 | 状态数 | 获取失败行为 | 适用场景 |
+| ---- | ---- | ------ | ------------ | -------- |
+| 互斥锁 | `pthread_mutex_t` | 2(锁/未锁) | 阻塞休眠 | 一般临界区保护,通用 |
+| 条件变量 | `pthread_cond_t` | 无状态(配合互斥锁) | 阻塞等待通知 | 等待某个条件成立(生产者-消费者) |
+| 自旋锁 | `pthread_spinlock_t` | 2(锁/未锁) | 原地自旋忙等 | 临界区极短;可用于中断上下文 |
+| 读写锁 | `pthread_rwlock_t` | 3(读锁/写锁/未锁) | 读-读并发,读-写/写-写阻塞 | 读多写少 |
+
+选择建议:实际开发中**用得最多的是互斥锁和条件变量**;临界区极短且不希望休眠时用自旋锁;读多写少时用读写锁。应根据场景选择,方能事半功倍。
+
+### 2.7 生产者-消费者模型完整示例
+
+下面把互斥锁与条件变量组合成一个完整的、可直接编译运行的生产者-消费者程序,使用一个固定容量的环形缓冲区,支持多个生产者与多个消费者。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <pthread.h>
+#include <string.h>
+#include <unistd.h>
+
+#define BUFFER_SIZE     8       /* 缓冲区容量 */
+#define PRODUCER_NUM    2       /* 生产者数量 */
+#define CONSUMER_NUM    3       /* 消费者数量 */
+#define ITEM_TOTAL      20      /* 生产者各自生产的总数 */
+
+static pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER; /* 保护缓冲区 */
+static pthread_cond_t  g_not_full  = PTHREAD_COND_INITIALIZER; /* 缓冲区非满 */
+static pthread_cond_t  g_not_empty = PTHREAD_COND_INITIALIZER; /* 缓冲区非空 */
+
+static int g_buffer[BUFFER_SIZE];   /* 环形缓冲区 */
+static int g_in = 0;                /* 生产写入位置 */
+static int g_out = 0;               /* 消费读出位置 */
+static int g_count = 0;             /* 当前元素个数 */
+
+static int g_stop = 0;              /* 生产结束标志 */
+
+/* 向缓冲区放入元素 */
+static void buffer_put(int value)
+{
+    pthread_mutex_lock(&g_mutex);
+
+    while (g_count == BUFFER_SIZE)                  /* 缓冲区满则等待 */
+        pthread_cond_wait(&g_not_full, &g_mutex);
+
+    g_buffer[g_in] = value;
+    g_in = (g_in + 1) % BUFFER_SIZE;
+    g_count++;
+
+    printf("生产者: 放入 %d, 当前元素 %d\n", value, g_count);
+
+    pthread_cond_signal(&g_not_empty);              /* 通知消费者非空 */
+    pthread_mutex_unlock(&g_mutex);
+}
+
+/* 从缓冲区取出元素,成功返回 0,无数据且已结束返回 -1 */
+static int buffer_get(int *value)
+{
+    int ret = 0;
+
+    pthread_mutex_lock(&g_mutex);
+
+    while (0 == g_count && !g_stop)                 /* 空且未结束则等待 */
+        pthread_cond_wait(&g_not_empty, &g_mutex);
+
+    if (0 == g_count && g_stop) {                   /* 已结束且无数据 */
+        ret = -1;
+    } else {
+        *value = g_buffer[g_out];
+        g_out = (g_out + 1) % BUFFER_SIZE;
+        g_count--;
+
+        printf("消费者: 取出 %d, 当前元素 %d\n", *value, g_count);
+
+        pthread_cond_signal(&g_not_full);           /* 通知生产者非满 */
+    }
+
+    pthread_mutex_unlock(&g_mutex);
+    return ret;
+}
+
+/* 生产者线程 */
+static void *producer_thread(void *arg)
+{
+    int id = *((int *)arg);
+    int i;
+
+    for (i = 0; i < ITEM_TOTAL; i++) {
+        buffer_put(id * 100 + i);
+        usleep(1000);                               /* 模拟生产耗时 */
+    }
+
+    pthread_exit(NULL);
+}
+
+/* 消费者线程 */
+static void *consumer_thread(void *arg)
+{
+    int value;
+
+    (void)arg;
+    for ( ; ; ) {
+        if (0 != buffer_get(&value))                /* 结束且无数据则退出 */
+            break;
+        usleep(2000);                               /* 模拟消费耗时 */
+    }
+
+    pthread_exit(NULL);
+}
+
+int main(void)
+{
+    pthread_t pid[PRODUCER_NUM];
+    pthread_t cid[CONSUMER_NUM];
+    int pnum[PRODUCER_NUM];
+    int i;
+
+    /* 创建生产者线程 */
+    for (i = 0; i < PRODUCER_NUM; i++) {
+        pnum[i] = i + 1;
+        if (pthread_create(&pid[i], NULL, producer_thread, &pnum[i])) {
+            fprintf(stderr, "create producer error\n");
+            exit(-1);
+        }
+    }
+
+    /* 创建消费者线程 */
+    for (i = 0; i < CONSUMER_NUM; i++) {
+        if (pthread_create(&cid[i], NULL, consumer_thread, NULL)) {
+            fprintf(stderr, "create consumer error\n");
+            exit(-1);
+        }
+    }
+
+    /* 等待生产者结束 */
+    for (i = 0; i < PRODUCER_NUM; i++)
+        pthread_join(pid[i], NULL);
+
+    /* 通知消费者生产已结束 */
+    pthread_mutex_lock(&g_mutex);
+    g_stop = 1;
+    pthread_cond_broadcast(&g_not_empty);           /* 唤醒所有消费者 */
+    pthread_mutex_unlock(&g_mutex);
+
+    /* 等待消费者结束 */
+    for (i = 0; i < CONSUMER_NUM; i++)
+        pthread_join(cid[i], NULL);
+
+    pthread_mutex_destroy(&g_mutex);
+    pthread_cond_destroy(&g_not_full);
+    pthread_cond_destroy(&g_not_empty);
+
+    printf("全部线程结束\n");
+    return 0;
+}
+```
+
+#### 逐段解释
+
+| 代码 | 说明 |
+| ---- | ---- |
+| `g_mutex` / `g_not_full` / `g_not_empty` | 一个互斥锁保护缓冲区状态,两个条件变量分别表示"非满"和"非空" |
+| `g_in` / `g_out` / `g_count` | 环形缓冲区的写指针、读指针、元素个数,全部在互斥锁保护下修改 |
+| `buffer_put()` | 进入时加锁;用 `while` 检查缓冲区是否满,满则 `pthread_cond_wait()` 释放锁并休眠;放入后 `signal` 非空条件 |
+| `buffer_get()` | 用 `while` 检查空且未结束;`g_stop` 用于优雅退出——生产结束后消费者把剩余元素消费完再退出 |
+| `pthread_cond_signal` vs `broadcast` | 每次放入/取出用 `signal` 唤醒一个等待者;结束时用 `broadcast` 唤醒所有消费者 |
+| `g_stop` | 共享退出标志,同样在锁保护下读写,避免遗漏唤醒导致消费者永久阻塞 |
+| 主线程 `join` 顺序 | 先等生产者全部结束,再设置 `g_stop` 并广播,最后等消费者退出,保证不丢数据 |
+
+编译运行:
+
+```bash
+gcc -Wall -pthread -o pc pc.c
+./pc
+```
+
+---
+
+## 3. API 速查表
+
+### 3.1 线程生命周期
+
+| 函数 | 一句话作用 |
+| ---- | ---------- |
+| `pthread_self()` | 获取当前线程 ID |
+| `pthread_equal()` | 比较两个线程 ID |
+| `pthread_create()` | 创建线程 |
+| `pthread_exit()` | 终止调用线程,可带退出码 |
+| `pthread_join()` | 阻塞等待线程终止并回收资源 |
+| `pthread_detach()` | 分离线程,终止后自动回收 |
+| `pthread_cancel()` | 向线程发送取消请求 |
+| `pthread_testcancel()` | 手动产生取消点 |
+| `pthread_setcancelstate()` | 设置可取消/不可取消 |
+| `pthread_setcanceltype()` | 设置延迟/异步取消 |
+| `pthread_cleanup_push()/pop()` | 注册/执行线程清理函数 |
+
+### 3.2 线程属性与数据
+
+| 函数 | 一句话作用 |
+| ---- | ---------- |
+| `pthread_attr_init()/destroy()` | 初始化/销毁线程属性对象 |
+| `pthread_attr_setstacksize()` | 设置线程栈大小 |
+| `pthread_attr_setdetachstate()` | 设置创建时是否分离 |
+| `pthread_once()` | 保证初始化代码只执行一次 |
+| `pthread_key_create()/delete()` | 创建/删除线程特有数据键 |
+| `pthread_setspecific()/getspecific()` | 设置/获取线程私有数据 |
+
+### 3.3 同步对象
+
+| 对象 | 初始化 | 加锁/等待 | 解锁/通知 | 销毁 |
+| ---- | ------ | --------- | --------- | ---- |
+| 互斥锁 | `pthread_mutex_init` / `PTHREAD_MUTEX_INITIALIZER` | `pthread_mutex_lock` / `trylock` | `pthread_mutex_unlock` | `pthread_mutex_destroy` |
+| 条件变量 | `pthread_cond_init` / `PTHREAD_COND_INITIALIZER` | `pthread_cond_wait` / `timedwait` | `pthread_cond_signal` / `broadcast` | `pthread_cond_destroy` |
+| 自旋锁 | `pthread_spin_init` | `pthread_spin_lock` / `trylock` | `pthread_spin_unlock` | `pthread_spin_destroy` |
+| 读写锁 | `pthread_rwlock_init` / `PTHREAD_RWLOCK_INITIALIZER` | `pthread_rwlock_rdlock` / `wrlock` | `pthread_rwlock_unlock` | `pthread_rwlock_destroy` |
+
+---
+
+## 4. 实验步骤与调试方法
+
+### 4.1 实验步骤(以互斥锁修正竞态为例)
+
+1. 在 Ubuntu 下建工作目录:
+
+   ```bash
+   mkdir -p ~/vscode_ws/12_thread_sync
+   cd ~/vscode_ws/12_thread_sync
+   ```
+
+2. 创建 `race.c`,粘贴 2.1 节的无锁版本,编译运行:
+
+   ```bash
+   gcc -Wall -pthread -o race race.c
+   ./race 10000000
+   ```
+
+   观察 `g_count` 通常小于 2000 万。
+
+3. 创建 `mutex.c`,粘贴 2.2.2 的加锁版本,用同样的参数运行,观察结果稳定为 2000 万。
+
+4. 把互斥锁分别替换为自旋锁、读写锁,比较运行耗时(可用 `time ./app`)。
+
+5. 编译生产者-消费者程序并运行,观察"放入/取出"配对是否正常、程序能否正常退出。
+
+### 4.2 调试方法
+
+| 现象 | 可能原因 | 排查手段 |
+| ---- | -------- | -------- |
+| 链接报 `undefined reference to 'pthread_create'` | 未链接 pthread 库 | 编译加 `-lpthread` 或 `-pthread` |
+| `g_count` 结果偏小 | 共享变量未加锁,存在竞态 | 用互斥锁/自旋锁保护读写 |
+| 程序卡死不退出 | 死锁;或条件变量等待无人唤醒 | 检查加锁顺序;确认有 `signal/broadcast`;用 `while` 重查条件 |
+| `pthread_join` 返回 `Invalid argument` | 目标线程已分离 | 分离的线程不能再 `join` |
+| 取消请求不生效 | 线程处于不可取消状态,或循环内无取消点 | 检查 `pthread_setcancelstate()`;加 `pthread_testcancel()` |
+| 清理函数不执行 | 线程用 `return` 退出,或 `pop` 参数为 0 | 改用 `pthread_exit()`,或 `pthread_cleanup_pop(1)` |
+| `pthread_cancel` 返回非 0 | pthread 函数失败返回错误码且不设置 errno | 用 `strerror(ret)` 翻译错误码 |
+| 多线程程序崩溃 | 使用了线程不安全函数或共享数据未保护 | 查 man 的 ATTRIBUTES;确认 `MT-Safe` |
+
+调试常用命令:
+
+```bash
+gcc -Wall -Wextra -pthread -g -o app app.c   # 打开警告与调试信息
+man 7 pthreads                                # 取消点、线程不安全函数列表
+man 7 signal                                  # 异步信号安全函数列表
+gdb ./app                                     # 调试
+# (gdb) info threads / thread apply all bt    # 查看所有线程与调用栈
+```
+
+死锁与数据竞争也可借助 `helgrind`、`ThreadSanitizer`(`gcc -fsanitize=thread`)等工具定位。
+
+---
+
+## 5. 跨平台对比:IMX6ULL vs STM32 vs RK3568
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+| 维度 | I.MX6ULL(本教程) | STM32(裸机 / FreeRTOS) | RK3568 |
+| ---- | ------------------ | ------------------------ | ------ |
+| 内核 | Cortex-A7(单核) | Cortex-M3/M4/M7 | Cortex-A55(多核) |
+| 操作系统 | 完整 Linux | 裸机或 FreeRTOS 等 RTOS | 完整 Linux |
+| 线程模型 | POSIX pthread(用户态线程) | 裸机无线程;RTOS 为任务(Task) | POSIX pthread,同 I.MX6ULL |
+| 创建接口 | `pthread_create()` | `xTaskCreate()`(FreeRTOS) | `pthread_create()` |
+| 同步机制 | mutex / cond / spin / rwlock(POSIX) | 信号量、互斥量、事件组(RTOS API) | 与 I.MX6ULL 相同 |
+| 调度 | 内核 CFS 抢占式调度 | 优先级抢占式调度(RTOS) | 内核 CFS 调度 |
+| 线程栈 | 每线程独立栈,可配置大小 | 每任务独立栈,创建时指定数组 | 每线程独立栈 |
+| 内存保护 | MMU,用户/内核态隔离 | 无 MMU(M 核),无进程概念 | MMU,用户/内核态隔离 |
+| 工具链 | `arm-linux-gnueabihf-gcc -pthread` | `arm-none-eabi-gcc` + RTOS 源码 | `aarch64-linux-gnu-gcc -pthread` |
+| 迁移要点 | pthread 接口 Linux 间通用 | 需从 RTOS API 改写为 pthread | 与 I.MX6ULL 代码几乎一致,注意 64 位与库版本 |
+
+结论:I.MX6ULL 与 RK3568 同属 Linux 用户态编程,本篇的 pthread API **可直接复用**;STM32 裸机没有线程概念,使用 FreeRTOS 时则是另一套任务 API(`xTaskCreate`、`xSemaphoreTake` 等),概念相似但接口完全不同,切忌混用。
+
+---
+
+## 6. 面试精选
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+### Q1:线程和进程有什么区别?什么时候用多线程、什么时候用多进程?
+
+**答**:
+
+- **调度单位不同**:线程是参与系统调度的最小单位,进程只是资源分配的容器;
+- **地址空间不同**:同一进程内多个线程共享进程地址空间,进程之间相互独立、隔离;
+- **资源开销不同**:线程创建和切换开销远小于进程;线程共享进程的文件描述符、代码段、数据段、堆等,额外只有线程栈、寄存器、TLS 等;
+- **通信方式不同**:线程间可直接读写全局变量(需同步),进程间需要 IPC;
+- **健壮性不同**:一个线程崩溃可能拖垮整个进程;进程崩溃一般不影响其它进程;
+- **私有部分**:线程有各自的调用栈、寄存器环境、线程本地存储。
+
+选择:需要频繁通信、频繁切换、要求高并发且共享数据多时用**多线程**(如数据处理、GUI);需要高隔离性、高健壮性、避免共享状态时用**多进程**(如网络服务器为每个请求 fork 子进程)。
+
+### Q2:pthread_create 的 start 函数为什么只能接收一个 void * 参数?如何传多个参数?
+
+**答**:`pthread_create()` 的 `start_routine` 签名固定为 `void *(*)(void *)`,这是 POSIX 的规定,所以只能传递一个 `void *`。要传多个参数时,把这些参数封装到一个**结构体**中,把结构体地址作为 `arg` 传入。
+
+关键约束:`arg` 指向的对象在线程整个生命周期内必须一直有效,因此应指向**全局变量或堆变量**,不能指向会失效的栈变量。例如:
+
+```c
+struct thread_arg {
+    int id;
+    char name[32];
+    int result;
+};
+
+static void *worker(void *arg)
+{
+    struct thread_arg *p = (struct thread_arg *)arg;
+    p->result = p->id * 2;
+    return NULL;
+}
+```
+
+返回值也是 `void *`,可返回结构体指针(同样不应指向线程栈),由 `pthread_join()` 取出。
+
+### Q3:pthread_join、pthread_detach、pthread_cancel 分别是什么?有什么区别?
+
+**答**:
+
+- **`pthread_join()`**:阻塞等待目标线程终止并回收资源,可获取退出码。未分离线程必须 join,否则会变成僵尸线程。线程之间关系对等,任意线程都能 join 另一个线程;**不能非阻塞调用**。
+- **`pthread_detach()`**:把线程设为分离状态,线程终止时系统自动回收资源,之后不能再 `join`,且不可逆。适合不关心返回值的"即发即忘"线程。
+- **`pthread_cancel()`**:向线程发送取消请求,**立即返回,不等待线程退出**。默认行为如同 `pthread_exit(PTHREAD_CANCELED)`。线程可设置 `PTHREAD_CANCEL_DISABLE` 不被取消,取消类型分 `DEFERRED`(到取消点才响应)和 `ASYNCHRONOUS`(任意时刻)。
+
+区别总结:`join` 是"等待并回收",`detach` 是"让系统自动回收",`cancel` 是"请求对方退出"。
+
+### Q4:互斥锁、自旋锁、读写锁、条件变量各适合什么场景?
+
+**答**:
+
+| 机制 | 特点 | 适用场景 |
+| ---- | ---- | -------- |
+| 互斥锁 | 获取失败阻塞休眠,通用 | 一般临界区保护,使用最广 |
+| 条件变量 | 无状态,配合互斥锁,等待条件成立 | 生产者-消费者、事件通知 |
+| 自旋锁 | 获取失败原地自旋,不休眠,临界区必须极短 | 临界区极短;可用于中断上下文 |
+| 读写锁 | 读-读可并发,读-写/写-写互斥 | 读远多于写的共享数据 |
+
+补充:互斥锁基于自旋锁实现;自旋锁占用 CPU,长时间自旋会降低效率;读写锁提高了读场景的并行性,但实现更复杂、写者可能饥饿。实际工程中用得最多的是互斥锁 + 条件变量组合。
+
+### Q5:什么是线程安全?什么是可重入?两者是什么关系?
+
+**答**:
+
+- **线程安全函数**:被多个线程同时调用时总能产生正确结果。可通过加锁等同步技术实现。
+- **可重入函数**:被同一进程的多个执行流(包括信号处理函数执行流)同时调用时总能产生正确结果。要求更严格,通常要求只使用局部变量、参数和返回值为值类型、调用的其它函数也可重入。
+- **关系**:可重入函数**一定是**线程安全函数;线程安全函数**不一定是**可重入函数(因为它可能修改外部全局变量,只是用锁保护了)。可重入是线程安全函数的真子集。
+- 再严格一层是**异步信号安全函数**(可在信号处理函数中安全调用),可重入函数一定是异步信号安全函数。
+- 实践:用 `man` 查 ATTRIBUTES,`MT-Safe` 表示线程安全、`MT-Unsafe` 表示线程不安全;有 `_r` 后缀的通常可重入。一个安全的信号处理函数只能调用异步信号安全函数(如 `write`),不能调用 `printf`、`malloc` 等。
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第十一、十二章

+ 709 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/02-进程线程与IPC/面试-进程线程与IPC.md

@@ -0,0 +1,709 @@
+---
+title: 面试-进程线程与IPC
+tags: [嵌入式Linux, 面试, 信号, 进程, 进程间通信, IPC, 多线程, 线程同步, pthread, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第八章 信号、第九章 进程、第十章 进程间通信、第十一章 线程、第十二章 线程同步"
+---
+
+# 面试-进程线程与IPC
+
+> 💡 **关联知识**:[[02-进程线程与IPC/01-信号机制]]、[[02-进程线程与IPC/02-进程管理]]、[[02-进程线程与IPC/03-进程间通信]]、[[02-进程线程与IPC/04-线程与线程同步]]
+
+本篇把「信号、进程、进程间通信、线程与同步」四大主题的高频面试题集中整理。每题按「答案要点 → 详细解答(含代码与对比表)→ 2 条追问」组织,可直接用于面试前突击复习。
+
+> ⚠️ **来源说明**:本篇以《I.MX6U嵌入式Linux C应用编程指南》第八~十二章为事实来源进行面试化整理,其中少量对比、术语(如信号量的 P/V 操作、UNIX domain socket 等)属于通用操作系统知识扩展。
+
+---
+
+## 一、信号
+
+### 1. 什么是信号?为什么说它是软件中断?
+
+**答案要点**:信号是事件发生时对进程的通知机制,是软件层次对中断机制的模拟;异步、无法预测到达时间;本质是 int 型编号。
+
+**详细解答**:信号与硬件中断相似——都能打断程序当前执行的正常流程,区别是它在软件层实现。因为产生信号的事件对进程而言随机出现、进程无法预知准确时间,所以信号提供了一种处理**异步事件**的方法。信号本质上是 int 类型的数字编号(类似中断号),内核为每个信号定义唯一编号,从 1 开始,程序中使用符号名(宏 `SIGxxx`)而非硬编码编号。
+
+信号的产生来源:
+
+| 来源 | 例子 |
+| ---- | ---- |
+| 硬件异常 | 除数为 0(`SIGFPE`)、非法指令(`SIGILL`)、无效内存引用(`SIGSEGV`) |
+| 终端特殊字符 | `Ctrl+C` → `SIGINT`,`Ctrl+Z` → `SIGTSTP`,`Ctrl+\` → `SIGQUIT` |
+| 进程调用 `kill()` | 向另一进程或进程组发送任意信号 |
+| kill 命令 | `kill -9 xxx`,内部即 `kill()` 系统调用 |
+| 软件事件 | 定时器超时(`SIGALRM`)、子进程退出(`SIGCHLD`) |
+
+**追问**:① 信号和条件变量里的"signal"是一回事吗?(不是,后者是线程同步通知,与 Linux 信号无关)② 进程能否给自己发信号?(可以,`raise()` 或 `kill(getpid(), sig)`)
+
+### 2. signal() 与 sigaction() 有什么区别?推荐用哪个?
+
+**答案要点**:都能设置信号处理方式;`signal()` 简单但移植性差、语义不完整;`sigaction()` 更复杂但更灵活、可移植,推荐使用。
+
+**详细解答**:
+
+| 对比项 | signal() | sigaction() |
+| ------ | -------- | ----------- |
+| 复杂度 | 简单 | 较复杂 |
+| 移植性 | 差(不同系统语义可能不同) | 好(POSIX 标准) |
+| 获取旧处理方式 | 返回值返回 | 通过 `oldact` 参数 |
+| 精细控制 | 无 | 可设置 `sa_mask`、`sa_flags` |
+| 处理函数 | `handler(int)` | `sa_handler` 或 `sa_sigaction` |
+
+`sigaction()` 关键成员与标志:
+
+- `sa_handler` / `sa_sigaction`:互斥,前者用于标准信号,后者配合 `SA_SIGINFO` 可获取 `siginfo_t`(含 `si_pid`、`si_value` 等);
+- `sa_mask`:执行处理函数期间额外阻塞的信号集,可避免竞态;
+- `sa_flags`:`SA_RESTART`(被中断的系统调用自动重启)、`SA_NODEFER`(不自动阻塞自身)、`SA_SIGINFO`、`SA_RESETHAND`(执行完恢复默认)、`SA_NOCLDWAIT`(子进程退出不产生僵尸)等。
+
+```c
+struct sigaction sig = {0};
+sig.sa_handler = sig_handler;
+sig.sa_flags = 0;
+sigaction(SIGINT, &sig, NULL);
+```
+
+**追问**:① `sigaction()` 能否设置 `SIGKILL`/`SIGSTOP`?(不能,这两个信号不可捕获、阻塞或忽略)② 信号处理函数为什么越简单越好?(可能在任何时间点打断主程序,复杂逻辑会增大竞态风险)
+
+### 3. Linux 信号如何分类?
+
+**答案要点**:按可靠性分可靠信号与不可靠信号;按实时性分实时信号与非实时信号。编号 1~31 为不可靠/非实时(标准信号),34~64 为可靠/实时信号。
+
+**详细解答**:
+
+- **不可靠信号**:信号值小于 `SIGRTMIN`(34)。早期 UNIX 的不可靠性指"可能错误响应、可能丢失";Linux 改进后不必重新注册处理函数,主要问题是**信号可能丢失**(同一信号在处理期间再次到达只记一次)。
+- **可靠信号**:`SIGRTMIN ~ SIGRTMAX`(34~64)。支持**排队**、不会丢失;配套发送函数 `sigqueue()`、绑定函数 `sigaction()`。
+- **实时信号与非实时信号**:实时信号都支持排队、都是可靠信号;非实时信号都不支持排队、都是不可靠信号。非实时信号又称标准信号。
+
+早期 UNIX 只定义了 31 种信号,Linux 3.x 支持 64 种。前 31 种有预定义用途和默认动作,后 32 种为可靠信号。可用 `kill -l` 查看。
+
+**追问**:① 标准信号和可靠信号在"多次发送"上的区别?(可靠信号排队每次传递,标准信号只传递一次)② `SIGUSR1`/`SIGUSR2` 属于哪类?有什么用途?(不可靠信号;供程序员自定义通知事件或进程同步)
+
+### 4. 常见信号及其默认行为有哪些?
+
+**答案要点**:重点掌握 `SIGINT`、`SIGKILL`、`SIGSTOP`、`SIGTERM`、`SIGCHLD`、`SIGALRM`、`SIGPIPE`、`SIGSEGV`。
+
+**详细解答**:
+
+| 信号 | 编号 | 描述 | 默认操作 |
+| ---- | ---- | ---- | -------- |
+| `SIGINT` | 2 | 终端中断符 Ctrl+C | 终止 |
+| `SIGQUIT` | 3 | 终端退出符 Ctrl+\ | 终止 + core |
+| `SIGKILL` | 9 | 终极终止,不可阻塞/忽略/捕获 | 终止 |
+| `SIGSEGV` | 11 | 无效内存引用 | 终止 + core |
+| `SIGPIPE` | 13 | 向已关闭管道/socket 写 | 终止 |
+| `SIGALRM` | 14 | 定时器超时(alarm) | 终止 |
+| `SIGTERM` | 15 | 标准终止信号(kill 默认) | 终止 |
+| `SIGCHLD` | 17 | 子进程终止或停止 | 忽略 |
+| `SIGCONT` | 18 | 使停止的进程继续 | 继续 |
+| `SIGSTOP` | 19 | 必停,不可阻塞/忽略/捕获 | 停止 |
+| `SIGTSTP` | 20 | 终端停止符 Ctrl+Z | 停止 |
+
+要点:`SIGKILL`/`SIGSTOP` 是仅有的两个不可忽略、不可捕获的信号,向内核和超级用户提供可靠的终止/停止手段。`SIGTERM` 是优雅终止的首选,程序可捕获它做资源清理;`SIGKILL` 应作为最后手段。`SIGCHLD` 默认忽略,父进程可捕获它异步回收子进程。
+
+**追问**:① 为什么 `kill -9` 能终止一切进程?(`SIGKILL` 不可被阻塞、忽略或捕获)② `SIGSEGV` 产生的核心转储文件有什么用?(可用于事后调试、定位崩溃现场)
+
+### 5. kill()、raise()、alarm()、pause() 的用法与区别?
+
+**答案要点**:`kill()` 向进程/进程组发信号;`raise()` 向自己发信号;`alarm()` 设定时器;`pause()` 挂起等待信号。
+
+**详细解答**:
+
+- `int kill(pid_t pid, int sig);` 参数 `pid` 取值含义:
+  - `pid > 0`:发送给指定进程;
+  - `pid == 0`:发送给当前进程组中的所有进程;
+  - `pid == -1`:发送给当前进程有权发送的每个进程(init 除外);
+  - `pid < -1`:发送给进程组 ID 为 `-pid` 的每个进程。
+  - `sig == 0` 时不发送信号但做错误检查,可用来判断进程是否存在(不存在返回 -1,`errno=ESRCH`)。发送需要权限:发送者的实际/有效用户 ID 必须等于接收者,root 例外。
+- `int raise(int sig);` 等价于 `kill(getpid(), sig)`,向自己发送信号。
+- `unsigned int alarm(unsigned int seconds);` 设置一次性闹钟,超时后内核发送 `SIGALRM`。每个进程只能有一个 alarm 闹钟;若之前已设置且未超时,返回剩余秒数并被新值替代。不能循环触发,如需循环要在处理函数中重新调用。
+- `int pause(void);` 使进程休眠,直到捕获到一个信号且处理函数返回,返回 -1、`errno` 置 `EINTR`。
+
+```c
+alarm(3);     /* 3 秒后发送 SIGALRM */
+pause();      /* 挂起等待,被信号唤醒后返回 */
+```
+
+`alarm()+pause()` 可模拟 `sleep()`,但存在竞态:若信号在 `alarm()` 之后、`pause()` 之前到达,`pause()` 会永久阻塞。更安全的做法是用 `sigsuspend()`。
+
+**追问**:① 为什么 `alarm+pause` 有竞态?如何解决?(用 `sigsuspend()` 把恢复掩码与挂起做成原子操作)② 子进程被暂停(SIGSTOP)会收到 `SIGCHLD` 吗?(会,若未设置 `SA_NOCLDSTOP`)
+
+### 6. 信号掩码、sigprocmask、sigpending、sigsuspend 分别是什么?
+
+**答案要点**:信号掩码是一组被阻塞的信号;`sigprocmask` 修改掩码;`sigpending` 查询挂起信号;`sigsuspend` 原子地替换掩码并挂起。
+
+**详细解答**:
+
+- **信号掩码**:内核为每个进程维护的一组信号。属于掩码的信号被阻塞、无法传递,直到从掩码移除后才得到处理。向掩码添加信号有三种方式:调用 `signal()/sigaction()` 注册处理函数时自动添加当前处理的信号(`SA_NODEFER` 除外);用 `sa_mask` 指定;用 `sigprocmask()` 显式添加/移除。
+- `int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);` `how` 取值:
+  - `SIG_BLOCK`:将 `set` 加入掩码(并集);
+  - `SIG_UNBLOCK`:从掩码移除;
+  - `SIG_SETMASK`:直接设置为 `set`。
+- `int sigpending(sigset_t *set);` 获取处于等待(挂起)状态的信号集。
+- `int sigsuspend(const sigset_t *mask);` 把进程信号掩码替换为 `mask` 并挂起进程,收到信号处理函数返回后恢复原掩码。它相当于原子地执行 `sigprocmask(SIG_SETMASK,...,&old); pause(); sigprocmask(SIG_SETMASK,&old,NULL);`,从而消除"恢复掩码到 pause 之间信号丢失"的竞态。
+
+```c
+sigset_t set;
+sigemptyset(&set);
+sigaddset(&set, SIGINT);
+sigprocmask(SIG_BLOCK, &set, NULL);   /* 阻塞 SIGINT */
+/* 受保护代码段 ... */
+sigprocmask(SIG_UNBLOCK, &set, NULL); /* 解除阻塞 */
+```
+
+**追问**:① `sigsuspend()` 返回值是什么?(总是返回 -1,`errno` 通常为 `EINTR`)② 信号掩码在多线程下属于进程还是线程?(属于**线程**,用 `pthread_sigmask()` 设置各自掩码)
+
+### 7. 实时信号与标准信号有什么区别?sigqueue 与 kill 有何不同?
+
+**答案要点**:实时信号支持排队、可携带伴随数据、传递顺序有保障、范围大;`sigqueue` 可发送实时信号并携带数据,`kill` 不能。
+
+**详细解答**:
+
+| 对比项 | 标准信号 | 实时信号 |
+| ------ | -------- | -------- |
+| 编号 | 1~31 | 34~64(`SIGRTMIN`~`SIGRTMAX`) |
+| 是否排队 | 否,多次发送只记一次 | 是,多次发送多次传递 |
+| 携带数据 | 否 | 是(`union sigval`,整型或指针) |
+| 传递顺序 | 无保障 | 编号越小优先级越高;同类型按发送顺序 |
+| 自定义数量 | 仅 `SIGUSR1`/`SIGUSR2` | 31 个可用 |
+
+使用实时信号的两个要求:
+
+1. 发送进程用 `int sigqueue(pid_t pid, int sig, const union sigval value);` 发送信号及伴随数据;
+2. 接收进程用 `sigaction()` 绑定处理函数并设置 `SA_SIGINFO`,使用 `sa_sigaction` 而非 `sa_handler`,才能通过 `siginfo_t->si_value` 取到伴随数据。
+
+```c
+union sigval v;
+v.sival_int = 10;
+sigqueue(pid, sig, v);                 /* 发送方 */
+
+static void handler(int sig, siginfo_t *info, void *ctx) {
+    sigval_t v = info->si_value;       /* 接收方取伴随数据 */
+    printf("data=%d\n", v.sival_int);
+}
+```
+
+**追问**:① `sigqueue` 的 `sig` 设为 0 有什么用途?(检查进程是否存在,与 `kill` 相同)② 实时信号一定比标准信号"快"吗?(不是,"实时"指可靠排队,与速度无关)
+
+---
+
+## 二、进程
+
+### 1. 程序和进程有什么区别?进程有哪些状态?
+
+**答案要点**:程序是磁盘上的静态可执行文件,进程是程序的一次运行过程、动态概念;进程有 6 种状态。
+
+**详细解答**:程序是静态文件,进程是可执行程序的实例、是程序被加载到内存中运行后的动态过程,有生命周期(创建到终止)。每个进程有唯一进程号 PID(正数)。
+
+Linux 进程的 6 种状态:
+
+| 状态 | 说明 |
+| ---- | ---- |
+| 就绪态(Ready) | 满足被调度条件但未获 CPU,得到 CPU 即可运行 |
+| 运行态 | 正被 CPU 调度执行 |
+| 僵尸态 | 进程已结束,父进程尚未"收尸" |
+| 可中断睡眠(浅睡) | 可被信号唤醒 |
+| 不可中断睡眠(深睡) | 无法被信号唤醒,须等条件成立 |
+| 暂停态 | 被信号暂停(如 `SIGSTOP`),可被 `SIGCONT` 恢复 |
+
+浅睡和深睡统称等待态/阻塞态,处于等待态的进程不参与调度。新创建的进程处于就绪态。
+
+**追问**:① 进程和线程谁被调度?(线程是调度最小单元,进程不是运行实体)② 阻塞态和暂停态有何区别?(阻塞在等事件,暂停是被强制停止,都可恢复)
+
+### 2. fork() 的返回值是什么?父子进程共享和复制了什么?
+
+**答案要点**:父进程返回子进程 PID,子进程返回 0,失败返回 -1;子进程复制数据段/堆/栈/文件描述符,共享代码段。
+
+**详细解答**:`pid_t fork(void);` 调用一次返回两次:
+
+- 父进程中返回子进程的 PID(>0);
+- 子进程中返回 0;
+- 失败返回 -1,不创建子进程并设置 `errno`。
+
+子进程是父进程的副本:拷贝父进程的数据段、堆、栈,并继承已打开的文件描述符(类似 `dup()`,父子对应 fd 指向**同一个文件表**,共享文件偏移量)。但父子**不共享**这些存储空间,各自修改各自的栈、堆互不影响。代码段(文本段)只读,父子共享同一份。子进程有独立的进程空间、PCB,被内核同等调度。
+
+现代 Linux 用**写时复制(copy-on-write)** 技术:fork 时并不立即复制全部内存,只有某方写入时才复制相应页,以提高效率。
+
+经典用法:`./app arg` 中 `arg` 由 shell 解析后通过加载器传给程序的引导代码,再传给 `main(argc, argv)`。
+
+**追问**:① 为什么父、子进程一般只有一个用 `exit()`、另一个用 `_exit()`?(避免刷新同一份 stdio 缓冲区导致重复输出)② fork 后父子谁先运行?(不确定,需同步)
+
+### 3. fork() 和 vfork() 有什么区别?
+
+**答案要点**:都创建子进程、返回值相同;`vfork()` 不复制地址空间、子进程先运行、共享父进程内存,专为立即 exec 设计。
+
+**详细解答**:
+
+| 对比项 | fork() | vfork() |
+| ------ | ------ | ------- |
+| 地址空间 | 复制(写时复制) | 不复制,子进程共享父进程内存 |
+| 执行顺序 | 不确定 | 保证子进程先运行,exec 后父进程才可能被调度 |
+| 效率 | 有复制开销 | 效率更高 |
+| 适用场景 | 通用 | 子进程立即调用 exec/_exit |
+
+`vfork()` 的子进程在调用 `exec` 或 `_exit` 之前运行在父进程的空间中;若在此期间修改父进程数据(除返回值变量)、调用函数或未 exec/_exit 就返回,可能带来未知结果。
+
+虽然 `vfork()` 效率更高,但容易引入难以察觉的 bug,且现代内核的 `fork()` 已用写时复制优化,所以**实际开发中应优先使用 `fork()`,除非速度极其关键**。
+
+**追问**:① vfork 的子进程为什么不能调用 `exit()`?(会刷新并关闭父进程的 stdio 缓冲区)② 子进程立即 exec 的意义是什么?(用新程序替换自身,避免继承父进程不需要的资源)
+
+### 4. exit() 和 _exit() 有什么区别?为什么子进程要用 _exit()?
+
+**答案要点**:`exit()` 会执行终止处理函数、刷新 stdio 缓冲,再调用 `_exit()`;`_exit()` 直接陷入内核终止。
+
+**详细解答**:两者都终止进程,但 `exit()` 多做三件事:
+
+1. 调用通过 `atexit()` 注册的进程终止处理函数;
+2. 刷新 stdio 流缓冲区;
+3. 调用 `_exit()` 系统调用。
+
+子进程应使用 `_exit()` 的原因:fork 后子进程**复制了父进程的 stdio 缓冲区**。若父子都用 `exit()`,两者都会刷新各自的缓冲区。例如 `printf("Hello World!");` 后不带换行(行缓冲未刷新),fork 后子进程也拷贝了这段未刷新的数据,父子各自 `exit()` 会各刷新一次,导致字符串被打印两次。用 `_exit()` 的子进程不刷新缓冲区,即可避免重复输出。
+
+其他避免方法:输出后加 `\n`(行缓冲立即刷新)、fork 前 `fflush()`、关闭 stdio 缓冲。注意 `return` 从 main 返回等价于 `exit()`。
+
+**追问**:① `atexit()` 注册的函数在 `_exit()` 时会执行吗?(不会)② 终止状态的哪些位有效?(int 中仅低 8 位有效,0 表示成功)
+
+### 5. 什么是僵尸进程和孤儿进程?如何避免僵尸进程?
+
+**答案要点**:子进程先于父进程结束且父进程未回收→僵尸进程;父进程先于子进程结束→孤儿进程(被 init 收养)。
+
+**详细解答**:
+
+- **孤儿进程**:父进程先结束,子进程被 init(PID=1)收养,`getppid()` 返回 1(图形界面下可能被 upstart 等会话守护进程收养)。
+- **僵尸进程**:子进程已结束,但父进程还没调用 `wait()` 回收其资源,子进程处于"已终止但 PCB 未释放"的状态。用 `ps` 查看状态为 `Z`。僵尸进程**无法被信号杀死**(连 `SIGKILL` 也不行),因为它已经死了,只是在等父进程回收。
+
+危害:大量僵尸进程会填满内核进程表,阻碍新进程创建。
+
+处理僵尸的方法:
+
+1. 父进程调用 `wait()`/`waitpid()` 回收(正常做法);
+2. 父进程调用 `waitpid(-1, NULL, WNOHANG)` 非阻塞轮询;
+3. 捕获 `SIGCHLD` 信号,在处理函数中循环 `while (waitpid(-1, NULL, WNOHANG) > 0);`;
+4. 将 `SIGCHLD` 设为 `SIG_IGN`,让内核把子进程交给 init 处理(无需显式 wait);
+5. 若父进程不处理就退出,init 会接管并自动回收。
+
+**追问**:① 为什么"收尸"必须由父进程做?(进程表项/退出状态需父进程读取后内核才释放)② `SIGCHLD` 处理函数里为什么用 while + WNOHANG?(多个子进程同时终止时信号可能合并,必须循环收完所有)
+
+### 6. wait() 和 waitpid() 有什么区别?status 怎么解析?
+
+**答案要点**:`wait()` 阻塞等待任意子进程;`waitpid()` 可指定子进程、可非阻塞、可处理暂停/恢复。
+
+**详细解答**:
+
+`pid_t wait(int *status);` 的行为:无终止子进程时阻塞;无子进程时返回 -1、`errno=ECHILD`;已有终止子进程时立即回收。
+
+`pid_t waitpid(pid_t pid, int *status, int options);` 的 `pid`:
+
+- `>0`:等待指定 PID;
+- `=0`:等待同进程组的任意子进程;
+- `<-1`:等待进程组 ID 为 `|pid|` 的任意子进程;
+- `=-1`:等待任意子进程,等价于 `wait()`。
+
+`options` 常用标志:`WNOHANG`(非阻塞,无状态改变返回 0)、`WUNTRACED`(也返回暂停的子进程)、`WCONTINUED`(也返回被 `SIGCONT` 恢复的子进程)。
+
+`status` 解析宏:
+
+| 宏 | 含义 |
+| -- | ---- |
+| `WIFEXITED(status)` | 子进程正常终止返回真 |
+| `WEXITSTATUS(status)` | 取退出码 |
+| `WIFSIGNALED(status)` | 被信号终止返回真 |
+| `WTERMSIG(status)` | 取导致终止的信号编号 |
+| `WCOREDUMP(status)` | 是否产生 core 文件 |
+
+**追问**:① `waitpid(-1,&s,0)` 与 `wait(&s)` 等价吗?(等价)② 为什么 `WNOHANG` 轮询会浪费 CPU?(忙等,应配合阻塞或信号机制)
+
+### 7. exec 族函数有哪些?fork + exec 有什么意义?
+
+**答案要点**:`execve` 是系统调用,其余 6 个是库函数;按参数形式(list/array)和是否查 PATH(p)、是否带 env(e)区分;fork+exec 实现"创建新进程运行另一个程序"。
+
+**详细解答**:
+
+| 函数 | 参数形式 | 查 PATH | 指定环境变量 |
+| ---- | -------- | ------- | ------------ |
+| `execl` | 可变参数列表 | 否 | 否 |
+| `execv` | 指针数组 | 否 | 否 |
+| `execlp` | 可变参数列表 | 是 | 否 |
+| `execvp` | 指针数组 | 是 | 否 |
+| `execle` | 可变参数列表 | 否 | 是 |
+| `execvpe` | 指针数组 | 是 | 是 |
+| `execve`(系统调用) | 数组 | 否 | 是 |
+
+命名规律:`l`=list(逐个参数,NULL 结尾);`v`=vector(字符串数组);`p`=PATH(只给文件名即可);`e`=environment(自定义环境变量)。成功调用 **exec 永不返回**,返回即表示出错。exec 用新程序替换当前进程的代码段、数据段、堆栈,从新程序的 `main()` 开始执行,PID 不变。
+
+fork+exec 的意义:子进程 fork 出一个副本后,立即 exec 载入新程序,既保留了"独立进程"的隔离性,又能执行任意可执行文件。这是 shell 执行命令、服务器处理请求的经典模式。相比直接写死在子进程分支,exec 更灵活、扩展性更好。
+
+**追问**:① `system()` 的内部实现是什么?(fork + execl(shell)+ waitpid)② exec 后进程 PID 会变吗?(不变,只是程序映像被替换)
+
+### 8. 守护进程是什么?如何编写?
+
+**答案要点**:运行在后台、独立于控制终端、长期运行的进程;编写步骤:fork+父退出、setsid、改工作目录、重设 umask、关文件描述符、重定向 0/1/2 到 /dev/null、忽略 SIGCHLD。
+
+**详细解答**:守护进程(Daemon)的特点:长期运行(直到系统关机)、与控制终端脱离(不受用户登录注销影响)、自成进程组和会话(pid=gid=sid)。Linux 大多数服务器(httpd、crond 等)都是守护进程,名字常以 d 结尾。
+
+编写步骤:
+
+1. **fork 后父进程退出**:让 shell 认为命令执行完毕,且保证子进程不是进程组组长(setsid 的前提);
+2. **子进程调用 `setsid()`**:创建新会话,成为会话首领和进程组组长,摆脱原会话、原进程组、原控制终端;
+3. **`chdir("/")`**:避免占用可卸载的文件系统;
+4. **`umask(0)`**:确保最大操作权限;
+5. **关闭继承的文件描述符**;
+6. **把 0/1/2 重定向到 `/dev/null`**;
+7. **`signal(SIGCHLD, SIG_IGN)`**:避免产生僵尸进程。
+
+```c
+pid = fork();
+if (pid > 0) exit(0);              /* 父进程退出 */
+setsid();                          /* 创建新会话 */
+chdir("/");                        /* 工作目录 */
+umask(0);                          /* 权限掩码 */
+for (i = 0; i < sysconf(_SC_OPEN_MAX); i++) close(i);
+open("/dev/null", O_RDWR);         /* 0 */
+dup(0);                            /* 1 */
+dup(0);                            /* 2 */
+signal(SIGCHLD, SIG_IGN);
+```
+
+守护进程常用**文件锁**实现单例运行:打开 `/var/run/name.pid`,用 `flock(fd, LOCK_EX|LOCK_NB)` 尝试加锁,失败说明已在运行;成功则写入 PID,程序退出自动解锁。
+
+**追问**:① 为什么用 setsid 而不是只 fork 两次?(setsid 才能真正脱离控制终端、成为会话首领)② 关闭终端时普通进程为什么会退出?(收到 SIGHUP;忽略 SIGHUP 也能保命,但不够规范)
+
+---
+
+## 三、进程间通信
+
+### 1. 什么是进程间通信?Linux 提供了哪些 IPC 机制?
+
+**答案要点**:两个进程之间的通信;机制分 UNIX IPC、System V IPC、POSIX IPC、Socket IPC 四类。
+
+**详细解答**:每个进程有独立、隔离的地址空间,同一进程内不同模块用全局变量即可通信,但两个不同进程通信较难,需要内核提供 IPC 机制。大型程序(GUI、服务器)常设计成多进程,需要 IPC;中小型程序通常单进程(可多线程),较少用到。
+
+Linux IPC 机制(从 UNIX 继承):
+
+| 类别 | 机制 |
+| ---- | ---- |
+| UNIX IPC | 管道、FIFO、信号 |
+| System V IPC | Signal V 信号量、消息队列、共享内存 |
+| POSIX IPC | POSIX 信号量、消息队列、共享内存 |
+| Socket IPC | 基于 socket 的进程间通信(可跨主机) |
+
+AT&T 贝尔实验室形成了 System V IPC(局限在单机内);BSD 形成了基于 socket 的 IPC(可跨主机)。POSIX IPC 在 System V IPC 基础上改进,弥补其不足。
+
+**追问**:① 信号算 IPC 吗?(算,是最原始的 IPC 形式之一)② 单机选哪种最快?(共享内存,配合信号量同步)
+
+### 2. 管道、流管道、有名管道(FIFO)有什么区别?
+
+**答案要点**:普通管道单工、限亲缘进程;流管道半双工、限亲缘进程;FIFO 突破两种限制,非亲缘进程也可通信。
+
+**详细解答**:
+
+| 类型 | 方向 | 通信进程限制 |
+| ---- | ---- | ------------ |
+| 普通管道 `pipe` | 单工(单向) | 只能在父子/兄弟进程间 |
+| 流管道 `s_pipe` | 半双工(双向) | 只能在父子/兄弟进程间 |
+| 有名管道 FIFO | 双向 | 无亲缘关系的进程也可通信 |
+
+管道被抽象成一个文件(pipe 文件类型),是 UNIX 最古老的 IPC 方法(20 世纪 70 年代早期出现)。普通管道要实现双向传输必须用两个管道;FIFO 突破了"亲缘关系"和"单工"两种限制,任何进程只要知道 FIFO 路径就能通信。
+
+**追问**:① 向已关闭的管道写会怎样?(产生 `SIGPIPE` 信号,默认终止进程)② 管道和 FIFO 的本质区别是什么?(FIFO 在文件系统中有名字,可被任意进程打开)
+
+### 3. 消息队列有什么特点和用途?
+
+**答案要点**:消息的链表,存放在内核中,由消息队列标识符标识;克服了信号信息量少、管道无格式、缓冲区受限等缺陷。
+
+**详细解答**:消息队列是 UNIX 下不同进程共享资源的一种机制,允许进程把**格式化的数据流**以消息形式发送给任意进程。有足够权限的进程可向队列添加消息,有读权限的进程可读走队列中的消息。消息队列包括 POSIX 消息队列和 System V 消息队列。
+
+相比其它 IPC 的优势:
+
+- 相比信号:能传递携带结构化的信息,不只是"发生了某事件";
+- 相比管道:数据有格式(带类型),不局限于无格式字节流;
+- 支持消息优先级和类型筛选,可按需读取。
+
+内核会为消息队列维护引用计数等状态,消息可长期存放在内核中,进程可异步收发。
+
+**追问**:① 消息队列和管道的核心区别?(有消息边界和类型,非字节流)② System V 和 POSIX 消息队列怎么选?(新项目优先 POSIX,接口更规范)
+
+### 4. 信号量的作用是什么?它和信号有什么区别?
+
+**答案要点**:信号量是一个计数器,用于控制多个进程/线程对共享资源的访问,常作锁机制;信号(signal)是异步事件通知,两者完全不同。
+
+**详细解答**:信号量是一个计数器,相当于内存中的标志。进程可根据它判定能否访问某些共享资源,也可修改它。除了共享资源访问控制外,还可用于进程同步。它常作为一种锁机制,防止某进程访问资源时其它进程也访问,因此主要作为进程间及同一进程内不同线程间的**同步手段**。Linux 提供一组信号量接口,声明在 `<sys/sem.h>` 中。
+
+信号量有两个核心操作:P(等待/减一,资源不足则阻塞)和 V(释放/加一,唤醒等待者)。二值信号量(0/1)可实现互斥,计数信号量可控制并发数量。
+
+与信号的区别:
+
+| 对比项 | 信号量(semaphore) | 信号(signal) |
+| ------ | ------------------- | -------------- |
+| 本质 | 计数器,用于同步/互斥 | 异步事件通知 |
+| 用途 | 控制资源访问、进程同步 | 通知进程某事件发生 |
+| 头文件 | `<sys/sem.h>` / `<semaphore.h>` | `<signal.h>` |
+
+**追问**:① 信号量初值设为 1 表示什么?(二值信号量,可实现互斥)② 信号量能携带数据吗?(不能,只表示资源计数)
+
+### 5. 共享内存为什么是最快的 IPC?为什么常与信号量配合?
+
+**答案要点**:多个进程映射访问同一块内存,无需数据拷贝,所以最快;但共享内存本身不提供同步,必须用信号量等机制协调读写。
+
+**详细解答**:共享内存就是映射一段能被其它进程访问的内存,由一个进程创建,其它进程都可访问,使多个进程访问同一块内存空间。它是**最快的 IPC 方式**,因为数据直接在内存中被多方读写,**不需要在内核与用户空间之间来回拷贝**(管道、消息队列都要经过内核缓冲区的拷贝)。
+
+但共享内存只解决"怎么共享",不解决"何时能读、何时能写"。多个进程同时读写会产生竞态,因此通常与信号量(或互斥锁)配合,实现进程间的同步与通信。
+
+```text
+进程 A ──┐                      ┌── 进程 B
+         ├─ 映射到同一物理内存 ─┤
+         │   (读写无需拷贝)    │
+         └── 用信号量同步访问 ───┘
+```
+
+**追问**:① 共享内存的虚拟地址在两个进程中相同吗?(不一定,各自映射,地址可不同)② 共享内存会随进程退出自动销毁吗?(取决于实现,System V 共享内存需显式删除)
+
+### 6. Socket IPC 与其它 IPC 有什么区别?
+
+**答案要点**:Socket 是基于网络的 IPC,可跨越不同主机;其它 IPC 一般限于同一台机器。
+
+**详细解答**:Socket 是一种 IPC 方法,基于网络,允许同一主机或通过网络连接的不同主机上的应用程序之间交换数据。典型客户端/服务器场景:
+
+- 各应用程序创建一个 socket(一个允许通信的"设备",双方都需要);
+- 服务器把自己的 socket 绑定到一个众所周知的地址(名称),使客户端能定位到它。
+
+与管道、消息队列、共享内存相比,socket 的最大优势是**可跨主机**,是实现网络服务的基石;其它 IPC 机制通常局限于单机。同一主机内部通信也可用 UNIX domain socket,效率高且不经过网络协议栈。Socket 编程在提高篇网络编程章节详细介绍。
+
+**追问**:① 本机进程间通信也用 socket 合适吗?(可用 UNIX domain socket,跨机时才必须用网络 socket)② socket 通信双方的标识是什么?(IP + 端口,服务器绑定知名端口)
+
+### 7. System V IPC 与 POSIX IPC 有什么区别?如何选择 IPC 机制?
+
+**答案要点**:POSIX IPC 在 System V IPC 基础上改进,接口更规范、更可移植;选择 IPC 要看是否需要跨主机、数据量、同步需求与实时性。
+
+**详细解答**:
+
+| 对比项 | System V IPC | POSIX IPC |
+| ------ | ------------ | --------- |
+| 出现时间 | 较早 | 较晚,改进自 System V |
+| 接口风格 | 较繁琐(key、flag) | 更简洁、符合 POSIX 标准 |
+| 可移植性 | 较差 | 更好(跨 UNIX 系统) |
+| 消息通知 | 无 | 支持消息通知/优先级 |
+| 涵盖 | 信号量、消息队列、共享内存 | 信号量、消息队列、共享内存 |
+
+选择 IPC 的参考:
+
+- 需要**跨主机**:socket;
+- 数据量**大、速度要求高**:共享内存(配信号量同步);
+- 需要**按类型/优先级读取**、有消息边界:消息队列;
+- 亲缘进程、简单单向数据流:管道/FIFO;
+- 需要**异步事件通知**:信号;
+- 控制并发/资源计数:信号量。
+
+**追问**:① 为什么新项目推荐 POSIX IPC?(接口更标准、可移植性更好)② 共享内存和消息队列哪个快?(共享内存,无内核拷贝,但需自行同步)
+
+---
+
+## 四、线程与同步
+
+### 1. 线程和进程有什么区别?为什么说系统调度的最小单元是线程?
+
+**答案要点**:线程是调度最小单元、进程是资源容器;同进程线程共享地址空间,进程间独立隔离;线程切换开销小、通信容易。
+
+**详细解答**:进程只是容器,真正运行的是进程中的线程。一个进程至少有一个主线程(`main()` 是主线程入口),可创建多个子线程并发运行。
+
+共享与私有:
+
+| 分类 | 内容 |
+| ---- | ---- |
+| 线程共享(属于进程) | 虚拟地址空间、代码段、数据段、堆、文件描述符、信号处理方式等 |
+| 线程私有 | 线程栈、寄存器环境、线程本地存储(TLS) |
+
+多进程 vs 多线程:
+
+| 对比项 | 多进程 | 多线程 |
+| ------ | ------ | ------ |
+| 切换开销 | 大 | 小 |
+| 通信 | 需 IPC,麻烦 | 共享内存,容易 |
+| 创建速度 | 慢 | 快 |
+| 多核利用 | 一般 | 更有优势 |
+| 健壮性 | 隔离性好 | 一个线程崩溃可能拖垮进程 |
+| 编程难度 | 较低 | 较高(线程安全、信号处理) |
+
+内核调度的是线程而非进程,所以说线程是系统调度的最小单元。多进程模型常用于大型应用(网络服务器),中小型程序多用多线程。
+
+**追问**:① 同一进程的两个线程的线程 ID 和进程 ID 相同吗?(进程 ID 相同,线程 ID 不同)② 线程能访问另一个线程的栈吗?(能,但极不安全,不应这样做)
+
+### 2. pthread_create 创建线程要注意什么?需要链接什么库?
+
+**答案要点**:start 函数签名固定为 `void *(*)(void *)`,只能传一个 `void *`;失败返回错误码而非设置 errno;需链接 `-lpthread`。
+
+**详细解答**:
+
+```c
+int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
+                   void *(*start_routine)(void *), void *arg);
+```
+
+- `thread`:成功时新线程 ID 存入;
+- `attr`:线程属性,`NULL` 为默认;
+- `start_routine`:新线程入口,签名固定;
+- `arg`:唯一参数,需传多个时封装结构体,且应指向全局/堆变量保证生命周期;
+- 返回值:成功 0,失败返回**错误号**(pthread 函数不设置 `errno`,用 `strerror(ret)` 翻译)。
+
+编译时必须链接 pthread 库,否则链接报 `undefined reference to 'pthread_create'`:
+
+```bash
+gcc -o testApp testApp.c -lpthread
+# 或
+gcc -pthread -o testApp testApp.c
+```
+
+创建后无法确定是新线程还是主线程先运行;若主线程不 `join` 或休眠就退出,可能导致新线程还没来得及运行进程就结束了。
+
+**追问**:① 为什么 pthread 函数失败不设置 errno?(设计上返回错误码更清晰,每个线程的 errno 只是兼容保留)② `attr` 传 NULL 和使用 `PTHREAD_CREATE_DETACHED` 有何区别?(前者默认可 join,后者创建即分离)
+
+### 3. pthread_join、pthread_detach、pthread_cancel 有什么区别?
+
+**答案要点**:join 等待并回收、detach 自动回收、cancel 请求退出。
+
+**详细解答**:
+
+| 函数 | 原型 | 作用 | 特点 |
+| ---- | ---- | ---- | ---- |
+| `pthread_join` | `int pthread_join(pthread_t, void **retval);` | 阻塞等待线程终止并回收,取退出码 | 不能非阻塞;未分离线程必须 join,否则成僵尸线程 |
+| `pthread_detach` | `int pthread_detach(pthread_t);` | 设为分离态,终止后系统自动回收 | 不可逆;之后不能再 join |
+| `pthread_cancel` | `int pthread_cancel(pthread_t);` | 发送取消请求 | 立即返回,不等待;默认如同 `pthread_exit(PTHREAD_CANCELED)` |
+
+线程关系对等:任意线程都能 join 另一个线程(与进程不同,进程只有父进程能 wait)。`PTHREAD_CANCELED` 实际是 `(void *)-1`。多个线程同时 join 同一线程结果不确定。`pthread_detach(pthread_self())` 可让线程自行分离。
+
+**追问**:① 僵尸线程是什么?(线程已终止但无人 join,资源未回收)② 已分离的线程还能 join 吗?(不能,返回 `EINVAL`/Invalid argument)
+
+### 4. 线程取消的状态、类型和取消点是什么?
+
+**答案要点**:状态分 ENABLE/DISABLE;类型分 DEFERRED/ASYNCHRONOUS;取消点是一系列可响应取消的函数。
+
+**详细解答**:
+
+- `pthread_setcancelstate(int state, int *oldstate);`
+  - `PTHREAD_CANCEL_ENABLE`:可被取消(默认);
+  - `PTHREAD_CANCEL_DISABLE`:不可取消,请求被挂起,直到状态变为 ENABLE。
+- `pthread_setcanceltype(int type, int *oldtype);`(仅 ENABLE 时有效)
+  - `PTHREAD_CANCEL_DEFERRED`:延迟取消,到达取消点才响应(默认);
+  - `PTHREAD_CANCEL_ASYNCHRONOUS`:可能在任意时刻取消(应用极少)。
+
+取消点是一系列函数,执行到它们时才真正响应取消请求,如 `sleep()`、`read()`、`write()`、`open()`、`close()`、`wait()`、`waitpid()`、`select()`、`pthread_join()`、`pthread_cond_wait()`、`sigwait()` 等。空循环(无取消点)中的线程永远无法被取消;可用 `pthread_testcancel()` 手动产生取消点。
+
+当线程 fork 子进程时,子进程继承取消状态和类型;调用 exec 时重置为默认(ENABLE + DEFERRED)。
+
+**追问**:① 为什么默认是 DEFERRED 而不是 ASYNCHRONOUS?(避免在关键代码执行中途被取消导致资源泄漏/不一致)② `pthread_testcancel()` 在无线程取消请求时做什么?(通常什么都不做,仅产生一个取消点)
+
+### 5. 为什么需要线程同步?竞态是怎么产生的?
+
+**答案要点**:多个线程并发访问共享资源形成竞态,导致数据不一致;需要同步机制保证同一时间只有一个线程访问。
+
+**详细解答**:线程同步是为了保护共享资源的访问,解决数据一致性问题。当以下条件同时满足时才有必要同步:
+
+- 变量可被多个线程访问;
+- 至少有一个线程会**修改**它。
+
+如果变量只读,或多线程访问的是各自私有的局部变量,则无一致性问题。竞态的本质是多个线程**并发访问**共享资源,出现读写交错:例如线程 A 读变量、计算、写回,写操作非原子,线程 B 在中间读到旧值。
+
+经典验证:两个线程各把全局变量递增 1000 万次,期望 2000 万,实际往往偏小。给读写过程加互斥锁后结果稳定正确。
+
+```c
+pthread_mutex_lock(&mutex);
+l_count = g_count;
+l_count++;
+g_count = l_count;
+pthread_mutex_unlock(&mutex);
+```
+
+**追问**:① 只读的共享变量需要加锁吗?(一般不需要,除非有写者)② `g_count++` 为什么不是原子的?(编译为读—改—写多条指令,可能被打断)
+
+### 6. 互斥锁如何初始化和使用?死锁是怎么产生的,如何避免?
+
+**答案要点**:两种初始化方式(宏、`pthread_mutex_init`);加锁/解锁配对;重复加锁或循环等待会死锁。
+
+**详细解答**:
+
+初始化:
+
+```c
+pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;   /* 定义时初始化 */
+/* 或 */
+pthread_mutex_init(&mutex, NULL);                     /* 动态/先定义后初始化 */
+```
+
+操作:
+
+| 函数 | 行为 |
+| ---- | ---- |
+| `pthread_mutex_lock` | 未锁则上锁;已锁则阻塞 |
+| `pthread_mutex_trylock` | 未锁则上锁;已锁返回 `EBUSY`,不阻塞 |
+| `pthread_mutex_unlock` | 解锁(不能解锁未锁定或他人锁定的锁) |
+| `pthread_mutex_destroy` | 销毁(不能销毁未解锁的锁) |
+
+死锁的典型场景:
+
+1. 同一线程对同一非递归锁加锁两次;
+2. 两个线程以不同顺序请求两把锁:
+
+```c
+/* 线程 A */                    /* 线程 B */
+lock(mutex1);                   lock(mutex2);
+lock(mutex2);                   lock(mutex1);   /* 循环等待 */
+```
+
+避免方法:① 所有线程按**相同顺序**加锁(定义锁的层级);② 用 `trylock` 尝试加锁,失败则释放已持有的所有锁后重试。互斥锁类型中 `PTHREAD_MUTEX_ERRORCHECK` 会在重复加锁时返回错误,可作调试工具;`PTHREAD_MUTEX_RECURSIVE` 允许同一线程多次加锁。
+
+**追问**:① `PTHREAD_MUTEX_RECURSIVE` 加锁两次需解锁几次?(两次,加锁与解锁次数必须相等)② 为什么要求所有线程遵守相同的加锁规则?(否则未加锁的访问仍会破坏数据一致性)
+
+### 7. 条件变量为什么必须和互斥锁一起用?为什么用 while 而不是 if?
+
+**答案要点**:条件检测需在互斥锁保护下进行;`wait` 返回后条件可能已变或有虚假唤醒,必须用 while 重新检查。
+
+**详细解答**:条件变量用于自动阻塞线程直到条件满足,它不保存状态。之所以要配合互斥锁,是因为条件本身由共享变量表示、对条件的检测通常需要访问共享资源,必须用互斥锁保护,否则会引发线程不安全。
+
+`pthread_cond_wait(&cond, &mutex)` 的工作流程:调用前线程必须持锁;函数**原子地**把线程加入等待队列并解锁;被唤醒返回时**重新加锁**。此时因为释放过锁,其它线程可能已经修改了共享变量,所以条件不一定还成立。
+
+必须用 while 而非 if 的两个理由:
+
+1. 多个线程等待同一条件,任一被唤醒的线程可能先修改共享变量使条件由真变假;
+2. 可能发生**虚假通知**。
+
+```c
+pthread_mutex_lock(&mutex);
+while (0 >= g_avail)                 /* 必须用 while 重新检查 */
+    pthread_cond_wait(&cond, &mutex);
+/* 处理 */
+pthread_mutex_unlock(&mutex);
+```
+
+通知函数:`pthread_cond_signal()` 至少唤醒一个线程,更高效;`pthread_cond_broadcast()` 唤醒所有线程,总能产生正确结果。若发送信号时无线程等待,信号会丢失(条件变量不保存状态)。
+
+**追问**:① `signal` 和 `broadcast` 怎么选?(只有一个等待者用 signal,多消费者/状态广播用 broadcast)② 条件变量能单独保存"已通知"状态吗?(不能,需借助共享变量)
+
+### 8. 自旋锁和互斥锁有什么区别?读写锁的规则和适用场景?
+
+**答案要点**:自旋锁忙等不休眠、临界区必须极短、可用于中断上下文;互斥锁阻塞休眠、开销大;读写锁读-读并发、读-写/写-写互斥,适合读多写少。
+
+**详细解答**:
+
+| 对比项 | 互斥锁 | 自旋锁 |
+| ------ | ------ | ------ |
+| 获取失败 | 阻塞休眠,唤醒后竞争 | 原地自旋,忙等占 CPU |
+| 底层实现 | 基于自旋锁实现(更上层) | 更底层 |
+| 开销 | 休眠/唤醒开销大 | 忙等开销,切换开销小 |
+| 临界区 | 可较长 | 必须极短 |
+| 中断上下文 | 不可用 | 可用(内核中自旋锁会自动禁止抢占) |
+| 重复加锁 | 不一定死锁(视类型) | 必然死锁 |
+
+读写锁有 3 种状态(读加锁、写加锁、未加锁),两条规则:
+
+- 写加锁时,所有试图加锁的线程(读或写)都阻塞;
+- 读加锁时,以读模式加锁的线程都能成功;以写模式加锁的线程阻塞,直到所有读锁释放。
+
+读-读可并发,所以读写锁比互斥锁并行性更高,适合**读的次数远大于写的次数**的场景。读写锁也称共享互斥锁。
+
+选型建议:一般临界区用互斥锁;等待条件成立用条件变量;临界区极短且不希望休眠(或中断上下文)用自旋锁;读多写少用读写锁。实际开发中用得最多的是互斥锁 + 条件变量。
+
+**追问**:① 自旋锁在单核上一定有效吗?(单核上自旋可能一直浪费 CPU,需谨慎)② 读写锁会导致写者饥饿吗?(可能,读者源源不断时写者可能长期拿不到锁)
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第八~十二章