فهرست منبع

lint: prettier 格式化嵌入式应用与Qt知识库(LF 行尾)

- npx prettier --write --end-of-line lf \**/*.md\
- 统一表格对齐、列表标记与空行,规范 markdown 排版
OpenCode 11 ساعت پیش
والد
کامیت
7549cfa4df
36فایلهای تغییر یافته به همراه4705 افزوده شده و 3970 حذف شده
  1. 57 55
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/01-Linux应用编程基础/01-应用编程概念与开发环境.md
  2. 143 128
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/01-Linux应用编程基础/02-文件IO基础与深入探究.md
  3. 177 164
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/01-Linux应用编程基础/03-标准IO库.md
  4. 127 116
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/01-Linux应用编程基础/04-文件属性与目录.md
  5. 97 87
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/01-Linux应用编程基础/05-字符串处理与系统信息.md
  6. 169 92
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/01-Linux应用编程基础/面试-Linux应用编程基础.md
  7. 158 146
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/02-进程线程与IPC/01-信号机制.md
  8. 127 114
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/02-进程线程与IPC/02-进程管理.md
  9. 109 97
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/02-进程线程与IPC/03-进程间通信.md
  10. 238 225
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/02-进程线程与IPC/04-线程与线程同步.md
  11. 129 117
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/02-进程线程与IPC/面试-进程线程与IPC.md
  12. 158 137
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/01-高级IO.md
  13. 102 85
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/02-GPIO与LED应用编程.md
  14. 84 71
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/03-输入设备与tslib.md
  15. 173 159
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/04-FrameBuffer与LCD应用编程.md
  16. 63 63
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/05-PWM与看门狗.md
  17. 268 247
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/06-摄像头串口与音频.md
  18. 147 90
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/面试-外设与高级IO编程.md
  19. 193 178
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/04-网络编程与项目实战/01-网络基础与socket编程.md
  20. 121 110
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/04-网络编程与项目实战/02-CAN应用编程.md
  21. 177 153
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/04-网络编程与项目实战/03-CMake入门与进阶.md
  22. 147 120
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/04-网络编程与项目实战/04-实战项目MQTT与视频监控.md
  23. 147 95
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/04-网络编程与项目实战/面试-网络编程与项目实战.md
  24. 69 62
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/01-C++基础与面向对象.md
  25. 90 78
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/02-Qt入门与项目结构.md
  26. 111 97
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/03-信号与槽.md
  27. 269 235
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/04-Qt控件详解.md
  28. 257 229
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/05-布局容器与模型视图.md
  29. 189 172
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/06-文本绘图与图表.md
  30. 68 48
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/07-多线程网络多媒体与数据库.md
  31. 73 18
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/08-嵌入式Qt交叉编译与项目实战.md
  32. 149 62
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/面试-Qt应用开发.md
  33. 52 52
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/PLAN.md
  34. 13 13
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/README.md
  35. 47 47
      X-Knowledge-Base/wiki/index.md
  36. 7 8
      X-Knowledge-Base/wiki/log.md

+ 57 - 55
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/01-Linux应用编程基础/01-应用编程概念与开发环境.md

@@ -1,6 +1,7 @@
 ---
 title: 应用编程概念与开发环境
-tags: [嵌入式Linux, Linux应用编程, 系统调用, 库函数, glibc, gcc, 交叉编译, IMX6ULL]
+tags:
+  [嵌入式Linux, Linux应用编程, 系统调用, 库函数, glibc, gcc, 交叉编译, IMX6ULL]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第一章 应用编程概念"
@@ -18,11 +19,11 @@ pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第一章 应用编
 
 在嵌入式 Linux 平台上做开发,代码大致落在三层上:
 
-| 形态 | 运行位置 | 运行环境 | 典型载体 | 典型产物 |
-| ---- | -------- | -------- | -------- | -------- |
-| 裸机编程 | 直接跑在硬件上 | 无操作系统 | 51、STM32 单片机 | `.hex` / `.bin` / `.elf` |
-| 驱动编程 | 内核空间(内核态) | Linux 内核 | 字符设备、平台设备驱动 | `.ko` 内核模块 |
-| 应用编程 | 用户空间(用户态) | Linux 应用层 | open/read/write 等系统调用 | 可执行文件(ELF) |
+| 形态     | 运行位置           | 运行环境     | 典型载体                   | 典型产物                 |
+| -------- | ------------------ | ------------ | -------------------------- | ------------------------ |
+| 裸机编程 | 直接跑在硬件上     | 无操作系统   | 51、STM32 单片机           | `.hex` / `.bin` / `.elf` |
+| 驱动编程 | 内核空间(内核态) | Linux 内核   | 字符设备、平台设备驱动     | `.ko` 内核模块           |
+| 应用编程 | 用户空间(用户态) | Linux 应用层 | open/read/write 等系统调用 | 可执行文件(ELF)        |
 
 三者的根本区别在于**硬件操作与用户逻辑是否隔离**、**有没有操作系统做中介**。
 
@@ -238,13 +239,13 @@ flowchart LR
 
 ### 3.1 系统调用与库函数对比
 
-| 对比项 | 系统调用 | 库函数(C 库) |
-| ------ | -------- | -------------- |
-| 归属 | 内核提供给应用层的接口,属于内核一部分 | 属于应用层 |
-| 运行空间 | 调用时从用户态陷入内核态 | 运行在用户空间 |
-| 缓存 | 无缓存 | 通常有缓存(stdio 缓冲),性能、效率通常更优 |
-| 可移植性 | 不同操作系统差异大(定义/功能/参数/返回值往往不同) | 不同操作系统接口定义几乎一样,可移植性更好 |
-| 例子 | `open`、`read`、`write`、`close` | `fopen`、`fread`、`fwrite`、`fclose`、`printf` |
+| 对比项   | 系统调用                                            | 库函数(C 库)                                 |
+| -------- | --------------------------------------------------- | ---------------------------------------------- |
+| 归属     | 内核提供给应用层的接口,属于内核一部分              | 属于应用层                                     |
+| 运行空间 | 调用时从用户态陷入内核态                            | 运行在用户空间                                 |
+| 缓存     | 无缓存                                              | 通常有缓存(stdio 缓冲),性能、效率通常更优   |
+| 可移植性 | 不同操作系统差异大(定义/功能/参数/返回值往往不同) | 不同操作系统接口定义几乎一样,可移植性更好     |
+| 例子     | `open`、`read`、`write`、`close`                    | `fopen`、`fread`、`fwrite`、`fclose`、`printf` |
 
 ```mermaid
 flowchart TB
@@ -328,11 +329,11 @@ int main(int argc, char **argv)
 
 此时参数个数为 2,且都以字符串形式传递:
 
-| 下标 | 值 | 含义 |
-| ---- | -- | ---- |
-| `argv[0]` | `"./hello"` | 程序自身路径与名字 |
-| `argv[1]` | `"112233"` | 用户传入的第 1 个参数 |
-| `argv[2]` | `NULL` | 数组结尾哨兵 |
+| 下标      | 值          | 含义                  |
+| --------- | ----------- | --------------------- |
+| `argv[0]` | `"./hello"` | 程序自身路径与名字    |
+| `argv[1]` | `"112233"`  | 用户传入的第 1 个参数 |
+| `argv[2]` | `NULL`      | 数组结尾哨兵          |
 
 完整示例:
 
@@ -437,10 +438,10 @@ int main(void)
 
 ### 7.1 两个阶段,两套编译器
 
-| 阶段 | 运行目标 | 编译器 | 产物运行位置 |
-| ---- | -------- | ------ | ------------ |
-| 入门篇(本模块) | PC 端 Ubuntu | 本地 gcc(x86) | Ubuntu 上直接运行 |
-| 提高篇(外设) | ALPHA/Mini I.MX6U(ARM 架构) | 交叉编译器 ARM gcc | 开发板上运行 |
+| 阶段             | 运行目标                      | 编译器             | 产物运行位置      |
+| ---------------- | ----------------------------- | ------------------ | ----------------- |
+| 入门篇(本模块) | PC 端 Ubuntu                  | 本地 gcc(x86)    | Ubuntu 上直接运行 |
+| 提高篇(外设)   | ALPHA/Mini I.MX6U(ARM 架构) | 交叉编译器 ARM gcc | 开发板上运行      |
 
 入门篇的示例都在 Ubuntu 下测试验证,所以用 `vscode + gcc` 即可;当代码要跑到 I.MX6U 这类 ARM 平台上时,必须用 **交叉编译工具(ARM 架构下的 gcc)** 编译,才能得到能在开发板上运行的可执行文件。
 
@@ -559,15 +560,15 @@ int main(int argc, char **argv)
 
 逐段解释:
 
-| 行 | 代码 | 说明 |
-| -- | ---- | ---- |
-| 1-7 | `#include ...` | `open` 需要 `<sys/types.h>`、`<sys/stat.h>`、`<fcntl.h>`;`write`/`close` 需要 `<unistd.h>`;`printf` 需要 `<stdio.h>`;`errno`/`strerror` 需要 `<errno.h>`、`<string.h>` |
-| 11 | `int main(int argc, char **argv)` | 需要接收命令行参数,因此用带参写法 |
-| 17-18 | 参数打印循环 | `argc` 是参数个数,`argv[i]` 是第 i 个字符串 |
-| 21 | `open(..., O_WRONLY \| O_CREAT, 0664)` | 只写打开,不存在则创建,权限 `rw-rw-r--` |
-| 22-25 | `if (-1 == fd)` | `open` 失败返回 -1;用 `strerror(errno)` 得到原因 |
-| 30-34 | `write(fd, &data, sizeof(data))` | 写入 4 字节;返回值是实际写入字节数 |
-| 39 | `close(fd)` | 不再使用时显式关闭,归还文件描述符 |
+| 行    | 代码                                   | 说明                                                                                                                                                                      |
+| ----- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 1-7   | `#include ...`                         | `open` 需要 `<sys/types.h>`、`<sys/stat.h>`、`<fcntl.h>`;`write`/`close` 需要 `<unistd.h>`;`printf` 需要 `<stdio.h>`;`errno`/`strerror` 需要 `<errno.h>`、`<string.h>` |
+| 11    | `int main(int argc, char **argv)`      | 需要接收命令行参数,因此用带参写法                                                                                                                                        |
+| 17-18 | 参数打印循环                           | `argc` 是参数个数,`argv[i]` 是第 i 个字符串                                                                                                                              |
+| 21    | `open(..., O_WRONLY \| O_CREAT, 0664)` | 只写打开,不存在则创建,权限 `rw-rw-r--`                                                                                                                                  |
+| 22-25 | `if (-1 == fd)`                        | `open` 失败返回 -1;用 `strerror(errno)` 得到原因                                                                                                                         |
+| 30-34 | `write(fd, &data, sizeof(data))`       | 写入 4 字节;返回值是实际写入字节数                                                                                                                                       |
+| 39    | `close(fd)`                            | 不再使用时显式关闭,归还文件描述符                                                                                                                                        |
 
 编译与运行:
 
@@ -616,14 +617,14 @@ LED on command sent
 
 ### 9.2 调试方法
 
-| 现象 | 可能原因 | 排查手段 |
-| ---- | -------- | -------- |
-| 编译报 `undefined reference to 'open'` | 漏包含头文件或库未链接 | 检查 `#include <fcntl.h>` 等;确认写法 |
-| `open error: No such file or directory` | 路径错误或文件不存在且未加 `O_CREAT` | `ls` 确认路径;加 `O_CREAT` |
-| `open error: Permission denied` | 权限不足 | 检查文件权限 `ls -l`,换路径或提权 |
-| 参数打印不对 | `argv` 下标越界 | 循环条件用 `i < argc` |
-| 程序运行无输出 | 可执行文件无执行权限 | `chmod +x testApp` |
-| 想知道失败原因 | 只看返回值不够 | 加 `perror("open error")` 或 `strerror(errno)` |
+| 现象                                    | 可能原因                             | 排查手段                                       |
+| --------------------------------------- | ------------------------------------ | ---------------------------------------------- |
+| 编译报 `undefined reference to 'open'`  | 漏包含头文件或库未链接               | 检查 `#include <fcntl.h>` 等;确认写法         |
+| `open error: No such file or directory` | 路径错误或文件不存在且未加 `O_CREAT` | `ls` 确认路径;加 `O_CREAT`                    |
+| `open error: Permission denied`         | 权限不足                             | 检查文件权限 `ls -l`,换路径或提权             |
+| 参数打印不对                            | `argv` 下标越界                      | 循环条件用 `i < argc`                          |
+| 程序运行无输出                          | 可执行文件无执行权限                 | `chmod +x testApp`                             |
+| 想知道失败原因                          | 只看返回值不够                       | 加 `perror("open error")` 或 `strerror(errno)` |
 
 调试常用命令:
 
@@ -635,11 +636,11 @@ echo $?                                  # 查看上一条命令/程序的退出
 
 `man` 手册章节编号:
 
-| 编号 | 内容 |
-| ---- | ---- |
-| 1 | Linux 命令 |
-| 2 | 系统调用 |
-| 3 | 标准 C 库函数 |
+| 编号 | 内容          |
+| ---- | ------------- |
+| 1    | Linux 命令    |
+| 2    | 系统调用      |
+| 3    | 标准 C 库函数 |
 
 ---
 
@@ -647,17 +648,17 @@ echo $?                                  # 查看上一条命令/程序的退出
 
 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
 
-| 维度 | I.MX6ULL(本教程) | STM32(裸机/RTOS) | RK3568 |
-| ---- | ------------------ | ------------------ | ------ |
-| 内核 | Cortex-A7 | Cortex-M3/M4/M7 | Cortex-A55 |
-| 有无 MMU | 有,跑完整 Linux | 无,通常裸机或 RTOS | 有,跑完整 Linux |
-| 运行模式 | 用户态 / 内核态 | 单一特权模式 | 用户态 / 内核态 |
-| 应用编程形态 | 系统调用 + glibc,ELF 可执行文件 | 直接寄存器操作 + HAL/LL 库,`main` 永不返回 | 系统调用 + glibc,ELF 可执行文件 |
-| 交叉工具链 | `arm-linux-gnueabihf-gcc` | `arm-none-eabi-gcc`(无 OS 支撑) | `aarch64-linux-gnu-gcc` |
-| 启动入口 | 内核加载 ELF/脚本 → `_start` → `main` | 复位向量 → 启动文件 → `main` | 内核加载 → `_start` → `main` |
-| 文件系统 | 有(ext4/UBIFS/rootfs) | 无(或用 FatFs 访问 SD 卡) | 有 |
-| `main` 之外 | 进程、内存、文件、网络全部由内核提供 | 所有资源自己管理 | 同 I.MX6ULL |
-| 迁移要点 | 系统调用/库函数接口在 Linux 间基本通用 | 与 Linux 应用是两套完全不同的编程模型 | 与 I.MX6ULL 代码几乎一致,注意 64 位与库版本 |
+| 维度         | I.MX6ULL(本教程)                     | STM32(裸机/RTOS)                          | RK3568                                       |
+| ------------ | -------------------------------------- | ------------------------------------------- | -------------------------------------------- |
+| 内核         | Cortex-A7                              | Cortex-M3/M4/M7                             | Cortex-A55                                   |
+| 有无 MMU     | 有,跑完整 Linux                       | 无,通常裸机或 RTOS                         | 有,跑完整 Linux                             |
+| 运行模式     | 用户态 / 内核态                        | 单一特权模式                                | 用户态 / 内核态                              |
+| 应用编程形态 | 系统调用 + glibc,ELF 可执行文件       | 直接寄存器操作 + HAL/LL 库,`main` 永不返回 | 系统调用 + glibc,ELF 可执行文件             |
+| 交叉工具链   | `arm-linux-gnueabihf-gcc`              | `arm-none-eabi-gcc`(无 OS 支撑)           | `aarch64-linux-gnu-gcc`                      |
+| 启动入口     | 内核加载 ELF/脚本 → `_start` → `main`  | 复位向量 → 启动文件 → `main`                | 内核加载 → `_start` → `main`                 |
+| 文件系统     | 有(ext4/UBIFS/rootfs)                | 无(或用 FatFs 访问 SD 卡)                 | 有                                           |
+| `main` 之外  | 进程、内存、文件、网络全部由内核提供   | 所有资源自己管理                            | 同 I.MX6ULL                                  |
+| 迁移要点     | 系统调用/库函数接口在 Linux 间基本通用 | 与 Linux 应用是两套完全不同的编程模型       | 与 I.MX6ULL 代码几乎一致,注意 64 位与库版本 |
 
 结论:I.MX6ULL 与 RK3568 同属"Linux 应用编程"范畴,本篇所学的系统调用、glibc、gcc/交叉编译流程**可直接复用**;STM32 裸机则是另一套世界观,没有系统调用、没有文件描述符、没有 `errno`,切忌混用概念。
 
@@ -670,6 +671,7 @@ echo $?                                  # 查看上一条命令/程序的退出
 ### Q1:系统调用和库函数有什么区别?
 
 **答**:
+
 - **归属不同**:系统调用是内核提供给应用层的接口,属于内核的一部分;库函数属于应用层。
 - **运行空间不同**:库函数运行在用户空间;调用系统调用会由用户态陷入内核态。
 - **缓存不同**:库函数通常有缓存(如 stdio 缓冲),系统调用无缓存,因此在性能和效率上库函数通常更优。

+ 143 - 128
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/01-Linux应用编程基础/02-文件IO基础与深入探究.md

@@ -1,6 +1,21 @@
 ---
 title: 文件 IO 基础与深入探究
-tags: [嵌入式Linux, Linux应用编程, 文件IO, 文件描述符, open, read, write, lseek, dup, fcntl, 原子操作, inode, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    Linux应用编程,
+    文件IO,
+    文件描述符,
+    open,
+    read,
+    write,
+    lseek,
+    dup,
+    fcntl,
+    原子操作,
+    inode,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第二章 文件I/O基础、第三章 深入探究文件I/O"
@@ -102,11 +117,11 @@ ulimit -n
 
 程序里 `open` 得到的 fd 通常从 **3** 开始,因为 `0、1、2` 已被系统默认占用:
 
-| 文件描述符 | 名称 | 宏 | 默认对应设备 |
-| ---------- | ---- | -- | ------------ |
-| 0 | 标准输入 | `STDIN_FILENO` | 键盘 |
-| 1 | 标准输出 | `STDOUT_FILENO` | LCD 显示器 |
-| 2 | 标准错误 | `STDERR_FILENO` | LCD 显示器 |
+| 文件描述符 | 名称     | 宏              | 默认对应设备 |
+| ---------- | -------- | --------------- | ------------ |
+| 0          | 标准输入 | `STDIN_FILENO`  | 键盘         |
+| 1          | 标准输出 | `STDOUT_FILENO` | LCD 显示器   |
+| 2          | 标准错误 | `STDERR_FILENO` | LCD 显示器   |
 
 这三个宏定义在 `<unistd.h>` 中:
 
@@ -143,25 +158,25 @@ man 2 open      # 2 表示系统调用;1 是命令,3 是标准 C 库函数
 
 ### 3.1 参数说明
 
-| 参数 | 类型 | 说明 |
-| ---- | ---- | ---- |
-| `pathname` | `const char *` | 文件路径,可含绝对/相对路径;若为符号链接会对其解引用 |
-| `flags` | `int` | 打开标志,可用 `|` 组合多个 |
-| `mode` | `mode_t` | 新建文件的访问权限,仅当 `flags` 含 `O_CREAT`/`O_TMPFILE` 时有效 |
+| 参数       | 类型           | 说明                                                             |
+| ---------- | -------------- | ---------------------------------------------------------------- |
+| `pathname` | `const char *` | 文件路径,可含绝对/相对路径;若为符号链接会对其解引用            |
+| `flags`    | `int`          | 打开标志,可用 `                                                 | ` 组合多个 |
+| `mode`     | `mode_t`       | 新建文件的访问权限,仅当 `flags` 含 `O_CREAT`/`O_TMPFILE` 时有效 |
 
 **返回值**:成功返回文件描述符(非负整数);失败返回 `-1`。
 
 ### 3.2 flags 常用标志
 
-| 标志 | 用途 | 说明 |
-| ---- | ---- | ---- |
-| `O_RDONLY` | 以只读方式打开 | 这三个是文件访问权限标志,**必须且只能包含其中一种**,决定 fd 能做什么操作 |
-| `O_WRONLY` | 以只写方式打开 | 同上 |
-| `O_RDWR` | 以可读可写方式打开 | 同上 |
-| `O_CREAT` | 文件不存在则创建 | 需传入第 3 个参数 `mode` 指定权限 |
-| `O_DIRECTORY` | 要求是目录 | `pathname` 不是目录则调用失败 |
-| `O_EXCL` | 与 `O_CREAT` 配合专门创建文件 | 若文件已存在则返回错误,可用于测试文件是否存在,使"测试+创建"成为**原子操作** |
-| `O_NOFOLLOW` | 不解引用符号链接 | 若 `pathname` 是符号链接直接返回错误;不加则解引用 |
+| 标志          | 用途                          | 说明                                                                          |
+| ------------- | ----------------------------- | ----------------------------------------------------------------------------- |
+| `O_RDONLY`    | 以只读方式打开                | 这三个是文件访问权限标志,**必须且只能包含其中一种**,决定 fd 能做什么操作    |
+| `O_WRONLY`    | 以只写方式打开                | 同上                                                                          |
+| `O_RDWR`      | 以可读可写方式打开            | 同上                                                                          |
+| `O_CREAT`     | 文件不存在则创建              | 需传入第 3 个参数 `mode` 指定权限                                             |
+| `O_DIRECTORY` | 要求是目录                    | `pathname` 不是目录则调用失败                                                 |
+| `O_EXCL`      | 与 `O_CREAT` 配合专门创建文件 | 若文件已存在则返回错误,可用于测试文件是否存在,使"测试+创建"成为**原子操作** |
+| `O_NOFOLLOW`  | 不解引用符号链接              | 若 `pathname` 是符号链接直接返回错误;不加则解引用                            |
 
 > 不同内核版本支持的 flags 存在差别,man 手册中会说明某标志从哪个版本开始支持。
 
@@ -183,12 +198,12 @@ S  U  U  U  G  G  G  O  O  O
    rwx   rwx   rwx
 ```
 
-| 字段 | 含义 |
-| ---- | ---- |
-| `O`(3 bit) | 其他用户的权限 |
+| 字段         | 含义                                      |
+| ------------ | ----------------------------------------- |
+| `O`(3 bit) | 其他用户的权限                            |
 | `G`(3 bit) | 同组用户(与文件所有者有相同组 ID)的权限 |
-| `U`(3 bit) | 文件所属用户的权限 |
-| `S`(3 bit) | 文件特殊权限,一般少用 |
+| `U`(3 bit) | 文件所属用户的权限                        |
+| `S`(3 bit) | 文件特殊权限,一般少用                    |
 
 每组 3 个 bit 按 `rwx` 顺序分配:最高位(权值 4)读、中间位(权值 2)写、最低位(权值 1)执行。
 
@@ -200,23 +215,23 @@ S  U  U  U  G  G  G  O  O  O
 
 实际编程中直接使用 Linux 已定义好的权限宏:
 
-| 宏定义 | 说明 |
-| ------ | ---- |
-| `S_IRUSR` | 文件所属者读 |
-| `S_IWUSR` | 文件所属者写 |
-| `S_IXUSR` | 文件所属者执行 |
-| `S_IRWXU` | 文件所属者读、写、执行 |
-| `S_IRGRP` | 同组用户读 |
-| `S_IWGRP` | 同组用户写 |
-| `S_IXGRP` | 同组用户执行 |
-| `S_IRWXG` | 同组用户读、写、执行 |
-| `S_IROTH` | 其他用户读 |
-| `S_IWOTH` | 其他用户写 |
-| `S_IXOTH` | 其他用户执行 |
-| `S_IRWXO` | 其他用户读、写、执行 |
-| `S_ISUID` | set-user-ID(特殊权限) |
+| 宏定义    | 说明                     |
+| --------- | ------------------------ |
+| `S_IRUSR` | 文件所属者读             |
+| `S_IWUSR` | 文件所属者写             |
+| `S_IXUSR` | 文件所属者执行           |
+| `S_IRWXU` | 文件所属者读、写、执行   |
+| `S_IRGRP` | 同组用户读               |
+| `S_IWGRP` | 同组用户写               |
+| `S_IXGRP` | 同组用户执行             |
+| `S_IRWXG` | 同组用户读、写、执行     |
+| `S_IROTH` | 其他用户读               |
+| `S_IWOTH` | 其他用户写               |
+| `S_IXOTH` | 其他用户执行             |
+| `S_IRWXO` | 其他用户读、写、执行     |
+| `S_ISUID` | set-user-ID(特殊权限)  |
 | `S_ISGID` | set-group-ID(特殊权限) |
-| `S_ISVTX` | sticky(特殊权限) |
+| `S_ISVTX` | sticky(特殊权限)       |
 
 这些宏可单独使用,也可位或组合:
 
@@ -261,11 +276,11 @@ if (-1 == fd)
 ssize_t write(int fd, const void *buf, size_t count);
 ```
 
-| 参数 | 说明 |
-| ---- | ---- |
-| `fd` | 文件描述符 |
-| `buf` | 待写入数据所在的缓冲区 |
-| `count` | 要写入的字节数 |
+| 参数    | 说明                   |
+| ------- | ---------------------- |
+| `fd`    | 文件描述符             |
+| `buf`   | 待写入数据所在的缓冲区 |
+| `count` | 要写入的字节数         |
 
 **返回值**:成功返回实际写入的字节数(0 表示未写入任何字节);若返回值小于 `count`,**这不是错误**(例如磁盘空间已满可能发生这种情况);写入出错返回 `-1`。
 
@@ -279,11 +294,11 @@ ssize_t write(int fd, const void *buf, size_t count);
 ssize_t read(int fd, void *buf, size_t count);
 ```
 
-| 参数 | 说明 |
-| ---- | ---- |
-| `fd` | 文件描述符 |
-| `buf` | 存放读取数据的缓冲区 |
-| `count` | 希望读取的字节数 |
+| 参数    | 说明                 |
+| ------- | -------------------- |
+| `fd`    | 文件描述符           |
+| `buf`   | 存放读取数据的缓冲区 |
+| `count` | 希望读取的字节数     |
 
 **返回值**:成功返回实际读取的字节数;实际值可能小于 `count`,也可能为 0(如已到文件末尾)。
 
@@ -318,15 +333,15 @@ int close(int fd);
 off_t lseek(int fd, off_t offset, int whence);
 ```
 
-| 参数 | 说明 |
-| ---- | ---- |
-| `fd` | 文件描述符 |
-| `offset` | 偏移量,以字节为单位 |
+| 参数     | 说明                         |
+| -------- | ---------------------------- |
+| `fd`     | 文件描述符                   |
+| `offset` | 偏移量,以字节为单位         |
 | `whence` | 偏移量参考基准,取以下宏之一 |
 
-| `whence` | 含义 |
-| -------- | ---- |
-| `SEEK_SET` | 指向 `offset` 字节处(从文件头算) |
+| `whence`   | 含义                                           |
+| ---------- | ---------------------------------------------- |
+| `SEEK_SET` | 指向 `offset` 字节处(从文件头算)             |
 | `SEEK_CUR` | 指向「当前位置 + offset」处,`offset` 可正可负 |
 | `SEEK_END` | 指向「文件末尾 + offset」处,`offset` 可正可负 |
 
@@ -419,15 +434,15 @@ err1:
 
 逐段解释:
 
-| 代码 | 说明 |
-| ---- | ---- |
-| `open("./src_file", O_RDONLY)` | 只读打开源文件;失败返回 -1 |
+| 代码                                                                            | 说明                                                                |
+| ------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
+| `open("./src_file", O_RDONLY)`                                                  | 只读打开源文件;失败返回 -1                                         |
 | `open("./dest_file", O_WRONLY \| O_CREAT \| O_EXCL, S_IRWXU\|S_IRGRP\|S_IROTH)` | 只写方式创建新文件;`O_EXCL` 保证文件已存在时失败;权限 `rwxr--r--` |
-| `lseek(fd1, 500, SEEK_SET)` | 把源文件读位置移到偏移 500 字节处 |
-| `read(fd1, buffer, 1024)` | 读 1KB 到 `buffer` |
-| `lseek(fd2, 0, SEEK_SET)` | 把目标文件写位置移到开头 |
-| `write(fd2, buffer, 1024)` | 写 1KB |
-| `goto err2/err1` | 出错时逐级释放资源,避免 fd 泄漏 |
+| `lseek(fd1, 500, SEEK_SET)`                                                     | 把源文件读位置移到偏移 500 字节处                                   |
+| `read(fd1, buffer, 1024)`                                                       | 读 1KB 到 `buffer`                                                  |
+| `lseek(fd2, 0, SEEK_SET)`                                                       | 把目标文件写位置移到开头                                            |
+| `write(fd2, buffer, 1024)`                                                      | 写 1KB                                                              |
+| `goto err2/err1`                                                                | 出错时逐级释放资源,避免 fd 泄漏                                    |
 
 编译运行:
 
@@ -548,8 +563,8 @@ int main(void)
 char *strerror(int errnum);
 ```
 
-| 参数 | 说明 |
-| ---- | ---- |
+| 参数     | 说明             |
+| -------- | ---------------- |
 | `errnum` | 错误编号 `errno` |
 
 **返回值**:对应错误编号的字符串描述。
@@ -594,9 +609,9 @@ Error: No such file or directory
 void perror(const char *s);
 ```
 
-| 参数 | 说明 |
-| ---- | ---- |
-| `s` | 错误提示字符串之前附加的信息,不加则传空字符串 |
+| 参数 | 说明                                           |
+| ---- | ---------------------------------------------- |
+| `s`  | 错误提示字符串之前附加的信息,不加则传空字符串 |
 
 **返回值**:无(`void`)。
 
@@ -688,12 +703,12 @@ void exit(int status);
 
 ### 11.3 三种终止方法对比
 
-| 方式 | 类型 | 是否刷新 stdio 缓冲 | 头文件 |
-| ---- | ---- | ------------------- | ------ |
-| `main` 中 `return` | 语言机制 | 是 | — |
-| `exit(status)` | C 库函数 | 是 | `<stdlib.h>` |
-| `_exit(status)` | 系统调用 | **否** | `<unistd.h>` |
-| `_Exit(status)` | 系统调用 | **否** | `<stdlib.h>` |
+| 方式               | 类型     | 是否刷新 stdio 缓冲 | 头文件       |
+| ------------------ | -------- | ------------------- | ------------ |
+| `main` 中 `return` | 语言机制 | 是                  | —            |
+| `exit(status)`     | C 库函数 | 是                  | `<stdlib.h>` |
+| `_exit(status)`    | 系统调用 | **否**              | `<unistd.h>` |
+| `_Exit(status)`    | 系统调用 | **否**              | `<stdlib.h>` |
 
 推荐使用 `exit()`。`return`、`exit()`、`_exit()/_Exit()` 的区别在标准 I/O 缓冲场景下会显现(见 [[01-Linux应用编程基础/03-标准IO库]])。
 
@@ -958,8 +973,8 @@ write(fd2, buffer2, 4);   /* 0xAA 0xBB 0xCC 0xDD */
 int dup(int oldfd);
 ```
 
-| 参数 | 说明 |
-| ---- | ---- |
+| 参数    | 说明                   |
+| ------- | ---------------------- |
 | `oldfd` | 需要被复制的文件描述符 |
 
 **返回值**:成功返回新的文件描述符(由系统按 fd 分配原则分配);失败返回 `-1` 并设置 `errno`。
@@ -980,9 +995,9 @@ fd2 = dup(fd1);   /* fd1=6 -> 得到 fd2=7 */
 int dup2(int oldfd, int newfd);
 ```
 
-| 参数 | 说明 |
-| ---- | ---- |
-| `oldfd` | 需要被复制的文件描述符 |
+| 参数    | 说明                                         |
+| ------- | -------------------------------------------- |
+| `oldfd` | 需要被复制的文件描述符                       |
 | `newfd` | 指定一个文件描述符(需是当前进程没有使用的) |
 
 **返回值**:成功返回手动指定的 `newfd`;失败返回 `-1` 并设置 `errno`。
@@ -1088,11 +1103,11 @@ flowchart TB
     end
 ```
 
-| 方式 | fd 是否各自独立 | 是否共享偏移量 | 典型用途 |
-| ---- | --------------- | -------------- | -------- |
-| 同进程多次 `open` | 是(多个文件表) | 否 | 需要独立读写位置 |
-| 不同进程各自 `open` | 是(多个文件表) | 否 | 进程间共享文件 |
-| `dup/dup2` 复制 | 是(同一文件表) | **是** | 接续写、重定向 |
+| 方式                | fd 是否各自独立  | 是否共享偏移量 | 典型用途         |
+| ------------------- | ---------------- | -------------- | ---------------- |
+| 同进程多次 `open`   | 是(多个文件表) | 否             | 需要独立读写位置 |
+| 不同进程各自 `open` | 是(多个文件表) | 否             | 进程间共享文件   |
+| `dup/dup2` 复制     | 是(同一文件表) | **是**         | 接续写、重定向   |
 
 文件共享存在**竞争冒险**,且要关心不同读写体之间是分别写还是接续写。
 
@@ -1145,10 +1160,10 @@ ssize_t pread(int fd, void *buf, size_t count, off_t offset);
 ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
 ```
 
-| 参数 | 说明 |
-| ---- | ---- |
+| 参数                 | 说明                       |
+| -------------------- | -------------------------- |
 | `fd`、`buf`、`count` | 与 `read`/`write` 意义相同 |
-| `offset` | 当前读或写的位置偏移量 |
+| `offset`             | 当前读或写的位置偏移量     |
 
 **返回值**:与 `read`/`write` 意义相同。
 
@@ -1222,21 +1237,21 @@ err:
 int fcntl(int fd, int cmd, ... /* arg */ );
 ```
 
-| 参数 | 说明 |
-| ---- | ---- |
-| `fd` | 文件描述符 |
-| `cmd` | 操作命令,以 `F_XXX` 开头 |
+| 参数  | 说明                              |
+| ----- | --------------------------------- |
+| `fd`  | 文件描述符                        |
+| `cmd` | 操作命令,以 `F_XXX` 开头         |
 | `...` | 可变参,第三个参数根据 `cmd` 传入 |
 
 `cmd` 大致分 5 类功能:
 
-| 功能 | cmd |
-| ---- | --- |
-| 复制文件描述符 | `F_DUPFD`、`F_DUPFD_CLOEXEC` |
-| 获取/设置文件描述符标志 | `F_GETFD`、`F_SETFD` |
-| 获取/设置文件状态标志 | `F_GETFL`、`F_SETFL` |
-| 获取/设置异步 IO 所有权 | `F_GETOWN`、`F_SETOWN` |
-| 获取/设置记录锁 | `F_GETLK`、`F_SETLK` |
+| 功能                    | cmd                          |
+| ----------------------- | ---------------------------- |
+| 复制文件描述符          | `F_DUPFD`、`F_DUPFD_CLOEXEC` |
+| 获取/设置文件描述符标志 | `F_GETFD`、`F_SETFD`         |
+| 获取/设置文件状态标志   | `F_GETFL`、`F_SETFL`         |
+| 获取/设置异步 IO 所有权 | `F_GETOWN`、`F_SETOWN`       |
+| 获取/设置记录锁         | `F_GETLK`、`F_SETLK`         |
 
 **返回值**:失败返回 `-1` 并设置 `errno`;成功时返回值与 `cmd` 有关(如 `F_DUPFD` 返回新 fd、`F_GETFD` 返回描述符标志、`F_GETFL` 返回文件状态标志)。
 
@@ -1342,11 +1357,11 @@ err:
 int ioctl(int fd, unsigned long request, ...);
 ```
 
-| 参数 | 说明 |
-| ---- | ---- |
-| `fd` | 文件描述符 |
+| 参数      | 说明                                                       |
+| --------- | ---------------------------------------------------------- |
+| `fd`      | 文件描述符                                                 |
 | `request` | 与具体要操作的对象有关,没有统一值,表示向 fd 请求相应操作 |
-| `...` | 可变参,第三个参数根据 `request` 决定 |
+| `...`     | 可变参,第三个参数根据 `request` 决定                      |
 
 **返回值**:成功返回 0,失败返回 -1。
 
@@ -1441,17 +1456,17 @@ xxd dest_file | head                              # 查看内容
 
 ### 20.2 调试方法
 
-| 现象 | 可能原因 | 排查手段 |
-| ---- | -------- | -------- |
-| `open` 返回 -1 | 文件不存在/权限不足/路径错误 | `perror("open error")`;`ls -l` 查权限;`pwd` 查路径 |
-| `read` 返回 0 | 已到文件末尾 | 结合 `lseek(fd,0,SEEK_END)` 看文件大小 |
-| `read` 返回值小于 `count` | 到达文件末尾,不是错误 | 用返回值而非 `count` 判断 |
-| `write` 少于 `count` | 磁盘空间满等,不是错误 | 检查磁盘空间 `df -h` |
-| 读到的内容和预期不符 | 偏移量没设置对 | 用 `lseek(..., SEEK_CUR)` 打印当前偏移量 |
-| 数据"少了"/互相覆盖 | 竞争冒险或分别写 | 加 `O_APPEND`,或用 `dup`/`pread` |
-| 文件逻辑大小与占用不符 | 空洞文件 | 对比 `ls -l` 与 `du` |
-| fd 泄漏 | 出错分支没 `close` | 用 `lsof -p <pid>` 观察,检查 `goto` 清理链 |
-| 6、7、8 而不是 3、4、5 | 父进程(如 VSCode)占用了 3、4、5 | 直接在系统 Terminal 运行验证 |
+| 现象                      | 可能原因                          | 排查手段                                             |
+| ------------------------- | --------------------------------- | ---------------------------------------------------- |
+| `open` 返回 -1            | 文件不存在/权限不足/路径错误      | `perror("open error")`;`ls -l` 查权限;`pwd` 查路径 |
+| `read` 返回 0             | 已到文件末尾                      | 结合 `lseek(fd,0,SEEK_END)` 看文件大小               |
+| `read` 返回值小于 `count` | 到达文件末尾,不是错误            | 用返回值而非 `count` 判断                            |
+| `write` 少于 `count`      | 磁盘空间满等,不是错误            | 检查磁盘空间 `df -h`                                 |
+| 读到的内容和预期不符      | 偏移量没设置对                    | 用 `lseek(..., SEEK_CUR)` 打印当前偏移量             |
+| 数据"少了"/互相覆盖       | 竞争冒险或分别写                  | 加 `O_APPEND`,或用 `dup`/`pread`                    |
+| 文件逻辑大小与占用不符    | 空洞文件                          | 对比 `ls -l` 与 `du`                                 |
+| fd 泄漏                   | 出错分支没 `close`                | 用 `lsof -p <pid>` 观察,检查 `goto` 清理链          |
+| 6、7、8 而不是 3、4、5    | 父进程(如 VSCode)占用了 3、4、5 | 直接在系统 Terminal 运行验证                         |
 
 常用工具:
 
@@ -1477,18 +1492,18 @@ strace -e trace=open,openat,read,write,lseek,close ./testApp_1
 
 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
 
-| 维度 | I.MX6ULL | STM32(裸机/RTOS) | RK3568 |
-| ---- | -------- | ------------------ | ------ |
-| 文件抽象 | 一切皆文件,fd 为索引 | 无 fd 概念,直接操作存储/寄存器 | 同 I.MX6ULL |
-| 打开方式 | `open` 返回 fd | 无;用 FatFs 的 `f_open()` 返回 `FIL` | `open` 返回 fd |
-| 读写接口 | `read`/`write`(系统调用) | `f_read`/`f_write`(库函数,操作 FILE) | `read`/`write` |
-| 偏移定位 | `lseek` | `f_lseek` | `lseek` |
-| 缓冲 | read/write 无用户缓冲;内核有 page cache | 通常无内核缓冲,由 FatFs 自管 | 同 I.MX6ULL |
-| 并发与原子 | 多进程共享文件,存在竞争冒险;`O_APPEND`/`pread` 解决 | 一般单任务,竞争问题多由 RTOS 信号量/临界区解决 | 同 I.MX6ULL |
-| dup/重定向 | `dup`/`dup2`/`fcntl` | 无此概念 | 同 I.MX6ULL |
-| 权限模型 | 有 uid/gid/rwx,`open` 受权限约束 | 无文件权限概念 | 同 I.MX6ULL |
-| 设备访问 | 通过 `/dev/xxx` + `ioctl` | 直接读写寄存器或 HAL 库 | 通过 `/dev/xxx` + `ioctl` |
-| 32/64 位 | 32 位(`off_t` 注意大文件) | 通常 32 位,无文件系统或 FatFs | 64 位,注意 `off_t`/`size_t` 宽度 |
+| 维度       | I.MX6ULL                                              | STM32(裸机/RTOS)                              | RK3568                            |
+| ---------- | ----------------------------------------------------- | ----------------------------------------------- | --------------------------------- |
+| 文件抽象   | 一切皆文件,fd 为索引                                 | 无 fd 概念,直接操作存储/寄存器                 | 同 I.MX6ULL                       |
+| 打开方式   | `open` 返回 fd                                        | 无;用 FatFs 的 `f_open()` 返回 `FIL`           | `open` 返回 fd                    |
+| 读写接口   | `read`/`write`(系统调用)                            | `f_read`/`f_write`(库函数,操作 FILE)         | `read`/`write`                    |
+| 偏移定位   | `lseek`                                               | `f_lseek`                                       | `lseek`                           |
+| 缓冲       | read/write 无用户缓冲;内核有 page cache              | 通常无内核缓冲,由 FatFs 自管                   | 同 I.MX6ULL                       |
+| 并发与原子 | 多进程共享文件,存在竞争冒险;`O_APPEND`/`pread` 解决 | 一般单任务,竞争问题多由 RTOS 信号量/临界区解决 | 同 I.MX6ULL                       |
+| dup/重定向 | `dup`/`dup2`/`fcntl`                                  | 无此概念                                        | 同 I.MX6ULL                       |
+| 权限模型   | 有 uid/gid/rwx,`open` 受权限约束                     | 无文件权限概念                                  | 同 I.MX6ULL                       |
+| 设备访问   | 通过 `/dev/xxx` + `ioctl`                             | 直接读写寄存器或 HAL 库                         | 通过 `/dev/xxx` + `ioctl`         |
+| 32/64 位   | 32 位(`off_t` 注意大文件)                           | 通常 32 位,无文件系统或 FatFs                  | 64 位,注意 `off_t`/`size_t` 宽度 |
 
 要点:
 

+ 177 - 164
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/01-Linux应用编程基础/03-标准IO库.md

@@ -1,6 +1,19 @@
 ---
 title: 标准 IO 库
-tags: [嵌入式Linux, Linux应用编程, 标准IO, FILE, fopen, fread, fwrite, 格式化IO, stdio缓冲, fileno, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    Linux应用编程,
+    标准IO,
+    FILE,
+    fopen,
+    fread,
+    fwrite,
+    格式化IO,
+    stdio缓冲,
+    fileno,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第四章 标准I/O库"
@@ -28,13 +41,13 @@ pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第四章 标准I/O
 
 ### 1.1 标准 I/O 与文件 I/O 的区别
 
-| 对比项 | 标准 I/O | 文件 I/O |
-| ------ | -------- | -------- |
-| 性质 | 标准 C 库函数 | Linux 系统调用 |
-| 实现 | 由文件 I/O 封装而来,内部调用文件 I/O | 内核直接提供 |
-| 操作对象 | FILE 指针 | 文件描述符 fd |
-| 可移植性 | 更高,不同操作系统接口几乎一致 | 不同操作系统差异大 |
-| 缓冲 | 用户空间有 stdio 缓冲区,带缓存,性能效率更优 | 用户空间不带缓存 |
+| 对比项   | 标准 I/O                                      | 文件 I/O           |
+| -------- | --------------------------------------------- | ------------------ |
+| 性质     | 标准 C 库函数                                 | Linux 系统调用     |
+| 实现     | 由文件 I/O 封装而来,内部调用文件 I/O         | 内核直接提供       |
+| 操作对象 | FILE 指针                                     | 文件描述符 fd      |
+| 可移植性 | 更高,不同操作系统接口几乎一致                | 不同操作系统差异大 |
+| 缓冲     | 用户空间有 stdio 缓冲区,带缓存,性能效率更优 | 用户空间不带缓存   |
 
 ```mermaid
 flowchart LR
@@ -60,11 +73,11 @@ flowchart LR
 
 ## 3. 标准输入、标准输出与标准错误
 
-| 设备 | 含义 | 通常对应 |
-| ---- | ---- | -------- |
-| 标准输入 | 系统标准输入设备 | 键盘 |
-| 标准输出 | 输出正常信息的设备 | 显示器 |
-| 标准错误 | 显示错误信息的设备 | 显示器 |
+| 设备     | 含义               | 通常对应 |
+| -------- | ------------------ | -------- |
+| 标准输入 | 系统标准输入设备   | 键盘     |
+| 标准输出 | 输出正常信息的设备 | 显示器   |
+| 标准错误 | 显示错误信息的设备 | 显示器   |
 
 用户通过标准输入与系统交互:进程从标准输入(stdin)得到输入数据,把正常输出(如 `printf` 打印的字符串)输出到标准输出(stdout),把错误信息输出到标准错误(stderr)。标准输出和标准错误都对应终端屏幕,标准输入对应键盘。
 
@@ -102,23 +115,23 @@ extern struct _IO_FILE *stderr;   /* Standard error stream.  */
 FILE *fopen(const char *path, const char *mode);
 ```
 
-| 参数 | 说明 |
-| ---- | ---- |
+| 参数   | 说明                             |
+| ------ | -------------------------------- |
 | `path` | 文件路径,可为绝对路径或相对路径 |
-| `mode` | 字符串,指定对该文件的读写权限 |
+| `mode` | 字符串,指定对该文件的读写权限   |
 
 **返回值**:成功返回指向 FILE 类型对象的指针,与打开/创建的文件关联,后续标准 I/O 操作围绕它进行;失败返回 `NULL` 并设置 `errno`。
 
 ### 4.1 mode 参数
 
-| mode | 说明 | 对应 open() 的 flags |
-| ---- | ---- | -------------------- |
-| `r` | 以只读方式打开文件 | `O_RDONLY` |
-| `r+` | 以可读、可写方式打开文件 | `O_RDWR` |
-| `w` | 以只写方式打开;文件存在则截断为 0,不存在则创建 | `O_WRONLY \| O_CREAT \| O_TRUNC` |
-| `w+` | 以可读、可写方式打开;文件存在则截断为 0,不存在则创建 | `O_RDWR \| O_CREAT \| O_TRUNC` |
-| `a` | 以只写方式打开,追加内容(文件末尾写入),不存在则创建 | `O_WRONLY \| O_CREAT \| O_APPEND` |
-| `a+` | 以可读、可写方式打开,追加方式写入,不存在则创建 | `O_RDWR \| O_CREAT \| O_APPEND` |
+| mode | 说明                                                   | 对应 open() 的 flags              |
+| ---- | ------------------------------------------------------ | --------------------------------- |
+| `r`  | 以只读方式打开文件                                     | `O_RDONLY`                        |
+| `r+` | 以可读、可写方式打开文件                               | `O_RDWR`                          |
+| `w`  | 以只写方式打开;文件存在则截断为 0,不存在则创建       | `O_WRONLY \| O_CREAT \| O_TRUNC`  |
+| `w+` | 以可读、可写方式打开;文件存在则截断为 0,不存在则创建 | `O_RDWR \| O_CREAT \| O_TRUNC`    |
+| `a`  | 以只写方式打开,追加内容(文件末尾写入),不存在则创建 | `O_WRONLY \| O_CREAT \| O_APPEND` |
+| `a+` | 以可读、可写方式打开,追加方式写入,不存在则创建       | `O_RDWR \| O_CREAT \| O_APPEND`   |
 
 ### 4.2 新建文件的权限
 
@@ -144,8 +157,8 @@ fopen(path, "w");    /* 只写打开,长度截断为 0,不存在则创建 */
 int fclose(FILE *stream);
 ```
 
-| 参数 | 说明 |
-| ---- | ---- |
+| 参数     | 说明          |
+| -------- | ------------- |
 | `stream` | FILE 类型指针 |
 
 **返回值**:成功返回 0;失败返回 `EOF`(也就是 -1),并设置 `errno`。
@@ -165,23 +178,23 @@ size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
 
 ### 5.1 fread()
 
-| 参数 | 说明 |
-| ---- | ---- |
-| `ptr` | 存放读取数据的缓冲区 |
-| `size` | 每个数据项的字节大小 |
-| `nmemb` | 读取数据项的个数(总共读取 `nmemb * size` 字节) |
-| `stream` | FILE 指针 |
+| 参数     | 说明                                             |
+| -------- | ------------------------------------------------ |
+| `ptr`    | 存放读取数据的缓冲区                             |
+| `size`   | 每个数据项的字节大小                             |
+| `nmemb`  | 读取数据项的个数(总共读取 `nmemb * size` 字节) |
+| `stream` | FILE 指针                                        |
 
 **返回值**:成功返回读取到的数据项数目(不一定等于字节数,除非 `size == 1`);若发生错误或到达文件末尾,返回值小于 `nmemb`。`fread()` 本身**无法区分**是错误还是文件末尾,需用 `ferror()` 或 `feof()` 判断。
 
 ### 5.2 fwrite()
 
-| 参数 | 说明 |
-| ---- | ---- |
-| `ptr` | 待写入数据的缓冲区 |
-| `size` | 每个数据项的字节大小 |
-| `nmemb` | 写入数据项的个数 |
-| `stream` | FILE 指针 |
+| 参数     | 说明                 |
+| -------- | -------------------- |
+| `ptr`    | 待写入数据的缓冲区   |
+| `size`   | 每个数据项的字节大小 |
+| `nmemb`  | 写入数据项的个数     |
+| `stream` | FILE 指针            |
 
 **返回值**:成功返回写入的数据项数目(不一定等于字节数,除非 `size == 1`);出错时返回值小于 `nmemb`(或等于 0)。
 
@@ -285,12 +298,12 @@ int getchar(void);                             /* 等价于 fgetc(stdin) */
 int putchar(int c);                            /* 等价于 fputc(c, stdout) */
 ```
 
-| 函数 | 行为要点 |
-| ---- | -------- |
-| `fgetc` | 每次读 1 字节;文件末尾或出错返回 `EOF`,用 `feof`/`ferror` 区分 |
-| `fputc` | 写 1 字节;失败返回 `EOF` |
+| 函数    | 行为要点                                                                                         |
+| ------- | ------------------------------------------------------------------------------------------------ |
+| `fgetc` | 每次读 1 字节;文件末尾或出错返回 `EOF`,用 `feof`/`ferror` 区分                                 |
+| `fputc` | 写 1 字节;失败返回 `EOF`                                                                        |
 | `fgets` | 最多读 `size - 1` 个字符,遇到换行符或 EOF 停止;**会把换行符保留在缓冲区中**,并自动追加 `'\0'` |
-| `fputs` | 输出字符串直到 `'\0'`,**不会自动加换行符**,这一点不像 `puts` |
+| `fputs` | 输出字符串直到 `'\0'`,**不会自动加换行符**,这一点不像 `puts`                                   |
 
 注意:`fgets` 与 `scanf("%s")` 不同,`fgets` 读取整行(含空格),而 `%s` 遇到空白就停。缓冲区必须足够大,`fgets` 最多写 `size` 字节(含 `'\0'`)。
 
@@ -333,10 +346,10 @@ long ftell(FILE *stream);
 
 ### 7.1 fseek()
 
-| 参数 | 说明 |
-| ---- | ---- |
-| `stream` | FILE 指针 |
-| `offset` | 与 `lseek()` 的 `offset` 意义相同 |
+| 参数     | 说明                                                                  |
+| -------- | --------------------------------------------------------------------- |
+| `stream` | FILE 指针                                                             |
+| `offset` | 与 `lseek()` 的 `offset` 意义相同                                     |
 | `whence` | 与 `lseek()` 的 `whence` 意义相同(`SEEK_SET`/`SEEK_CUR`/`SEEK_END`) |
 
 **返回值**:成功返回 **0**;出错返回 **-1** 并设置 `errno`。**注意与 `lseek()` 不同**:`lseek()` 成功返回当前位置偏移量。
@@ -516,13 +529,13 @@ int sprintf(char *buf, const char *format, ...);
 int snprintf(char *buf, size_t size, const char *format, ...);
 ```
 
-| 函数 | 输出目标 | 说明 |
-| ---- | -------- | ---- |
-| `printf` | 标准输出 | 将格式化数据写入标准输出 |
-| `fprintf` | FILE 指针指定文件 | 用 FILE 指针指定 |
-| `dprintf` | 文件描述符指定文件 | 用 fd 指定 |
-| `sprintf` | 用户缓冲区 `buf` | 有缓冲区溢出风险 |
-| `snprintf` | 用户缓冲区 `buf` | 用 `size` 显式指定缓冲区大小,超出部分丢弃,更安全 |
+| 函数       | 输出目标           | 说明                                               |
+| ---------- | ------------------ | -------------------------------------------------- |
+| `printf`   | 标准输出           | 将格式化数据写入标准输出                           |
+| `fprintf`  | FILE 指针指定文件  | 用 FILE 指针指定                                   |
+| `dprintf`  | 文件描述符指定文件 | 用 fd 指定                                         |
+| `sprintf`  | 用户缓冲区 `buf`   | 有缓冲区溢出风险                                   |
+| `snprintf` | 用户缓冲区 `buf`   | 用 `size` 显式指定缓冲区大小,超出部分丢弃,更安全 |
 
 用法示例:
 
@@ -556,42 +569,42 @@ snprintf(buf, sizeof(buf), "%d", 100);
 
 **type 类型**
 
-| 字符 | 数据类型 | 含义 | 示例 |
-| ---- | -------- | ---- | ---- |
-| `d`/`i` | `int` | 有符号十进制整数(`i` 是老式写法) | `printf("%d\n", 123)` → `123` |
-| `o` | `unsigned int` | 无符号八进制整数(默认不输出前缀 0,可用 `#`) | `printf("%o\n", 123)` → `173` |
-| `u` | `unsigned int` | 无符号十进制整数 | `printf("%u\n", 123)` → `123` |
-| `x`/`X` | `unsigned int` | 无符号十六进制整数,`x`/`X` 区分字母大小写 | `%x`→`7b`;`%X`→`7B` |
-| `f`/`F` | `double` | 浮点数,默认保留小数点后 6 位 | `printf("%f\n", 520.1314)` → `520.131400` |
-| `e`/`E` | `double` | 科学计数法表示浮点数 | `printf("%e\n", 520.1314)` → `5.201314e+02` |
-| `g`/`G` | `double` | 根据数值长度选最短方式输出(`%f`/`%e`) | `%g`→`1.23e-07  0.123` |
-| `s` | `char *` | 字符串,输出到终止字符 `'\0'` | `printf("%s\n", "Hello World")` |
-| `p` | `void *` | 十六进制表示的指针 | `printf("%p\n", ptr)` → `0x400624` |
-| `c` | `char` | 字符,把输入数字按 ASCII 转成字符 | `printf("%c\n", 64)` → `A` |
+| 字符    | 数据类型       | 含义                                           | 示例                                        |
+| ------- | -------------- | ---------------------------------------------- | ------------------------------------------- |
+| `d`/`i` | `int`          | 有符号十进制整数(`i` 是老式写法)             | `printf("%d\n", 123)` → `123`               |
+| `o`     | `unsigned int` | 无符号八进制整数(默认不输出前缀 0,可用 `#`) | `printf("%o\n", 123)` → `173`               |
+| `u`     | `unsigned int` | 无符号十进制整数                               | `printf("%u\n", 123)` → `123`               |
+| `x`/`X` | `unsigned int` | 无符号十六进制整数,`x`/`X` 区分字母大小写     | `%x`→`7b`;`%X`→`7B`                        |
+| `f`/`F` | `double`       | 浮点数,默认保留小数点后 6 位                  | `printf("%f\n", 520.1314)` → `520.131400`   |
+| `e`/`E` | `double`       | 科学计数法表示浮点数                           | `printf("%e\n", 520.1314)` → `5.201314e+02` |
+| `g`/`G` | `double`       | 根据数值长度选最短方式输出(`%f`/`%e`)        | `%g`→`1.23e-07  0.123`                      |
+| `s`     | `char *`       | 字符串,输出到终止字符 `'\0'`                  | `printf("%s\n", "Hello World")`             |
+| `p`     | `void *`       | 十六进制表示的指针                             | `printf("%p\n", ptr)` → `0x400624`          |
+| `c`     | `char`         | 字符,把输入数字按 ASCII 转成字符              | `printf("%c\n", 64)` → `A`                  |
 
 **flags 标志**
 
-| 字符 | 名称 | 作用 |
-| ---- | ---- | ---- |
-| `#` | 井号 | `o` 时输出前缀 0;`x`/`X` 时输出前缀 `0x`/`0X`;浮点时强制输出小数点;`g`/`G` 保留尾部 0 |
-| `0` | 数字 0 | `type` 不为 `c`/`s`(即输出数字)时,在前面补 0 直到占满最小输出宽度;不指定则默认用空格补齐 |
-| `-` | 减号 | 默认右对齐;使用后变左对齐、在右边填空格;同时指定 `0` 和 `-` 时 `-` 覆盖 `0` |
-| `' '` | 空格 | 输出正数时前面加一个空格,负数时加负号 |
-| `+` | 加号 | 输出的数字前面都带符号(正 `+`、负 `-`);同时指定 `+` 和空格时 `+` 覆盖空格 |
+| 字符  | 名称   | 作用                                                                                         |
+| ----- | ------ | -------------------------------------------------------------------------------------------- |
+| `#`   | 井号   | `o` 时输出前缀 0;`x`/`X` 时输出前缀 `0x`/`0X`;浮点时强制输出小数点;`g`/`G` 保留尾部 0     |
+| `0`   | 数字 0 | `type` 不为 `c`/`s`(即输出数字)时,在前面补 0 直到占满最小输出宽度;不指定则默认用空格补齐 |
+| `-`   | 减号   | 默认右对齐;使用后变左对齐、在右边填空格;同时指定 `0` 和 `-` 时 `-` 覆盖 `0`                |
+| `' '` | 空格   | 输出正数时前面加一个空格,负数时加负号                                                       |
+| `+`   | 加号   | 输出的数字前面都带符号(正 `+`、负 `-`);同时指定 `+` 和空格时 `+` 覆盖空格                 |
 
 **width 最小输出宽度**
 
-| 取值 | 说明 | 示例 |
-| ---- | ---- | ---- |
-| 数字 | 十进制正数 | `printf("%06d", 1000)` → `001000` |
-| `*` | 星号,宽度由参数列表指定 | `printf("%0*d", 6, 1000)` → `001000` |
+| 取值 | 说明                     | 示例                                 |
+| ---- | ------------------------ | ------------------------------------ |
+| 数字 | 十进制正数               | `printf("%06d", 1000)` → `001000`    |
+| `*`  | 星号,宽度由参数列表指定 | `printf("%0*d", 6, 1000)` → `001000` |
 
 **precision 精度**(以点号 `.` 开头)
 
-| 取值 | 说明 |
-| ---- | ---- |
-| 数字 | 十进制正数 |
-| `*` | 星号,精度由参数列表指定;如 `printf("%.*s\n", 5, "hello world")` → `hello` |
+| 取值 | 说明                                                                        |
+| ---- | --------------------------------------------------------------------------- |
+| 数字 | 十进制正数                                                                  |
+| `*`  | 星号,精度由参数列表指定;如 `printf("%.*s\n", 5, "hello world")` → `hello` |
 
 对不同类型的含义:
 
@@ -602,17 +615,17 @@ snprintf(buf, sizeof(buf), "%d", 100);
 
 **length 长度修饰符**(与 type 组合确定不同长度类型)
 
-| length | `d`、`i` | `u`、`o`、`x`、`X` | `f`、`F`、`e`、`E`、`g`、`G` | `c` | `s` | `p` |
-| ------ | -------- | ------------------ | ----------------------------- | --- | --- | --- |
-| 无 | `int` | `unsigned int` | `double` | `char` | `char *` | `void *` |
-| `hh` | `signed char` | `unsigned char` | | | | |
-| `h` | `short int` | `unsigned short int` | | | | |
-| `l` | `long int` | `unsigned long int` | | `wint_t` | `wchar_t` | |
-| `ll` | `long long int` | `unsigned long long int` | | | | |
-| `L` | | | `long double` | | | |
-| `j` | `intmax_t` | `uintmax_t` | | | | |
-| `z` | `size_t` | `ssize_t` | | | | |
-| `t` | `ptrdiff_t` | `ptrdiff_t` | | | | |
+| length | `d`、`i`        | `u`、`o`、`x`、`X`       | `f`、`F`、`e`、`E`、`g`、`G` | `c`      | `s`       | `p`      |
+| ------ | --------------- | ------------------------ | ---------------------------- | -------- | --------- | -------- |
+| 无     | `int`           | `unsigned int`           | `double`                     | `char`   | `char *`  | `void *` |
+| `hh`   | `signed char`   | `unsigned char`          |                              |          |           |          |
+| `h`    | `short int`     | `unsigned short int`     |                              |          |           |          |
+| `l`    | `long int`      | `unsigned long int`      |                              | `wint_t` | `wchar_t` |          |
+| `ll`   | `long long int` | `unsigned long long int` |                              |          |           |          |
+| `L`    |                 |                          | `long double`                |          |           |          |
+| `j`    | `intmax_t`      | `uintmax_t`              |                              |          |           |          |
+| `z`    | `size_t`        | `ssize_t`                |                              |          |           |          |
+| `t`    | `ptrdiff_t`     | `ptrdiff_t`              |                              |          |           |          |
 
 ```c
 printf("%hd\n", 12345);    /* 以 short int 类型转换 */
@@ -662,11 +675,11 @@ int fscanf(FILE *stream, const char *format, ...);
 int sscanf(const char *str, const char *format, ...);
 ```
 
-| 函数 | 数据来源 | 说明 |
-| ---- | -------- | ---- |
-| `scanf` | 标准输入 | 将用户输入的数据格式化转换 |
+| 函数     | 数据来源          | 说明                                                    |
+| -------- | ----------------- | ------------------------------------------------------- |
+| `scanf`  | 标准输入          | 将用户输入的数据格式化转换                              |
 | `fscanf` | FILE 指针指定文件 | 从文件中读取并转换;`fscanf(stdin, ...)` 等价于 `scanf` |
-| `sscanf` | 字符串 `str` | 从字符串缓冲区读取并转换 |
+| `sscanf` | 字符串 `str`      | 从字符串缓冲区读取并转换                                |
 
 `scanf` 从 format 最左端开始,每遇到一个转换说明便与下一个输入数据"匹配",匹配则继续,否则结束对后面输入的处理;转换后的数据存入对应的输入地址。每个输入地址都必须有一个转换说明与之一一对应。
 
@@ -701,19 +714,19 @@ free(buf);
 
 **type 类型**
 
-| 字符 | 数据类型 | 含义 | 示例 |
-| ---- | -------- | ---- | ---- |
-| `d` | `int` | 有符号十进制整数 | `scanf("%d", &a)`,输入 `100` |
-| `i` | `int` | 有符号整数,自动识别进制:`0x`/`0X` 开头为 16 进制,`0` 开头为 8 进制,否则 10 进制 | `scanf("%i", &a)`,输入 `0x100` |
-| `o` | `unsigned int` | 无符号八进制整数 | 输入 `0100` |
-| `u` | `unsigned int` | 无符号十进制整数 | 输入 `100` |
-| `x` | `unsigned int` | 无符号十六进制整数,数字以 `0x` 开头 | 输入 `0x100` |
-| `X` | `unsigned int` | 无符号十六进制整数,数字以 `0X` 开头 | 输入 `0X100` |
-| `f`/`e`/`E`/`g`/`a` | `float` | 带符号浮点数 | `scanf("%f", &a)`,输入 `10.123` |
-| `s` | `char *` | 匹配字符串,不匹配空白字符(空格、制表符、换行符),空白作为分隔符;会自动加 `'\0'`,缓冲区必须足够大 | `scanf("%s", buf)` |
-| `c` | `char` | 匹配一个字符 | `scanf("%c", &c)` |
-| `p` | `void *` | 匹配一个指针值 | `scanf("%p", &a)` |
-| `[` | `char *` | 匹配一组字符集合,以字符串形式存储 | `scanf("%[a-z0-9]", buf)` |
+| 字符                | 数据类型       | 含义                                                                                                  | 示例                             |
+| ------------------- | -------------- | ----------------------------------------------------------------------------------------------------- | -------------------------------- |
+| `d`                 | `int`          | 有符号十进制整数                                                                                      | `scanf("%d", &a)`,输入 `100`    |
+| `i`                 | `int`          | 有符号整数,自动识别进制:`0x`/`0X` 开头为 16 进制,`0` 开头为 8 进制,否则 10 进制                   | `scanf("%i", &a)`,输入 `0x100`  |
+| `o`                 | `unsigned int` | 无符号八进制整数                                                                                      | 输入 `0100`                      |
+| `u`                 | `unsigned int` | 无符号十进制整数                                                                                      | 输入 `100`                       |
+| `x`                 | `unsigned int` | 无符号十六进制整数,数字以 `0x` 开头                                                                  | 输入 `0x100`                     |
+| `X`                 | `unsigned int` | 无符号十六进制整数,数字以 `0X` 开头                                                                  | 输入 `0X100`                     |
+| `f`/`e`/`E`/`g`/`a` | `float`        | 带符号浮点数                                                                                          | `scanf("%f", &a)`,输入 `10.123` |
+| `s`                 | `char *`       | 匹配字符串,不匹配空白字符(空格、制表符、换行符),空白作为分隔符;会自动加 `'\0'`,缓冲区必须足够大 | `scanf("%s", buf)`               |
+| `c`                 | `char`         | 匹配一个字符                                                                                          | `scanf("%c", &c)`                |
+| `p`                 | `void *`       | 匹配一个指针值                                                                                        | `scanf("%p", &a)`                |
+| `[`                 | `char *`       | 匹配一组字符集合,以字符串形式存储                                                                    | `scanf("%[a-z0-9]", buf)`        |
 
 `[` 的规则:`%[a-z]` 匹配 a 到 z(含两端);`%[0-9]` 匹配数字;减号是连字符,放在两个字符之间不参与匹配;要匹配减号可放中括号旁,如 `%[-a-z]`;要排除字符用 `^` 放最前,如 `%[^a-z-]` 匹配除 a 到 z 及 `-` 之外的字符。
 
@@ -727,14 +740,14 @@ scanf("%4s", buf);   /* 字符串不超过 4 个字符;输入 abcdefg,只取
 
 **length 长度修饰符**
 
-| length | `d`、`i` | `u`、`o`、`x`、`X` | `e`、`f`、`g` | `c` | `s` |
-| ------ | -------- | ------------------ | ------------- | --- | --- |
-| 无 | `int` | `unsigned int` | `float` | `char` | `char *` |
-| `h` | `short int` | `unsigned short int` | | | |
-| `hh` | `signed char` | `unsigned char` | | | |
-| `j` | `intmax_t` | `uintmax_t` | | | |
-| `l` | `long int` | `unsigned long int` | `double` | `wchar_t` | `wchar_t *` |
-| `L` | `long long int` | `unsigned long long int` | `long double` | | |
+| length | `d`、`i`        | `u`、`o`、`x`、`X`       | `e`、`f`、`g` | `c`       | `s`         |
+| ------ | --------------- | ------------------------ | ------------- | --------- | ----------- |
+| 无     | `int`           | `unsigned int`           | `float`       | `char`    | `char *`    |
+| `h`    | `short int`     | `unsigned short int`     |               |           |             |
+| `hh`   | `signed char`   | `unsigned char`          |               |           |             |
+| `j`    | `intmax_t`      | `uintmax_t`              |               |           |             |
+| `l`    | `long int`      | `unsigned long int`      | `double`      | `wchar_t` | `wchar_t *` |
+| `L`    | `long long int` | `unsigned long long int` | `long double` |           |             |
 
 ```c
 scanf("%hd", var);    /* short int */
@@ -1006,18 +1019,18 @@ time ./normal_io     # 约 0.13~0.14 秒,只把数据拷到内核缓冲区
 int setvbuf(FILE *stream, char *buf, int mode, size_t size);
 ```
 
-| 参数 | 说明 |
-| ---- | ---- |
-| `stream` | FILE 指针,指定对应文件;每个文件都可设置自己的 stdio 缓冲区 |
-| `buf` | 不为 `NULL` 时指向 `size` 大小的内存区域作为缓冲区,应以动态或静态方式分配在堆上,**不要用栈上局部变量**;为 `NULL` 时 stdio 库自动分配(除非 `mode` 为非缓冲模式) |
-| `mode` | 缓冲区类型,见下表 |
-| `size` | 缓冲区大小 |
+| 参数     | 说明                                                                                                                                                                |
+| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `stream` | FILE 指针,指定对应文件;每个文件都可设置自己的 stdio 缓冲区                                                                                                        |
+| `buf`    | 不为 `NULL` 时指向 `size` 大小的内存区域作为缓冲区,应以动态或静态方式分配在堆上,**不要用栈上局部变量**;为 `NULL` 时 stdio 库自动分配(除非 `mode` 为非缓冲模式) |
+| `mode`   | 缓冲区类型,见下表                                                                                                                                                  |
+| `size`   | 缓冲区大小                                                                                                                                                          |
 
-| mode | 名称 | 行为 |
-| ---- | ---- | ---- |
+| mode     | 名称   | 行为                                                                                                              |
+| -------- | ------ | ----------------------------------------------------------------------------------------------------------------- |
 | `_IONBF` | 无缓冲 | 每个标准 I/O 函数立即调用 `write()`/`read()`,忽略 `buf` 和 `size`。**stderr 默认属于此类**,保证错误信息立即输出 |
-| `_IOLBF` | 行缓冲 | 输入或输出中遇到换行符 `'\n'` 时,标准 I/O 才执行文件 I/O 操作。**终端设备默认采用行缓冲**,如标准输入、标准输出 |
-| `_IOFBF` | 全缓冲 | 填满 stdio 缓冲区后才进行文件 I/O 操作。**默认普通磁盘上的常规文件常用这种模式** |
+| `_IOLBF` | 行缓冲 | 输入或输出中遇到换行符 `'\n'` 时,标准 I/O 才执行文件 I/O 操作。**终端设备默认采用行缓冲**,如标准输入、标准输出  |
+| `_IOFBF` | 全缓冲 | 填满 stdio 缓冲区后才进行文件 I/O 操作。**默认普通磁盘上的常规文件常用这种模式**                                  |
 
 **返回值**:成功返回 0,失败返回非 0 值并设置 `errno`。
 
@@ -1178,9 +1191,9 @@ int fileno(FILE *stream);
 FILE *fdopen(int fd, const char *mode);
 ```
 
-| 函数 | 方向 | 说明 |
-| ---- | ---- | ---- |
-| `fileno()` | FILE 指针 → 文件描述符 | 根据 FILE 指针得到整数 fd;转换错误返回 -1 并设置 `errno`。得到 fd 后即可用 `read`/`write`/`lseek`/`fcntl` 等操作文件 |
+| 函数       | 方向                   | 说明                                                                                                                                                    |
+| ---------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `fileno()` | FILE 指针 → 文件描述符 | 根据 FILE 指针得到整数 fd;转换错误返回 -1 并设置 `errno`。得到 fd 后即可用 `read`/`write`/`lseek`/`fcntl` 等操作文件                                   |
 | `fdopen()` | 文件描述符 → FILE 指针 | 给定 fd 得到对应 FILE 指针,之后可用 `fread`/`fwrite` 等标准 I/O 操作。`mode` 与 `fopen()` 的 `mode` 含义相同;若与 fd 的访问模式不一致,会导致调用失败 |
 
 ### 11.1 混合使用的缓冲陷阱
@@ -1221,33 +1234,33 @@ cat test_file          # 查看写入内容
 
 ### 12.2 关键实验清单
 
-| 实验 | 命令/源码 | 观察点 |
-| ---- | --------- | ------ |
-| fwrite 写文件 | fwrite 示例 | `test_file` 内容为 `Hello World!` |
-| fread 读文件 | fread 示例 | 返回实际读取字节数;末尾时 `ferror` 为假 |
-| fseek+ftell 求大小 | 求大小示例 | 结果与 `ls -l` 一致 |
-| 行缓冲 | 两个 `printf` + 死循环 | 只有带 `\n` 的输出 |
-| 无缓冲 | `setvbuf(stdout, NULL, _IONBF, 0)` | 两行都输出 |
-| fflush | 第二个 `printf` 后 `fflush(stdout)` | 两行都输出 |
-| fclose 刷新 | 第二个 `printf` 后 `fclose(stdout)` | 两行都输出 |
-| 程序退出刷新 | 去掉死循环,正常 return | 两行都输出 |
-| `_exit` 不刷新 | 第二个 `printf` 后 `_exit(0)` | 第二行**不**输出 |
-| 直接 I/O 对齐 | 去掉 4096 对齐/改偏移/改大小 | `write` 报 `Invalid argument` |
-| 混合缓冲 | `printf("print"); write(...)` | 先 `write` 后 `print` |
+| 实验               | 命令/源码                           | 观察点                                   |
+| ------------------ | ----------------------------------- | ---------------------------------------- |
+| fwrite 写文件      | fwrite 示例                         | `test_file` 内容为 `Hello World!`        |
+| fread 读文件       | fread 示例                          | 返回实际读取字节数;末尾时 `ferror` 为假 |
+| fseek+ftell 求大小 | 求大小示例                          | 结果与 `ls -l` 一致                      |
+| 行缓冲             | 两个 `printf` + 死循环              | 只有带 `\n` 的输出                       |
+| 无缓冲             | `setvbuf(stdout, NULL, _IONBF, 0)`  | 两行都输出                               |
+| fflush             | 第二个 `printf` 后 `fflush(stdout)` | 两行都输出                               |
+| fclose 刷新        | 第二个 `printf` 后 `fclose(stdout)` | 两行都输出                               |
+| 程序退出刷新       | 去掉死循环,正常 return             | 两行都输出                               |
+| `_exit` 不刷新     | 第二个 `printf` 后 `_exit(0)`       | 第二行**不**输出                         |
+| 直接 I/O 对齐      | 去掉 4096 对齐/改偏移/改大小        | `write` 报 `Invalid argument`            |
+| 混合缓冲           | `printf("print"); write(...)`       | 先 `write` 后 `print`                    |
 
 ### 12.3 调试方法
 
-| 现象 | 可能原因 | 排查手段 |
-| ---- | -------- | -------- |
-| `fopen` 返回 `NULL` | 路径/权限/模式不匹配 | `perror("fopen error")`;`ls -l` |
-| `fread` 返回小于 `nmemb` | 到末尾或出错 | `feof(fp)` / `ferror(fp)` 区分 |
-| 输出"少了一行" | stdio 行缓冲,无换行符 | 补 `\n`、`fflush()` 或改缓冲模式 |
-| `O_DIRECT` 未定义 | 未定义 `_GNU_SOURCE` | `#define _GNU_SOURCE` 或 `gcc -D_GNU_SOURCE` |
-| 直接 I/O 报 `Invalid argument` | 三个对齐条件未满足 | 检查缓冲区地址、偏移量、数据大小是否块大小整数倍 |
-| 程序退出后数据才出现 | 退出时自动刷新 stdio | 用 `fflush`/`fclose` 控制时机 |
-| `_exit` 后数据丢失 | `_exit` 不刷新 stdio 缓冲 | 改用 `exit()` 或先 `fflush()` |
-| 混合读写顺序错乱 | stdio 缓冲与文件 I/O 不同步 | 切换前 `fflush()` |
-| `fileno`/`fdopen` 失败 | 模式不匹配或参数非法 | 检查 `mode` 与 fd 访问模式是否一致 |
+| 现象                           | 可能原因                    | 排查手段                                         |
+| ------------------------------ | --------------------------- | ------------------------------------------------ |
+| `fopen` 返回 `NULL`            | 路径/权限/模式不匹配        | `perror("fopen error")`;`ls -l`                 |
+| `fread` 返回小于 `nmemb`       | 到末尾或出错                | `feof(fp)` / `ferror(fp)` 区分                   |
+| 输出"少了一行"                 | stdio 行缓冲,无换行符      | 补 `\n`、`fflush()` 或改缓冲模式                 |
+| `O_DIRECT` 未定义              | 未定义 `_GNU_SOURCE`        | `#define _GNU_SOURCE` 或 `gcc -D_GNU_SOURCE`     |
+| 直接 I/O 报 `Invalid argument` | 三个对齐条件未满足          | 检查缓冲区地址、偏移量、数据大小是否块大小整数倍 |
+| 程序退出后数据才出现           | 退出时自动刷新 stdio        | 用 `fflush`/`fclose` 控制时机                    |
+| `_exit` 后数据丢失             | `_exit` 不刷新 stdio 缓冲   | 改用 `exit()` 或先 `fflush()`                    |
+| 混合读写顺序错乱               | stdio 缓冲与文件 I/O 不同步 | 切换前 `fflush()`                                |
+| `fileno`/`fdopen` 失败         | 模式不匹配或参数非法        | 检查 `mode` 与 fd 访问模式是否一致               |
 
 常用工具:
 
@@ -1264,16 +1277,16 @@ stdbuf -o0 ./testApp                          # 不改代码,强制标准输
 
 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
 
-| 维度 | I.MX6ULL | STM32(裸机/RTOS) | RK3568 |
-| ---- | -------- | ------------------ | ------ |
-| 标准 I/O 支持 | 完整 glibc,`<stdio.h>` 全部可用 | 部分工具链有 `printf`,但通常无文件流;FatFs 提供 `f_open`/`f_read` 等 | 完整 glibc/aarch64,全部可用 |
-| 流对象 | `FILE *`(stdin/stdout/stderr) | 无 `FILE`(裸机);RTOS 下可能有 retarget 的 `printf` | `FILE *` |
-| 重定向 | `freopen`、`dup2` 可把 stdout 重定向到文件/串口 | 通过重写 `_write`/`fputc` 把 `printf` 指向串口 | 同 I.MX6ULL |
-| stdio 缓冲 | 有,可 `setvbuf` 控制 | 通常无缓冲或简单缓冲 | 有,可 `setvbuf` |
-| 内核缓冲/page cache | 有,可 `fsync`/`O_DIRECT` | 无此概念 | 有,`fsync`/`O_DIRECT` |
-| 行缓冲 | 终端默认行缓冲 | 无终端流概念 | 同 I.MX6ULL |
-| 浮点格式化 | 完整支持(注意软/硬浮点 ABI 与 `arm-linux-gnueabihf`) | `printf` 浮点常需开启 FPU/完整库;有体积开销 | 完整支持 |
-| 典型问题 | `printf` 不换行不输出、程序退出才刷新 | `printf` 需 retarget 才有输出 | 与 I.MX6ULL 类似 |
+| 维度                | I.MX6ULL                                               | STM32(裸机/RTOS)                                                     | RK3568                       |
+| ------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------- | ---------------------------- |
+| 标准 I/O 支持       | 完整 glibc,`<stdio.h>` 全部可用                       | 部分工具链有 `printf`,但通常无文件流;FatFs 提供 `f_open`/`f_read` 等 | 完整 glibc/aarch64,全部可用 |
+| 流对象              | `FILE *`(stdin/stdout/stderr)                        | 无 `FILE`(裸机);RTOS 下可能有 retarget 的 `printf`                  | `FILE *`                     |
+| 重定向              | `freopen`、`dup2` 可把 stdout 重定向到文件/串口        | 通过重写 `_write`/`fputc` 把 `printf` 指向串口                         | 同 I.MX6ULL                  |
+| stdio 缓冲          | 有,可 `setvbuf` 控制                                  | 通常无缓冲或简单缓冲                                                   | 有,可 `setvbuf`             |
+| 内核缓冲/page cache | 有,可 `fsync`/`O_DIRECT`                              | 无此概念                                                               | 有,`fsync`/`O_DIRECT`       |
+| 行缓冲              | 终端默认行缓冲                                         | 无终端流概念                                                           | 同 I.MX6ULL                  |
+| 浮点格式化          | 完整支持(注意软/硬浮点 ABI 与 `arm-linux-gnueabihf`) | `printf` 浮点常需开启 FPU/完整库;有体积开销                           | 完整支持                     |
+| 典型问题            | `printf` 不换行不输出、程序退出才刷新                  | `printf` 需 retarget 才有输出                                          | 与 I.MX6ULL 类似             |
 
 要点:
 

+ 127 - 116
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/01-Linux应用编程基础/04-文件属性与目录.md

@@ -1,6 +1,17 @@
 ---
 title: 文件属性与目录
-tags: [嵌入式Linux, Linux应用编程, 文件属性, stat, 目录, 权限, 硬链接, 软链接, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    Linux应用编程,
+    文件属性,
+    stat,
+    目录,
+    权限,
+    硬链接,
+    软链接,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第五章 文件属性与目录"
@@ -18,15 +29,15 @@ pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第五章 文件属
 
 Windows 通过后缀识别文件类型;**Linux 不会根据后缀识别文件类型**,后缀只是给人看的,仍应按内容规范命名(`.c` / `.h` / `.sh`)。Linux 下一切皆文件,共 7 种类型,由 `ls -l` 输出第 1 个字符或 `stat` 命令标记:
 
-| 字符 | 文件类型 | 说明 |
-| ---- | -------- | ---- |
-| `-` | 普通文件 regular file | 文本文件、二进制文件、`.o`、可执行文件等 |
-| `d` | 目录文件 directory | 文件夹也是一种文件 |
-| `c` | 字符设备 character | 对应硬件设备,按字节流访问,如串口、按键、LED |
-| `b` | 块设备 block | 对应硬件设备,按块访问,如硬盘、loop 设备 |
-| `l` | 符号链接 symbolic link | 类似 Windows 快捷方式 |
-| `s` | 套接字 socket | 用于网络通信(进程间通信) |
-| `p` | 管道 pipe | 用于进程间通信 |
+| 字符 | 文件类型               | 说明                                          |
+| ---- | ---------------------- | --------------------------------------------- |
+| `-`  | 普通文件 regular file  | 文本文件、二进制文件、`.o`、可执行文件等      |
+| `d`  | 目录文件 directory     | 文件夹也是一种文件                            |
+| `c`  | 字符设备 character     | 对应硬件设备,按字节流访问,如串口、按键、LED |
+| `b`  | 块设备 block           | 对应硬件设备,按块访问,如硬盘、loop 设备     |
+| `l`  | 符号链接 symbolic link | 类似 Windows 快捷方式                         |
+| `s`  | 套接字 socket          | 用于网络通信(进程间通信)                    |
+| `p`  | 管道 pipe              | 用于进程间通信                                |
 
 **普通文件**分两类:**文本文件**(内容为 ASCII 字符,方便人阅读,如 `.c`/`.h`/`.sh`/`.txt`)和**二进制文件**(存真正的数字,如可执行文件、`.o`、`.bin`)。
 
@@ -61,20 +72,20 @@ int fstat(int fd, struct stat *buf);
 int lstat(const char *pathname, struct stat *buf);
 ```
 
-| 函数 | 起点 | 说明 |
-| ---- | ---- | ---- |
-| `stat` | 文件路径 | 无需先打开;若 `pathname` 是符号链接,**解引用**所指向的文件 |
-| `fstat` | 文件描述符 | 使用前先 `open`;若 `fd` 指向符号链接,得到所指文件属性 |
-| `lstat` | 文件路径 | **不解引用**,查阅链接文件本身的属性 |
+| 函数    | 起点       | 说明                                                         |
+| ------- | ---------- | ------------------------------------------------------------ |
+| `stat`  | 文件路径   | 无需先打开;若 `pathname` 是符号链接,**解引用**所指向的文件 |
+| `fstat` | 文件描述符 | 使用前先 `open`;若 `fd` 指向符号链接,得到所指文件属性      |
+| `lstat` | 文件路径   | **不解引用**,查阅链接文件本身的属性                         |
 
 选择原则:文件已打开用 `fstat`;不想打开文件用 `stat`;要查链接本身用 `lstat`。参数:`pathname` 文件路径(或 `fd` 文件描述符),`buf` 是传出参数,指向 `struct stat`。返回值:成功 `0`,失败 `-1` 并设置 `errno`。
 
-| 常见 errno | 含义 | 常见 errno | 含义 |
-| ---------- | ---- | ---------- | ---- |
-| `ENOENT` | 文件不存在 | `ELOOP` | 符号链接层级过多 |
-| `EACCES` | 路径中某目录无搜索权限 | `ENAMETOOLONG` | 路径名过长 |
-| `ENOTDIR` | 路径中某段不是目录 | `EFAULT` | `buf` 非法地址 |
-| `EPERM` | 权限不足 | `EBADF` | `fstat` 的 `fd` 无效 |
+| 常见 errno | 含义                   | 常见 errno     | 含义                 |
+| ---------- | ---------------------- | -------------- | -------------------- |
+| `ENOENT`   | 文件不存在             | `ELOOP`        | 符号链接层级过多     |
+| `EACCES`   | 路径中某目录无搜索权限 | `ENAMETOOLONG` | 路径名过长           |
+| `ENOTDIR`  | 路径中某段不是目录     | `EFAULT`       | `buf` 非法地址       |
+| `EPERM`    | 权限不足               | `EBADF`        | `fstat` 的 `fd` 无效 |
 
 ### 2.1 struct stat 结构体
 
@@ -99,33 +110,33 @@ struct stat
 };
 ```
 
-| 字段 | 含义 | 字段 | 含义 |
-| ---- | ---- | ---- | ---- |
-| `st_dev` | 文件所在设备,不常用 | `st_rdev` | 设备号,仅设备文件使用 |
-| `st_ino` | inode 编号 | `st_size` | 文件逻辑大小,字节 |
-| `st_mode` | 文件类型 + 权限(核心) | `st_blksize` | 存储块大小 |
-| `st_nlink` | 硬链接数 | `st_blocks` | 占用块数 |
-| `st_uid`/`st_gid` | 所有者用户 ID / 组 ID | `st_atim`/`st_mtim`/`st_ctim` | 访问/内容修改/状态改变时间 |
+| 字段              | 含义                    | 字段                          | 含义                       |
+| ----------------- | ----------------------- | ----------------------------- | -------------------------- |
+| `st_dev`          | 文件所在设备,不常用    | `st_rdev`                     | 设备号,仅设备文件使用     |
+| `st_ino`          | inode 编号              | `st_size`                     | 文件逻辑大小,字节         |
+| `st_mode`         | 文件类型 + 权限(核心) | `st_blksize`                  | 存储块大小                 |
+| `st_nlink`        | 硬链接数                | `st_blocks`                   | 占用块数                   |
+| `st_uid`/`st_gid` | 所有者用户 ID / 组 ID   | `st_atim`/`st_mtim`/`st_ctim` | 访问/内容修改/状态改变时间 |
 
 ### 2.2 st_mode 变量
 
 `st_mode` 是 32 位无符号整数,记录文件类型(高 4 bit)与权限(低 12 bit);权限部分与 `open` 的 `mode` 参数一致(S/U/G/O)。
 
-| 权限宏 | 值 | 含义 | 权限宏 | 值 | 含义 |
-| ------ | -- | ---- | ------ | -- | ---- |
-| `S_IRWXU` | `00700` | 所有者 读写执行 | `S_IRGRP` | `00040` | 同组 读 |
-| `S_IRUSR` | `00400` | 所有者 读 | `S_IWGRP` | `00020` | 同组 写 |
-| `S_IWUSR` | `00200` | 所有者 写 | `S_IXGRP` | `00010` | 同组 执行 |
-| `S_IXUSR` | `00100` | 所有者 执行 | `S_IROTH` | `00004` | 其它 读 |
-| `S_IRWXG` | `00070` | 同组 读写执行 | `S_IWOTH` | `00002` | 其它 写 |
-| `S_IRWXO` | `00007` | 其它 读写执行 | `S_IXOTH` | `00001` | 其它 执行 |
-
-| 类型宏 | 值 | 类型 | 类型宏 | 值 | 类型 |
-| ------ | -- | ---- | ------ | -- | ---- |
-| `S_IFSOCK` | `0140000` | 套接字 | `S_IFDIR` | `0040000` | 目录 |
-| `S_IFLNK` | `0120000` | 符号链接 | `S_IFCHR` | `0020000` | 字符设备 |
-| `S_IFREG` | `0100000` | 普通文件 | `S_IFIFO` | `0010000` | 管道 |
-| `S_IFBLK` | `0060000` | 块设备 | `S_IFMT` | `0170000` | 类型字段位掩码 |
+| 权限宏    | 值      | 含义            | 权限宏    | 值      | 含义      |
+| --------- | ------- | --------------- | --------- | ------- | --------- |
+| `S_IRWXU` | `00700` | 所有者 读写执行 | `S_IRGRP` | `00040` | 同组 读   |
+| `S_IRUSR` | `00400` | 所有者 读       | `S_IWGRP` | `00020` | 同组 写   |
+| `S_IWUSR` | `00200` | 所有者 写       | `S_IXGRP` | `00010` | 同组 执行 |
+| `S_IXUSR` | `00100` | 所有者 执行     | `S_IROTH` | `00004` | 其它 读   |
+| `S_IRWXG` | `00070` | 同组 读写执行   | `S_IWOTH` | `00002` | 其它 写   |
+| `S_IRWXO` | `00007` | 其它 读写执行   | `S_IXOTH` | `00001` | 其它 执行 |
+
+| 类型宏     | 值        | 类型     | 类型宏    | 值        | 类型           |
+| ---------- | --------- | -------- | --------- | --------- | -------------- |
+| `S_IFSOCK` | `0140000` | 套接字   | `S_IFDIR` | `0040000` | 目录           |
+| `S_IFLNK`  | `0120000` | 符号链接 | `S_IFCHR` | `0020000` | 字符设备       |
+| `S_IFREG`  | `0100000` | 普通文件 | `S_IFIFO` | `0010000` | 管道           |
+| `S_IFBLK`  | `0060000` | 块设备   | `S_IFMT`  | `0170000` | 类型字段位掩码 |
 
 ```c
 if (st.st_mode & S_IXUSR) { /* 所有者具有执行权限 */ }
@@ -161,10 +172,10 @@ graph LR
 
 Linux 是多用户系统,每个文件关联一个用户(所有者)和一个用户组;系统通过 **UID** / **GID** 识别,而非用户名。
 
-| ID 类型 | 作用 |
-| ------- | ---- |
-| 实际用户 ID / 实际组 ID | 标识进程究竟是谁(执行该程序的用户) |
-| 有效用户 ID / 有效组 ID / 附属组 ID | 用于文件访问权限检查 |
+| ID 类型                             | 作用                                 |
+| ----------------------------------- | ------------------------------------ |
+| 实际用户 ID / 实际组 ID             | 标识进程究竟是谁(执行该程序的用户) |
+| 有效用户 ID / 有效组 ID / 附属组 ID | 用于文件访问权限检查                 |
 
 一般情况下进程的**有效用户 ID = 实际用户 ID**(绝大多数文件没有 set-user-ID 位)。`st_uid` / `st_gid` 记录文件的用户 ID 与组 ID。
 
@@ -177,10 +188,10 @@ uid_t getuid(void);
 gid_t getgid(void);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
+| 参数              | 含义                  |
+| ----------------- | --------------------- |
 | `pathname` / `fd` | 文件路径 / 文件描述符 |
-| `owner` / `group` | 新的用户 ID / 组 ID |
+| `owner` / `group` | 新的用户 ID / 组 ID   |
 
 返回值:成功 `0`,失败 `-1` 并设置 `errno`(如 `EPERM`)。修改限制:只有超级用户进程能更改文件的用户 ID;普通用户可将组 ID 改为自己所从属的任意附属组 ID(前提有效用户 ID 等于文件用户 ID);超级用户可将组 ID 改为任意值。只改其中一个 ID 时,另一个传入文件当前值(先用 `stat` 获取)。`getuid` / `getgid` 返回当前进程的**实际**用户/组 ID,加 `sudo` 时 `getuid()` 返回 0。
 
@@ -194,11 +205,11 @@ gid_t getgid(void);
 
 9 个普通权限位按所有者(U)、同组(G)、其它(O)划分,每类含读(r)、写(w)、执行(x),宏见 2.2 节。3 个特殊权限位:
 
-| 宏 | 值 | 含义 |
-| -- | -- | ---- |
-| `S_ISUID` | `04000` | set-user-ID 位 |
+| 宏        | 值      | 含义            |
+| --------- | ------- | --------------- |
+| `S_ISUID` | `04000` | set-user-ID 位  |
 | `S_ISGID` | `02000` | set-group-ID 位 |
-| `S_ISVTX` | `01000` | Sticky 位 |
+| `S_ISVTX` | `01000` | Sticky 位       |
 
 - 若文件设置了 set-user-ID 位,进程操作该文件时内核会把进程的**有效用户 ID** 设为该文件的用户 ID;
 - 若设置了 set-group-ID 位,内核会把进程的**有效组 ID** 设为该文件的组 ID;
@@ -217,10 +228,10 @@ gid_t getgid(void);
 
 目录拥有与普通文件相同的权限位,但含义不同:
 
-| 目录权限 | 含义 |
-| -------- | ---- |
-| 读(r) | 可列出目录内容(如 `ls`) |
-| 写(w) | 可在目录下创建、删除文件 |
+| 目录权限  | 含义                                     |
+| --------- | ---------------------------------------- |
+| 读(r)   | 可列出目录内容(如 `ls`)                |
+| 写(w)   | 可在目录下创建、删除文件                 |
 | 执行(x) | 可访问目录下的文件(读写执行其中的文件) |
 
 结论:只读无执行时只能看到文件名、无法访问文件属性;有执行无读时知道文件名可访问、但不能列出目录内容;创建/删除文件需要目录的**写 + 执行**权限;要对文件读写执行,除文件本身权限外还需拥有**所在目录的执行权限**。
@@ -240,11 +251,11 @@ int fchmod(int fd, mode_t mode);
 mode_t umask(mode_t mask);
 ```
 
-| 函数 | 说明 | 返回值 |
-| ---- | ---- | ------ |
-| `access` | 检查文件存在性/读/写/执行权限,任一不通过即失败 | 通过 `0`,否则 `-1` |
+| 函数               | 说明                                                          | 返回值              |
+| ------------------ | ------------------------------------------------------------- | ------------------- |
+| `access`           | 检查文件存在性/读/写/执行权限,任一不通过即失败               | 通过 `0`,否则 `-1` |
 | `chmod` / `fchmod` | 修改权限,`mode` 可用八进制或权限宏;链接文件改的是所指向文件 | 成功 `0`,失败 `-1` |
-| `umask` | 设置进程权限掩码,新建文件实际权限 = `mode & ~umask` | 返回旧 umask |
+| `umask`            | 设置进程权限掩码,新建文件实际权限 = `mode & ~umask`          | 返回旧 umask        |
 
 修改权限要么是 root 进程,要么进程有效用户 ID 与文件用户 ID 匹配。umask 是**进程属性**、不能屏蔽特殊权限位、通常继承自父进程。例如 `mode=0777`、`umask=0002` 时实际权限为 `0775`。
 
@@ -252,10 +263,10 @@ mode_t umask(mode_t mask);
 
 ## 5. 文件的时间属性
 
-| 字段 | 说明 |
-| ---- | ---- |
-| `st_atim` | 文件最后被访问的时间(如 `read`) |
-| `st_mtim` | 文件内容最后被修改的时间(如 `write`) |
+| 字段      | 说明                                                        |
+| --------- | ----------------------------------------------------------- |
+| `st_atim` | 文件最后被访问的时间(如 `read`)                           |
+| `st_mtim` | 文件内容最后被修改的时间(如 `write`)                      |
 | `st_ctim` | 文件状态最后被改变的时间(inode 变更,如 `chmod`、`chown`) |
 
 ### 5.1 utime / utimes
@@ -285,12 +296,12 @@ int utimensat(int dirfd, const char *pathname,
               const struct timespec times[2], int flags);
 ```
 
-| `tv_nsec` 取值 | 效果 |
-| -------------- | ---- |
-| `times == NULL` | 两个时间戳都设为当前时间 |
-| `UTIME_NOW` | 该时间戳设为当前时间(忽略 `tv_sec`) |
-| `UTIME_OMIT` | 该时间戳保持不变(忽略 `tv_sec`) |
-| 其它值 | 设置为 `tv_sec` + `tv_nsec` 指定值 |
+| `tv_nsec` 取值  | 效果                                  |
+| --------------- | ------------------------------------- |
+| `times == NULL` | 两个时间戳都设为当前时间              |
+| `UTIME_NOW`     | 该时间戳设为当前时间(忽略 `tv_sec`) |
+| `UTIME_OMIT`    | 该时间戳保持不变(忽略 `tv_sec`)     |
+| 其它值          | 设置为 `tv_sec` + `tv_nsec` 指定值    |
 
 `utimensat` 的 `dirfd` 可为目录 fd 或 `AT_FDCWD`;`flags` 可为 `0` 或 `AT_SYMLINK_NOFOLLOW`(修改链接本身时间戳)。
 
@@ -298,16 +309,16 @@ int utimensat(int dirfd, const char *pathname,
 
 ## 6. 软链接与硬链接
 
-| 对比项 | 硬链接 | 软链接(符号链接) |
-| ------ | ------ | ------------------ |
-| inode | 与源文件**相同** | 与源文件**不同** |
-| 数据块 | 与源文件共享 | 存的是源文件路径 |
-| 关系 | 完全平等 | 主从关系 |
-| 源文件删除 | 仍可访问(链接数减 1) | 变悬空链接,指向无效路径 |
-| 链接数统计 | 计入 `st_nlink` | 不计入 |
-| 对目录 | 不能创建(root+底层支持除外) | 可以 |
-| 跨文件系统 | 要求同文件系统 | 可以 |
-| 目标不存在 | 不允许 | 允许(悬空链接) |
+| 对比项     | 硬链接                        | 软链接(符号链接)       |
+| ---------- | ----------------------------- | ------------------------ |
+| inode      | 与源文件**相同**              | 与源文件**不同**         |
+| 数据块     | 与源文件共享                  | 存的是源文件路径         |
+| 关系       | 完全平等                      | 主从关系                 |
+| 源文件删除 | 仍可访问(链接数减 1)        | 变悬空链接,指向无效路径 |
+| 链接数统计 | 计入 `st_nlink`               | 不计入                   |
+| 对目录     | 不能创建(root+底层支持除外) | 可以                     |
+| 跨文件系统 | 要求同文件系统                | 可以                     |
+| 目标不存在 | 不允许                        | 允许(悬空链接)         |
 
 `ln 源文件 链接文件` 创建硬链接,`ln -s 源文件 链接文件` 创建软链接;源文件本身也算一个硬链接。
 
@@ -318,10 +329,10 @@ int symlink(const char *target, const char *linkpath);   /* 软链接 */
 ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
 ```
 
-| 函数 | 参数 | 返回值 |
-| ---- | ---- | ------ |
-| `link` | `oldpath` 源文件;`newpath` 新硬链接(已存在则报错) | 成功 `0`,失败 `-1` |
-| `symlink` | `target` 被链接源路径;`linkpath` 软链接路径 | 成功 `0`,失败 `-1` |
+| 函数       | 参数                                                 | 返回值                    |
+| ---------- | ---------------------------------------------------- | ------------------------- |
+| `link`     | `oldpath` 源文件;`newpath` 新硬链接(已存在则报错) | 成功 `0`,失败 `-1`       |
+| `symlink`  | `target` 被链接源路径;`linkpath` 软链接路径         | 成功 `0`,失败 `-1`       |
 | `readlink` | `pathname` 必须是软链接;`buf` 缓冲区;`bufsiz` 大小 | 成功返回字节数,失败 `-1` |
 
 **重要**:不能对软链接用 `open` + `read` 读取其存储的路径——`open` 会打开所指向的文件,必须用 `readlink`。
@@ -367,16 +378,16 @@ int chdir(const char *path);
 int fchdir(int fd);
 ```
 
-| 函数 | 说明 | 返回值 |
-| ---- | ---- | ------ |
-| `mkdir` | `mode` 最终权限 `mode & ~umask`;已存在则失败 | 成功 `0`,失败 `-1` |
-| `rmdir` | 目录必须为空;不能是软链接 | 成功 `0`,失败 `-1` |
-| `opendir` | 打开目录得到 `DIR *` 句柄 | 成功 `DIR *`,失败 `NULL` |
-| `readdir` | 读取下一条目录项,按天然次序不排序 | `struct dirent *` / `NULL` |
-| `rewinddir` | 目录流重置到起点 | 无 |
-| `closedir` | 关闭目录释放资源 | 成功 `0`,失败 `-1` |
-| `getcwd` | 取当前工作目录绝对路径;`buf=NULL,size=0` 时内部按需分配需 `free` | 成功 `buf`,失败 `NULL` |
-| `chdir` / `fchdir` | 以路径 / 文件描述符切换工作目录,目录须存在 | 成功 `0`,失败 `-1` |
+| 函数               | 说明                                                              | 返回值                     |
+| ------------------ | ----------------------------------------------------------------- | -------------------------- |
+| `mkdir`            | `mode` 最终权限 `mode & ~umask`;已存在则失败                     | 成功 `0`,失败 `-1`        |
+| `rmdir`            | 目录必须为空;不能是软链接                                        | 成功 `0`,失败 `-1`        |
+| `opendir`          | 打开目录得到 `DIR *` 句柄                                         | 成功 `DIR *`,失败 `NULL`  |
+| `readdir`          | 读取下一条目录项,按天然次序不排序                                | `struct dirent *` / `NULL` |
+| `rewinddir`        | 目录流重置到起点                                                  | 无                         |
+| `closedir`         | 关闭目录释放资源                                                  | 成功 `0`,失败 `-1`        |
+| `getcwd`           | 取当前工作目录绝对路径;`buf=NULL,size=0` 时内部按需分配需 `free` | 成功 `buf`,失败 `NULL`    |
+| `chdir` / `fchdir` | 以路径 / 文件描述符切换工作目录,目录须存在                       | 成功 `0`,失败 `-1`        |
 
 ```c
 struct dirent {
@@ -598,15 +609,15 @@ umask 0000 && touch a && ls -l a   # 验证 umask 对新建文件权限的影响
 
 常见问题:
 
-| 现象 | 原因 | 处理 |
-| ---- | ---- | ---- |
-| `stat error: No such file or directory` | 路径不存在 / 相对路径基准不对 | 用 `getcwd` 打印当前工作目录 |
-| `chown error: Operation not permitted` | 普通用户改属主 | 加 `sudo`,或只改组 ID |
-| `chmod error: Operation not permitted` | 既非 root 也非文件所有者 | 用所有者账户或 `sudo` |
-| `readlink error: Invalid argument` | 传入的不是软链接 | 先用 `lstat`/`S_ISLNK` 判断 |
-| `rmdir error: Directory not empty` | 目录非空 | 先清空目录再删 |
-| 时间戳格式化为 `1970-...` | 结构体成员用错(如 `st_atime`) | 统一用 `st_atim.tv_sec` |
-| `link error: Invalid cross-device link` | 硬链接跨文件系统 | 改用软链接 `symlink` |
+| 现象                                    | 原因                            | 处理                         |
+| --------------------------------------- | ------------------------------- | ---------------------------- |
+| `stat error: No such file or directory` | 路径不存在 / 相对路径基准不对   | 用 `getcwd` 打印当前工作目录 |
+| `chown error: Operation not permitted`  | 普通用户改属主                  | 加 `sudo`,或只改组 ID       |
+| `chmod error: Operation not permitted`  | 既非 root 也非文件所有者        | 用所有者账户或 `sudo`        |
+| `readlink error: Invalid argument`      | 传入的不是软链接                | 先用 `lstat`/`S_ISLNK` 判断  |
+| `rmdir error: Directory not empty`      | 目录非空                        | 先清空目录再删               |
+| 时间戳格式化为 `1970-...`               | 结构体成员用错(如 `st_atime`) | 统一用 `st_atim.tv_sec`      |
+| `link error: Invalid cross-device link` | 硬链接跨文件系统                | 改用软链接 `symlink`         |
 
 调试技巧:出错立即 `perror()`;`strace -e trace=openat,stat,lstat,readlink,unlink ./test_attr` 观察系统调用;`ltrace` 观察 C 库函数;对同一软链接分别 `stat` / `lstat`,观察 `st_mode` 与 `st_ino` 差异。
 
@@ -614,17 +625,17 @@ umask 0000 && touch a && ls -l a   # 验证 umask 对新建文件权限的影响
 
 ## 11. 跨平台对比:IMX6ULL vs STM32 vs RK3568
 
-| 维度 | I.MX6ULL(本教程) | STM32(裸机/HAL) | RK3568(Linux) |
-| ---- | ---------------- | ----------------- | --------------- |
-| 运行环境 | Linux 用户态(Cortex-A7) | 裸机 / RTOS(Cortex-M) | Linux 用户态(Cortex-A55) |
-| 文件概念 | 一切皆文件,7 种类型 | 无文件系统(或 FatFS) | 同 Linux,7 种类型 |
-| 获取属性 | `stat`/`fstat`/`lstat` + `struct stat` | FatFS `f_stat` / `FILINFO` | 同 `stat` 系列 |
-| 权限模型 | UID/GID + rwx + 特殊位 | 无(或简单只读标志) | 同 Linux(含 ACL/SELinux 可选) |
-| 链接 | 硬链接、软链接 | 无 | 硬链接、软链接 |
-| 目录遍历 | `opendir`/`readdir` | `f_readdir`(FatFS) | 同 `opendir`/`readdir` |
-| 时间戳 | atime/mtime/ctime + RTC | 依赖 RTC,通常无文件时间 | 同 Linux |
-| 设备文件 | `/dev` 虚拟文件系统,`open` 设备节点 | 直接寄存器/HAL 操作 | `/dev` 设备节点、sysfs |
-| 典型场景 | 应用编程、驱动配合 | 实时控制、低功耗 | 高性能边缘计算、多媒体 |
+| 维度     | I.MX6ULL(本教程)                     | STM32(裸机/HAL)          | RK3568(Linux)                 |
+| -------- | -------------------------------------- | -------------------------- | ------------------------------- |
+| 运行环境 | Linux 用户态(Cortex-A7)              | 裸机 / RTOS(Cortex-M)    | Linux 用户态(Cortex-A55)      |
+| 文件概念 | 一切皆文件,7 种类型                   | 无文件系统(或 FatFS)     | 同 Linux,7 种类型              |
+| 获取属性 | `stat`/`fstat`/`lstat` + `struct stat` | FatFS `f_stat` / `FILINFO` | 同 `stat` 系列                  |
+| 权限模型 | UID/GID + rwx + 特殊位                 | 无(或简单只读标志)       | 同 Linux(含 ACL/SELinux 可选) |
+| 链接     | 硬链接、软链接                         | 无                         | 硬链接、软链接                  |
+| 目录遍历 | `opendir`/`readdir`                    | `f_readdir`(FatFS)       | 同 `opendir`/`readdir`          |
+| 时间戳   | atime/mtime/ctime + RTC                | 依赖 RTC,通常无文件时间   | 同 Linux                        |
+| 设备文件 | `/dev` 虚拟文件系统,`open` 设备节点   | 直接寄存器/HAL 操作        | `/dev` 设备节点、sysfs          |
+| 典型场景 | 应用编程、驱动配合                     | 实时控制、低功耗           | 高性能边缘计算、多媒体          |
 
 要点:STM32 上的「文件」通常只是存储介质上的 FAT 记录,没有 inode、权限、链接概念;IMX6ULL/RK3568 是完整 Linux,文件属性体系完全一致,应用代码可无缝迁移。
 

+ 97 - 87
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/01-Linux应用编程基础/05-字符串处理与系统信息.md

@@ -1,6 +1,16 @@
 ---
 title: 字符串处理与系统信息
-tags: [嵌入式Linux, Linux应用编程, 字符串, 正则表达式, 时间, proc文件系统, 内存分配, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    Linux应用编程,
+    字符串,
+    正则表达式,
+    时间,
+    proc文件系统,
+    内存分配,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第六章 字符串处理、第七章 系统信息与系统资源"
@@ -28,14 +38,14 @@ int fgetc(FILE *stream);                 /* 从指定流读一个字符 */
 char *fgets(char *s, int size, FILE *stream); /* 从指定流读一行,限制长度 */
 ```
 
-| 函数 | 目标/来源 | 换行符处理 | 返回值 |
-| ---- | --------- | ---------- | ------ |
-| `puts` | stdout | 自动追加 `\n`(`\0`→`\n`) | 非负数 / `EOF` |
-| `putchar` | stdout | 否 | 出错 `EOF` |
-| `fputc` / `fputs` | 指定 `stream` | 否 | 成功返回字符/非负数,失败 `EOF` |
-| `gets` | stdin | 读走并丢弃 `\n`,替换为 `\0` | `s` / `NULL` |
-| `getchar` / `fgetc` | stdin / 指定流 | 作为普通字符读取 | 字符(int) / `EOF` |
-| `fgets` | 指定流 | 读走 `\n` 并保留,末尾补 `\0` | `s` / `NULL` |
+| 函数                | 目标/来源      | 换行符处理                    | 返回值                          |
+| ------------------- | -------------- | ----------------------------- | ------------------------------- |
+| `puts`              | stdout         | 自动追加 `\n`(`\0`→`\n`)    | 非负数 / `EOF`                  |
+| `putchar`           | stdout         | 否                            | 出错 `EOF`                      |
+| `fputc` / `fputs`   | 指定 `stream`  | 否                            | 成功返回字符/非负数,失败 `EOF` |
+| `gets`              | stdin          | 读走并丢弃 `\n`,替换为 `\0`  | `s` / `NULL`                    |
+| `getchar` / `fgetc` | stdin / 指定流 | 作为普通字符读取              | 字符(int) / `EOF`               |
+| `fgets`             | 指定流         | 读走 `\n` 并保留,末尾补 `\0` | `s` / `NULL`                    |
 
 `printf` 是格式化输出,功能更强;`puts` 等更简单且同样使用 stdio 缓冲。`gets` 不检查缓冲区溢出,编译告警,应改用 `fgets`。
 
@@ -74,17 +84,17 @@ char *strstr(const char *haystack, const char *needle);          /* 查找子串
 void bzero(void *s, size_t n);                                   /* 内存清 0 */
 ```
 
-| 函数 | 要点 | 返回值 |
-| ---- | ---- | ------ |
-| `strlen` | 参数须以 `\0` 结尾,不含结束符 | 字节长度 |
-| `strcat` | 覆盖 `dest` 原 `\0`,只留一个 `\0`,须保证空间足够 | `dest` |
-| `strncat` | 最多追加 `n` 字符;`src` 足够长时追加 `n+1` 字节 | `dest` |
-| `strcpy` | 连 `\0` 一起复制,须保证空间 | `dest` |
-| `strncpy` | `n <= strlen(src)` 不带 `\0`;`n` 更大时补 `\0` | `dest` |
-| `memset` / `bzero` | 以字节填充 `c` / 清 0 | `s` / 无 |
-| `strcmp` / `strncmp` | 按 ASCII 逐字符比较,最多 n 个 | `<0` 小于、`0` 相等、`>0` 大于 |
-| `strchr` / `strrchr` | 正/反向找字符,`\0` 也是字符串一部分 | 找到返回指针,否则 `NULL` |
-| `strstr` | 找子串首次出现位置 | 指针 / `NULL` |
+| 函数                 | 要点                                               | 返回值                         |
+| -------------------- | -------------------------------------------------- | ------------------------------ |
+| `strlen`             | 参数须以 `\0` 结尾,不含结束符                     | 字节长度                       |
+| `strcat`             | 覆盖 `dest` 原 `\0`,只留一个 `\0`,须保证空间足够 | `dest`                         |
+| `strncat`            | 最多追加 `n` 字符;`src` 足够长时追加 `n+1` 字节   | `dest`                         |
+| `strcpy`             | 连 `\0` 一起复制,须保证空间                       | `dest`                         |
+| `strncpy`            | `n <= strlen(src)` 不带 `\0`;`n` 更大时补 `\0`    | `dest`                         |
+| `memset` / `bzero`   | 以字节填充 `c` / 清 0                              | `s` / 无                       |
+| `strcmp` / `strncmp` | 按 ASCII 逐字符比较,最多 n 个                     | `<0` 小于、`0` 相等、`>0` 大于 |
+| `strchr` / `strrchr` | 正/反向找字符,`\0` 也是字符串一部分               | 找到返回指针,否则 `NULL`      |
+| `strstr`             | 找子串首次出现位置                                 | 指针 / `NULL`                  |
 
 **`sizeof` 与 `strlen`**:`sizeof` 是编译期操作符,算内存大小;`strlen` 是运行时库函数,算到 `\0` 的长度。对 `char str[50]="abc"; char *p=str;`,`sizeof(str)=50`、`strlen(str)=3`、`sizeof(p)=8`(64 位)、`strlen(p)=3`。函数参数中的数组会退化为指针。
 
@@ -124,12 +134,12 @@ float strtof(const char *nptr, char **endptr);
 long double strtold(const char *nptr, char **endptr);
 ```
 
-| 函数 | 返回类型 | 进制支持 |
-| ---- | -------- | -------- |
-| `atoi` / `atol` / `atoll` | int / long / long long | 仅十进制 |
-| `strtol` / `strtoll` | long / long long | `base` 2~36,或 0 自动识别 |
-| `strtoul` / `strtoull` | unsigned long / unsigned long long | 同上 |
-| `atof` / `strtod` / `strtof` / `strtold` | double / double / float / long double | 浮点 |
+| 函数                                     | 返回类型                              | 进制支持                   |
+| ---------------------------------------- | ------------------------------------- | -------------------------- |
+| `atoi` / `atol` / `atoll`                | int / long / long long                | 仅十进制                   |
+| `strtol` / `strtoll`                     | long / long long                      | `base` 2~36,或 0 自动识别 |
+| `strtoul` / `strtoull`                   | unsigned long / unsigned long long    | 同上                       |
+| `atof` / `strtod` / `strtof` / `strtold` | double / double / float / long double | 浮点                       |
 
 `atoi` 系列跳过前导空格,遇数字或正负号开始,遇非数字或 `\0` 结束。`strtol` 的 `endptr` 若非 `NULL` 存第一个无效字符地址(无数字时存 `nptr` 并返回 0);`base=0` 时 `0x` 前缀为 16 进制、`0` 前缀为 8 进制。
 
@@ -202,22 +212,22 @@ void regfree(regex_t *preg);
 typedef struct { regoff_t rm_so; regoff_t rm_eo; } regmatch_t; /* rm_so 起始, rm_eo 结束 */
 ```
 
-| 函数 | 作用 | 返回值 |
-| ---- | ---- | ------ |
-| `regcomp` | 把正则编译为 `regex_t` | 成功 0,失败返回非 0 错误码 |
-| `regexec` | 执行匹配,结果写入 `pmatch` | 匹配 0;`REG_NOMATCH` 未匹配;其它为错误 |
-| `regerror` | 错误码转描述字符串 | 字符串长度 |
-| `regfree` | 释放资源 | 无 |
+| 函数       | 作用                        | 返回值                                   |
+| ---------- | --------------------------- | ---------------------------------------- |
+| `regcomp`  | 把正则编译为 `regex_t`      | 成功 0,失败返回非 0 错误码              |
+| `regexec`  | 执行匹配,结果写入 `pmatch` | 匹配 0;`REG_NOMATCH` 未匹配;其它为错误 |
+| `regerror` | 错误码转描述字符串          | 字符串长度                               |
+| `regfree`  | 释放资源                    | 无                                       |
 
 `cflags` 常用 `REG_EXTENDED`;`pmatch` 中 `rm_so` 为起始偏移、`rm_eo` 为结束偏移(左闭右开),`rm_so == -1` 表示未匹配;`eflags` 常传 0。子串内容 `string + rm_so`,长度 `rm_eo - rm_so`。
 
-| 元字符 | 含义 | 元字符 | 含义 |
-| ------ | ---- | ------ | ---- |
-| `.` | 任意单字符 | `*` | 前一项 0 次或多次 |
-| `^` / `$` | 行首 / 行尾 | `+` | 前一项 1 次或多次(扩展) |
-| `[...]` | 字符集合 | `?` | 前一项 0 次或 1 次(扩展) |
-| `[^...]` | 否定字符集 | `{n,m}` | 前一项 n~m 次(扩展) |
-| `\` | 转义 | `\|` / `()` | 或 / 分组(扩展) |
+| 元字符    | 含义        | 元字符      | 含义                       |
+| --------- | ----------- | ----------- | -------------------------- |
+| `.`       | 任意单字符  | `*`         | 前一项 0 次或多次          |
+| `^` / `$` | 行首 / 行尾 | `+`         | 前一项 1 次或多次(扩展)  |
+| `[...]`   | 字符集合    | `?`         | 前一项 0 次或 1 次(扩展) |
+| `[^...]`  | 否定字符集  | `{n,m}`     | 前一项 n~m 次(扩展)      |
+| `\`       | 转义        | `\|` / `()` | 或 / 分组(扩展)          |
 
 ```c
 #include <stdio.h>
@@ -286,12 +296,12 @@ struct sysinfo {
 
 `sysconf` 运行时获取系统配置,成功返回配置值,参数无效返回 -1 并 `errno=EINVAL`:
 
-| 参数 | 含义 | 参数 | 含义 |
-| ---- | ---- | ---- | ---- |
-| `_SC_ARG_MAX` | exec 参数最大长度 | `_SC_OPEN_MAX` | 进程可打开最大文件数 |
-| `_SC_CHILD_MAX` | 每用户最大并发进程数 | `_SC_PAGESIZE` | 系统页大小 |
-| `_SC_HOST_NAME_MAX` | 主机名最大长度 | `_SC_TTY_NAME_MAX` | 终端名最大长度 |
-| `_SC_LOGIN_NAME_MAX` | 登录名最大长度 | `_SC_CLK_TCK` | 系统节拍率 |
+| 参数                 | 含义                 | 参数               | 含义                 |
+| -------------------- | -------------------- | ------------------ | -------------------- |
+| `_SC_ARG_MAX`        | exec 参数最大长度    | `_SC_OPEN_MAX`     | 进程可打开最大文件数 |
+| `_SC_CHILD_MAX`      | 每用户最大并发进程数 | `_SC_PAGESIZE`     | 系统页大小           |
+| `_SC_HOST_NAME_MAX`  | 主机名最大长度       | `_SC_TTY_NAME_MAX` | 终端名最大长度       |
+| `_SC_LOGIN_NAME_MAX` | 登录名最大长度       | `_SC_CLK_TCK`      | 系统节拍率           |
 
 ---
 
@@ -331,14 +341,14 @@ struct tm {
 };
 ```
 
-| 函数 | 作用 | 可重入版本 |
-| ---- | ---- | ---------- |
-| `ctime` | time_t → 本地时间字符串 | `ctime_r` |
-| `localtime` | time_t → `struct tm`(本地时间) | `localtime_r` |
-| `gmtime` | time_t → `struct tm`(UTC 时间) | `gmtime_r` |
-| `mktime` | `struct tm` → time_t(与 localtime 相反) | — |
-| `asctime` | `struct tm` → 固定格式字符串 | `asctime_r` |
-| `strftime` | `struct tm` → 自定义格式字符串 | — |
+| 函数        | 作用                                      | 可重入版本    |
+| ----------- | ----------------------------------------- | ------------- |
+| `ctime`     | time_t → 本地时间字符串                   | `ctime_r`     |
+| `localtime` | time_t → `struct tm`(本地时间)          | `localtime_r` |
+| `gmtime`    | time_t → `struct tm`(UTC 时间)          | `gmtime_r`    |
+| `mktime`    | `struct tm` → time_t(与 localtime 相反) | —             |
+| `asctime`   | `struct tm` → 固定格式字符串              | `asctime_r`   |
+| `strftime`  | `struct tm` → 自定义格式字符串            | —             |
 
 **注意 `tm_year` 需加 1900、`tm_mon` 需加 1。** `strftime` 常用说明符:`%Y` 4 位年、`%y` 2 位年、`%m` 月、`%d` 日、`%H` 24 时、`%I` 12 时、`%M` 分、`%S` 秒、`%A` 完整星期、`%a` 星期缩写、`%B` 完整月份、`%b` 月份缩写、`%p` AM/PM、`%j` 年内第几天、`%F`=`%Y-%m-%d`、`%T`=`%H:%M:%S`。返回值:不超 `max` 返回写入字节数,否则返回 0。
 
@@ -414,12 +424,12 @@ void *memalign(size_t alignment, size_t size);
 void *pvalloc(size_t size);
 ```
 
-| 函数 | 对齐要求 | 返回 |
-| ---- | -------- | ---- |
-| `malloc` / `calloc` | 固定较小边界(32 位 8 字节,64 位 16 字节) | 指针 / `NULL` |
-| `posix_memalign` | `alignment` 为 2 的幂且是 `sizeof(void*)` 倍数 | 成功 0,失败非 0 |
-| `aligned_alloc` | `alignment` 为 2 的幂,`size` 为其整数倍 | 指针 / `NULL` |
-| `memalign` / `valloc` | 同 `aligned_alloc` / 页对齐(均已过时) | 指针 / `NULL` |
+| 函数                  | 对齐要求                                       | 返回             |
+| --------------------- | ---------------------------------------------- | ---------------- |
+| `malloc` / `calloc`   | 固定较小边界(32 位 8 字节,64 位 16 字节)    | 指针 / `NULL`    |
+| `posix_memalign`      | `alignment` 为 2 的幂且是 `sizeof(void*)` 倍数 | 成功 0,失败非 0 |
+| `aligned_alloc`       | `alignment` 为 2 的幂,`size` 为其整数倍       | 指针 / `NULL`    |
+| `memalign` / `valloc` | 同 `aligned_alloc` / 页对齐(均已过时)        | 指针 / `NULL`    |
 
 `calloc` 等价于 `malloc` + `memset`。进程终止时内核回收全部内存,但仍应显式 `free`:提升可读性、可维护性,及时归还避免内存泄漏。
 
@@ -452,15 +462,15 @@ graph TD
 
 `/proc` 是**虚拟文件系统**,以文件方式向应用层暴露内核数据;动态创建、只存在于内存、不占磁盘(与 devfs 类似)。以 PID 命名的目录记录进程信息;虚拟文件含义:
 
-| 文件 | 含义 | 文件 | 含义 |
-| ---- | ---- | ---- | ---- |
-| `cmdline` | 内核启动参数 | `meminfo` | 物理内存与交换分区使用情况 |
-| `cpuinfo` | CPU 相关信息 | `modules` | 加载的模块列表 |
-| `iomem` | IO 设备内存使用 | `mounts` | 挂载的文件系统列表 |
-| `interrupts` | 被占用的中断及占用者 | `partitions` | 系统识别的分区表 |
-| `ioports` | IO 端口使用 | `swaps` | 交换分区利用情况 |
-| `kcore` | 物理内存映像,不可读 | `version` | 内核版本信息 |
-| `loadavg` | 系统平均负载 | `uptime` | 系统运行时间 |
+| 文件         | 含义                 | 文件         | 含义                       |
+| ------------ | -------------------- | ------------ | -------------------------- |
+| `cmdline`    | 内核启动参数         | `meminfo`    | 物理内存与交换分区使用情况 |
+| `cpuinfo`    | CPU 相关信息         | `modules`    | 加载的模块列表             |
+| `iomem`      | IO 设备内存使用      | `mounts`     | 挂载的文件系统列表         |
+| `interrupts` | 被占用的中断及占用者 | `partitions` | 系统识别的分区表           |
+| `ioports`    | IO 端口使用          | `swaps`      | 交换分区利用情况           |
+| `kcore`      | 物理内存映像,不可读 | `version`    | 内核版本信息               |
+| `loadavg`    | 系统平均负载         | `uptime`     | 系统运行时间               |
 
 读取方式:`cat /proc/version`,或程序中 `open` + `read`:
 
@@ -607,15 +617,15 @@ cat /proc/version; head -20 /proc/cpuinfo; head -20 /proc/meminfo
 
 常见问题:
 
-| 现象 | 原因 | 处理 |
-| ---- | ---- | ---- |
-| `strlen` 异常大 | 字符串没有 `\0` | 确保以 `\0` 结尾;容量用 `sizeof` |
-| `strcpy` 段错误 | 目标缓冲区太小 | 改用 `strncpy`/`snprintf` |
-| `strtol` 返回 0 | 进制/前缀不匹配 | 检查 `base`;`base=0` 时 `0x` 才识别 |
-| `regcomp` 出错 | 正则语法错误 | `regerror` 打印描述;确认 `REG_EXTENDED` |
-| `mktime` 偏差一个月 | `tm_mon` 未加 1 | `tm_mon` 为 0~11 |
-| 时间显示 1970 | `struct tm` 未初始化 | 先 `localtime_r` 再 `strftime` |
-| `sysinfo` 内存偏小 | 忘了乘 `mem_unit` | 字节数 = 字段 × `mem_unit` |
+| 现象                | 原因                 | 处理                                     |
+| ------------------- | -------------------- | ---------------------------------------- |
+| `strlen` 异常大     | 字符串没有 `\0`      | 确保以 `\0` 结尾;容量用 `sizeof`        |
+| `strcpy` 段错误     | 目标缓冲区太小       | 改用 `strncpy`/`snprintf`                |
+| `strtol` 返回 0     | 进制/前缀不匹配      | 检查 `base`;`base=0` 时 `0x` 才识别     |
+| `regcomp` 出错      | 正则语法错误         | `regerror` 打印描述;确认 `REG_EXTENDED` |
+| `mktime` 偏差一个月 | `tm_mon` 未加 1      | `tm_mon` 为 0~11                         |
+| 时间显示 1970       | `struct tm` 未初始化 | 先 `localtime_r` 再 `strftime`           |
+| `sysinfo` 内存偏小  | 忘了乘 `mem_unit`    | 字节数 = 字段 × `mem_unit`               |
 
 调试技巧:`strace` 看系统调用、`ltrace` 看库函数、`valgrind --leak-check=full` 查泄漏;正则先用 `grep -E` 验证语法。
 
@@ -623,16 +633,16 @@ cat /proc/version; head -20 /proc/cpuinfo; head -20 /proc/meminfo
 
 ## 14. 跨平台对比:IMX6ULL vs STM32 vs RK3568
 
-| 维度 | I.MX6ULL(本教程) | STM32(裸机/HAL) | RK3568(Linux) |
-| ---- | ---------------- | ----------------- | --------------- |
-| 字符串库 | glibc 完整 | newlib 子集 | glibc 完整 |
-| 正则 | POSIX `regex.h` | 通常无,需第三方 | POSIX `regex.h` |
-| 系统信息 | `uname`/`sysinfo`/`sysconf` | 直接读寄存器/UID | 同 Linux |
-| 时间 | `time`/`gettimeofday` + RTC | SysTick + RTC,无时区 | 同 Linux,常带 NTP |
-| 进程时间 | `times`/`clock` | DWT 周期计数器 | 同 Linux |
-| 休眠 | `sleep`/`usleep`/`nanosleep` | `HAL_Delay` 忙等 | 同 Linux |
-| 堆内存 | `malloc`/`free` | newlib 堆,需配 heap | 同 Linux |
-| /proc | 有,丰富 | 无 | 有 |
+| 维度     | I.MX6ULL(本教程)           | STM32(裸机/HAL)     | RK3568(Linux)    |
+| -------- | ---------------------------- | --------------------- | ------------------ |
+| 字符串库 | glibc 完整                   | newlib 子集           | glibc 完整         |
+| 正则     | POSIX `regex.h`              | 通常无,需第三方      | POSIX `regex.h`    |
+| 系统信息 | `uname`/`sysinfo`/`sysconf`  | 直接读寄存器/UID      | 同 Linux           |
+| 时间     | `time`/`gettimeofday` + RTC  | SysTick + RTC,无时区 | 同 Linux,常带 NTP |
+| 进程时间 | `times`/`clock`              | DWT 周期计数器        | 同 Linux           |
+| 休眠     | `sleep`/`usleep`/`nanosleep` | `HAL_Delay` 忙等      | 同 Linux           |
+| 堆内存   | `malloc`/`free`              | newlib 堆,需配 heap  | 同 Linux           |
+| /proc    | 有,丰富                     | 无                    | 有                 |
 
 要点:STM32 无 `/proc`、无 POSIX 正则、无时区/日历时间体系;IMX6ULL 与 RK3568 同属 Linux,本篇 API 通用。
 

+ 169 - 92
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/01-Linux应用编程基础/面试-Linux应用编程基础.md

@@ -1,6 +1,17 @@
 ---
 title: 面试-Linux应用编程基础
-tags: [嵌入式Linux, 应用编程, 面试, 系统调用, 文件IO, 标准IO, 文件属性, 字符串, 系统信息]
+tags:
+  [
+    嵌入式Linux,
+    应用编程,
+    面试,
+    系统调用,
+    文件IO,
+    标准IO,
+    文件属性,
+    字符串,
+    系统信息,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 ---
@@ -12,25 +23,29 @@ updated: 2026-09-18
 覆盖《I.MX6U嵌入式Linux C应用编程指南》第一章至第七章核心考点,共 6 大主题、30 道题。每题含「答案要点 / 详细解答(代码示例、对比表)/ 2 条追问」。
 
 ---
+
 ## 一、应用编程概念(5 题)
+
 ### Q1.1 什么是系统调用?系统调用和库函数有什么区别?
 
 **答案要点**:系统调用是内核提供给应用层的编程接口,是应用层进入内核的入口;库函数是应用层封装的函数库,构建于系统调用之上。
 
 **详细解答**:应用程序通过系统调用请求内核执行打开文件、读写、控制硬件等操作。两者的主要区别:
 
-| 对比项 | 系统调用 | 库函数 |
-| ------ | -------- | ------ |
-| 归属 | 内核的一部分 | 应用层 |
-| 运行空间 | 调用时从用户态陷入内核态 | 运行在用户空间 |
-| 缓冲 | 无缓存 | 通常有缓存 |
-| 可移植性 | 各系统不同 | 各系统接口几乎一致,更好 |
+| 对比项   | 系统调用                 | 库函数                   |
+| -------- | ------------------------ | ------------------------ |
+| 归属     | 内核的一部分             | 应用层                   |
+| 运行空间 | 调用时从用户态陷入内核态 | 运行在用户空间           |
+| 缓冲     | 无缓存                   | 通常有缓存               |
+| 可移植性 | 各系统不同               | 各系统接口几乎一致,更好 |
 
 库函数有的不调用系统调用(如 `strlen`、`memcpy`),有的封装系统调用(`fopen` 内部调 `open`、`fread` 内部调 `read`)。从使用者角度看都是 C 函数,只需知道调用的是哪一类即可。
 
 **追问**
+
 1. `printf` 和 `write` 谁更快?为什么?(`printf` 有 stdio 缓冲,连续小量输出更快;`write` 每次直接陷入内核)
 2. 为什么说库函数可移植性更好?(多数系统都实现 C 库,接口定义基本一致;系统调用的定义/参数/返回值各系统不同)
+
 ### Q1.2 应用编程与裸机编程、驱动编程有什么区别?
 
 **答案要点**:裸机编程无操作系统、软硬件代码混在一起;驱动编程基于内核驱动框架、负责底层硬件操作;应用编程基于操作系统、运行在用户态、只实现用户逻辑。
@@ -44,8 +59,10 @@ updated: 2026-09-18
 应用与驱动通过系统调用(设备文件)交互,代码隔离。
 
 **追问**
+
 1. 用户态和内核态如何切换?(通过系统调用/中断/异常由用户态陷入内核态)
 2. 应用程序能直接访问硬件寄存器吗?为什么?(不能,需通过驱动/设备文件,否则破坏隔离与安全)
+
 ### Q1.3 什么是文件描述符 fd?0、1、2 分别是什么?一个进程最多能打开多少文件?
 
 **答案要点**:fd 是一个非负整数,内核用它索引所有已打开文件;0/1/2 分别是标准输入/输出/错误;默认上限 1024(0~1023)。
@@ -61,8 +78,10 @@ write(STDOUT_FILENO, "hello\n", 6);
 `ulimit -n` 可查看进程可打开最大文件数,默认 1024;超过限制内核会发送信号并结束进程。
 
 **追问**
+
 1. fd 是进程私有还是全系统共享?(进程私有;不同进程同一 fd 编号互不相关)
 2. 关闭 fd 后,另一个 `open` 会分配哪个编号?(当前最小可用编号,通常就是刚释放的那个)
+
 ### Q1.4 main 函数的 `argc` 和 `argv` 分别是什么?
 
 **答案要点**:`argc` 是参数个数(含程序自身路径),`argv` 是字符串指针数组,`argv[0]` 是程序路径、`argv[1]` 起是真实参数。
@@ -77,8 +96,10 @@ int main(int argc, char *argv[]) {
 ```
 
 **追问**
+
 1. `argv` 最后一个元素是什么?(`argv[argc]` 为 `NULL`)
 2. 如何让程序接收选项参数?(用 `getopt`/`getopt_long`,参数放入 `optarg`)
+
 ### Q1.5 glibc 是什么?如何确定当前系统的 glibc 版本?
 
 **答案要点**:glibc 是 GNU C 库、Linux 下的标准 C 函数库,通常以动态库 `libc.so.6` 形式提供。
@@ -91,34 +112,39 @@ ls -l /lib/x86_64-linux-gnu/libc.so.6   # 看软链接指向
 ```
 
 **追问**
+
 1. 为什么应用编程可以基本忽略系统调用与库函数的差异?(都表现为 C 函数,使用方式一致)
 2. 交叉编译时 glibc 版本为何重要?(目标板 glibc 版本需不低于编译链所用版本,否则运行时报符号缺失)
 
 ---
+
 ## 二、文件 I/O(5 题)
+
 ### Q2.1 `open` 的 flags 有哪些?什么时候需要第三个参数 `mode`?
 
 **答案要点**:访问模式 `O_RDONLY`/`O_WRONLY`/`O_RDWR` 必须且只能选一个;`O_CREAT`/`O_EXCL`/`O_TRUNC`/`O_APPEND`/`O_NOFOLLOW`/`O_DIRECTORY` 等可组合;仅当含 `O_CREAT`(或 `O_TMPFILE`)时 `mode` 才有效。
 
 **详细解答**:
 
-| 标志 | 用途 |
-| ---- | ---- |
-| `O_RDONLY` / `O_WRONLY` / `O_RDWR` | 只读 / 只写 / 读写,三选一 |
-| `O_CREAT` | 文件不存在则创建,需配合 `mode` |
-| `O_EXCL` | 与 `O_CREAT` 合用专门创建;已存在则报错 |
-| `O_TRUNC` | 打开时把文件长度截断为 0 |
-| `O_APPEND` | 每次写前把偏移量移到文件末尾(原子) |
-| `O_NOFOLLOW` | 路径是符号链接时不解析,直接报错 |
-| `O_DIRECTORY` | 路径不是目录则失败 |
+| 标志                               | 用途                                    |
+| ---------------------------------- | --------------------------------------- |
+| `O_RDONLY` / `O_WRONLY` / `O_RDWR` | 只读 / 只写 / 读写,三选一              |
+| `O_CREAT`                          | 文件不存在则创建,需配合 `mode`         |
+| `O_EXCL`                           | 与 `O_CREAT` 合用专门创建;已存在则报错 |
+| `O_TRUNC`                          | 打开时把文件长度截断为 0                |
+| `O_APPEND`                         | 每次写前把偏移量移到文件末尾(原子)    |
+| `O_NOFOLLOW`                       | 路径是符号链接时不解析,直接报错        |
+| `O_DIRECTORY`                      | 路径不是目录则失败                      |
 
 ```c
 int fd = open("/home/dt/hello", O_RDWR | O_CREAT, S_IRWXU | S_IRGRP | S_IROTH);
 ```
 
 **追问**
+
 1. `O_EXCL` 解决什么问题?(把「判断是否存在」与「创建」合成原子操作,避免两个进程都创建同一文件)
 2. `O_TRUNC` 和 `O_APPEND` 能一起用吗?效果?(可以,但语义矛盾,实际以追加为主,需谨慎)
+
 ### Q2.2 `read`/`write` 的返回值含义?返回的字节数小于请求数算错吗?
 
 **答案要点**:返回实际读/写的字节数;小于 `count` 通常不是错误,0 表示读到文件末尾或未写入;出错返回 -1。
@@ -138,8 +164,10 @@ while ((n = read(fd, buf, sizeof(buf))) > 0) {
 ```
 
 **追问**
+
 1. `read` 返回 0 一定表示文件结束吗?(对普通文件是;对管道/终端等需结合场景)
 2. `write` 返回 0 代表什么?(未写入任何字节,通常表示异常或特殊设备行为)
+
 ### Q2.3 `lseek` 的三个 `whence` 是什么?如何用它求文件大小?
 
 **答案要点**:`SEEK_SET` 相对文件头、`SEEK_CUR` 相对当前位置、`SEEK_END` 相对文件末尾;`lseek(fd, 0, SEEK_END)` 得到文件大小。
@@ -152,8 +180,10 @@ lseek(fd, 0, SEEK_SET);                /* 重新定位到文件头 */
 ```
 
 **追问**
+
 1. 空洞文件是什么?(`lseek` 跳过一段后写入,中间未写区域形成空洞,逻辑大小包含空洞,但不占实际块)
 2. 设备文件能 `lseek` 吗?(可以,但部分设备不支持,返回错误)
+
 ### Q2.4 一个进程内多次 `open` 同一个文件会发生什么?
 
 **答案要点**:得到多个不同 fd;内存中只有一份动态文件(共享);各 fd 的读写偏移量相互独立(分别写),需用 `O_APPEND` 才能接续写。
@@ -166,19 +196,21 @@ fd2 = open("f", O_RDWR | O_APPEND);   /* fd1/fd2 写操作接续 */
 ```
 
 **追问**
+
 1. `dup` 复制出的 fd 与原 fd 的偏移量是否独立?(不独立,指向同一文件表,共享偏移量)
 2. 引用计数为 0 时会发生什么?(内核关闭动态文件,回收相关资源)
+
 ### Q2.5 什么是竞争冒险和原子操作?Linux 提供了哪些原子化手段?
 
 **答案要点**:竞争冒险是多个进程/线程无序操作共享资源导致结果不可预期;原子操作是不可分割、要么全做要么全不做的操作。
 
 **详细解答**:两进程 A、B 都对同一文件追加写且未用 `O_APPEND`:A `lseek` 到末尾后时间片耗尽,B 也 `lseek` 到同一末尾并写入,A 恢复后再从旧位置写,覆盖了 B 的数据。原子化手段:
 
-| 手段 | 原子化的操作 |
-| ---- | ------------ |
-| `O_APPEND` | 「定位到文件末尾 + 写入」 |
-| `pread` / `pwrite` | 「定位 + 读/写」,且不改变文件表偏移量 |
-| `O_CREAT \| O_EXCL` | 「判断文件是否存在 + 创建」 |
+| 手段                | 原子化的操作                           |
+| ------------------- | -------------------------------------- |
+| `O_APPEND`          | 「定位到文件末尾 + 写入」              |
+| `pread` / `pwrite`  | 「定位 + 读/写」,且不改变文件表偏移量 |
+| `O_CREAT \| O_EXCL` | 「判断文件是否存在 + 创建」            |
 
 ```c
 /* pread 不改变当前偏移量 */
@@ -186,11 +218,14 @@ ssize_t n = pread(fd, buf, sizeof(buf), 1024);
 ```
 
 **追问**
+
 1. `pread` 会改变文件偏移量吗?(不会,这是它与 `lseek`+`read` 的关键区别)
 2. 应用层加锁能替代原子操作吗?(可以配合文件锁,但原子系统调用更简单可靠)
 
 ---
+
 ## 三、标准 I/O(5 题)
+
 ### Q3.1 `FILE` 指针和文件描述符 fd 是什么关系?如何互转?
 
 **答案要点**:`FILE` 是标准 I/O 使用的结构体指针,内部封装了 fd 及 stdio 缓冲区;用 `fileno` 把 `FILE *` 转为 fd,用 `fdopen` 把 fd 转为 `FILE *`。
@@ -206,19 +241,21 @@ FILE *fp2 = fdopen(fd, "r");  /* fd -> FILE* */
 混用时要注意缓冲:`printf("print"); write(STDOUT_FILENO,"write\n",6);` 会先输出 `write`,因为 `print` 还在行缓冲中,而 `write` 直接进内核缓冲区。
 
 **追问**
+
 1. 混用标准 I/O 和文件 I/O 要注意什么?(缓冲不同步,可能乱序;必要时 `fflush`)
 2. `fdopen` 的 `mode` 与 fd 不一致会怎样?(失败)
+
 ### Q3.2 标准输入、输出、错误分别是什么?三种缓冲模式是什么?
 
 **答案要点**:stdin=0、stdout=1、stderr=2;三种模式为无缓冲 `_IONBF`、行缓冲 `_IOLBF`、全缓冲 `_IOFBF`。
 
 **详细解答**:
 
-| 模式 | 行为 | 默认用于 |
-| ---- | ---- | -------- |
-| `_IONBF` 无缓冲 | 每次调用立即 `read`/`write` | `stderr` |
-| `_IOLBF` 行缓冲 | 遇换行符才刷出 | 终端(stdin/stdout) |
-| `_IOFBF` 全缓冲 | 缓冲区填满才刷出 | 磁盘普通文件 |
+| 模式            | 行为                        | 默认用于             |
+| --------------- | --------------------------- | -------------------- |
+| `_IONBF` 无缓冲 | 每次调用立即 `read`/`write` | `stderr`             |
+| `_IOLBF` 行缓冲 | 遇换行符才刷出              | 终端(stdin/stdout) |
+| `_IOFBF` 全缓冲 | 缓冲区填满才刷出            | 磁盘普通文件         |
 
 ```c
 setvbuf(stdout, NULL, _IONBF, 0);   /* 改为无缓冲 */
@@ -226,8 +263,10 @@ setbuf(fp, NULL);                   /* 等价无缓冲 */
 ```
 
 **追问**
+
 1. 为什么 `stderr` 默认无缓冲?(保证错误信息能立即输出)
 2. 为什么 `printf` 不带换行时不立即显示?(stdout 行缓冲,数据仍在 stdio 缓冲区)
+
 ### Q3.3 如何强制刷新 stdio 缓冲区?哪些情况会自动刷新?
 
 **答案要点**:用 `fflush(stream)` 强制刷新(`NULL` 表示全部);文件关闭(`fclose`)时、程序正常退出时也会自动刷新。
@@ -242,29 +281,33 @@ fflush(stdout);          /* 立即刷出 */
 自动刷新场景:`fclose` 关闭文件时;使用 `exit()`、`return` 或正常结束程序时。注意:`_exit` / `_Exit` 终止程序**不会**刷新 stdio 缓冲区。
 
 **追问**
+
 1. `_exit` 和 `exit` 的区别?(`exit` 会刷新 stdio、执行 atexit;`_exit` 直接陷入内核终止,不刷新)
 2. `fflush(stdin)` 有用吗?(不标准,通常无效或未定义,不要依赖)
+
 ### Q3.4 标准 I/O 与文件 I/O 有哪些常用函数对应关系?
 
 **答案要点**:标准 I/O 是系统调用的封装,且自带用户空间缓冲。
 
 **详细解答**:
 
-| 文件 I/O(系统调用) | 标准 I/O(库函数) |
-| -------------------- | ------------------ |
-| `open` | `fopen` / `freopen` |
-| `close` | `fclose` |
-| `read` | `fread` |
-| `write` | `fwrite` |
-| `lseek` | `fseek` / `ftell` / `rewind` |
-| — | `fgets` / `fputs` / `fprintf` / `fscanf` |
-| fd | `FILE *` |
+| 文件 I/O(系统调用) | 标准 I/O(库函数)                       |
+| -------------------- | ---------------------------------------- |
+| `open`               | `fopen` / `freopen`                      |
+| `close`              | `fclose`                                 |
+| `read`               | `fread`                                  |
+| `write`              | `fwrite`                                 |
+| `lseek`              | `fseek` / `ftell` / `rewind`             |
+| —                    | `fgets` / `fputs` / `fprintf` / `fscanf` |
+| fd                   | `FILE *`                                 |
 
 标准 I/O 的 `fseek` 只能用 `SEEK_SET`/`SEEK_CUR`/`SEEK_END`,且受 stdio 缓冲影响。
 
 **追问**
+
 1. 何时选文件 I/O、何时选标准 I/O?(需要精细控制/设备操作用文件 I/O;文本处理、格式化输入输出用标准 I/O)
 2. `fseek` 后为什么有时要 `fflush`?(读写模式切换需刷新缓冲区,避免数据错乱)
+
 ### Q3.5 为什么标准 I/O 通常比文件 I/O 效率高?直接 I/O 又是什么?
 
 **答案要点**:标准 I/O 有用户空间 stdio 缓冲,减少系统调用次数;直接 I/O 绕过内核缓冲,直接访问磁盘。
@@ -272,30 +315,35 @@ fflush(stdout);          /* 立即刷出 */
 **详细解答**:文件 I/O 每次 `read`/`write` 都会陷入内核;标准 I/O 先把数据攒在 stdio 缓冲区,攒够再一次性 `write`,显著减少陷入次数。缓冲层次:应用 → stdio 缓冲区(用户空间)→ 内核缓冲区 → 磁盘。`fflush` 刷 stdio 缓冲,`fsync`/`fdatasync`/`sync` 刷内核缓冲,`open` 加 `O_DIRECT` 则绕过内核缓冲(直接 I/O),但有对齐限制且性能通常更低。
 
 **追问**
+
 1. `fsync` 和 `fdatasync` 的区别?(`fsync` 刷数据 + 元数据,`fdatasync` 只刷数据)
 2. 拷贝大文件时如何提升可靠性?(拷贝完 `fsync` 目标文件,或 `O_SYNC`/`O_DSYNC`)
 
 ---
+
 ## 四、文件属性与目录(5 题)
+
 ### Q4.1 Linux 有哪 7 种文件类型?`ls -l` 如何表示?
 
 **答案要点**:普通文件 `-`、目录 `d`、字符设备 `c`、块设备 `b`、符号链接 `l`、套接字 `s`、管道 `p`。
 
 **详细解答**:`ls -l` 第一列首字符标识类型。普通文件分文本文件与二进制文件;设备文件对应硬件但**不占磁盘**(虚拟文件系统,关机消失),常在 `/dev`;符号链接类似 Windows 快捷方式;管道和套接字用于进程间通信,后者可跨主机。Linux **不通过后缀识别类型**。
 
-| 类型 | 字符 | 典型 |
-| ---- | ---- | ---- |
-| 普通文件 | `-` | `.c`、`.o`、可执行文件 |
-| 目录 | `d` | 文件夹 |
-| 字符设备 | `c` | 串口、按键 |
-| 块设备 | `b` | 硬盘、loop |
-| 符号链接 | `l` | 快捷方式 |
-| 套接字 | `s` | 网络通信 |
-| 管道 | `p` | 进程间通信 |
+| 类型     | 字符 | 典型                   |
+| -------- | ---- | ---------------------- |
+| 普通文件 | `-`  | `.c`、`.o`、可执行文件 |
+| 目录     | `d`  | 文件夹                 |
+| 字符设备 | `c`  | 串口、按键             |
+| 块设备   | `b`  | 硬盘、loop             |
+| 符号链接 | `l`  | 快捷方式               |
+| 套接字   | `s`  | 网络通信               |
+| 管道     | `p`  | 进程间通信             |
 
 **追问**
+
 1. 如何用编程方式判断类型?(`stat` 取 `st_mode`,用 `S_ISREG`/`S_ISDIR` 等宏)
 2. 设备文件为什么关机就消失?(由 devfs 虚拟产生,由内存维护,不写磁盘)
+
 ### Q4.2 `stat`、`fstat`、`lstat` 有什么区别?
 
 **答案要点**:`stat` 从路径且解引用链接;`fstat` 从 fd;`lstat` 从路径且不解引用链接。
@@ -309,8 +357,10 @@ lstat("link", &st);   /* 链接本身, S_ISLNK(st.st_mode) 为真 */
 ```
 
 **追问**
+
 1. 判断路径是否为符号链接该用哪个?(`lstat` + `S_ISLNK`)
 2. `struct stat` 的 `st_size` 对符号链接意味着什么?(链接数据块中路径字符串的长度)
+
 ### Q4.3 文件权限如何检查?目录权限的含义有何不同?`umask` 和 `access` 各有什么作用?
 
 **答案要点**:以进程有效用户/组、附属组对应文件所有者/组/其它角色检查;目录 r=可列出、w=可增删文件、x=可访问其中文件;`umask` 屏蔽新建权限,`access` 检查权限(用实际 ID)。
@@ -324,23 +374,25 @@ if (access("f", R_OK | W_OK) == 0) puts("rw ok");
 ```
 
 **追问**
+
 1. set-user-ID 位的作用?(进程操作该文件时有效用户 ID 变为文件所有者 ID)
 2. 为什么只读目录无法访问文件?(缺执行权限,无法通过目录解析文件)
+
 ### Q4.4 硬链接和软链接的区别与各自限制?
 
 **答案要点**:硬链接共享同一 inode、计入 `st_nlink`;软链接独立 inode、数据块存目标路径、不计入链接数。
 
 **详细解答**:
 
-| 对比项 | 硬链接 | 软链接 |
-| ------ | ------ | ------ |
-| inode | 相同 | 不同 |
-| 数据块 | 共享 | 存目标路径 |
-| 源文件删除 | 仍可访问 | 悬空 |
-| 链接数 | 计入 | 不计入 |
-| 目录 | 不能(一般) | 可以 |
-| 跨文件系统 | 不能 | 可以 |
-| 目标不存在 | 不允许 | 允许 |
+| 对比项     | 硬链接       | 软链接     |
+| ---------- | ------------ | ---------- |
+| inode      | 相同         | 不同       |
+| 数据块     | 共享         | 存目标路径 |
+| 源文件删除 | 仍可访问     | 悬空       |
+| 链接数     | 计入         | 不计入     |
+| 目录       | 不能(一般) | 可以       |
+| 跨文件系统 | 不能         | 可以       |
+| 目标不存在 | 不允许       | 允许       |
 
 ```c
 link("src", "hard");         /* 硬链接 */
@@ -351,8 +403,10 @@ char buf[100]; readlink("soft", buf, sizeof(buf));  /* 读软链接路径 */
 硬链接不能跨文件系统(inode 只在单个文件系统内唯一)、一般不能指向目录(会形成目录环)。读取软链接存储的路径必须用 `readlink`,不能 `open`+`read`。
 
 **追问**
+
 1. 删除源文件后软链接还能读吗?(不能,成悬空链接,`readlink` 仍能读出路径字符串)
 2. `unlink` 一个软链接删的是什么?(软链接本身,不影响源文件)
+
 ### Q4.5 三个时间戳分别是什么?目录如何遍历?如何删除与重命名文件?
 
 **答案要点**:`atime` 访问、`mtime` 内容修改、`ctime` 状态改变;目录用 `opendir`/`readdir`/`closedir`;删除用 `unlink`/`remove`/`rmdir`,重命名用 `rename`。
@@ -372,23 +426,26 @@ closedir(d);
 `unlink` 删文件并把链接计数减 1;`remove` 自动区分文件/空目录;`rmdir` 只能删空目录;`rename` 只改目录条目,inode 不变,要求同一文件系统,不能重命名 `.`/`..`。软链接的 `unlink`/`remove` 都是删链接本身。
 
 **追问**
+
 1. `readdir` 返回 `NULL` 如何区分末尾和出错?(先置 `errno=0`,返回 `NULL` 后看 `errno` 是否为 0)
 2. `rename` 会移动文件数据吗?(不会,只操作目录条目,inode 与数据块不变)
 
 ---
+
 ## 五、字符串处理(5 题)
+
 ### Q5.1 `sizeof` 和 `strlen` 有什么区别?
 
 **答案要点**:`sizeof` 是编译期操作符,算内存大小;`strlen` 是运行时库函数,算到 `\0` 的长度。
 
 **详细解答**:
 
-| 对比项 | `sizeof` | `strlen` |
-| ------ | -------- | -------- |
-| 本质 | 操作符关键字 | C 库函数 |
-| 对象 | 类型/变量 | 以 `\0` 结尾的字符串 |
-| 时机 | 编译时 | 运行时 |
-| 结果 | 占用内存大小 | 字符串实际长度 |
+| 对比项 | `sizeof`     | `strlen`             |
+| ------ | ------------ | -------------------- |
+| 本质   | 操作符关键字 | C 库函数             |
+| 对象   | 类型/变量    | 以 `\0` 结尾的字符串 |
+| 时机   | 编译时       | 运行时               |
+| 结果   | 占用内存大小 | 字符串实际长度       |
 
 ```c
 char str[50] = "abc";
@@ -398,19 +455,21 @@ printf("%zu %zu %zu %zu\n", sizeof(str), strlen(str), sizeof(p), strlen(p));
 ```
 
 **追问**
+
 1. 函数参数 `char s[]` 的 `sizeof(s)` 是多少?(指针大小,数组退化为指针)
 2. `strlen` 对不含 `\0` 的数组会怎样?(越界读取,未定义行为)
+
 ### Q5.2 `strcpy` / `strncpy` / `memcpy` 有什么区别?
 
 **答案要点**:`strcpy` 复制到 `\0`;`strncpy` 限长且可能不补 `\0`;`memcpy` 按字节复制、不关心 `\0`。
 
 **详细解答**:
 
-| 函数 | 边界 | 是否复制 `\0` | 场景 |
-| ---- | ---- | ------------- | ---- |
-| `strcpy` | 到 `\0` | 是 | 已知源长度安全时 |
-| `strncpy` | 最多 n 字节 | `n <= len` 时不带 | 固定长度字段 |
-| `memcpy` | n 字节 | 不关心 | 结构体/二进制 |
+| 函数      | 边界        | 是否复制 `\0`     | 场景             |
+| --------- | ----------- | ----------------- | ---------------- |
+| `strcpy`  | 到 `\0`     | 是                | 已知源长度安全时 |
+| `strncpy` | 最多 n 字节 | `n <= len` 时不带 | 固定长度字段     |
+| `memcpy`  | n 字节      | 不关心            | 结构体/二进制    |
 
 ```c
 char d[16];
@@ -421,21 +480,23 @@ memcpy(d, "abc", 3);                        /* 不关 '\0' */
 `memcpy` 要求源与目标不重叠,重叠用 `memmove`。
 
 **追问**
+
 1. `strncpy(d, s, n)` 在 `n > strlen(s)` 时如何?(不足部分用 `\0` 填充)
 2. 拼接函数 `strcat` 有什么风险?(可能溢出,用 `strncat` 限定长度)
+
 ### Q5.3 字符串拼接、比较、查找各用什么函数?
 
 **答案要点**:拼接 `strcat`/`strncat`;比较 `strcmp`/`strncmp`;查找字符 `strchr`/`strrchr`、子串 `strstr`。
 
 **详细解答**:
 
-| 函数 | 作用 | 返回值 |
-| ---- | ---- | ------ |
-| `strcat`/`strncat` | 追加到末尾 | `dest` |
-| `strcmp`/`strncmp` | 按 ASCII 比较 | `<0`/`0`/`>0` |
-| `strchr` | 首次出现字符 | 指针 / `NULL` |
-| `strrchr` | 最后一次出现字符 | 指针 / `NULL` |
-| `strstr` | 首次出现子串 | 指针 / `NULL` |
+| 函数               | 作用             | 返回值        |
+| ------------------ | ---------------- | ------------- |
+| `strcat`/`strncat` | 追加到末尾       | `dest`        |
+| `strcmp`/`strncmp` | 按 ASCII 比较    | `<0`/`0`/`>0` |
+| `strchr`           | 首次出现字符     | 指针 / `NULL` |
+| `strrchr`          | 最后一次出现字符 | 指针 / `NULL` |
+| `strstr`           | 首次出现子串     | 指针 / `NULL` |
 
 ```c
 char s[64] = "Linux ";
@@ -445,8 +506,10 @@ char *p = strstr("I love my home", "home");  /* 指向 "home" */
 ```
 
 **追问**
+
 1. `strcat` 是否检查 `dest` 容量?(不检查,必须保证足够空间)
 2. `strchr(s, '\0')` 返回什么?(指向字符串结束符的指针)
+
 ### Q5.4 字符串与数字如何互转?
 
 **答案要点**:字符串转整型用 `atoi`/`atol`/`atoll`/`strtol`;转浮点用 `atof`/`strtod`;数字转字符串用 `sprintf`/`snprintf`。
@@ -461,20 +524,22 @@ char s[20]; snprintf(s, sizeof(s), "%d", i);
 ```
 
 **追问**
+
 1. `atoi` 遇到 `"abc"` 返回什么?(0,且无法区分「本来就是 0」和「转换失败」)
 2. 如何判断转换是否成功?(用 `strtol` 检查 `endptr` 与 `errno`)
+
 ### Q5.5 POSIX 正则表达式的使用流程是什么?
 
 **答案要点**:`regcomp` 编译 → `regexec` 匹配 → `regfree` 释放,出错时用 `regerror` 取描述。
 
 **详细解答**:
 
-| 函数 | 作用 | 返回值 |
-| ---- | ---- | ------ |
-| `regcomp` | 编译正则 | 0 成功,非 0 错误码 |
-| `regexec` | 执行匹配 | 0 匹配,`REG_NOMATCH` 不匹配 |
-| `regerror` | 错误码转描述 | 长度 |
-| `regfree` | 释放资源 | 无 |
+| 函数       | 作用         | 返回值                       |
+| ---------- | ------------ | ---------------------------- |
+| `regcomp`  | 编译正则     | 0 成功,非 0 错误码          |
+| `regexec`  | 执行匹配     | 0 匹配,`REG_NOMATCH` 不匹配 |
+| `regerror` | 错误码转描述 | 长度                         |
+| `regfree`  | 释放资源     | 无                           |
 
 `REG_EXTENDED` 启用扩展语法(`+`、`?`、`{}`、`|`、`()`)。`regmatch_t` 的 `rm_so`/`rm_eo` 是匹配子串的起止偏移(左闭右开)。
 
@@ -487,23 +552,26 @@ regfree(&reg);
 ```
 
 **追问**
+
 1. 如何循环提取所有匹配项?(每次把指针前移 `rm_eo` 再 `regexec`,空匹配时前移 1)
 2. `REG_EXTENDED` 与默认(BRE)语法区别?(BRE 中 `+`/`?`/`|`/`()` 需转义才生效)
 
 ---
+
 ## 六、系统信息与系统资源(5 题)
+
 ### Q6.1 如何获取系统信息?`uname`、`sysinfo`、`gethostname`、`sysconf` 各做什么?
 
 **答案要点**:`uname` 取操作系统/内核/架构;`sysinfo` 取内存/进程/运行时长;`gethostname` 取主机名;`sysconf` 取运行时配置。
 
 **详细解答**:
 
-| 函数 | 获取内容 | 头文件 |
-| ---- | -------- | ------ |
-| `uname` | sysname/nodename/release/version/machine | `<sys/utsname.h>` |
-| `sysinfo` | uptime、totalram、freeram、procs 等 | `<sys/sysinfo.h>` |
-| `gethostname` | 主机名 | `<unistd.h>` |
-| `sysconf` | `_SC_PAGESIZE`、`_SC_CLK_TCK`、`_SC_OPEN_MAX` 等 | `<unistd.h>` |
+| 函数          | 获取内容                                         | 头文件            |
+| ------------- | ------------------------------------------------ | ----------------- |
+| `uname`       | sysname/nodename/release/version/machine         | `<sys/utsname.h>` |
+| `sysinfo`     | uptime、totalram、freeram、procs 等              | `<sys/sysinfo.h>` |
+| `gethostname` | 主机名                                           | `<unistd.h>`      |
+| `sysconf`     | `_SC_PAGESIZE`、`_SC_CLK_TCK`、`_SC_OPEN_MAX` 等 | `<unistd.h>`      |
 
 ```c
 struct sysinfo si;
@@ -514,8 +582,10 @@ if (sysinfo(&si) == 0)
 内存字节数 = 字段值 × `mem_unit`。
 
 **追问**
+
 1. 如何取系统节拍率和页大小?(`sysconf(_SC_CLK_TCK)`、`sysconf(_SC_PAGESIZE)`)
 2. `/proc/meminfo` 与 `sysinfo` 有何关系?(都是内核内存信息的展现方式,`/proc` 以虚拟文件呈现)
+
 ### Q6.2 解释 GMT、UTC、时区、RTC 与 jiffies。
 
 **答案要点**:GMT 是格林威治标准时间;UTC 是世界协调时间(比 GMT 精准);时区配置在 `/usr/share/zoneinfo`,本地时间由 `/etc/localtime` 定义;RTC 是断电仍计时的硬件时钟;jiffies 记录系统节拍数。
@@ -523,23 +593,25 @@ if (sysinfo(&si) == 0)
 **详细解答**:Linux 开机读取一次 RTC 初始化系统时钟(即初始化 jiffies),之后内核自行维护系统时钟;关机时把系统时钟写回 RTC。jiffies 是内核全局变量,节拍率(如 100Hz)越低精度越低、中断负担越小,默认 100Hz。北京时间(东八区)比 UTC 早 8 小时。
 
 **追问**
+
 1. 如何查看/修改系统时区?(`date` 查看,`/etc/localtime` 软链接到 zoneinfo 下目标文件)
 2. RTC 和系统时钟谁先存在?(RTC 由电池供电长期存在;系统时钟开机时由 RTC 初始化)
+
 ### Q6.3 获取时间与时间转换函数有哪些?
 
 **答案要点**:获取时间 `time`(秒)、`gettimeofday`(微秒);转换 `ctime`/`localtime`/`gmtime`/`mktime`/`asctime`/`strftime`。
 
 **详细解答**:
 
-| 函数 | 方向 | 说明 |
-| ---- | ---- | ---- |
-| `time` | → time_t | 自 1970-01-01 UTC 起的秒数 |
-| `gettimeofday` | → timeval | 秒 + 微秒 |
-| `localtime`/`localtime_r` | time_t → tm | 本地时间 |
-| `gmtime`/`gmtime_r` | time_t → tm | UTC 时间 |
-| `mktime` | tm → time_t | 与 localtime 相反 |
-| `ctime`/`asctime` | → 字符串 | 固定格式 |
-| `strftime` | tm → 字符串 | 自定义格式 |
+| 函数                      | 方向        | 说明                       |
+| ------------------------- | ----------- | -------------------------- |
+| `time`                    | → time_t    | 自 1970-01-01 UTC 起的秒数 |
+| `gettimeofday`            | → timeval   | 秒 + 微秒                  |
+| `localtime`/`localtime_r` | time_t → tm | 本地时间                   |
+| `gmtime`/`gmtime_r`       | time_t → tm | UTC 时间                   |
+| `mktime`                  | tm → time_t | 与 localtime 相反          |
+| `ctime`/`asctime`         | → 字符串    | 固定格式                   |
+| `strftime`                | tm → 字符串 | 自定义格式                 |
 
 ```c
 time_t t = time(NULL);
@@ -552,8 +624,10 @@ strftime(s, sizeof(s), "%Y-%m-%d %H:%M:%S", &tm);
 注意 `tm_year` 加 1900、`tm_mon` 加 1;推荐使用可重入的 `_r` 版本。
 
 **追问**
+
 1. `%Y` 和 `%y`、`%m` 和 `%M` 分别是什么?(4 位年/2 位年;月/分)
 2. 夏令时字段是什么?(`tm_isdst`)
+
 ### Q6.4 进程时间是什么?`times` 和 `clock` 有什么区别?
 
 **答案要点**:进程时间 = 用户 CPU 时间 + 系统 CPU 时间;`times` 可区分用户/系统时间,`clock` 只给总时间。
@@ -567,8 +641,10 @@ printf("user=%.3fs\n", tb.tms_utime / (double)tck);
 ```
 
 **追问**
+
 1. 为什么进程时间可能远小于程序运行时间?(进程休眠/阻塞期间不占 CPU)
 2. `clock()` 能区分用户态和内核态吗?(不能)
+
 ### Q6.5 随机数、休眠、堆内存与 `/proc` 各要注意什么?
 
 **答案要点**:随机数用 `rand`/`srand`(伪随机,种子常用 `time(NULL)`);休眠 `sleep`/`usleep`/`nanosleep`;堆内存 `malloc`/`free`(`calloc` 置 0);`/proc` 是虚拟文件系统,用 `open`+`read` 或 `cat` 读取。
@@ -588,6 +664,7 @@ memset(p, 0, 1024); free(p);       /* 及时释放 */
 `/proc` 下常见文件:`version`(内核版本)、`cpuinfo`、`meminfo`、`stat`、`uptime`、`cmdline` 等。
 
 **追问**
+
 1. 进程退出内核会回收内存,为什么还要 `free`?(避免阶段性命周期内的内存泄漏,提升可维护性)
 2. `sleep` 和裸机 `for` 循环延时的本质区别?(`sleep` 主动让出 CPU,后者一直占用 CPU 空转)
 

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

@@ -1,6 +1,18 @@
 ---
 title: 信号机制
-tags: [嵌入式Linux, Linux应用编程, 进程, 信号, signal, sigaction, 信号集, 阻塞, 实时信号, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    Linux应用编程,
+    进程,
+    信号,
+    signal,
+    sigaction,
+    信号集,
+    阻塞,
+    实时信号,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第八章 信号:基础"
@@ -33,14 +45,14 @@ pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第八章 信号:
 
 ### 1.2 信号从哪里来
 
-| 来源 | 说明 | 例子 |
-| ---- | ---- | ---- |
-| 硬件异常 | 硬件检测到错误条件通知内核,内核再发给相关进程 | 除数为 0、数组越界引用无法访问的内存 |
-| 终端特殊字符 | 终端下输入产生信号的字符 | `Ctrl+C` → `SIGINT`;`Ctrl+\` → `SIGQUIT`;`Ctrl+Z` → `SIGTSTP` |
-| `kill()` 系统调用 | 进程把任意信号发给另一个进程或进程组 | 受限:收发双方所有者相同,或发送者是 root |
-| `kill` 命令 | 用户在终端向其它进程发信号,内部就是 `kill()` | `kill -9 xxx` 杀死 PID 为 xxx 的进程 |
-| 软件事件 | 检测到某种软件条件已发生 | 定时器超时、CPU 时间超限、子进程退出 |
-| 进程自身 | 进程可以给自己发信号 | `raise()`、`abort()` |
+| 来源              | 说明                                           | 例子                                                            |
+| ----------------- | ---------------------------------------------- | --------------------------------------------------------------- |
+| 硬件异常          | 硬件检测到错误条件通知内核,内核再发给相关进程 | 除数为 0、数组越界引用无法访问的内存                            |
+| 终端特殊字符      | 终端下输入产生信号的字符                       | `Ctrl+C` → `SIGINT`;`Ctrl+\` → `SIGQUIT`;`Ctrl+Z` → `SIGTSTP` |
+| `kill()` 系统调用 | 进程把任意信号发给另一个进程或进程组           | 受限:收发双方所有者相同,或发送者是 root                       |
+| `kill` 命令       | 用户在终端向其它进程发信号,内部就是 `kill()`  | `kill -9 xxx` 杀死 PID 为 xxx 的进程                            |
+| 软件事件          | 检测到某种软件条件已发生                       | 定时器超时、CPU 时间超限、子进程退出                            |
+| 进程自身          | 进程可以给自己发信号                           | `raise()`、`abort()`                                            |
 
 绝大多数到达进程的信号来自内核。
 
@@ -117,30 +129,30 @@ flowchart LR
 
 标准信号(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 |
+| 信号名称         | 编号 | 描述                               | 系统默认操作  |
+| ---------------- | ---- | ---------------------------------- | ------------- |
+| 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` 停止进程(注意停止 ≠ 终止,而是暂停)。
 
@@ -175,15 +187,15 @@ typedef void (*sig_t)(int);
 sig_t signal(int signum, sig_t handler);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
-| `signum` | 需要设置的信号,可使用信号名(宏)或数字编号,建议用信号名 |
+| 参数      | 含义                                                                                   |
+| --------- | -------------------------------------------------------------------------------------- |
+| `signum`  | 需要设置的信号,可使用信号名(宏)或数字编号,建议用信号名                             |
 | `handler` | `sig_t` 类型函数指针:自定义处理函数(捕获)、`SIG_IGN`(忽略)、`SIG_DFL`(系统默认) |
 
-| 返回值 | 含义 |
-| ------ | ---- |
-| 成功 | 返回指向此前信号处理函数的指针 |
-| 失败 | 返回 `SIG_ERR`,并设置 `errno` |
+| 返回值 | 含义                           |
+| ------ | ------------------------------ |
+| 成功   | 返回指向此前信号处理函数的指针 |
+| 失败   | 返回 `SIG_ERR`,并设置 `errno` |
 
 `SIG_IGN`、`SIG_DFL` 的定义:
 
@@ -204,16 +216,16 @@ sig_t signal(int signum, sig_t handler);
 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
-| `signum` | 需要设置的信号(`SIGKILL`、`SIGSTOP` 除外) |
-| `act` | 指向 `struct sigaction`,描述新处理方式;为 `NULL` 表示不改变当前处理方式 |
-| `oldact` | 不为 `NULL` 时,返回信号之前的处理方式;不关心可传 `NULL` |
+| 参数     | 含义                                                                      |
+| -------- | ------------------------------------------------------------------------- |
+| `signum` | 需要设置的信号(`SIGKILL`、`SIGSTOP` 除外)                               |
+| `act`    | 指向 `struct sigaction`,描述新处理方式;为 `NULL` 表示不改变当前处理方式 |
+| `oldact` | 不为 `NULL` 时,返回信号之前的处理方式;不关心可传 `NULL`                 |
 
-| 返回值 | 含义 |
-| ------ | ---- |
-| 成功 | 返回 0 |
-| 失败 | 返回 -1,并设置 `errno` |
+| 返回值 | 含义                    |
+| ------ | ----------------------- |
+| 成功   | 返回 0                  |
+| 失败   | 返回 -1,并设置 `errno` |
 
 `sigaction()` 允许单独获取处理函数而不设置,还可以通过标志精细控制调用处理函数时的行为,因此灵活性与可移植性更好。
 
@@ -229,24 +241,24 @@ struct sigaction {
 };
 ```
 
-| 成员 | 含义 |
-| ---- | ---- |
-| `sa_handler` | 指定信号处理函数,与 `signal()` 的 `handler` 相同 |
-| `sa_sigaction` | 替代处理函数,能获取更多信息(通过 `siginfo_t`)。与 `sa_handler` 互斥,通过 `SA_SIGINFO` 标志选择 |
-| `sa_mask` | 执行处理函数前,把这组信号加入进程信号掩码;处理函数返回后恢复。进入处理函数时,当前处理的信号会被**自动**加入掩码,保证同种信号不重入 |
-| `sa_restorer` | 已过时,不要使用 |
-| `sa_flags` | 一组控制标志,多个用 `\|` 组合 |
+| 成员           | 含义                                                                                                                                   |
+| -------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
+| `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` |
+| `SA_NOCLDWAIT` | 若 `signum` 是 `SIGCHLD`,子进程终止时不转为僵尸进程                                                                         |
+| `SA_NODEFER`   | 不阻塞从自身处理函数中接收的此信号(默认会阻塞同种信号以避免竞态)                                                           |
+| `SA_RESETHAND` | 执行完处理函数后,把处理方式重置为系统默认操作                                                                               |
+| `SA_RESTART`   | 被信号中断的系统调用,在信号处理完成后自动重新发起                                                                           |
+| `SA_SIGINFO`   | 使用 `sa_sigaction` 而非 `sa_handler`                                                                                        |
 
 ### 4.4 siginfo_t
 
@@ -295,24 +307,24 @@ siginfo_t {
 int kill(pid_t pid, int sig);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
-| `pid` | 目标进程/进程组,取值见下表 |
+| 参数  | 含义                                                                        |
+| ----- | --------------------------------------------------------------------------- |
+| `pid` | 目标进程/进程组,取值见下表                                                 |
 | `sig` | 需要发送的信号;为 0 时不发送信号,但仍执行错误检查,可用于判断进程是否存在 |
 
-| 返回值 | 含义 |
-| ------ | ---- |
-| 成功 | 返回 0 |
-| 失败 | 返回 -1,并设置 `errno`(如 `ESRCH` 表示进程不存在) |
+| 返回值 | 含义                                                 |
+| ------ | ---------------------------------------------------- |
+| 成功   | 返回 0                                               |
+| 失败   | 返回 -1,并设置 `errno`(如 `ESRCH` 表示进程不存在) |
 
 `pid` 不同取值含义:
 
-| pid 取值 | 含义 |
-| -------- | ---- |
-| `pid > 0` | 发送给 pid 指定的进程 |
-| `pid == 0` | 发送给当前进程所在进程组中的每个进程 |
+| pid 取值    | 含义                                                       |
+| ----------- | ---------------------------------------------------------- |
+| `pid > 0`   | 发送给 pid 指定的进程                                      |
+| `pid == 0`  | 发送给当前进程所在进程组中的每个进程                       |
 | `pid == -1` | 发送给当前进程有权发送信号的每个进程,但进程 1(init)除外 |
-| `pid < -1` | 发送给 ID 为 `-pid` 的进程组中的每个进程 |
+| `pid < -1`  | 发送给 ID 为 `-pid` 的进程组中的每个进程                   |
 
 权限规则:root 进程可以把信号发给任何进程;普通用户进程的基本规则是**发送者进程的实际/有效用户 ID 必须等于接收者进程的实际/有效用户 ID**。
 
@@ -326,14 +338,14 @@ int kill(pid_t pid, int sig);
 int raise(int sig);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
+| 参数  | 含义           |
+| ----- | -------------- |
 | `sig` | 需要发送的信号 |
 
-| 返回值 | 含义 |
-| ------ | ---- |
-| 成功 | 返回 0 |
-| 失败 | 返回非零值 |
+| 返回值 | 含义       |
+| ------ | ---------- |
+| 成功   | 返回 0     |
+| 失败   | 返回非零值 |
 
 `raise()` 等价于 `kill(getpid(), sig);`,其中 `getpid()` 用于获取进程自身 PID。
 
@@ -345,16 +357,16 @@ int raise(int sig);
 int sigqueue(pid_t pid, int sig, const union sigval value);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
-| `pid` | 接收信号的进程 PID |
-| `sig` | 需要发送的信号;为 0 时用于检查进程是否存在 |
-| `value` | 信号的伴随数据,`union sigval` 类型 |
+| 参数    | 含义                                        |
+| ------- | ------------------------------------------- |
+| `pid`   | 接收信号的进程 PID                          |
+| `sig`   | 需要发送的信号;为 0 时用于检查进程是否存在 |
+| `value` | 信号的伴随数据,`union sigval` 类型         |
 
-| 返回值 | 含义 |
-| ------ | ---- |
-| 成功 | 返回 0 |
-| 失败 | 返回 -1,并设置 `errno` |
+| 返回值 | 含义                    |
+| ------ | ----------------------- |
+| 成功   | 返回 0                  |
+| 失败   | 返回 -1,并设置 `errno` |
 
 ```c
 typedef union sigval {
@@ -377,13 +389,13 @@ typedef union sigval {
 unsigned int alarm(unsigned int seconds);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
+| 参数      | 含义                                         |
+| --------- | -------------------------------------------- |
 | `seconds` | 定时秒数;为 0 表示取消之前设置的 alarm 闹钟 |
 
-| 返回值 | 含义 |
-| ------ | ---- |
-| 成功 | 若之前已设置闹钟且未超时,返回旧闹钟的剩余秒数;否则返回 0 |
+| 返回值 | 含义                                                       |
+| ------ | ---------------------------------------------------------- |
+| 成功   | 若之前已设置闹钟且未超时,返回旧闹钟的剩余秒数;否则返回 0 |
 
 要点:
 
@@ -429,11 +441,11 @@ 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` |
+| 函数                         | 参数                            | 返回值                                           |
+| ---------------------------- | ------------------------------- | ------------------------------------------------ |
+| `sigemptyset` / `sigfillset` | `set` 指向待初始化信号集        | 成功 0;失败 -1 并设 `errno`                     |
+| `sigaddset` / `sigdelset`    | `set` 信号集;`signum` 目标信号 | 成功 0;失败 -1 并设 `errno`                     |
+| `sigismember`                | `set` 信号集;`signum` 待测信号 | 在集合中返回 1;不在返回 0;失败 -1 并设 `errno` |
 
 典型用法:
 
@@ -489,24 +501,24 @@ void psignal(int sig, const char *s);
 int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
-| `how` | 操作方式,见下表 |
-| `set` | 要加入/移除的信号集;为 `NULL` 表示不改动当前掩码 |
-| `oldset` | 不为 `NULL` 时,在修改前获取当前信号掩码存入其中 |
+| 参数     | 含义                                              |
+| -------- | ------------------------------------------------- |
+| `how`    | 操作方式,见下表                                  |
+| `set`    | 要加入/移除的信号集;为 `NULL` 表示不改动当前掩码 |
+| `oldset` | 不为 `NULL` 时,在修改前获取当前信号掩码存入其中  |
 
-| 返回值 | 含义 |
-| ------ | ---- |
-| 成功 | 返回 0 |
-| 失败 | 返回 -1,并设置 `errno` |
+| 返回值 | 含义                    |
+| ------ | ----------------------- |
+| 成功   | 返回 0                  |
+| 失败   | 返回 -1,并设置 `errno` |
 
 `how` 取值:
 
-| 宏 | 含义 |
-| -- | ---- |
-| `SIG_BLOCK` | 把 `set` 中所有信号加入信号掩码(当前值 ∪ set) |
-| `SIG_UNBLOCK` | 把 `set` 中所有信号从信号掩码移除 |
-| `SIG_SETMASK` | 信号掩码直接设置为 `set` |
+| 宏            | 含义                                            |
+| ------------- | ----------------------------------------------- |
+| `SIG_BLOCK`   | 把 `set` 中所有信号加入信号掩码(当前值 ∪ set) |
+| `SIG_UNBLOCK` | 把 `set` 中所有信号从信号掩码移除               |
+| `SIG_SETMASK` | 信号掩码直接设置为 `set`                        |
 
 ```c
 sigset_t sig_set;
@@ -534,12 +546,12 @@ sigprocmask(SIG_UNBLOCK, &sig_set, NULL); /* 解除阻塞 */
 int sigsuspend(const sigset_t *mask);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
+| 参数   | 含义           |
+| ------ | -------------- |
 | `mask` | 指向一个信号集 |
 
-| 返回值 | 含义 |
-| ------ | ---- |
+| 返回值      | 含义                                                                                  |
+| ----------- | ------------------------------------------------------------------------------------- |
 | 总是返回 -1 | 设置 `errno` 指示错误,通常为 `EINTR`(被信号中断);调用失败时 `errno` 设为 `EFAULT` |
 
 `sigsuspend()` 把进程信号掩码替换为 `mask`,然后挂起进程,直到捕获到信号被唤醒(如果捕获的信号本身是 `mask` 集合成员,则不会唤醒、继续挂起),并从信号处理函数返回;一旦返回,`sigsuspend()` 会把信号掩码恢复为调用前的值。
@@ -566,14 +578,14 @@ sigprocmask(SIG_SETMASK, &old_mask, NULL);
 int sigpending(sigset_t *set);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
+| 参数  | 含义                                       |
+| ----- | ------------------------------------------ |
 | `set` | 处于等待状态的信号会存放到它指向的信号集中 |
 
-| 返回值 | 含义 |
-| ------ | ---- |
-| 成功 | 返回 0 |
-| 失败 | 返回 -1,并设置 `errno` |
+| 返回值 | 含义                    |
+| ------ | ----------------------- |
+| 成功   | 返回 0                  |
+| 失败   | 返回 -1,并设置 `errno` |
 
 ```c
 sigset_t sig_set;
@@ -651,10 +663,10 @@ void abort(void);
 int sigwait(const sigset_t *set, int *sig);
 ```
 
-| 返回值 | 含义 |
-| ------ | ---- |
-| 成功 | 返回 0 |
-| 失败 | 返回错误编号(不设置 `errno`) |
+| 返回值 | 含义                           |
+| ------ | ------------------------------ |
+| 成功   | 返回 0                         |
+| 失败   | 返回错误编号(不设置 `errno`) |
 
 与 `sigsuspend()` 的区别:`sigsuspend()` 仍会调用在 `sigaction` 中注册的处理函数,返回 -1 和 `EINTR`;`sigwait()` 则**不调用**处理函数,直接把信号取走。多线程程序里常用 `sigwait()` 让某个线程专职处理信号。
 
@@ -865,15 +877,15 @@ gcc -Wall -o sig_demo sig_demo.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` | 允许生成核心转储文件 |
+| 命令                           | 用途                                   |
+| ------------------------------ | -------------------------------------- |
+| `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
 
@@ -902,15 +914,15 @@ kill -9 <pid>          # 只有 SIGKILL 才能杀死它
 
 ## 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 |
+| 维度         | 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 通用。信号更适合"用户态异步通知",硬件事件仍由驱动 + 中断处理。
 

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

@@ -1,6 +1,19 @@
 ---
 title: 进程管理
-tags: [嵌入式Linux, Linux应用编程, 进程, fork, exec, 环境变量, 虚拟地址空间, 僵尸进程, 守护进程, 单例模式, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    Linux应用编程,
+    进程,
+    fork,
+    exec,
+    环境变量,
+    虚拟地址空间,
+    僵尸进程,
+    守护进程,
+    单例模式,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第九章 进程"
@@ -48,10 +61,10 @@ sequenceDiagram
 
 进程终止分正常终止与异常终止:
 
-| 类别 | 方式 |
-| ---- | ---- |
+| 类别     | 方式                                                                  |
+| -------- | --------------------------------------------------------------------- |
 | 正常终止 | `main()` 中 `return` 返回;调用 `exit()`;调用 `_exit()` 或 `_Exit()` |
-| 异常终止 | 调用 `abort()`;进程接收到某个信号(如 `SIGKILL`) |
+| 异常终止 | 调用 `abort()`;进程接收到某个信号(如 `SIGKILL`)                    |
 
 ### 1.3 注册进程终止处理函数 atexit()
 
@@ -61,14 +74,14 @@ sequenceDiagram
 int atexit(void (*function)(void));
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
+| 参数       | 含义                                             |
+| ---------- | ------------------------------------------------ |
 | `function` | 函数指针,指向注册的函数;该函数无参数、无返回值 |
 
-| 返回值 | 含义 |
-| ------ | ---- |
-| 成功 | 返回 0 |
-| 失败 | 返回非 0 |
+| 返回值 | 含义     |
+| ------ | -------- |
+| 成功   | 返回 0   |
+| 失败   | 返回非 0 |
 
 注意:如果程序使用 `_exit()` 或 `_Exit()` 终止进程而不是 `exit()`,则**不会执行**注册的终止处理函数。
 
@@ -147,14 +160,14 @@ int main(void)
 char *getenv(const char *name);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
+| 参数   | 含义                 |
+| ------ | -------------------- |
 | `name` | 要获取的环境变量名称 |
 
-| 返回值 | 含义 |
-| ------ | ---- |
-| 存在 | 返回该环境变量值字符串的指针 |
-| 不存在 | 返回 `NULL` |
+| 返回值 | 含义                         |
+| ------ | ---------------------------- |
+| 存在   | 返回该环境变量值字符串的指针 |
+| 不存在 | 返回 `NULL`                  |
 
 注意:不应该修改 `getenv()` 返回的字符串,修改它意味着修改环境变量的值。需要修改应使用下面提供的函数。
 
@@ -173,16 +186,16 @@ int setenv(const char *name, const char *value, int overwrite);
 
 `setenv()`:`name` 为变量名,`value` 为值,`overwrite` 决定变量已存在时是否覆盖。
 
-| 参数 | 含义 |
-| ---- | ---- |
-| `name` | 需要添加或修改的环境变量名称 |
-| `value` | 环境变量的值 |
+| 参数        | 含义                                                                                   |
+| ----------- | -------------------------------------------------------------------------------------- |
+| `name`      | 需要添加或修改的环境变量名称                                                           |
+| `value`     | 环境变量的值                                                                           |
 | `overwrite` | 环境变量已存在时:为 0 不改变现有值(本次调用无影响);非 0 则覆盖;不存在时都表示添加 |
 
-| 返回值 | 含义 |
-| ------ | ---- |
-| 成功 | 返回 0 |
-| 失败 | 返回 -1,并设置 `errno` |
+| 返回值 | 含义                    |
+| ------ | ----------------------- |
+| 成功   | 返回 0                  |
+| 失败   | 返回 -1,并设置 `errno` |
 
 `setenv()` 与 `putenv()` 的两个区别:
 
@@ -228,13 +241,13 @@ NAME=value ./app
 
 历史沿袭至今,C 语言程序一直由以下几部分组成:
 
-| 段 | 名称 | 内容 | 特性 |
-| -- | ---- | ---- | ---- |
-| 正文段 | 代码段 | CPU 执行的机器语言指令 | 只读,防止意外修改;可被多个进程共享 |
-| 初始化数据段 | 数据段 | 显式初始化的全局变量和静态变量 | 加载时从可执行文件读取值 |
-| 未初始化数据段 | bss 段 | 未显式初始化的全局变量和静态变量 | 程序执行前系统将其全部初始化为 0;可执行文件只记录位置与大小 |
-| 栈 | stack | 函数内局部变量、函数调用保存的信息、实参、返回值 | 动态增长收缩,由栈帧组成 |
-| 堆 | heap | 运行时动态分配的内存(如 `malloc`) | 可在运行时动态分配 |
+| 段             | 名称   | 内容                                             | 特性                                                         |
+| -------------- | ------ | ------------------------------------------------ | ------------------------------------------------------------ |
+| 正文段         | 代码段 | CPU 执行的机器语言指令                           | 只读,防止意外修改;可被多个进程共享                         |
+| 初始化数据段   | 数据段 | 显式初始化的全局变量和静态变量                   | 加载时从可执行文件读取值                                     |
+| 未初始化数据段 | bss 段 | 未显式初始化的全局变量和静态变量                 | 程序执行前系统将其全部初始化为 0;可执行文件只记录位置与大小 |
+| 栈             | stack  | 函数内局部变量、函数调用保存的信息、实参、返回值 | 动态增长收缩,由栈帧组成                                     |
+| 堆             | heap   | 运行时动态分配的内存(如 `malloc`)              | 可在运行时动态分配                                           |
 
 > **bss** 一词来源于早期汇编操作符,意思是"由符号开始的块"(block started by symbol)。
 
@@ -302,11 +315,11 @@ pid_t fork(void);
 
 理解 `fork()` 的关键:调用完成后存在两个进程,各自从 `fork()` 的返回处继续执行,因此 **`fork()` 会返回两次**——子进程返回一个值,父进程返回一个值。
 
-| 返回位置 | 返回值 | 含义 |
-| -------- | ------ | ---- |
-| 父进程 | 子进程的 PID | 调用成功 |
-| 子进程 | 0 | 调用成功 |
-| 父进程 | -1(并设 `errno`) | 调用失败,不创建子进程 |
+| 返回位置 | 返回值             | 含义                   |
+| -------- | ------------------ | ---------------------- |
+| 父进程   | 子进程的 PID       | 调用成功               |
+| 子进程   | 0                  | 调用成功               |
+| 父进程   | -1(并设 `errno`) | 调用失败,不创建子进程 |
 
 ### 5.1 子进程拷贝了什么
 
@@ -335,10 +348,10 @@ flowchart TD
 
 对应地,如果父进程 `fork()` **之后**父子进程**各自 `open()`** 同一个文件,则两个文件描述符指向**不同的文件表**,各自有独立的偏移量,一方修改偏移量不影响另一方,写入数据会出现**覆盖**。
 
-| 场景 | 文件描述符 | 文件表 | 偏移量 | 写入效果 |
-| ---- | ---------- | ------ | ------ | -------- |
-| `open` 后 `fork` | 子进程继承副本 | 同一份 | 共享 | 接续写 |
-| `fork` 后各自 `open` | 各进程独立 | 各一份 | 独立 | 可能覆盖 |
+| 场景                 | 文件描述符     | 文件表 | 偏移量 | 写入效果 |
+| -------------------- | -------------- | ------ | ------ | -------- |
+| `open` 后 `fork`     | 子进程继承副本 | 同一份 | 共享   | 接续写   |
+| `fork` 后各自 `open` | 各进程独立     | 各一份 | 独立   | 可能覆盖 |
 
 ### 5.3 fork() 的使用场景
 
@@ -452,14 +465,14 @@ fork();
 pid_t wait(int *status);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
+| 参数     | 含义                                            |
+| -------- | ----------------------------------------------- |
 | `status` | 存放子进程终止状态信息;可为 `NULL`,表示不接收 |
 
-| 返回值 | 含义 |
-| ------ | ---- |
-| 成功 | 返回终止的子进程 PID |
-| 失败 | 返回 -1 |
+| 返回值 | 含义                 |
+| ------ | -------------------- |
+| 成功   | 返回终止的子进程 PID |
+| 失败   | 返回 -1              |
 
 `wait()` 执行的动作:
 
@@ -469,13 +482,13 @@ pid_t wait(int *status);
 
 `status` 检查宏:
 
-| 宏 | 含义 |
-| -- | ---- |
-| `WIFEXITED(status)` | 子进程正常终止时返回 true |
+| 宏                    | 含义                                          |
+| --------------------- | --------------------------------------------- |
+| `WIFEXITED(status)`   | 子进程正常终止时返回 true                     |
 | `WEXITSTATUS(status)` | 返回子进程退出状态(`exit`/`_exit` 指定的值) |
-| `WIFSIGNALED(status)` | 子进程被信号终止时返回 true |
-| `WTERMSIG(status)` | 返回导致子进程终止的信号编号 |
-| `WCOREDUMP(status)` | 子进程终止时产生核心转储文件时返回 true |
+| `WIFSIGNALED(status)` | 子进程被信号终止时返回 true                   |
+| `WTERMSIG(status)`    | 返回导致子进程终止的信号编号                  |
+| `WCOREDUMP(status)`   | 子进程终止时产生核心转储文件时返回 true       |
 
 获取到的 `status` 并不是 `exit()` 时指定的状态,需要通过 `WEXITSTATUS` 宏转换。
 
@@ -494,28 +507,28 @@ pid_t wait(int *status);
 pid_t waitpid(pid_t pid, int *status, int options);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
-| `pid` | 取值含义见下表 |
-| `status` | 与 `wait()` 的 `status` 意义相同 |
-| `options` | 位掩码,见下表 |
+| 参数      | 含义                             |
+| --------- | -------------------------------- |
+| `pid`     | 取值含义见下表                   |
+| `status`  | 与 `wait()` 的 `status` 意义相同 |
+| `options` | 位掩码,见下表                   |
 
 `pid` 取值:
 
-| pid 取值 | 含义 |
-| -------- | ---- |
-| `pid > 0` | 等待进程号为 pid 的子进程 |
-| `pid == 0` | 等待与调用进程同一进程组的所有子进程 |
-| `pid < -1` | 等待进程组标识符与 `\|pid\|` 相等的所有子进程 |
+| pid 取值    | 含义                                                              |
+| ----------- | ----------------------------------------------------------------- |
+| `pid > 0`   | 等待进程号为 pid 的子进程                                         |
+| `pid == 0`  | 等待与调用进程同一进程组的所有子进程                              |
+| `pid < -1`  | 等待进程组标识符与 `\|pid\|` 相等的所有子进程                     |
 | `pid == -1` | 等待任意子进程;`wait(&status)` 与 `waitpid(-1, &status, 0)` 等价 |
 
 `options` 标志:
 
-| 标志 | 含义 |
-| ---- | ---- |
-| `WNOHANG` | 子进程没有状态改变时立即返回(非阻塞等待,可轮询);返回 0 表示没有发生改变 |
-| `WUNTRACED` | 除返回终止子进程状态外,还返回因信号而停止的子进程状态 |
-| `WCONTINUED` | 返回因收到 `SIGCONT` 而恢复运行的子进程状态 |
+| 标志         | 含义                                                                        |
+| ------------ | --------------------------------------------------------------------------- |
+| `WNOHANG`    | 子进程没有状态改变时立即返回(非阻塞等待,可轮询);返回 0 表示没有发生改变 |
+| `WUNTRACED`  | 除返回终止子进程状态外,还返回因信号而停止的子进程状态                      |
+| `WCONTINUED` | 返回因收到 `SIGCONT` 而恢复运行的子进程状态                                 |
 
 `waitid()` 与 `waitpid()` 类似,提供更多扩展功能,可查阅 man 手册。
 
@@ -583,16 +596,16 @@ stateDiagram-v2
 int execve(const char *filename, char *const argv[], char *const envp[]);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
-| `filename` | 需要载入当前进程空间的新程序路径名,绝对路径或相对路径均可 |
-| `argv` | 传给新程序的命令行参数,字符串指针数组,以 `NULL` 结束,对应 `main` 的 `argv`;`argv[0]` 是新程序自身路径名 |
-| `envp` | 新程序的环境变量列表,字符串指针数组,以 `NULL` 结束,字符串格式为 `name=value`;对应新程序的 `environ` |
+| 参数       | 含义                                                                                                        |
+| ---------- | ----------------------------------------------------------------------------------------------------------- |
+| `filename` | 需要载入当前进程空间的新程序路径名,绝对路径或相对路径均可                                                  |
+| `argv`     | 传给新程序的命令行参数,字符串指针数组,以 `NULL` 结束,对应 `main` 的 `argv`;`argv[0]` 是新程序自身路径名 |
+| `envp`     | 新程序的环境变量列表,字符串指针数组,以 `NULL` 结束,字符串格式为 `name=value`;对应新程序的 `environ`     |
 
-| 返回值 | 含义 |
-| ------ | ---- |
-| 成功 | **永不返回** |
-| 失败 | 返回 -1,并设置 `errno` |
+| 返回值 | 含义                    |
+| ------ | ----------------------- |
+| 成功   | **永不返回**            |
+| 失败   | 返回 -1,并设置 `errno` |
 
 `execve()` 把新程序加载到进程内存空间,用新程序替换旧程序:栈、数据、堆会被新程序的相应部件替换,然后从新程序的 `main()` 开始执行。对它的成功调用永不返回,也无需检查返回值——一旦返回就表明发生错误。
 
@@ -615,14 +628,14 @@ int execvpe(const char *file, char *const argv[], char *const envp[]);
 
 区别归纳:
 
-| 函数 | 参数形式 | 是否查 PATH | 是否自定义环境变量 |
-| ---- | -------- | ----------- | ------------------ |
-| `execl` | 可变参数列表,以 `NULL` 结尾 | 否(需路径) | 否 |
-| `execv` | `argv` 数组 | 否(需路径) | 否 |
-| `execlp` | 可变参数列表 | **是**(可只给文件名) | 否 |
-| `execvp` | `argv` 数组 | **是** | 否 |
-| `execle` | 可变参数列表 + `envp` | 否 | **是** |
-| `execvpe` | `argv` 数组 + `envp` | **是** | **是** |
+| 函数      | 参数形式                     | 是否查 PATH            | 是否自定义环境变量 |
+| --------- | ---------------------------- | ---------------------- | ------------------ |
+| `execl`   | 可变参数列表,以 `NULL` 结尾 | 否(需路径)           | 否                 |
+| `execv`   | `argv` 数组                  | 否(需路径)           | 否                 |
+| `execlp`  | 可变参数列表                 | **是**(可只给文件名) | 否                 |
+| `execvp`  | `argv` 数组                  | **是**                 | 否                 |
+| `execle`  | 可变参数列表 + `envp`        | 否                     | **是**             |
+| `execvpe` | `argv` 数组 + `envp`         | **是**                 | **是**             |
 
 - **`l` 表示 list(参数列表),`v` 表示 vector(数组)**:决定参数以可变参数还是字符串数组传递。
 - **`p` 表示 PATH**:允许只提供文件名,系统在 `PATH` 环境变量指定的目录中查找;也兼容相对/绝对路径。
@@ -662,8 +675,8 @@ execle("./newApp", "./newApp", "Hello", "World", NULL, env_arr);
 int system(const char *command);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
+| 参数      | 含义                                                            |
+| --------- | --------------------------------------------------------------- |
 | `command` | 需要执行的 shell 命令字符串,如 `"ls -al"`、`"echo HelloWorld"` |
 
 `system()` 内部通过 `fork()`、`execl()`、`waitpid()` 实现:先 `fork()` 创建子进程运行 shell,再由 shell 执行 `command` 指定的命令。
@@ -683,14 +696,14 @@ int system(const char *command);
 
 ### 11.1 进程的六种状态
 
-| 状态 | 含义 |
-| ---- | ---- |
-| 就绪态(Ready) | 满足被 CPU 调度的所有条件但尚未被调度执行,得到 CPU 就能直接运行 |
-| 运行态 | 当前正在被 CPU 调度运行 |
-| 僵尸态 | 进程已结束,但父进程还未给它"收尸"(即僵尸进程) |
-| 可中断睡眠状态 | 浅度睡眠,可被信号唤醒 |
-| 不可中断睡眠状态 | 深度睡眠,无法被信号唤醒,只能等条件成立 |
-| 暂停态 | 进程暂停运行(不是终止),一般由信号暂停(如 `SIGSTOP`),可用 `SIGCONT` 恢复 |
+| 状态             | 含义                                                                          |
+| ---------------- | ----------------------------------------------------------------------------- |
+| 就绪态(Ready)  | 满足被 CPU 调度的所有条件但尚未被调度执行,得到 CPU 就能直接运行              |
+| 运行态           | 当前正在被 CPU 调度运行                                                       |
+| 僵尸态           | 进程已结束,但父进程还未给它"收尸"(即僵尸进程)                              |
+| 可中断睡眠状态   | 浅度睡眠,可被信号唤醒                                                        |
+| 不可中断睡眠状态 | 深度睡眠,无法被信号唤醒,只能等条件成立                                      |
+| 暂停态           | 进程暂停运行(不是终止),一般由信号暂停(如 `SIGSTOP`),可用 `SIGCONT` 恢复 |
 
 浅度睡眠与深度睡眠统称为**等待态(阻塞态)**:表示进程在等待某种条件成立后进入就绪态,处于等待态的进程无法参与系统调度。新创建的进程处于就绪态。
 
@@ -1090,16 +1103,16 @@ gcc -Wall -o proc_demo proc_demo.c
 
 ### 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 等) |
+| 命令                 | 用途                                               |
+| -------------------- | -------------------------------------------------- |
+| `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 返回值与文件共享
 
@@ -1132,16 +1145,16 @@ ps -ajx | grep proc_demo     # TTY 应为 ?
 
 ## 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 |
+| 维度           | 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        |
 
 要点:
 

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

@@ -1,6 +1,18 @@
 ---
 title: 进程间通信
-tags: [嵌入式Linux, Linux应用编程, IPC, 管道, FIFO, 消息队列, 信号量, 共享内存, Socket, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    Linux应用编程,
+    IPC,
+    管道,
+    FIFO,
+    消息队列,
+    信号量,
+    共享内存,
+    Socket,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十章 进程间通信简介"
@@ -72,15 +84,15 @@ flowchart TD
 
 > ⚠️ **来源说明**:下表"适用场景与限制"的细节为扩展知识,机制分类本身来自教材。
 
-| 机制 | 类别 | 通信方向 | 能否跨非亲缘进程 | 数据形式 | 典型适用场景 | 主要限制 |
-| ---- | ---- | -------- | ---------------- | -------- | ------------ | -------- |
-| 管道 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 | 全双工 | 是(可跨主机) | 字节流/数据报 | 网络通信、跨主机、跨语言 | 协议栈开销 |
+| 机制                      | 类别           | 通信方向    | 能否跨非亲缘进程 | 数据形式            | 典型适用场景                   | 主要限制           |
+| ------------------------- | -------------- | ----------- | ---------------- | ------------------- | ------------------------------ | ------------------ |
+| 管道 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     | 全双工      | 是(可跨主机)   | 字节流/数据报       | 网络通信、跨主机、跨语言       | 协议栈开销         |
 
 ---
 
@@ -90,11 +102,11 @@ flowchart TD
 
 管道包括三种:
 
-| 类型 | 单工/双工 | 适用范围 |
-| ---- | --------- | -------- |
-| 普通管道 pipe | 单工,数据只能单向传输 | 只能在父子或兄弟进程间使用 |
-| 流管道 s_pipe | 半双工,可以双向传输 | 只能在父子或兄弟进程间使用 |
-| 有名管道 name_pipe(FIFO) | 双向 | 允许在不相关(非父子/兄弟)的进程间通信 |
+| 类型                       | 单工/双工              | 适用范围                                |
+| -------------------------- | ---------------------- | --------------------------------------- |
+| 普通管道 pipe              | 单工,数据只能单向传输 | 只能在父子或兄弟进程间使用              |
+| 流管道 s_pipe              | 半双工,可以双向传输   | 只能在父子或兄弟进程间使用              |
+| 有名管道 name_pipe(FIFO) | 双向                   | 允许在不相关(非父子/兄弟)的进程间通信 |
 
 - **普通管道**:用于具有亲缘关系的进程间通信,数据只能单向传输;要实现双向传输必须使用**两个管道**。
 - **流管道**:去除了普通管道的单向限制,以半双工方式双向传输,但仍只能在亲缘进程间通信。
@@ -110,14 +122,14 @@ flowchart TD
 int pipe(int pipefd[2]);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
+| 参数     | 含义                                             |
+| -------- | ------------------------------------------------ |
 | `pipefd` | 输出参数,`pipefd[0]` 为读端,`pipefd[1]` 为写端 |
 
-| 返回值 | 含义 |
-| ------ | ---- |
-| 成功 | 返回 0 |
-| 失败 | 返回 -1,并设置 `errno` |
+| 返回值 | 含义                    |
+| ------ | ----------------------- |
+| 成功   | 返回 0                  |
+| 失败   | 返回 -1,并设置 `errno` |
 
 典型用法:父进程 `pipe()` 后 `fork()`,子进程继承两个 fd;关闭不用的一端后即可单向通信(父写 `pipefd[1]`,子读 `pipefd[0]`,或反之)。
 
@@ -129,13 +141,13 @@ flowchart LR
 
 **读写阻塞规则**:
 
-| 情况 | 行为 |
-| ---- | ---- |
-| 管道为空,仍有写端打开 | `read()` 阻塞,直到有数据写入 |
-| 所有写端都已关闭 | `read()` 不再阻塞,返回 0(读到 EOF) |
-| 管道缓冲区已满 | `write()` 阻塞,直到有空间 |
-| 所有读端都已关闭 | `write()` 触发 `SIGPIPE` 信号,默认终止进程(`write` 返回 -1,`errno` 为 `EPIPE`) |
-| 写入量 ≤ `PIPE_BUF`(Linux 默认 4096) | 写入是原子的,不会与其它进程的写交错 |
+| 情况                                   | 行为                                                                               |
+| -------------------------------------- | ---------------------------------------------------------------------------------- |
+| 管道为空,仍有写端打开                 | `read()` 阻塞,直到有数据写入                                                      |
+| 所有写端都已关闭                       | `read()` 不再阻塞,返回 0(读到 EOF)                                              |
+| 管道缓冲区已满                         | `write()` 阻塞,直到有空间                                                         |
+| 所有读端都已关闭                       | `write()` 触发 `SIGPIPE` 信号,默认终止进程(`write` 返回 -1,`errno` 为 `EPIPE`) |
+| 写入量 ≤ `PIPE_BUF`(Linux 默认 4096) | 写入是原子的,不会与其它进程的写交错                                               |
 
 要点:管道是**无格式字节流**,没有消息边界;读端关闭后写端继续写会收到 `SIGPIPE`,因此健壮的程序应处理或忽略 `SIGPIPE`。
 
@@ -152,23 +164,23 @@ FIFO 在文件系统中以一个**文件**存在(文件类型为 `p`),因
 int mkfifo(const char *pathname, mode_t mode);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
-| `pathname` | FIFO 文件路径 |
-| `mode` | 权限位(会与 `umask` 作用) |
+| 参数       | 含义                        |
+| ---------- | --------------------------- |
+| `pathname` | FIFO 文件路径               |
+| `mode`     | 权限位(会与 `umask` 作用) |
 
-| 返回值 | 含义 |
-| ------ | ---- |
-| 成功 | 返回 0 |
-| 失败 | 返回 -1,并设置 `errno`(如 `EEXIST`) |
+| 返回值 | 含义                                   |
+| ------ | -------------------------------------- |
+| 成功   | 返回 0                                 |
+| 失败   | 返回 -1,并设置 `errno`(如 `EEXIST`) |
 
 创建后,用 `open()`、`read()`、`write()`、`close()` 像普通文件一样操作,规则与 pipe 一致。**打开阻塞规则**:
 
-| 打开方式 | 行为 |
-| -------- | ---- |
-| `open(path, O_RDONLY)` | 阻塞,直到有进程以写方式打开该 FIFO |
-| `open(path, O_WRONLY)` | 阻塞,直到有进程以读方式打开该 FIFO |
-| 加 `O_NONBLOCK` 只读打开 | 立即成功返回 |
+| 打开方式                 | 行为                                  |
+| ------------------------ | ------------------------------------- |
+| `open(path, O_RDONLY)`   | 阻塞,直到有进程以写方式打开该 FIFO   |
+| `open(path, O_WRONLY)`   | 阻塞,直到有进程以读方式打开该 FIFO   |
+| 加 `O_NONBLOCK` 只读打开 | 立即成功返回                          |
 | 加 `O_NONBLOCK` 只写打开 | 若无读者,返回 -1,`errno` 为 `ENXIO` |
 
 ### 3.3 shell 中的管道
@@ -214,12 +226,12 @@ 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` |
+| 函数     | 参数                                                                                             | 返回值                                       |
+| -------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------- |
+| `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` 必须为正):
 
@@ -232,11 +244,11 @@ struct msgbuf {
 
 `msgtyp` 取值规则:
 
-| msgtyp | 含义 |
-| ------ | ---- |
-| `0` | 接收队列中的第一条消息 |
-| `> 0` | 接收第一条类型为 `msgtyp` 的消息 |
-| `< 0` | 接收类型值 ≤ `|msgtyp|` 的消息中类型最小的第一条 |
+| msgtyp | 含义                             |
+| ------ | -------------------------------- |
+| `0`    | 接收队列中的第一条消息           |
+| `> 0`  | 接收第一条类型为 `msgtyp` 的消息 |
+| `< 0`  | 接收类型值 ≤ `                   | msgtyp | ` 的消息中类型最小的第一条 |
 
 `msgsnd` / `msgrcv` 在队列满 / 为空时默认阻塞,加 `IPC_NOWAIT` 则非阻塞返回(`errno` 为 `EAGAIN` / `ENOMSG`)。
 
@@ -262,11 +274,11 @@ 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` 而定 |
+| 函数     | 参数                                                                                              | 返回值                                    |
+| -------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------- |
+| `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 {
@@ -284,12 +296,12 @@ struct sembuf {
 
 `semctl` 的常用 `cmd`:
 
-| cmd | 含义 |
-| --- | ---- |
-| `SETVAL` | 设置某个信号量的初值(需传 `union semun.val`) |
-| `GETVAL` | 获取某个信号量的当前值 |
-| `IPC_RMID` | 删除整个信号量集 |
-| `IPC_STAT` / `IPC_SET` | 获取/设置状态信息 |
+| cmd                    | 含义                                           |
+| ---------------------- | ---------------------------------------------- |
+| `SETVAL`               | 设置某个信号量的初值(需传 `union semun.val`) |
+| `GETVAL`               | 获取某个信号量的当前值                         |
+| `IPC_RMID`             | 删除整个信号量集                               |
+| `IPC_STAT` / `IPC_SET` | 获取/设置状态信息                              |
 
 `union semun` 需由用户自行定义(部分系统在头文件中提供):
 
@@ -323,12 +335,12 @@ 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` |
+| 函数     | 参数                                                                               | 返回值                                          |
+| -------- | ---------------------------------------------------------------------------------- | ----------------------------------------------- |
+| `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`                    |
 
 使用流程:
 
@@ -348,14 +360,14 @@ flowchart LR
 
 `mmap()` 也能实现内存映射,常用于**共享内存**与文件映射。两者定位不同:
 
-| 维度 | System V 共享内存 | mmap |
-| ---- | ----------------- | ---- |
-| 载体 | 匿名内核共享内存段 | 文件或匿名映射(`MAP_SHARED`) |
-| 持久化 | 直到 `IPC_RMID`,不随进程退出消失 | 文件映射随文件持久化;匿名映射随进程组消失 |
-| 生命周期管理 | `shmctl(IPC_RMID)` | `unlink` 文件 / 自动回收 |
-| 接口 | `shmget/shmat/shmdt/shmctl` | `mmap/munmap/msync` |
-| 与文件 IO 关系 | 无文件对象 | 可与文件内容直接关联,读写即文件读写 |
-| 常见用途 | 纯进程间大块数据共享 | 文件映射、设备映射(如 FrameBuffer)、匿名共享内存 |
+| 维度           | System V 共享内存                 | mmap                                               |
+| -------------- | --------------------------------- | -------------------------------------------------- |
+| 载体           | 匿名内核共享内存段                | 文件或匿名映射(`MAP_SHARED`)                     |
+| 持久化         | 直到 `IPC_RMID`,不随进程退出消失 | 文件映射随文件持久化;匿名映射随进程组消失         |
+| 生命周期管理   | `shmctl(IPC_RMID)`                | `unlink` 文件 / 自动回收                           |
+| 接口           | `shmget/shmat/shmdt/shmctl`       | `mmap/munmap/msync`                                |
+| 与文件 IO 关系 | 无文件对象                        | 可与文件内容直接关联,读写即文件读写               |
+| 常见用途       | 纯进程间大块数据共享              | 文件映射、设备映射(如 FrameBuffer)、匿名共享内存 |
 
 二者都需要自行配合同步机制(信号量、互斥锁、信号)使用。
 
@@ -754,17 +766,17 @@ gcc -Wall -o shm_demo   shm_demo.c
 
 ### 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` 等系统调用 |
+| 命令               | 用途                                                   |
+| ------------------ | ------------------------------------------------------ |
+| `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
 
@@ -787,15 +799,15 @@ gcc -Wall -o shm_demo   shm_demo.c
 
 ## 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 |
+| 维度       | 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     |
 
 要点:
 

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

@@ -1,6 +1,19 @@
 ---
 title: 线程与线程同步
-tags: [嵌入式Linux, Linux应用编程, 多线程, pthread, 线程同步, 互斥锁, 条件变量, 自旋锁, 读写锁, 生产者消费者, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    Linux应用编程,
+    多线程,
+    pthread,
+    线程同步,
+    互斥锁,
+    条件变量,
+    自旋锁,
+    读写锁,
+    生产者消费者,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十一章 线程、第十二章 线程同步"
@@ -100,15 +113,15 @@ stateDiagram-v2
 
 多线程也有缺点:编程难度高,需要考虑线程安全、信号处理等问题,编写与调试比单线程困难。多进程模型通常用于大型应用程序(如网络服务器),中小型程序使用较少。
 
-| 对比项 | 多进程 | 多线程 |
-| ------ | ------ | ------ |
-| 地址空间 | 各自独立、隔离 | 共享同一进程地址空间 |
-| 切换开销 | 大 | 小 |
-| 创建速度 | 慢 | 快 |
-| 通信方式 | 需要 IPC(管道、共享内存等) | 直接读写全局变量即可 |
-| 健壮性 | 一个进程崩溃不影响其它进程 | 一个线程崩溃可能拖垮整个进程 |
-| 资源占用 | 每进程独立资源 | 共享进程资源,额外仅线程栈等 |
-| 典型场景 | 网络服务器、大型系统 | 中小型并发程序、GUI、数据处理 |
+| 对比项   | 多进程                       | 多线程                        |
+| -------- | ---------------------------- | ----------------------------- |
+| 地址空间 | 各自独立、隔离               | 共享同一进程地址空间          |
+| 切换开销 | 大                           | 小                            |
+| 创建速度 | 慢                           | 快                            |
+| 通信方式 | 需要 IPC(管道、共享内存等) | 直接读写全局变量即可          |
+| 健壮性   | 一个进程崩溃不影响其它进程   | 一个线程崩溃可能拖垮整个进程  |
+| 资源占用 | 每进程独立资源               | 共享进程资源,额外仅线程栈等  |
+| 典型场景 | 网络服务器、大型系统         | 中小型并发程序、GUI、数据处理 |
 
 ### 1.3 并发、并行与串行
 
@@ -128,9 +141,9 @@ stateDiagram-v2
 
 每个进程有进程 ID(`pid_t`,非负整数),每个线程也有其标识,称为**线程 ID**。进程 ID 在整个系统中唯一,而**线程 ID 只在所属进程上下文中才有意义**。线程 ID 使用 `pthread_t` 数据类型表示。
 
-| 函数 | 原型 | 参数 | 返回值 | 说明 |
-| ---- | ---- | ---- | ------ | ---- |
-| `pthread_self` | `pthread_t pthread_self(void);` | 无 | 当前线程 ID | 总是成功 |
+| 函数            | 原型                                             | 参数        | 返回值                   | 说明                         |
+| --------------- | ------------------------------------------------ | ----------- | ------------------------ | ---------------------------- |
+| `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()` 比较,而不是直接用 `==`(调试打印时可以临时按其实际类型打印)。
@@ -151,16 +164,16 @@ int main(void)
 
 ### 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`) |
+| 项                   | 内容                                                                                                            |
+| -------------------- | --------------------------------------------------------------------------------------------------------------- |
+| 原型                 | `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`)                                                              |
 
 关键点:
 
@@ -220,12 +233,12 @@ gcc -Wall -o testApp testApp.c -lpthread
 - 调用 `pthread_cancel()` 取消线程;
 - **注意**:如果进程中任意线程调用 `exit()`、`_exit()` 或 `_Exit()`,将导致**整个进程**终止。
 
-| 项 | 内容 |
-| -- | ---- |
-| 原型 | `void pthread_exit(void *retval);` |
-| 头文件 | `<pthread.h>` |
+| 项            | 内容                                             |
+| ------------- | ------------------------------------------------ |
+| 原型          | `void pthread_exit(void *retval);`               |
+| 头文件        | `<pthread.h>`                                    |
 | 参数 `retval` | 线程返回值(退出码),可由 `pthread_join()` 获取 |
-| 返回值 | 无 |
+| 返回值        | 无                                               |
 
 要点:
 
@@ -268,13 +281,13 @@ int main(void)
 
 类似父进程用 `wait()/waitpid()` 回收子进程,线程用 `pthread_join()` 阻塞等待线程终止并获取退出码、回收资源。
 
-| 项 | 内容 |
-| -- | ---- |
-| 原型 | `int pthread_join(pthread_t thread, void **retval);` |
-| 头文件 | `<pthread.h>` |
-| 参数 `thread` | 需要等待的线程 ID |
+| 项            | 内容                                                                                                                |
+| ------------- | ------------------------------------------------------------------------------------------------------------------- |
+| 原型          | `int pthread_join(pthread_t thread, void **retval);`                                                                |
+| 头文件        | `<pthread.h>`                                                                                                       |
+| 参数 `thread` | 需要等待的线程 ID                                                                                                   |
 | 参数 `retval` | 非 `NULL` 时将目标线程退出状态复制到 `*retval`;若线程被取消,则 `*retval` 为 `PTHREAD_CANCELED`;不关心可传 `NULL` |
-| 返回值 | 成功返回 `0`;失败返回错误码 |
+| 返回值        | 成功返回 `0`;失败返回错误码                                                                                        |
 
 要点:
 
@@ -327,22 +340,22 @@ int main(void)
 
 有时需要向线程发送请求,要求它立刻退出,称为**取消线程**。
 
-| 项 | 内容 |
-| -- | ---- |
-| 原型 | `int pthread_cancel(pthread_t thread);` |
-| 头文件 | `<pthread.h>` |
-| 参数 | 目标线程 ID |
-| 返回值 | 成功返回 `0`;失败返回错误码 |
-| 说明 | 发出请求后立即返回,不等待目标线程退出 |
+| 项     | 内容                                    |
+| ------ | --------------------------------------- |
+| 原型   | `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);` |
+| 设置取消类型 | `int pthread_setcanceltype(int type, int *oldtype);`    |
 
 两者"设置新值 + 取回旧值"都是**原子操作**,`oldstate/oldtype` 可为 `NULL`。成功返回 `0`,失败返回非 0 错误码。
 
@@ -362,14 +375,14 @@ int main(void)
 
 取消点是一系列函数,只有执行到这些函数时才会真正响应取消请求。系统认为未到取消点时代码正在执行关键工作,不应被停止。常见取消点函数包括:
 
-| 类别 | 函数 |
-| ---- | ---- |
-| 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()` |
+| 类别          | 函数                                                                                                     |
+| ------------- | -------------------------------------------------------------------------------------------------------- |
+| 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` 查看完整列表。
 
@@ -429,12 +442,12 @@ int main(void)
 
 默认线程终止时可由其它线程 `pthread_join()` 获取状态并回收资源。若不关心返回值、希望线程终止时系统**自动回收**,可将其**分离**。
 
-| 项 | 内容 |
-| -- | ---- |
-| 原型 | `int pthread_detach(pthread_t thread);` |
-| 头文件 | `<pthread.h>` |
-| 参数 | 需要分离的线程 ID |
-| 返回值 | 成功返回 `0`;失败返回错误码 |
+| 项     | 内容                                    |
+| ------ | --------------------------------------- |
+| 原型   | `int pthread_detach(pthread_t thread);` |
+| 头文件 | `<pthread.h>`                           |
+| 参数   | 需要分离的线程 ID                       |
+| 返回值 | 成功返回 `0`;失败返回错误码            |
 
 要点:
 
@@ -490,13 +503,13 @@ int main(void)
 
 类似进程用 `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` 则移除并执行该函数 |
+| 项        | 内容                                                                                                          |
+| --------- | ------------------------------------------------------------------------------------------------------------- |
+| 原型      | `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` 则移除并执行该函数                                                    |
 
 清理函数栈中的函数在以下情况被执行:
 
@@ -565,13 +578,13 @@ int main(void)
 
 创建线程时可通过 `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);` |
+| 函数            | 原型                                                                                                                                                                                    |
+| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 初始化/销毁属性 | `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` 取值:
 
@@ -710,12 +723,12 @@ static void func(int loops)
 
 多线程环境下,有些初始化代码段只能执行一次。用 `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`;失败返回错误编码 |
+| 项     | 内容                                                                          |
+| ------ | ----------------------------------------------------------------------------- |
+| 原型   | `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()` 时另一个线程也调用,后者会阻塞等待,直到第一个完成初始化返回。调用成功返回时,可确定所有状态都已初始化完成。
 
@@ -770,12 +783,12 @@ int main(void)
 
 涉及 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`  | `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()` 会被自动调用以释放私有数据。
 
@@ -905,19 +918,19 @@ Linux 信号模型基于进程设计,信号问世远早于线程,因此两
 
 单线程程序用 `sigprocmask()` 设置进程信号掩码;多线程用 `pthread_sigmask()` 设置各线程的信号掩码,用法与 `sigprocmask()` 完全一样。
 
-| 项 | 内容 |
-| -- | ---- |
-| 原型 | `int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);` |
-| 头文件 | `<signal.h>` |
+| 项     | 内容                                                                   |
+| ------ | ---------------------------------------------------------------------- |
+| 原型   | `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()` 类似,但向指定线程发送并携带伴随数据 |
+| 函数               | 原型                                                                         | 说明                                                                  |
+| ------------------ | ---------------------------------------------------------------------------- | --------------------------------------------------------------------- |
+| `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()` 针对某个线程。
 
@@ -1050,13 +1063,13 @@ 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 错误码 |
+| 项           | 内容                                                                               |
+| ------------ | ---------------------------------------------------------------------------------- |
+| 原型         | `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;
@@ -1069,11 +1082,11 @@ pthread_mutex_init(mutex, NULL);
 
 #### 2.2.2 加锁与解锁
 
-| 函数 | 原型 | 说明 | 返回值 |
-| ---- | ---- | ---- | ------ |
-| `pthread_mutex_lock` | `int pthread_mutex_lock(pthread_mutex_t *mutex);` | 未锁定则上锁并返回;已被锁定则**阻塞**直到解锁 | 成功 0,失败非 0 |
+| 函数                    | 原型                                                 | 说明                                               | 返回值                      |
+| ----------------------- | ---------------------------------------------------- | -------------------------------------------------- | --------------------------- |
+| `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 |
+| `pthread_mutex_unlock`  | `int pthread_mutex_unlock(pthread_mutex_t *mutex);`  | 解锁                                               | 成功 0,失败非 0            |
 
 以下行为均属错误:
 
@@ -1135,10 +1148,10 @@ flowchart TD
 
 #### 2.2.3 销毁
 
-| 项 | 内容 |
-| -- | ---- |
-| 原型 | `int pthread_mutex_destroy(pthread_mutex_t *mutex);` |
-| 返回值 | 成功返回 `0`;失败返回非 0 错误码 |
+| 项     | 内容                                                 |
+| ------ | ---------------------------------------------------- |
+| 原型   | `int pthread_mutex_destroy(pthread_mutex_t *mutex);` |
+| 返回值 | 成功返回 `0`;失败返回非 0 错误码                    |
 
 - 不能销毁还没有解锁的互斥锁;
 - 没有初始化的互斥锁也不能销毁;
@@ -1166,19 +1179,19 @@ pthread_mutex_lock(mutex2);     pthread_mutex_lock(mutex1);
 
 `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);` |
+| 函数            | 原型                                                                                                                                                   |
+| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| 初始化/销毁属性 | `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 相仿 |
+| 类型                       | 行为                                                                                                        |
+| -------------------------- | ----------------------------------------------------------------------------------------------------------- |
+| `PTHREAD_MUTEX_NORMAL`     | 标准类型,不做错误检查或死锁检测;同一线程重复加锁会死锁;对未锁定或由其它线程锁定的锁解锁结果不确定        |
+| `PTHREAD_MUTEX_ERRORCHECK` | 提供错误检查:同一线程重复加锁、解锁他人锁定的锁、解锁未锁定的锁都会返回错误;运行较慢,可作调试工具        |
+| `PTHREAD_MUTEX_RECURSIVE`  | 递归锁,允许同一线程在解锁前多次加锁并维护加锁次数;解锁次数必须等于加锁次数才真正释放                      |
+| `PTHREAD_MUTEX_DEFAULT`    | 默认行为;用 `PTHREAD_MUTEX_INITIALIZER` 或 `attr` 为 `NULL` 初始化的锁属于此类,Linux 下行为与 NORMAL 相仿 |
 
 使用方式:
 
@@ -1211,10 +1224,10 @@ pthread_mutex_destroy(&mutex);
 
 #### 2.3.1 初始化与销毁
 
-| 函数 | 原型 |
-| ---- | ---- |
+| 函数   | 原型                                                                           |
+| ------ | ------------------------------------------------------------------------------ |
 | 初始化 | `int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);` |
-| 销毁 | `int pthread_cond_destroy(pthread_cond_t *cond);` |
+| 销毁   | `int pthread_cond_destroy(pthread_cond_t *cond);`                              |
 
 也可用宏 `PTHREAD_COND_INITIALIZER` 初始化:
 
@@ -1234,11 +1247,11 @@ pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
 
 #### 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`    | `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 |
 
 要点:
 
@@ -1349,11 +1362,11 @@ sequenceDiagram
 
 > ⚠️ **来源说明**:`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`;其它错误返回相应错误码 |
+| 项             | 内容                                                                                                        |
+| -------------- | ----------------------------------------------------------------------------------------------------------- |
+| 原型           | `int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);` |
+| 参数 `abstime` | **绝对时间**(从 Epoch 起的秒 + 纳秒),不是相对时间                                                        |
+| 返回值         | 成功 0;超时返回 `ETIMEDOUT`;其它错误返回相应错误码                                                        |
 
 ```c
 #include <time.h>
@@ -1388,20 +1401,20 @@ pthread_mutex_unlock(&mutex);
 
 自旋锁适用于**需要保护的临界区执行时间很短**的场景:持锁线程很快释放锁,自旋等待的线程只需等待很短时间,效率高。
 
-| 对比项 | 互斥锁 | 自旋锁 |
-| ------ | ------ | ------ |
-| 实现层次 | 基于自旋锁实现,更上层 | 更底层 |
-| 获取失败行为 | 阻塞休眠,唤醒后竞争 | 原地自旋,忙等 |
-| 开销 | 休眠/唤醒开销大 | 忙等占用 CPU,但切换开销小 |
-| 适用场景 | 临界区可能较长 | 临界区极短 |
-| 中断上下文 | 不能用于中断服务函数 | 可用于中断服务函数,内核中会自动禁止抢占 |
+| 对比项       | 互斥锁                 | 自旋锁                                   |
+| ------------ | ---------------------- | ---------------------------------------- |
+| 实现层次     | 基于自旋锁实现,更上层 | 更底层                                   |
+| 获取失败行为 | 阻塞休眠,唤醒后竞争   | 原地自旋,忙等                           |
+| 开销         | 休眠/唤醒开销大        | 忙等占用 CPU,但切换开销小               |
+| 适用场景     | 临界区可能较长         | 临界区极短                               |
+| 中断上下文   | 不能用于中断服务函数   | 可用于中断服务函数,内核中会自动禁止抢占 |
 
 #### 2.4.1 初始化与销毁
 
-| 函数 | 原型 |
-| ---- | ---- |
+| 函数   | 原型                                                            |
+| ------ | --------------------------------------------------------------- |
 | 初始化 | `int pthread_spin_init(pthread_spinlock_t *lock, int pshared);` |
-| 销毁 | `int pthread_spin_destroy(pthread_spinlock_t *lock);` |
+| 销毁   | `int pthread_spin_destroy(pthread_spinlock_t *lock);`           |
 
 `pshared` 取值:
 
@@ -1412,11 +1425,11 @@ pthread_mutex_unlock(&mutex);
 
 #### 2.4.2 加锁与解锁
 
-| 函数 | 原型 | 说明 |
-| ---- | ---- | ---- |
-| `pthread_spin_lock` | `int pthread_spin_lock(pthread_spinlock_t *lock);` | 未锁定则上锁;已锁定则自旋等待 |
+| 函数                   | 原型                                                  | 说明                             |
+| ---------------------- | ----------------------------------------------------- | -------------------------------- |
+| `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);` | 解锁 |
+| `pthread_spin_unlock`  | `int pthread_spin_unlock(pthread_spinlock_t *lock);`  | 解锁                             |
 
 成功返回 `0`,失败返回非 0 错误码。
 
@@ -1503,10 +1516,10 @@ int main(int argc, char *argv[])
 
 #### 2.5.1 初始化与销毁
 
-| 函数 | 原型 |
-| ---- | ---- |
+| 函数   | 原型                                                                                   |
+| ------ | -------------------------------------------------------------------------------------- |
 | 初始化 | `int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);` |
-| 销毁 | `int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);` |
+| 销毁   | `int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);`                                |
 
 也可用宏 `PTHREAD_RWLOCK_INITIALIZER`:
 
@@ -1518,11 +1531,11 @@ pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
 
 #### 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_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` |
 
@@ -1592,9 +1605,9 @@ int main(int argc, char *argv[])
 
 读写锁只有一个属性——**进程共享属性**,与互斥锁、自旋锁相同。
 
-| 函数 | 原型 |
-| ---- | ---- |
-| 初始化/销毁属性 | `int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);`<br/>`int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);` |
+| 函数              | 原型                                                                                                                                                                   |
+| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 初始化/销毁属性   | `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` 取值:
@@ -1618,12 +1631,12 @@ pthread_rwlockattr_destroy(&attr);
 
 ### 2.6 各同步机制对比
 
-| 机制 | 类型 | 状态数 | 获取失败行为 | 适用场景 |
-| ---- | ---- | ------ | ------------ | -------- |
-| 互斥锁 | `pthread_mutex_t` | 2(锁/未锁) | 阻塞休眠 | 一般临界区保护,通用 |
-| 条件变量 | `pthread_cond_t` | 无状态(配合互斥锁) | 阻塞等待通知 | 等待某个条件成立(生产者-消费者) |
-| 自旋锁 | `pthread_spinlock_t` | 2(锁/未锁) | 原地自旋忙等 | 临界区极短;可用于中断上下文 |
-| 读写锁 | `pthread_rwlock_t` | 3(读锁/写锁/未锁) | 读-读并发,读-写/写-写阻塞 | 读多写少 |
+| 机制     | 类型                 | 状态数               | 获取失败行为               | 适用场景                          |
+| -------- | -------------------- | -------------------- | -------------------------- | --------------------------------- |
+| 互斥锁   | `pthread_mutex_t`    | 2(锁/未锁)         | 阻塞休眠                   | 一般临界区保护,通用              |
+| 条件变量 | `pthread_cond_t`     | 无状态(配合互斥锁) | 阻塞等待通知               | 等待某个条件成立(生产者-消费者) |
+| 自旋锁   | `pthread_spinlock_t` | 2(锁/未锁)         | 原地自旋忙等               | 临界区极短;可用于中断上下文      |
+| 读写锁   | `pthread_rwlock_t`   | 3(读锁/写锁/未锁)  | 读-读并发,读-写/写-写阻塞 | 读多写少                          |
 
 选择建议:实际开发中**用得最多的是互斥锁和条件变量**;临界区极短且不希望休眠时用自旋锁;读多写少时用读写锁。应根据场景选择,方能事半功倍。
 
@@ -1776,15 +1789,15 @@ int main(void)
 
 #### 逐段解释
 
-| 代码 | 说明 |
-| ---- | ---- |
-| `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` 并广播,最后等消费者退出,保证不丢数据 |
+| 代码                                     | 说明                                                                                                       |
+| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
+| `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` 并广播,最后等消费者退出,保证不丢数据                                 |
 
 编译运行:
 
@@ -1799,39 +1812,39 @@ gcc -Wall -pthread -o pc pc.c
 
 ### 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()` | 注册/执行线程清理函数 |
+| 函数                           | 一句话作用                 |
+| ------------------------------ | -------------------------- |
+| `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()` | 设置/获取线程私有数据 |
+| 函数                                  | 一句话作用               |
+| ------------------------------------- | ------------------------ |
+| `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` |
+| 对象     | 初始化                                               | 加锁/等待                          | 解锁/通知                           | 销毁                     |
+| -------- | ---------------------------------------------------- | ---------------------------------- | ----------------------------------- | ------------------------ |
+| 互斥锁   | `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` |
 
 ---
 
@@ -1863,16 +1876,16 @@ gcc -Wall -pthread -o pc pc.c
 
 ### 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` |
+| 现象                                             | 可能原因                                 | 排查手段                                                     |
+| ------------------------------------------------ | ---------------------------------------- | ------------------------------------------------------------ |
+| 链接报 `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`                         |
 
 调试常用命令:
 
@@ -1892,18 +1905,18 @@ gdb ./app                                     # 调试
 
 > ⚠️ **来源说明**:本节不属于《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(本教程)                    | 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` 等),概念相似但接口完全不同,切忌混用。
 
@@ -1963,12 +1976,12 @@ static void *worker(void *arg)
 
 **答**:
 
-| 机制 | 特点 | 适用场景 |
-| ---- | ---- | -------- |
-| 互斥锁 | 获取失败阻塞休眠,通用 | 一般临界区保护,使用最广 |
-| 条件变量 | 无状态,配合互斥锁,等待条件成立 | 生产者-消费者、事件通知 |
-| 自旋锁 | 获取失败原地自旋,不休眠,临界区必须极短 | 临界区极短;可用于中断上下文 |
-| 读写锁 | 读-读可并发,读-写/写-写互斥 | 读远多于写的共享数据 |
+| 机制     | 特点                                     | 适用场景                     |
+| -------- | ---------------------------------------- | ---------------------------- |
+| 互斥锁   | 获取失败阻塞休眠,通用                   | 一般临界区保护,使用最广     |
+| 条件变量 | 无状态,配合互斥锁,等待条件成立         | 生产者-消费者、事件通知      |
+| 自旋锁   | 获取失败原地自旋,不休眠,临界区必须极短 | 临界区极短;可用于中断上下文 |
+| 读写锁   | 读-读可并发,读-写/写-写互斥             | 读远多于写的共享数据         |
 
 补充:互斥锁基于自旋锁实现;自旋锁占用 CPU,长时间自旋会降低效率;读写锁提高了读场景的并行性,但实现更复杂、写者可能饥饿。实际工程中用得最多的是互斥锁 + 条件变量组合。
 

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

@@ -1,6 +1,18 @@
 ---
 title: 面试-进程线程与IPC
-tags: [嵌入式Linux, 面试, 信号, 进程, 进程间通信, IPC, 多线程, 线程同步, pthread, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    面试,
+    信号,
+    进程,
+    进程间通信,
+    IPC,
+    多线程,
+    线程同步,
+    pthread,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第八章 信号、第九章 进程、第十章 进程间通信、第十一章 线程、第十二章 线程同步"
@@ -26,13 +38,13 @@ pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第八章 信号、
 
 信号的产生来源:
 
-| 来源 | 例子 |
-| ---- | ---- |
-| 硬件异常 | 除数为 0(`SIGFPE`)、非法指令(`SIGILL`)、无效内存引用(`SIGSEGV`) |
-| 终端特殊字符 | `Ctrl+C` → `SIGINT`,`Ctrl+Z` → `SIGTSTP`,`Ctrl+\` → `SIGQUIT` |
-| 进程调用 `kill()` | 向另一进程或进程组发送任意信号 |
-| kill 命令 | `kill -9 xxx`,内部即 `kill()` 系统调用 |
-| 软件事件 | 定时器超时(`SIGALRM`)、子进程退出(`SIGCHLD`) |
+| 来源              | 例子                                                                  |
+| ----------------- | --------------------------------------------------------------------- |
+| 硬件异常          | 除数为 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)`)
 
@@ -42,13 +54,13 @@ pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第八章 信号、
 
 **详细解答**:
 
-| 对比项 | signal() | sigaction() |
-| ------ | -------- | ----------- |
-| 复杂度 | 简单 | 较复杂 |
-| 移植性 | 差(不同系统语义可能不同) | 好(POSIX 标准) |
-| 获取旧处理方式 | 返回值返回 | 通过 `oldact` 参数 |
-| 精细控制 | 无 | 可设置 `sa_mask`、`sa_flags` |
-| 处理函数 | `handler(int)` | `sa_handler` 或 `sa_sigaction` |
+| 对比项         | signal()                   | sigaction()                    |
+| -------------- | -------------------------- | ------------------------------ |
+| 复杂度         | 简单                       | 较复杂                         |
+| 移植性         | 差(不同系统语义可能不同) | 好(POSIX 标准)               |
+| 获取旧处理方式 | 返回值返回                 | 通过 `oldact` 参数             |
+| 精细控制       | 无                         | 可设置 `sa_mask`、`sa_flags`   |
+| 处理函数       | `handler(int)`             | `sa_handler` 或 `sa_sigaction` |
 
 `sigaction()` 关键成员与标志:
 
@@ -85,19 +97,19 @@ sigaction(SIGINT, &sig, NULL);
 
 **详细解答**:
 
-| 信号 | 编号 | 描述 | 默认操作 |
-| ---- | ---- | ---- | -------- |
-| `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 | 停止 |
+| 信号      | 编号 | 描述                         | 默认操作    |
+| --------- | ---- | ---------------------------- | ----------- |
+| `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` 默认忽略,父进程可捕获它异步回收子进程。
 
@@ -159,13 +171,13 @@ sigprocmask(SIG_UNBLOCK, &set, NULL); /* 解除阻塞 */
 
 **详细解答**:
 
-| 对比项 | 标准信号 | 实时信号 |
-| ------ | -------- | -------- |
-| 编号 | 1~31 | 34~64(`SIGRTMIN`~`SIGRTMAX`) |
-| 是否排队 | 否,多次发送只记一次 | 是,多次发送多次传递 |
-| 携带数据 | 否 | 是(`union sigval`,整型或指针) |
-| 传递顺序 | 无保障 | 编号越小优先级越高;同类型按发送顺序 |
-| 自定义数量 | 仅 `SIGUSR1`/`SIGUSR2` | 31 个可用 |
+| 对比项     | 标准信号               | 实时信号                             |
+| ---------- | ---------------------- | ------------------------------------ |
+| 编号       | 1~31                   | 34~64(`SIGRTMIN`~`SIGRTMAX`)       |
+| 是否排队   | 否,多次发送只记一次   | 是,多次发送多次传递                 |
+| 携带数据   | 否                     | 是(`union sigval`,整型或指针)     |
+| 传递顺序   | 无保障                 | 编号越小优先级越高;同类型按发送顺序 |
+| 自定义数量 | 仅 `SIGUSR1`/`SIGUSR2` | 31 个可用                            |
 
 使用实时信号的两个要求:
 
@@ -197,14 +209,14 @@ static void handler(int sig, siginfo_t *info, void *ctx) {
 
 Linux 进程的 6 种状态:
 
-| 状态 | 说明 |
-| ---- | ---- |
-| 就绪态(Ready) | 满足被调度条件但未获 CPU,得到 CPU 即可运行 |
-| 运行态 | 正被 CPU 调度执行 |
-| 僵尸态 | 进程已结束,父进程尚未"收尸" |
-| 可中断睡眠(浅睡) | 可被信号唤醒 |
-| 不可中断睡眠(深睡) | 无法被信号唤醒,须等条件成立 |
-| 暂停态 | 被信号暂停(如 `SIGSTOP`),可被 `SIGCONT` 恢复 |
+| 状态                 | 说明                                            |
+| -------------------- | ----------------------------------------------- |
+| 就绪态(Ready)      | 满足被调度条件但未获 CPU,得到 CPU 即可运行     |
+| 运行态               | 正被 CPU 调度执行                               |
+| 僵尸态               | 进程已结束,父进程尚未"收尸"                    |
+| 可中断睡眠(浅睡)   | 可被信号唤醒                                    |
+| 不可中断睡眠(深睡) | 无法被信号唤醒,须等条件成立                    |
+| 暂停态               | 被信号暂停(如 `SIGSTOP`),可被 `SIGCONT` 恢复 |
 
 浅睡和深睡统称等待态/阻塞态,处于等待态的进程不参与调度。新创建的进程处于就绪态。
 
@@ -234,12 +246,12 @@ Linux 进程的 6 种状态:
 
 **详细解答**:
 
-| 对比项 | fork() | vfork() |
-| ------ | ------ | ------- |
-| 地址空间 | 复制(写时复制) | 不复制,子进程共享父进程内存 |
-| 执行顺序 | 不确定 | 保证子进程先运行,exec 后父进程才可能被调度 |
-| 效率 | 有复制开销 | 效率更高 |
-| 适用场景 | 通用 | 子进程立即调用 exec/_exit |
+| 对比项   | fork()           | vfork()                                     |
+| -------- | ---------------- | ------------------------------------------- |
+| 地址空间 | 复制(写时复制) | 不复制,子进程共享父进程内存                |
+| 执行顺序 | 不确定           | 保证子进程先运行,exec 后父进程才可能被调度 |
+| 效率     | 有复制开销       | 效率更高                                    |
+| 适用场景 | 通用             | 子进程立即调用 exec/_exit                   |
 
 `vfork()` 的子进程在调用 `exec` 或 `_exit` 之前运行在父进程的空间中;若在此期间修改父进程数据(除返回值变量)、调用函数或未 exec/_exit 就返回,可能带来未知结果。
 
@@ -303,13 +315,13 @@ Linux 进程的 6 种状态:
 
 `status` 解析宏:
 
-| 宏 | 含义 |
-| -- | ---- |
-| `WIFEXITED(status)` | 子进程正常终止返回真 |
-| `WEXITSTATUS(status)` | 取退出码 |
-| `WIFSIGNALED(status)` | 被信号终止返回真 |
-| `WTERMSIG(status)` | 取导致终止的信号编号 |
-| `WCOREDUMP(status)` | 是否产生 core 文件 |
+| 宏                    | 含义                 |
+| --------------------- | -------------------- |
+| `WIFEXITED(status)`   | 子进程正常终止返回真 |
+| `WEXITSTATUS(status)` | 取退出码             |
+| `WIFSIGNALED(status)` | 被信号终止返回真     |
+| `WTERMSIG(status)`    | 取导致终止的信号编号 |
+| `WCOREDUMP(status)`   | 是否产生 core 文件   |
 
 **追问**:① `waitpid(-1,&s,0)` 与 `wait(&s)` 等价吗?(等价)② 为什么 `WNOHANG` 轮询会浪费 CPU?(忙等,应配合阻塞或信号机制)
 
@@ -319,15 +331,15 @@ Linux 进程的 6 种状态:
 
 **详细解答**:
 
-| 函数 | 参数形式 | 查 PATH | 指定环境变量 |
-| ---- | -------- | ------- | ------------ |
-| `execl` | 可变参数列表 | 否 | 否 |
-| `execv` | 指针数组 | 否 | 否 |
-| `execlp` | 可变参数列表 | 是 | 否 |
-| `execvp` | 指针数组 | 是 | 否 |
-| `execle` | 可变参数列表 | 否 | 是 |
-| `execvpe` | 指针数组 | 是 | 是 |
-| `execve`(系统调用) | 数组 | 否 | 是 |
+| 函数                 | 参数形式     | 查 PATH | 指定环境变量 |
+| -------------------- | ------------ | ------- | ------------ |
+| `execl`              | 可变参数列表 | 否      | 否           |
+| `execv`              | 指针数组     | 否      | 否           |
+| `execlp`             | 可变参数列表 | 是      | 否           |
+| `execvp`             | 指针数组     | 是      | 否           |
+| `execle`             | 可变参数列表 | 否      | 是           |
+| `execvpe`            | 指针数组     | 是      | 是           |
+| `execve`(系统调用) | 数组         | 否      | 是           |
 
 命名规律:`l`=list(逐个参数,NULL 结尾);`v`=vector(字符串数组);`p`=PATH(只给文件名即可);`e`=environment(自定义环境变量)。成功调用 **exec 永不返回**,返回即表示出错。exec 用新程序替换当前进程的代码段、数据段、堆栈,从新程序的 `main()` 开始执行,PID 不变。
 
@@ -380,12 +392,12 @@ signal(SIGCHLD, SIG_IGN);
 
 Linux IPC 机制(从 UNIX 继承):
 
-| 类别 | 机制 |
-| ---- | ---- |
-| UNIX IPC | 管道、FIFO、信号 |
-| System V IPC | Signal V 信号量、消息队列、共享内存 |
-| POSIX IPC | POSIX 信号量、消息队列、共享内存 |
-| Socket IPC | 基于 socket 的进程间通信(可跨主机) |
+| 类别         | 机制                                 |
+| ------------ | ------------------------------------ |
+| 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 基础上改进,弥补其不足。
 
@@ -397,11 +409,11 @@ AT&T 贝尔实验室形成了 System V IPC(局限在单机内);BSD 形成
 
 **详细解答**:
 
-| 类型 | 方向 | 通信进程限制 |
-| ---- | ---- | ------------ |
-| 普通管道 `pipe` | 单工(单向) | 只能在父子/兄弟进程间 |
-| 流管道 `s_pipe` | 半双工(双向) | 只能在父子/兄弟进程间 |
-| 有名管道 FIFO | 双向 | 无亲缘关系的进程也可通信 |
+| 类型            | 方向           | 通信进程限制             |
+| --------------- | -------------- | ------------------------ |
+| 普通管道 `pipe` | 单工(单向)   | 只能在父子/兄弟进程间    |
+| 流管道 `s_pipe` | 半双工(双向) | 只能在父子/兄弟进程间    |
+| 有名管道 FIFO   | 双向           | 无亲缘关系的进程也可通信 |
 
 管道被抽象成一个文件(pipe 文件类型),是 UNIX 最古老的 IPC 方法(20 世纪 70 年代早期出现)。普通管道要实现双向传输必须用两个管道;FIFO 突破了"亲缘关系"和"单工"两种限制,任何进程只要知道 FIFO 路径就能通信。
 
@@ -433,11 +445,11 @@ AT&T 贝尔实验室形成了 System V IPC(局限在单机内);BSD 形成
 
 与信号的区别:
 
-| 对比项 | 信号量(semaphore) | 信号(signal) |
-| ------ | ------------------- | -------------- |
-| 本质 | 计数器,用于同步/互斥 | 异步事件通知 |
-| 用途 | 控制资源访问、进程同步 | 通知进程某事件发生 |
-| 头文件 | `<sys/sem.h>` / `<semaphore.h>` | `<signal.h>` |
+| 对比项 | 信号量(semaphore)             | 信号(signal)     |
+| ------ | ------------------------------- | ------------------ |
+| 本质   | 计数器,用于同步/互斥           | 异步事件通知       |
+| 用途   | 控制资源访问、进程同步          | 通知进程某事件发生 |
+| 头文件 | `<sys/sem.h>` / `<semaphore.h>` | `<signal.h>`       |
 
 **追问**:① 信号量初值设为 1 表示什么?(二值信号量,可实现互斥)② 信号量能携带数据吗?(不能,只表示资源计数)
 
@@ -477,13 +489,13 @@ AT&T 贝尔实验室形成了 System V IPC(局限在单机内);BSD 形成
 
 **详细解答**:
 
-| 对比项 | System V IPC | POSIX IPC |
-| ------ | ------------ | --------- |
-| 出现时间 | 较早 | 较晚,改进自 System V |
-| 接口风格 | 较繁琐(key、flag) | 更简洁、符合 POSIX 标准 |
-| 可移植性 | 较差 | 更好(跨 UNIX 系统) |
-| 消息通知 | 无 | 支持消息通知/优先级 |
-| 涵盖 | 信号量、消息队列、共享内存 | 信号量、消息队列、共享内存 |
+| 对比项   | System V IPC               | POSIX IPC                  |
+| -------- | -------------------------- | -------------------------- |
+| 出现时间 | 较早                       | 较晚,改进自 System V      |
+| 接口风格 | 较繁琐(key、flag)        | 更简洁、符合 POSIX 标准    |
+| 可移植性 | 较差                       | 更好(跨 UNIX 系统)       |
+| 消息通知 | 无                         | 支持消息通知/优先级        |
+| 涵盖     | 信号量、消息队列、共享内存 | 信号量、消息队列、共享内存 |
 
 选择 IPC 的参考:
 
@@ -508,21 +520,21 @@ AT&T 贝尔实验室形成了 System V IPC(局限在单机内);BSD 形成
 
 共享与私有:
 
-| 分类 | 内容 |
-| ---- | ---- |
+| 分类                 | 内容                                                         |
+| -------------------- | ------------------------------------------------------------ |
 | 线程共享(属于进程) | 虚拟地址空间、代码段、数据段、堆、文件描述符、信号处理方式等 |
-| 线程私有 | 线程栈、寄存器环境、线程本地存储(TLS) |
+| 线程私有             | 线程栈、寄存器环境、线程本地存储(TLS)                      |
 
 多进程 vs 多线程:
 
-| 对比项 | 多进程 | 多线程 |
-| ------ | ------ | ------ |
-| 切换开销 | 大 | 小 |
-| 通信 | 需 IPC,麻烦 | 共享内存,容易 |
-| 创建速度 | 慢 | 快 |
-| 多核利用 | 一般 | 更有优势 |
-| 健壮性 | 隔离性好 | 一个线程崩溃可能拖垮进程 |
-| 编程难度 | 较低 | 较高(线程安全、信号处理) |
+| 对比项   | 多进程       | 多线程                     |
+| -------- | ------------ | -------------------------- |
+| 切换开销 | 大           | 小                         |
+| 通信     | 需 IPC,麻烦 | 共享内存,容易             |
+| 创建速度 | 慢           | 快                         |
+| 多核利用 | 一般         | 更有优势                   |
+| 健壮性   | 隔离性好     | 一个线程崩溃可能拖垮进程   |
+| 编程难度 | 较低         | 较高(线程安全、信号处理) |
 
 内核调度的是线程而非进程,所以说线程是系统调度的最小单元。多进程模型常用于大型应用(网络服务器),中小型程序多用多线程。
 
@@ -563,11 +575,11 @@ gcc -pthread -o testApp testApp.c
 
 **详细解答**:
 
-| 函数 | 原型 | 作用 | 特点 |
-| ---- | ---- | ---- | ---- |
-| `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)` |
+| 函数             | 原型                                          | 作用                             | 特点                                                        |
+| ---------------- | --------------------------------------------- | -------------------------------- | ----------------------------------------------------------- |
+| `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())` 可让线程自行分离。
 
@@ -631,12 +643,12 @@ pthread_mutex_init(&mutex, NULL);                     /* 动态/先定义后初
 
 操作:
 
-| 函数 | 行为 |
-| ---- | ---- |
-| `pthread_mutex_lock` | 未锁则上锁;已锁则阻塞 |
+| 函数                    | 行为                                 |
+| ----------------------- | ------------------------------------ |
+| `pthread_mutex_lock`    | 未锁则上锁;已锁则阻塞               |
 | `pthread_mutex_trylock` | 未锁则上锁;已锁返回 `EBUSY`,不阻塞 |
-| `pthread_mutex_unlock` | 解锁(不能解锁未锁定或他人锁定的锁) |
-| `pthread_mutex_destroy` | 销毁(不能销毁未解锁的锁) |
+| `pthread_mutex_unlock`  | 解锁(不能解锁未锁定或他人锁定的锁) |
+| `pthread_mutex_destroy` | 销毁(不能销毁未解锁的锁)           |
 
 死锁的典型场景:
 
@@ -684,14 +696,14 @@ pthread_mutex_unlock(&mutex);
 
 **详细解答**:
 
-| 对比项 | 互斥锁 | 自旋锁 |
-| ------ | ------ | ------ |
-| 获取失败 | 阻塞休眠,唤醒后竞争 | 原地自旋,忙等占 CPU |
-| 底层实现 | 基于自旋锁实现(更上层) | 更底层 |
-| 开销 | 休眠/唤醒开销大 | 忙等开销,切换开销小 |
-| 临界区 | 可较长 | 必须极短 |
-| 中断上下文 | 不可用 | 可用(内核中自旋锁会自动禁止抢占) |
-| 重复加锁 | 不一定死锁(视类型) | 必然死锁 |
+| 对比项     | 互斥锁                   | 自旋锁                             |
+| ---------- | ------------------------ | ---------------------------------- |
+| 获取失败   | 阻塞休眠,唤醒后竞争     | 原地自旋,忙等占 CPU               |
+| 底层实现   | 基于自旋锁实现(更上层) | 更底层                             |
+| 开销       | 休眠/唤醒开销大          | 忙等开销,切换开销小               |
+| 临界区     | 可较长                   | 必须极短                           |
+| 中断上下文 | 不可用                   | 可用(内核中自旋锁会自动禁止抢占) |
+| 重复加锁   | 不一定死锁(视类型)     | 必然死锁                           |
 
 读写锁有 3 种状态(读加锁、写加锁、未加锁),两条规则:
 

+ 158 - 137
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/01-高级IO.md

@@ -1,6 +1,24 @@
 ---
 title: 高级IO
-tags: [嵌入式Linux, Linux应用编程, 高级IO, 非阻塞IO, IO多路复用, select, poll, epoll, 异步IO, SIGIO, mmap, 存储映射IO, 文件锁, flock, fcntl, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    Linux应用编程,
+    高级IO,
+    非阻塞IO,
+    IO多路复用,
+    select,
+    poll,
+    epoll,
+    异步IO,
+    SIGIO,
+    mmap,
+    存储映射IO,
+    文件锁,
+    flock,
+    fcntl,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十三章 高级I/O"
@@ -29,11 +47,11 @@ pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十三章 高级I
 
 ### 1.2 两种打开方式
 
-| 方式 | 做法 | 说明 |
-| ---- | ---- | ---- |
-| 默认(阻塞) | `open(path, O_RDONLY)` | 不指定 `O_NONBLOCK` 即为阻塞 |
-| 非阻塞 | `open(path, O_RDONLY \| O_NONBLOCK)` | 后续 I/O 以非阻塞方式进行 |
-| 改造已有 fd | `fcntl(fd, F_GETFL)` → `flag \|= O_NONBLOCK` → `fcntl(fd, F_SETFL, flag)` | 用于别人打开、自己继承来的 fd(如标准输入 0) |
+| 方式         | 做法                                                                      | 说明                                          |
+| ------------ | ------------------------------------------------------------------------- | --------------------------------------------- |
+| 默认(阻塞) | `open(path, O_RDONLY)`                                                    | 不指定 `O_NONBLOCK` 即为阻塞                  |
+| 非阻塞       | `open(path, O_RDONLY \| O_NONBLOCK)`                                      | 后续 I/O 以非阻塞方式进行                     |
+| 改造已有 fd  | `fcntl(fd, F_GETFL)` → `flag \|= O_NONBLOCK` → `fcntl(fd, F_SETFL, flag)` | 用于别人打开、自己继承来的 fd(如标准输入 0) |
 
 `open` 时无法指定 `O_ASYNC`,但 `O_NONBLOCK` 可以在 `open` 时直接指定。普通文件设置与否没有影响。
 
@@ -43,12 +61,12 @@ pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十三章 高级I
 
 ### 1.4 阻塞 vs 非阻塞对比
 
-| 维度 | 阻塞 I/O | 非阻塞 I/O |
-| ---- | -------- | ---------- |
-| 无数据时 | 挂起休眠,交出 CPU | 立即返回错误 |
-| CPU 占用 | 几乎为 0 | 轮询时接近 100% |
-| 并发读取 | 单线程做不到(会被第一个 I/O 卡住) | 轮询可以做到 |
-| 适用场景 | 单设备、愿意等待 | 多设备非阻塞 + 配合多路复用 |
+| 维度     | 阻塞 I/O                            | 非阻塞 I/O                  |
+| -------- | ----------------------------------- | --------------------------- |
+| 无数据时 | 挂起休眠,交出 CPU                  | 立即返回错误                |
+| CPU 占用 | 几乎为 0                            | 轮询时接近 100%             |
+| 并发读取 | 单线程做不到(会被第一个 I/O 卡住) | 轮询可以做到                |
+| 适用场景 | 单设备、愿意等待                    | 多设备非阻塞 + 配合多路复用 |
 
 ```mermaid
 flowchart LR
@@ -116,6 +134,7 @@ int main(void)
 ```
 
 逐段说明:
+
 - `O_NONBLOCK` 让鼠标 `read` 无数据时立刻返回,循环不会被卡死。
 - 键盘 fd 是继承来的,只能用 `F_GETFL`/`F_SETFL` 两步加标志,不能重新 `open`。
 - 循环里虽然"并发"了,但空转轮询导致 **CPU 占用率极高**——这正是引出 I/O 多路复用的动机。
@@ -148,32 +167,32 @@ int select(int nfds, fd_set *readfds, fd_set *writefds,
            fd_set *exceptfds, struct timeval *timeout);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
-| `nfds` | 三个集合中**最大文件描述符编号值 + 1** |
-| `readfds` | 关心"是否可读"的 fd 集合,不关心传 `NULL` |
-| `writefds` | 关心"是否可写"的 fd 集合,不关心传 `NULL` |
-| `exceptfds` | 关心"异常情况"的 fd 集合(不是出错),不关心传 `NULL` |
-| `timeout` | `NULL`:一直阻塞;`0`(两成员为 0):不阻塞只轮询一次;否则为等待上限 |
+| 参数        | 含义                                                                  |
+| ----------- | --------------------------------------------------------------------- |
+| `nfds`      | 三个集合中**最大文件描述符编号值 + 1**                                |
+| `readfds`   | 关心"是否可读"的 fd 集合,不关心传 `NULL`                             |
+| `writefds`  | 关心"是否可写"的 fd 集合,不关心传 `NULL`                             |
+| `exceptfds` | 关心"异常情况"的 fd 集合(不是出错),不关心传 `NULL`                 |
+| `timeout`   | `NULL`:一直阻塞;`0`(两成员为 0):不阻塞只轮询一次;否则为等待上限 |
 
 `fd_set` 是位掩码集合,全部操作由四个宏完成:
 
-| 宏 | 作用 |
-| -- | ---- |
-| `FD_ZERO(fd_set *set)` | 将集合初始化为空 |
-| `FD_SET(int fd, fd_set *set)` | 把 fd 加入集合 |
-| `FD_CLR(int fd, fd_set *set)` | 把 fd 从集合移除 |
+| 宏                              | 作用                  |
+| ------------------------------- | --------------------- |
+| `FD_ZERO(fd_set *set)`          | 将集合初始化为空      |
+| `FD_SET(int fd, fd_set *set)`   | 把 fd 加入集合        |
+| `FD_CLR(int fd, fd_set *set)`   | 把 fd 从集合移除      |
 | `FD_ISSET(int fd, fd_set *set)` | fd 是集合成员则返回真 |
 
 集合容量由 `FD_SETSIZE` 决定,Linux 下为 **1024**。使用前必须 `FD_ZERO` 初始化。
 
 **返回值**(三种情况):
 
-| 返回值 | 含义 |
-| ------ | ---- |
-| `-1` | 出错,设置 `errno`(`EBADF` fd 非法、`EINTR` 被信号打断等) |
-| `0` | 超时,没有任何 fd 就绪,三个集合被清空 |
-| `> 0` | 处于就绪态的 fd 总数(同一 fd 在多个集合就绪会被重复统计) |
+| 返回值 | 含义                                                        |
+| ------ | ----------------------------------------------------------- |
+| `-1`   | 出错,设置 `errno`(`EBADF` fd 非法、`EINTR` 被信号打断等) |
+| `0`    | 超时,没有任何 fd 就绪,三个集合被清空                      |
+| `> 0`  | 处于就绪态的 fd 总数(同一 fd 在多个集合就绪会被重复统计)  |
 
 **关键特性(值-结果参数)**:`select()` 返回时会**修改**传入的集合,使其只保留就绪的 fd。因此若在循环中重复调用,**每次调用前都必须重新 `FD_ZERO` 并重新 `FD_SET`**。
 
@@ -185,10 +204,10 @@ int select(int nfds, fd_set *readfds, fd_set *writefds,
 int poll(struct pollfd *fds, nfds_t nfds, int timeout);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
-| `fds` | `struct pollfd` 数组,每个元素指定一个 fd 及关心的事件 |
-| `nfds` | 数组元素个数(`nfds_t` 实际为无符号整型) |
+| 参数      | 含义                                                                |
+| --------- | ------------------------------------------------------------------- |
+| `fds`     | `struct pollfd` 数组,每个元素指定一个 fd 及关心的事件              |
+| `nfds`    | 数组元素个数(`nfds_t` 实际为无符号整型)                           |
 | `timeout` | `-1`:一直阻塞;`0`:不阻塞只检查一次;`> 0`:最多阻塞 timeout 毫秒 |
 
 ```c
@@ -201,19 +220,19 @@ struct pollfd {
 
 `events` 是调用者填写(关心的条件),`revents` 由内核在返回时填写,`poll()` 不会改动 `events`。常用事件标志:
 
-| 标志 | events 可填 | revents 可返回 | 说明 |
-| ---- | :---------: | :------------: | ---- |
-| `POLLIN` | ● | ● | 有数据可读 |
-| `POLLRDNORM` | ● | ● | 等价于 `POLLIN` |
-| `POLLRDBAND` | ● | ● | 可读优先级数据(Linux 通常不用) |
-| `POLLPRI` | ● | ● | 可读高优先级数据(GPIO 中断用它) |
-| `POLLRDHUP` | ● | ● | 对端套接字关闭 |
-| `POLLOUT` | ● | ● | 可写入数据 |
-| `POLLWRNORM` | ● | ● | 等价于 `POLLOUT` |
-| `POLLWRBAND` | ● | ● | 优先级数据可写 |
-| `POLLERR` | — | ● | 有错误发生 |
-| `POLLHUP` | — | ● | 出现挂断 |
-| `POLLNVAL` | — | ● | 文件描述符未打开 |
+| 标志         | events 可填 | revents 可返回 | 说明                              |
+| ------------ | :---------: | :------------: | --------------------------------- |
+| `POLLIN`     |           |              | 有数据可读                        |
+| `POLLRDNORM` |           |              | 等价于 `POLLIN`                   |
+| `POLLRDBAND` |           |               | 可读优先级数据(Linux 通常不用)  |
+| `POLLPRI`    |            |               | 可读高优先级数据(GPIO 中断用它) |
+| `POLLRDHUP`  |           |              | 对端套接字关闭                    |
+| `POLLOUT`    |            |              | 可写入数据                        |
+| `POLLWRNORM` |            |              | 等价于 `POLLOUT`                  |
+| `POLLWRBAND` |           |              | 优先级数据可写                    |
+| `POLLERR`    |            |              | 有错误发生                        |
+| `POLLHUP`    |            |               | 出现挂断                          |
+| `POLLNVAL`   |           |              | 文件描述符未打开                  |
 
 不关心某 fd 时,可把 `events` 设为 0,或把 `fd` 设为负数(`-fd`),后者会让该元素被忽略且 `revents` 恒为 0。实际编程中最常用的是 `POLLIN`、`POLLOUT`;GPIO 中断场景用 `POLLPRI`。
 
@@ -370,15 +389,15 @@ out:
 
 > ⚠️ **来源说明**:epoll 部分不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。原书明确说明"本书并不会介绍 epoll 相关内容",仅提示当需要检查大量文件描述符时可用 epoll 解决 select/poll 性能低的问题,且 epoll 在性能上与异步 I/O 相似、并有一些胜过异步 I/O 的优点。
 
-| 维度 | select | poll | epoll |
-| ---- | ------ | ---- | ----- |
-| 数据结构 | `fd_set` 位图 | `pollfd` 数组 | 内核红黑树 + 就绪链表 |
-| fd 上限 | `FD_SETSIZE`=1024 | 无硬上限(受内存限制) | 无硬上限 |
-| 每次调用开销 | O(n),且需重建/拷贝集合 | O(n) 轮询 | O(1) 取就绪链表(`epoll_wait`) |
-| 是否修改入参 | 是(值-结果),需重复初始化 | 否(`revents` 与 `events` 分离) | 否 |
-| 触发模式 | 只有水平触发 | 只有水平触发 | 支持 LT 与 ET |
-| 大量 fd 场景 | 差 | 差 | 优 |
-| 少量 fd 场景 | 够用、可移植性好 | 够用 | 略重 |
+| 维度         | select                      | poll                             | epoll                           |
+| ------------ | --------------------------- | -------------------------------- | ------------------------------- |
+| 数据结构     | `fd_set` 位图               | `pollfd` 数组                    | 内核红黑树 + 就绪链表           |
+| fd 上限      | `FD_SETSIZE`=1024           | 无硬上限(受内存限制)           | 无硬上限                        |
+| 每次调用开销 | O(n),且需重建/拷贝集合     | O(n) 轮询                        | O(1) 取就绪链表(`epoll_wait`) |
+| 是否修改入参 | 是(值-结果),需重复初始化 | 否(`revents` 与 `events` 分离) | 否                              |
+| 触发模式     | 只有水平触发                | 只有水平触发                     | 支持 LT 与 ET                   |
+| 大量 fd 场景 | 差                          | 差                               | 优                              |
+| 少量 fd 场景 | 够用、可移植性好            | 够用                             | 略重                            |
 
 epoll 三个核心函数:
 
@@ -592,31 +611,31 @@ int main(void)
 
 ### 3.3 异步 I/O 的两个缺陷与优化
 
-| 缺陷 | 说明 |
-| ---- | ---- |
-| 信号可能丢失 | `SIGIO` 是标准(非实时、不可靠)信号,不支持排队。处理函数执行期间又来了多次 `SIGIO`,只会传递一次,其余丢失 |
-| 无法得知发生了什么事件 | 处理函数里直接 `read()`,并不知道 fd 到底可读、可写还是异常 |
+| 缺陷                   | 说明                                                                                                         |
+| ---------------------- | ------------------------------------------------------------------------------------------------------------ |
+| 信号可能丢失           | `SIGIO` 是标准(非实时、不可靠)信号,不支持排队。处理函数执行期间又来了多次 `SIGIO`,只会传递一次,其余丢失 |
+| 无法得知发生了什么事件 | 处理函数里直接 `read()`,并不知道 fd 到底可读、可写还是异常                                                  |
 
 优化方法:
 
 - **用实时信号替换 SIGIO**:`fcntl(fd, F_SETSIG, SIGRTMIN);`(`arg=0` 则恢复默认 `SIGIO`)。使用 `F_SETSIG` 需要先定义 `_GNU_SOURCE` 宏。
 - **用 `sigaction()` + `SA_SIGINFO` 注册处理函数**,通过 `siginfo_t` 获取更多信息:
 
-| 字段 | 含义 |
-| ---- | ---- |
-| `si_signo` | 引发处理函数的信号值 |
-| `si_fd` | 发生异步 I/O 事件的 fd |
-| `si_code` | 发生了什么事件(见下表) |
-| `si_band` | 位掩码,值与 `poll()` 的 `revents` 相同 |
-
-| `si_code` | `si_band` 掩码值 | 说明 |
-| --------- | ---------------- | ---- |
-| `POLL_IN` | `POLLIN \| POLLRDNORM` | 可读取数据 |
-| `POLL_OUT` | `POLLOUT \| POLLWRNORM \| POLLWRBAND` | 可写入数据 |
-| `POLL_MSG` | `POLLIN \| POLLRDNORM \| POLLMSG` | 不使用 |
-| `POLL_ERR` | `POLLERR` | I/O 错误 |
-| `POLL_PRI` | `POLLPRI \| POLLRDNORM` | 可读高优先级数据 |
-| `POLL_HUP` | `POLLHUP \| POLLERR` | 出现宕机 |
+| 字段       | 含义                                    |
+| ---------- | --------------------------------------- |
+| `si_signo` | 引发处理函数的信号值                    |
+| `si_fd`    | 发生异步 I/O 事件的 fd                  |
+| `si_code`  | 发生了什么事件(见下表)                |
+| `si_band`  | 位掩码,值与 `poll()` 的 `revents` 相同 |
+
+| `si_code`  | `si_band` 掩码值                      | 说明             |
+| ---------- | ------------------------------------- | ---------------- |
+| `POLL_IN`  | `POLLIN \| POLLRDNORM`                | 可读取数据       |
+| `POLL_OUT` | `POLLOUT \| POLLWRNORM \| POLLWRBAND` | 可写入数据       |
+| `POLL_MSG` | `POLLIN \| POLLRDNORM \| POLLMSG`     | 不使用           |
+| `POLL_ERR` | `POLLERR`                             | I/O 错误         |
+| `POLL_PRI` | `POLLPRI \| POLLRDNORM`               | 可读高优先级数据 |
+| `POLL_HUP` | `POLLHUP \| POLLERR`                  | 出现宕机         |
 
 ### 3.4 完整例程:优化后的异步 I/O
 
@@ -719,24 +738,24 @@ void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)
 int   munmap(void *addr, size_t length);
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
-| `addr` | 映射区起始地址,通常传 `NULL` 由系统选择 |
-| `length` | 映射长度(字节),`length` 不能大于文件大小 |
-| `prot` | 保护要求:`PROT_EXEC`/`PROT_READ`/`PROT_WRITE`/`PROT_NONE`,可位或组合 |
-| `flags` | 必须二选一:`MAP_SHARED`/`MAP_PRIVATE`,可再或其它标志 |
-| `fd` | 要映射的文件描述符 |
-| `offset` | 文件映射偏移量,通常为 0,必须为系统页大小整数倍 |
+| 参数     | 含义                                                                   |
+| -------- | ---------------------------------------------------------------------- |
+| `addr`   | 映射区起始地址,通常传 `NULL` 由系统选择                               |
+| `length` | 映射长度(字节),`length` 不能大于文件大小                            |
+| `prot`   | 保护要求:`PROT_EXEC`/`PROT_READ`/`PROT_WRITE`/`PROT_NONE`,可位或组合 |
+| `flags`  | 必须二选一:`MAP_SHARED`/`MAP_PRIVATE`,可再或其它标志                 |
+| `fd`     | 要映射的文件描述符                                                     |
+| `offset` | 文件映射偏移量,通常为 0,必须为系统页大小整数倍                       |
 
 `flags` 的关键取值:
 
-| 标志 | 说明 |
-| ---- | ---- |
-| `MAP_SHARED` | 写入映射区会更新到文件,并允许其它进程共享 |
-| `MAP_PRIVATE` | 写时复制一份私有副本,对映射区的修改不写回文件;解除映射后修改丢弃 |
-| `MAP_FIXED` | 强制使用 `addr` 指定的地址,失败才放弃,不利于移植,通常不用 |
-| `MAP_ANONYMOUS` | 匿名映射,忽略 `fd` 和 `offset`,不涉及文件,无法与其它进程共享 |
-| `MAP_LOCKED` | 对映射区上锁 |
+| 标志            | 说明                                                               |
+| --------------- | ------------------------------------------------------------------ |
+| `MAP_SHARED`    | 写入映射区会更新到文件,并允许其它进程共享                         |
+| `MAP_PRIVATE`   | 写时复制一份私有副本,对映射区的修改不写回文件;解除映射后修改丢弃 |
+| `MAP_FIXED`     | 强制使用 `addr` 指定的地址,失败才放弃,不利于移植,通常不用       |
+| `MAP_ANONYMOUS` | 匿名映射,忽略 `fd` 和 `offset`,不涉及文件,无法与其它进程共享    |
+| `MAP_LOCKED`    | 对映射区上锁                                                       |
 
 **返回值**:成功返回映射区起始地址;失败返回 `(void *)-1`,即 `MAP_FAILED`,并设置 `errno`。
 
@@ -744,10 +763,10 @@ int   munmap(void *addr, size_t length);
 
 **与映射区相关的两个信号**:
 
-| 信号 | 触发条件 |
-| ---- | -------- |
-| `SIGSEGV` | 映射区被指定为只读,进程却试图写入 |
-| `SIGBUS` | 映射区某部分在访问时已不存在(如另一进程把文件截断) |
+| 信号      | 触发条件                                             |
+| --------- | ---------------------------------------------------- |
+| `SIGSEGV` | 映射区被指定为只读,进程却试图写入                   |
+| `SIGBUS`  | 映射区某部分在访问时已不存在(如另一进程把文件截断) |
 
 **解除映射**:`munmap(addr, length)`,`addr` 必须是页大小整数倍。进程终止时自动解除映射,但**`close()` 关闭文件不会解除映射**。
 
@@ -760,11 +779,11 @@ int msync(void *addr, size_t length, int flags);     // 把映射区数据刷回
 
 `msync` 的 `flags`:
 
-| 标志 | 说明 |
-| ---- | ---- |
-| `MS_ASYNC` | 异步同步,调用后不等数据写完磁盘即返回 |
-| `MS_SYNC` | 同步同步,等数据全部写入磁盘后才返回 |
-| `MS_INVALIDATE` | 可选,使同一文件的其它映射失效 |
+| 标志            | 说明                                   |
+| --------------- | -------------------------------------- |
+| `MS_ASYNC`      | 异步同步,调用后不等数据写完磁盘即返回 |
+| `MS_SYNC`       | 同步同步,等数据全部写入磁盘后才返回   |
+| `MS_INVALIDATE` | 可选,使同一文件的其它映射失效         |
 
 注意:`munmap()` **不会**把映射区内容写到磁盘;`MAP_SHARED` 下由内核在某个时刻自动更新文件;`MAP_PRIVATE` 下解除映射后修改全部丢弃。
 
@@ -884,14 +903,14 @@ int main(void)
 
 ### 4.6 普通 I/O 与存储映射 I/O 对比
 
-| 维度 | 普通 I/O(read/write) | 存储映射 I/O(mmap) |
-| ---- | ---------------------- | -------------------- |
-| 数据拷贝 | 用户缓冲区↔内核缓冲区多次拷贝 | 直接操作映射区,减少拷贝 |
-| 函数调用链 | 层层调用,开销较大 | 直接内存访问 |
-| 大数据量 | 效率低 | 效率高 |
-| 小数据量 | 方便、划算 | 不划算(页对齐、映射开销) |
-| 文件大小 | 无限制 | 映射时长度固定,且不能大于文件 |
-| 典型场景 | 小文件、普通读写 | 视频/图像处理、Framebuffer(LCD)编程 |
+| 维度       | 普通 I/O(read/write)        | 存储映射 I/O(mmap)                  |
+| ---------- | ----------------------------- | ------------------------------------- |
+| 数据拷贝   | 用户缓冲区↔内核缓冲区多次拷贝 | 直接操作映射区,减少拷贝              |
+| 函数调用链 | 层层调用,开销较大            | 直接内存访问                          |
+| 大数据量   | 效率低                        | 效率高                                |
+| 小数据量   | 方便、划算                    | 不划算(页对齐、映射开销)            |
+| 文件大小   | 无限制                        | 映射时长度固定,且不能大于文件        |
+| 典型场景   | 小文件、普通读写              | 视频/图像处理、Framebuffer(LCD)编程 |
 
 ---
 
@@ -901,10 +920,10 @@ int main(void)
 
 多个进程同时操作同一文件容易产生竞争,导致内容混乱。文件锁是内核提供的锁机制,用来对"文件"这一共享资源加锁保护。它与互斥锁/自旋锁/读写锁的区别是:后者主要用在多线程环境做线程同步,文件锁用在对同一文件的多个进程间。
 
-| 类型 | 特点 |
-| ---- | ---- |
+| 类型     | 特点                                                                                           |
+| -------- | ---------------------------------------------------------------------------------------------- |
 | 建议性锁 | 本质是协议。程序访问前先加锁,成功了再访问;但不加锁也能直接访问,对不遵守协议的程序没有约束力 |
-| 强制性锁 | 内核会检查每次 I/O,非锁拥有者无法访问。对性能影响大,一般不建议使用 |
+| 强制性锁 | 内核会检查每次 I/O,非锁拥有者无法访问。对性能影响大,一般不建议使用                           |
 
 Linux 提供 `flock()`、`fcntl()`、`lockf()` 三个加锁函数。
 
@@ -916,11 +935,11 @@ Linux 提供 `flock()`、`fcntl()`、`lockf()` 三个加锁函数。
 int flock(int fd, int operation);
 ```
 
-| 参数/标志 | 说明 |
-| --------- | ---- |
-| `LOCK_SH` | 共享锁,可被多个进程同时拥有 |
-| `LOCK_EX` | 排它锁(互斥锁),同时只能被一个进程拥有 |
-| `LOCK_UN` | 解锁 |
+| 参数/标志 | 说明                                                                        |
+| --------- | --------------------------------------------------------------------------- |
+| `LOCK_SH` | 共享锁,可被多个进程同时拥有                                                |
+| `LOCK_EX` | 排它锁(互斥锁),同时只能被一个进程拥有                                    |
+| `LOCK_UN` | 解锁                                                                        |
 | `LOCK_NB` | 非阻塞方式获取锁,无法获取立即返回,`errno=EWOULDBLOCK`(可与上面位或组合) |
 
 返回值:成功 0;失败 -1 并设置 `errno`。同一文件不会同时具有共享锁和互斥锁。
@@ -1068,11 +1087,11 @@ int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );
 
 与锁相关的 `cmd`:
 
-| cmd | 作用 |
-| --- | ---- |
-| `F_GETLK` | 测试某把锁能否加上;若会被现有锁阻塞,则把现有锁信息写回 `flockptr`,否则把 `l_type` 改为 `F_UNLCK` |
-| `F_SETLK` | 加锁/解锁,失败立即返回,`errno` 为 `EACCES` 或 `EAGAIN` |
-| `F_SETLKW` | `F_SETLK` 的阻塞版本(W=wait),等待锁可用 |
+| cmd        | 作用                                                                                                |
+| ---------- | --------------------------------------------------------------------------------------------------- |
+| `F_GETLK`  | 测试某把锁能否加上;若会被现有锁阻塞,则把现有锁信息写回 `flockptr`,否则把 `l_type` 改为 `F_UNLCK` |
+| `F_SETLK`  | 加锁/解锁,失败立即返回,`errno` 为 `EACCES` 或 `EAGAIN`                                            |
+| `F_SETLKW` | `F_SETLK` 的阻塞版本(W=wait),等待锁可用                                                          |
 
 ```c
 struct flock {
@@ -1084,26 +1103,27 @@ struct flock {
 };
 ```
 
-| `l_type` | 含义 |
-| -------- | ---- |
-| `F_RDLCK` | 共享读锁 |
-| `F_WRLCK` | 独占写锁 |
+| `l_type`  | 含义         |
+| --------- | ------------ |
+| `F_RDLCK` | 共享读锁     |
+| `F_WRLCK` | 独占写锁     |
 | `F_UNLCK` | 解锁一个区域 |
 
 **fcntl 与 flock 的两大区别**:`flock` 只能锁整个文件,`fcntl` 可锁文件的某个区域(精确到字节);`flock` 只支持建议性锁,`fcntl` 支持建议性与强制性锁。
 
 **区域规则**:
+
 - 锁区域可从文件末尾或越过末尾开始,但不能在文件起始位置之前开始;
 - `l_len = 0` 表示锁区域从起始位置一直延伸到文件最大偏移处,且是动态的(后续追加的数据也在锁范围内);
 - 锁整个文件:`l_whence = SEEK_SET`、`l_start = 0`、`l_len = 0`。
 
 **读锁/写锁兼容性**(与线程读写锁类似):多个进程可在同一字节上持有共享读锁;同一字节上只能有一个写锁。已有读锁的字节上加不了写锁;已有写锁的字节上加不了任何锁。
 
-| 当前锁 \ 请求锁 | 读锁 F_RDLCK | 写锁 F_WRLCK |
-| --------------- | :----------: | :----------: |
-| 无锁 | 可 | 可 |
-| 读锁 | 可(共享) | 不可(阻塞/失败) |
-| 写锁 | 不可(阻塞/失败) | 不可(阻塞/失败) |
+| 当前锁 \ 请求锁 |   读锁 F_RDLCK    |   写锁 F_WRLCK    |
+| --------------- | :---------------: | :---------------: |
+| 无锁            |                |                 |
+| 读锁            |    可(共享)     | 不可(阻塞/失败) |
+| 写锁            | 不可(阻塞/失败) | 不可(阻塞/失败) |
 
 加锁权限要求:加读锁需对文件有读权限,加写锁需有写权限。
 
@@ -1227,6 +1247,7 @@ scp testApp root@<开发板IP>:/home/root/
 ```
 
 **调试要点**:
+
 - 非阻塞 `read` 返回 -1 且 `errno=EAGAIN` 属正常,用 `perror` 会打印 "Resource temporarily unavailable"。
 - 找鼠标设备文件:`sudo od -x /dev/input/eventX`,移动/点击鼠标有输出即为目标设备。
 - `select` 每次循环都要重新 `FD_ZERO`/`FD_SET`,否则第二次就绪集合会残留。
@@ -1237,15 +1258,15 @@ scp testApp root@<开发板IP>:/home/root/
 
 ## 7. 跨平台对比(IMX6ULL vs STM32 vs RK3568)
 
-| 维度 | IMX6ULL(Cortex-A7 + Linux) | STM32(Cortex-M) | RK3568(Cortex-A55 + Linux) |
-| ---- | ---------------------------- | ----------------- | ---------------------------- |
-| 运行环境 | 完整 Linux,进程/文件抽象 | 裸机或 RTOS,无文件系统抽象 | 完整 Linux,性能更强 |
-| I/O 模型 | select/poll/epoll、信号驱动、mmap、文件锁 | 靠中断 + DMA + 状态机轮询 | 同 IMX6ULL,且 fd 数量/吞吐更高 |
-| "文件"概念 | 一切皆文件,设备即 `/dev` 节点 | 无(寄存器直接读写) | 一切皆文件 |
-| 并发读取多设备 | I/O 多路复用,单线程即可 | 多中断/DMA,或 RTOS 多任务 | 多路复用 + 多线程 |
-| 大数据量传输 | mmap/Framebuffer 直接映射显存 | DMA 搬运 | mmap/DMA,带宽更高 |
-| 多进程互斥 | flock/fcntl 文件锁 | RTOS 信号量/互斥量(无文件锁) | 同 IMX6ULL |
-| 适用 | 工业 HMI、网关、中低端嵌入式 Linux | 实时控制、低成本节点 | 边缘计算、多路视频、AI 网关 |
+| 维度           | IMX6ULL(Cortex-A7 + Linux)              | STM32(Cortex-M)              | RK3568(Cortex-A55 + Linux)    |
+| -------------- | ----------------------------------------- | ------------------------------ | ------------------------------- |
+| 运行环境       | 完整 Linux,进程/文件抽象                 | 裸机或 RTOS,无文件系统抽象    | 完整 Linux,性能更强            |
+| I/O 模型       | select/poll/epoll、信号驱动、mmap、文件锁 | 靠中断 + DMA + 状态机轮询      | 同 IMX6ULL,且 fd 数量/吞吐更高 |
+| "文件"概念     | 一切皆文件,设备即 `/dev` 节点            | 无(寄存器直接读写)           | 一切皆文件                      |
+| 并发读取多设备 | I/O 多路复用,单线程即可                  | 多中断/DMA,或 RTOS 多任务     | 多路复用 + 多线程               |
+| 大数据量传输   | mmap/Framebuffer 直接映射显存             | DMA 搬运                       | mmap/DMA,带宽更高              |
+| 多进程互斥     | flock/fcntl 文件锁                        | RTOS 信号量/互斥量(无文件锁) | 同 IMX6ULL                      |
+| 适用           | 工业 HMI、网关、中低端嵌入式 Linux        | 实时控制、低成本节点           | 边缘计算、多路视频、AI 网关     |
 
 结论:Linux 应用层的高级 I/O 能力是"Cortex-A + Linux"相对于 MCU 的核心差异之一;同一套 select/poll/epoll/mmap 代码在 IMX6ULL 与 RK3568 上基本可移植,仅性能与 fd 规模不同。
 

+ 102 - 85
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/02-GPIO与LED应用编程.md

@@ -1,6 +1,23 @@
 ---
 title: GPIO与LED应用编程
-tags: [嵌入式Linux, Linux应用编程, sysfs, GPIO, LED, LED子系统, trigger, brightness, export, direction, edge, poll, POLLPRI, ioctl, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    Linux应用编程,
+    sysfs,
+    GPIO,
+    LED,
+    LED子系统,
+    trigger,
+    brightness,
+    export,
+    direction,
+    edge,
+    poll,
+    POLLPRI,
+    ioctl,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十五章 点亮LED、第十六章 GPIO应用编程"
@@ -20,10 +37,10 @@ ALPHA/Mini I.MX6U 开发板上共有 3 颗 LED,只有 1 颗能被用户控制
 
 Linux 下"一切皆文件",应用层操控硬件同样通过文件 I/O 实现,但有两条路:
 
-| 方式 | 位置 | 特点 | 典型设备 |
-| ---- | ---- | ---- | -------- |
-| 设备节点 | `/dev/` 下 | 驱动自定义接口,常配合 `ioctl`;接口未必标准化 | LCD、触摸屏、摄像头、串口 |
-| sysfs 属性文件 | `/sys/` 下 | 内核把设备属性导出成文件,读写文件即读写属性;简单设备常用 | LED、GPIO、PWM |
+| 方式           | 位置       | 特点                                                       | 典型设备                  |
+| -------------- | ---------- | ---------------------------------------------------------- | ------------------------- |
+| 设备节点       | `/dev/` 下 | 驱动自定义接口,常配合 `ioctl`;接口未必标准化             | LCD、触摸屏、摄像头、串口 |
+| sysfs 属性文件 | `/sys/` 下 | 内核把设备属性导出成文件,读写文件即读写属性;简单设备常用 | LED、GPIO、PWM            |
 
 `/dev` 下的文件称为**设备节点**,是硬件设备向应用层提供的接口。而 sysfs 则是另一条途径:把内核对象(硬件设备)导出成**目录**、对象属性导出成**文件**、对象关系导出成**链接文件**。
 
@@ -47,26 +64,26 @@ flowchart TD
 
 sysfs 是**基于内存的虚拟文件系统**,与 proc 类似,作用是把内核信息以文件形式提供给应用层。它把系统上的设备和总线组织成分级文件,展示设备驱动模型中各组件的层次关系。挂载点是 `/sys`。
 
-| 内核要素 | sysfs 中的表现 |
-| -------- | -------------- |
-| 内核对象(如硬件设备) | 目录 |
-| 对象属性(如设备属性) | 文件 |
-| 对象关系 | 链接文件 |
+| 内核要素               | sysfs 中的表现 |
+| ---------------------- | -------------- |
+| 内核对象(如硬件设备) | 目录           |
+| 对象属性(如设备属性) | 文件           |
+| 对象关系               | 链接文件       |
 
 /sys 下的主要子目录:
 
-| 子目录 | 说明 |
-| ------ | ---- |
-| `/sys/devices` | 系统中所有设备存放处,是 sysfs 最重要的目录结构 |
-| `/sys/block` | 块设备目录(过时接口,多链接到 `/sys/devices`) |
-| `/sys/bus` | 按总线类型分类(如 I2C 设备挂在 I2C 总线下),多为链接 |
-| `/sys/class` | 按**功能**分类(如 `/sys/class/leds`、`/sys/class/input`),多为链接 |
-| `/sys/dev` | 按`主设备号:次设备号`命名,链接到 `/sys/devices` |
-| `/sys/firmware` | 内核固件描述 |
-| `/sys/fs` | 系统所有文件系统及挂载点 |
-| `/sys/kernel` | 内核可调参数 |
-| `/sys/module` | 所有模块信息 |
-| `/sys/power` | 电源选项,可控制系统电源状态 |
+| 子目录          | 说明                                                                 |
+| --------------- | -------------------------------------------------------------------- |
+| `/sys/devices`  | 系统中所有设备存放处,是 sysfs 最重要的目录结构                      |
+| `/sys/block`    | 块设备目录(过时接口,多链接到 `/sys/devices`)                      |
+| `/sys/bus`      | 按总线类型分类(如 I2C 设备挂在 I2C 总线下),多为链接               |
+| `/sys/class`    | 按**功能**分类(如 `/sys/class/leds`、`/sys/class/input`),多为链接 |
+| `/sys/dev`      | 按`主设备号:次设备号`命名,链接到 `/sys/devices`                     |
+| `/sys/firmware` | 内核固件描述                                                         |
+| `/sys/fs`       | 系统所有文件系统及挂载点                                             |
+| `/sys/kernel`   | 内核可调参数                                                         |
+| `/sys/module`   | 所有模块信息                                                         |
+| `/sys/power`    | 电源选项,可控制系统电源状态                                         |
 
 读属性文件 = 读设备属性;写属性文件 = 设置属性、控制设备。
 
@@ -78,11 +95,11 @@ sysfs 是**基于内存的虚拟文件系统**,与 proc 类似,作用是把
 
 进入 `/sys/class/leds`,可以看到 `sys-led` 文件夹,这就是底板上的用户 LED。进入其中,主要关注三个属性文件:
 
-| 属性文件 | 读写 | 含义 |
-| -------- | ---- | ---- |
-| `brightness` | 可读可写 | 设置/获取亮度等级。`0` 表示灭,正整数表示亮,值越大越亮(PWM 控制的 LED 有亮度等级;GPIO 控制的只有亮/灭两态) |
-| `max_brightness` | 只读 | 最大亮度等级 |
-| `trigger` | 可读可写 | 触发模式,读为当前模式,写为设置模式 |
+| 属性文件         | 读写     | 含义                                                                                                           |
+| ---------------- | -------- | -------------------------------------------------------------------------------------------------------------- |
+| `brightness`     | 可读可写 | 设置/获取亮度等级。`0` 表示灭,正整数表示亮,值越大越亮(PWM 控制的 LED 有亮度等级;GPIO 控制的只有亮/灭两态) |
+| `max_brightness` | 只读     | 最大亮度等级                                                                                                   |
+| `trigger`        | 可读可写 | 触发模式,读为当前模式,写为设置模式                                                                           |
 
 > 注意:对本文这块 GPIO 控制的 LED,只有亮和灭两种状态,不存在连续亮度等级。
 
@@ -90,12 +107,12 @@ sysfs 是**基于内存的虚拟文件系统**,与 proc 类似,作用是把
 
 `cat trigger` 会列出所有支持的触发模式,方括号 `[...]` 括起来的是当前模式:
 
-| 模式 | 说明 |
-| ---- | ---- |
-| `none` | 无触发,LED 由 brightness 手动控制 |
-| `mmc0` | 对 mmc0 设备读写时 LED 闪烁 |
-| `timer` | 由定时器控制,有规律地一明一灭 |
-| `heartbeat` | 心跳呼吸模式,模仿心跳亮灭变化 |
+| 模式        | 说明                               |
+| ----------- | ---------------------------------- |
+| `none`      | 无触发,LED 由 brightness 手动控制 |
+| `mmc0`      | 对 mmc0 设备读写时 LED 闪烁        |
+| `timer`     | 由定时器控制,有规律地一明一灭     |
+| `heartbeat` | 心跳呼吸模式,模仿心跳亮灭变化     |
 
 系统启动后通常把某颗 LED 设为 `heartbeat` 作为系统运行指示灯。
 
@@ -212,31 +229,31 @@ file testApp          # 应为 32 位 ARM 可执行文件
 
 进入 `/sys/class/gpio`,可以看到两个文件和 5 个 `gpiochipX` 文件夹:
 
-| 条目 | 说明 |
-| ---- | ---- |
-| `gpiochip0/32/64/96/128` | I.MX6UL/I.MX6ULL 的 5 个 GPIO 控制器,分别对应 GPIO1~GPIO5 |
-| `export` | 只写。把指定编号的 GPIO 引脚导出,导出的引脚才能使用;写入后在 `/sys/class/gpio` 下生成 `gpioX` 文件夹 |
-| `unexport` | 只写。删除已导出的 GPIO 引脚,写入后对应 `gpioX` 文件夹消失 |
+| 条目                     | 说明                                                                                                   |
+| ------------------------ | ------------------------------------------------------------------------------------------------------ |
+| `gpiochip0/32/64/96/128` | I.MX6UL/I.MX6ULL 的 5 个 GPIO 控制器,分别对应 GPIO1~GPIO5                                             |
+| `export`                 | 只写。把指定编号的 GPIO 引脚导出,导出的引脚才能使用;写入后在 `/sys/class/gpio` 下生成 `gpioX` 文件夹 |
+| `unexport`               | 只写。删除已导出的 GPIO 引脚,写入后对应 `gpioX` 文件夹消失                                            |
 
 ### 5.2 gpiochipX 与引脚编号
 
 每个 `gpiochipX` 目录下关注三个只读属性文件:
 
-| 属性文件 | 说明 |
-| -------- | ---- |
-| `base` | 该控制器管理的引脚组中最小的编号,与 `gpiochipX` 中的 X 相同 |
-| `label` | 该组 GPIO 的名字 |
-| `ngpio` | 该控制器管理的引脚数量(编号范围 `base ~ base+ngpio-1`) |
+| 属性文件 | 说明                                                         |
+| -------- | ------------------------------------------------------------ |
+| `base`   | 该控制器管理的引脚组中最小的编号,与 `gpiochipX` 中的 X 相同 |
+| `label`  | 该组 GPIO 的名字                                             |
+| `ngpio`  | 该控制器管理的引脚数量(编号范围 `base ~ base+ngpio-1`)     |
 
 I.MX6ULL 的映射关系:
 
-| 控制器 | gpiochip | base |
-| ------ | -------- | ---- |
-| GPIO1 | gpiochip0 | 0 |
-| GPIO2 | gpiochip32 | 32 |
-| GPIO3 | gpiochip64 | 64 |
-| GPIO4 | gpiochip96 | 96 |
-| GPIO5 | gpiochip128 | 128 |
+| 控制器 | gpiochip    | base |
+| ------ | ----------- | ---- |
+| GPIO1  | gpiochip0   | 0    |
+| GPIO2  | gpiochip32  | 32   |
+| GPIO3  | gpiochip64  | 64   |
+| GPIO4  | gpiochip96  | 96   |
+| GPIO5  | gpiochip128 | 128  |
 
 **编号计算公式**:`sysfs 编号 = 该组 base + IO 序号`。
 
@@ -266,12 +283,12 @@ flowchart LR
 
 导出成功后生成的 `gpioX` 目录下,重点关注四个属性文件:
 
-| 属性文件 | 读写 | 取值 | 说明 |
-| -------- | ---- | ---- | ---- |
-| `direction` | 可读可写 | `"out"` / `"in"` | 配置为输出或输入模式 |
-| `value` | 可读可写 | `"0"` / `"1"` | 输出模式:写 0 输出低、写 1 输出高;输入模式:读获取当前电平 |
-| `active_low` | 可读可写 | `"0"` / `"1"` | 极性控制,默认 0 |
-| `edge` | 可读可写 | `"none"`/`"rising"`/`"falling"`/`"both"` | 中断触发方式(配置前需先设输入模式) |
+| 属性文件     | 读写     | 取值                                     | 说明                                                         |
+| ------------ | -------- | ---------------------------------------- | ------------------------------------------------------------ |
+| `direction`  | 可读可写 | `"out"` / `"in"`                         | 配置为输出或输入模式                                         |
+| `value`      | 可读可写 | `"0"` / `"1"`                            | 输出模式:写 0 输出低、写 1 输出高;输入模式:读获取当前电平 |
+| `active_low` | 可读可写 | `"0"` / `"1"`                            | 极性控制,默认 0                                             |
+| `edge`       | 可读可写 | `"none"`/`"rising"`/`"falling"`/`"both"` | 中断触发方式(配置前需先设输入模式)                         |
 
 `active_low` 的极性作用:
 
@@ -289,12 +306,12 @@ echo "1" > value   # 输出低
 echo "0" > value   # 输出高
 ```
 
-| `edge` 取值 | 触发方式 |
-| ----------- | -------- |
-| `none` | 非中断引脚 |
-| `rising` | 上升沿触发 |
-| `falling` | 下降沿触发 |
-| `both` | 边沿触发(上升沿和下降沿) |
+| `edge` 取值 | 触发方式                   |
+| ----------- | -------------------------- |
+| `none`      | 非中断引脚                 |
+| `rising`    | 上升沿触发                 |
+| `falling`   | 下降沿触发                 |
+| `both`      | 边沿触发(上升沿和下降沿) |
 
 配置中断后可用 `poll()` 监听引脚电平变化。
 
@@ -686,12 +703,12 @@ sequenceDiagram
 #define LED_SET_BLINK       _IOW(LED_IOCTL_MAGIC, 2, int)  //参数为闪烁周期ms
 ```
 
-| 宏 | 方向 | 用途 |
-| -- | ---- | ---- |
-| `_IO(type, nr)` | 无参数 | 纯命令,如开/关 |
-| `_IOW(type, nr, type)` | 用户→内核 | 下发一个整型等参数 |
-| `_IOR(type, nr, type)` | 内核→用户 | 读取内核返回的数据 |
-| `_IOWR(type, nr, type)` | 双向 | 既下发又回读 |
+| 宏                      | 方向      | 用途               |
+| ----------------------- | --------- | ------------------ |
+| `_IO(type, nr)`         | 无参数    | 纯命令,如开/关    |
+| `_IOW(type, nr, type)`  | 用户→内核 | 下发一个整型等参数 |
+| `_IOR(type, nr, type)`  | 内核→用户 | 读取内核返回的数据 |
+| `_IOWR(type, nr, type)` | 双向      | 既下发又回读       |
 
 **应用侧完整例程**:
 
@@ -755,12 +772,12 @@ int main(int argc, char *argv[])
 int ioctl(int fd, unsigned long request, ... /* arg */);
 ```
 
-| 项 | 说明 |
-| -- | ---- |
-| `fd` | 设备节点打开得到的文件描述符 |
+| 项        | 说明                                                                        |
+| --------- | --------------------------------------------------------------------------- |
+| `fd`      | 设备节点打开得到的文件描述符                                                |
 | `request` | 命令码,由 `_IO`/`_IOW`/`_IOR`/`_IOWR` 生成,包含方向、类型、序号、数据大小 |
-| `arg` | 可选参数,通常是整型或无类型指针;`_IOW` 表示的参数按指针传递 |
-| 返回值 | 通常成功返回 0,失败返回 -1 并设置 `errno` |
+| `arg`     | 可选参数,通常是整型或无类型指针;`_IOW` 表示的参数按指针传递               |
+| 返回值    | 通常成功返回 0,失败返回 -1 并设置 `errno`                                  |
 
 设备节点出现的前提是内核驱动已注册并创建了该节点(或由 `mdev`/`udev` 根据设备号创建)。没有对应驱动时,`open("/dev/led")` 会报 `No such file or directory`。
 
@@ -835,26 +852,26 @@ scp testApp_led testApp_out testApp_in testApp_intr root@<开发板IP>:/home/roo
 
 ### 10.4 常见问题排查
 
-| 现象 | 原因/处理 |
-| ---- | --------- |
-| `echo N > export` 报错 | 该 GPIO 已被内核/驱动占用;换未被占用的引脚 |
+| 现象                          | 原因/处理                                              |
+| ----------------------------- | ------------------------------------------------------ |
+| `echo N > export` 报错        | 该 GPIO 已被内核/驱动占用;换未被占用的引脚            |
 | 输出模式下 `cat value` 无意义 | 输出模式不能用 `cat value` 读电平,需外接 LED 或万用表 |
 | 写 `brightness` 后 LED 仍闪烁 | 没有先把 `trigger` 设为 `none`,触发模式覆盖了亮度控制 |
-| 中断只触发一次 | 未 `lseek` 回文件头或未先 `read` 清状态 |
-| `open /dev/led` 失败 | 内核未注册对应 misc 驱动,没有创建设备节点 |
+| 中断只触发一次                | 未 `lseek` 回文件头或未先 `read` 清状态                |
+| `open /dev/led` 失败          | 内核未注册对应 misc 驱动,没有创建设备节点             |
 
 ---
 
 ## 11. 跨平台对比(IMX6ULL vs STM32 vs RK3568)
 
-| 维度 | IMX6ULL(Cortex-A7 + Linux) | STM32(Cortex-M) | RK3568(Cortex-A55 + Linux) |
-| ---- | ---------------------------- | ----------------- | ---------------------------- |
+| 维度          | IMX6ULL(Cortex-A7 + Linux)                                       | STM32(Cortex-M)                              | RK3568(Cortex-A55 + Linux)           |
+| ------------- | ------------------------------------------------------------------ | ---------------------------------------------- | -------------------------------------- |
 | GPIO 控制方式 | 应用层 sysfs(`/sys/class/gpio`、`/sys/class/leds`)或 `/dev` 节点 | 直接读写寄存器 / HAL 库(`HAL_GPIO_WritePin`) | 同 IMX6ULL,也常用 pinctrl/gpio 子系统 |
-| 抽象层次 | 内核 pinctrl/gpio 子系统 + 设备驱动框架 | 寄存器/厂商库,无统一框架 | 内核 pinctrl/gpio 子系统 |
-| LED 控制 | LED 框架导出 `/sys/class/leds/*/brightness`、`trigger` | 普通 GPIO,无 LED 子系统(自行实现) | LED 框架,同 IMX6ULL |
-| 中断 | 配置 `edge` 后 `poll(POLLPRI)` 或异步 I/O | EXTI 外部中断 + 中断服务函数 | 同 IMX6ULL,内核中断子系统 |
-| 性能 | 低配 Linux,适合工业 HMI | 实时性强、成本低,适合实时控制节点 | 高性能 Linux,适合边缘计算/视频 |
-| 开发门槛 | 需懂 sysfs/驱动模型 | 需懂寄存器/外设 | 需懂 sysfs/设备树/pinctrl |
+| 抽象层次      | 内核 pinctrl/gpio 子系统 + 设备驱动框架                            | 寄存器/厂商库,无统一框架                      | 内核 pinctrl/gpio 子系统               |
+| LED 控制      | LED 框架导出 `/sys/class/leds/*/brightness`、`trigger`             | 普通 GPIO,无 LED 子系统(自行实现)           | LED 框架,同 IMX6ULL                   |
+| 中断          | 配置 `edge` 后 `poll(POLLPRI)` 或异步 I/O                          | EXTI 外部中断 + 中断服务函数                   | 同 IMX6ULL,内核中断子系统             |
+| 性能          | 低配 Linux,适合工业 HMI                                           | 实时性强、成本低,适合实时控制节点             | 高性能 Linux,适合边缘计算/视频        |
+| 开发门槛      | 需懂 sysfs/驱动模型                                                | 需懂寄存器/外设                                | 需懂 sysfs/设备树/pinctrl              |
 
 结论:STM32 是"直接操作硬件",Cortex-A 平台是"通过内核子系统和文件接口间接操作硬件"。同一份 sysfs GPIO 代码在 IMX6ULL 与 RK3568 上基本通用,差别只在编号映射与设备树配置。
 

+ 84 - 71
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/03-输入设备与tslib.md

@@ -1,6 +1,7 @@
 ---
 title: 输入设备与tslib
-tags: [嵌入式Linux, Linux应用编程, input子系统, 输入设备, 触摸屏, tslib, IMX6ULL]
+tags:
+  [嵌入式Linux, Linux应用编程, input子系统, 输入设备, 触摸屏, tslib, IMX6ULL]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十七章 输入设备应用编程、第十八章 使用tslib库"
@@ -64,12 +65,12 @@ struct input_event {
 
 `time` 是事件发生时间(通常不是重点),重点是其馀三个成员:
 
-| 成员 | 含义 |
-| ---- | ---- |
-| `type` | 事件类型(哪一大类事件) |
-| `code` | 事件代码(该类中的哪一个具体事件) |
-| `value` | 事件值,解释随 `code` 变化 |
-事件类型宏(`<linux/input.h>`):
+| 成员                              | 含义                               |
+| --------------------------------- | ---------------------------------- |
+| `type`                            | 事件类型(哪一大类事件)           |
+| `code`                            | 事件代码(该类中的哪一个具体事件) |
+| `value`                           | 事件值,解释随 `code` 变化         |
+| 事件类型宏(`<linux/input.h>`): |
 
 ```c
 
@@ -84,6 +85,7 @@ struct input_event {
 #define EV_CNT   (EV_MAX+1)
 
 ```
+
 常见 `code`:
 
 ```c
@@ -107,6 +109,7 @@ struct input_event {
 #define BTN_TOUCH          330  //触摸按下/松开
 
 ```
+
 `value` 的解释随 `code` 变化:按键事件中 `value=1` 按下、`0` 松开、`2` 长按;绝对位移事件中 `code=ABS_X` 时 `value` 就是 X 坐标,`code=ABS_Y` 时就是 Y 坐标。这些宏定义在 `input-event-codes.h`,被 `<linux/input.h>` 包含。
 
 ---
@@ -217,9 +220,9 @@ scp testApp root@192.168.1.10:/home/root/
 
 触摸屏是**绝对位移设备**,上报 `EV_ABS`:
 
-| 类型 | 特点 | 承载事件 |
-| ---- | ---- | -------- |
-| 单点触摸 | 一轮完整数据只含一个触摸点 | `ABS_X`、`ABS_Y` 等 `ABS_XXX` |
+| 类型     | 特点                       | 承载事件                              |
+| -------- | -------------------------- | ------------------------------------- |
+| 单点触摸 | 一轮完整数据只含一个触摸点 | `ABS_X`、`ABS_Y` 等 `ABS_XXX`         |
 | 多点触摸 | 一轮完整数据可含多个触摸点 | `ABS_MT_POSITION_X/Y` 等 `ABS_MT_XXX` |
 
 除位移事件外还会上报:
@@ -261,10 +264,10 @@ ABS_MT_POSITION_Y
 SYN_REPORT
 ```
 
-| 概念 | 层级 | 含义 |
-| ---- | ---- | ---- |
-| slot | 硬件 | 触摸点编号,按触碰先后分配 |
-| ID | 软件 | 触摸点生命周期的标识;手离开后销毁,再触碰即为新 ID |
+| 概念 | 层级 | 含义                                                |
+| ---- | ---- | --------------------------------------------------- |
+| slot | 硬件 | 触摸点编号,按触碰先后分配                          |
+| ID   | 软件 | 触摸点生命周期的标识;手离开后销毁,再触碰即为新 ID |
 
 同一手指触碰、离开、再触碰,两次都是 slot=0,但生命周期不同,因此 ID 不同。
 
@@ -306,6 +309,7 @@ struct input_absinfo {
 };
 
 ```
+
 例程 `17_input/read_slot.c` 打开设备后执行:
 
 ```c
@@ -316,6 +320,7 @@ max_slots = info.maximum + 1 - info.minimum;
 printf("max_slots: %d\n", max_slots);
 
 ```
+
 结果为 `max_slots: 5` 时说明是 5 点触摸屏,该值正是下面多点程序申请 `struct ts_mt` 数组的依据。
 
 ## 8. 单点触摸应用程序(read_ts.c)
@@ -329,6 +334,7 @@ scp testApp root@192.168.1.10:/home/root/
 ```
 
 ## 9. 多点触摸应用程序(read_mt.c)
+
 按 slot 分别维护每个触摸点的坐标与 ID:
 
 ```c
@@ -472,6 +478,7 @@ int main(int argc, char *argv[])
 }
 
 ```
+
 - `main` 先用 `ioctl(fd, EVIOCGABS(ABS_MT_SLOT), &slot)` 求最大触摸点数,再按 `max_slots` 申请 `struct ts_mt` 数组:`mt[0]` 对应 slot 0,依次类推;
 - `ts_read()` 把一轮数据填入数组:`ABS_MT_SLOT` 更新当前 slot,后续 X/Y、TRACKING_ID 都写入该 slot;`SYN_REPORT` 时同步坐标并返回;
 - `id` 初值 `-2` 表示未上报,`-1` 表示删除(松开),`>=0` 表示创建(按下)。单点靠 `BTN_TOUCH` 判断动作,多点靠 **ID** 判断各手指动作。
@@ -484,7 +491,9 @@ scp testApp root@192.168.1.10:/home/root/
 ```
 
 ## 10. tslib 库
+
 tslib 是专门为触摸屏开发的 Linux **应用层函数库**,开源。它是触摸屏驱动与应用层之间的适配层:
+
 - 把应用层读取 `struct input_event` 并解析的过程**封装**,向上提供 API;
 - 从触摸屏获得原始坐标,经**去噪、去抖、坐标变换**,转换为屏幕坐标;
 - 通过配置文件 `ts.conf` 提供配置参数;
@@ -500,6 +509,7 @@ flowchart LR
 ```
 
 ### 10.1 tslib 移植
+
 进入 git 仓库 `https://github.com/libts/tslib/releases` 下载源码。出厂系统已移植 **1.16**,为统一版本下载 `tslib-1.16.tar.gz`。编译分三步:配置 → 编译 → 安装。
 
 ```bash
@@ -515,27 +525,28 @@ make
 make install
 
 ```
+
 - `--host`:库文件运行的平台,通常设为交叉编译器名称前缀(`arm-poky-linux-gnueabi-gcc` 的前缀即 `arm-poky-linux-gnueabi`);
 - `--prefix`:安装路径;`./configure --help` 可看全部选项。
-安装目录:`bin/`(测试工具)、`etc/ts.conf`(配置)、`include/tslib.h`(头文件)、`lib/`(库文件与 `ts/` 插件库)、`share/`(可忽略)。
-`ts.conf` 常用配置:
+  安装目录:`bin/`(测试工具)、`etc/ts.conf`(配置)、`include/tslib.h`(头文件)、`lib/`(库文件与 `ts/` 插件库)、`share/`(可忽略)。
+  `ts.conf` 常用配置:
 
-| 配置 | 作用 |
-| ---- | ---- |
-| `module_raw input` | 取消注释后使能支持 input 输入事件 |
-| `module pthres pmin=1` | 支持按压力测试时启用,`pmin` 调节灵敏度 |
-| `module dejitter delta=100` | 去噪插件,过滤噪声样本 |
-| `module linear` | 坐标变换(X/Y 互换、旋转等) |
+| 配置                        | 作用                                    |
+| --------------------------- | --------------------------------------- |
+| `module_raw input`          | 取消注释后使能支持 input 输入事件       |
+| `module pthres pmin=1`      | 支持按压力测试时启用,`pmin` 调节灵敏度 |
+| `module dejitter delta=100` | 去噪插件,过滤噪声样本                  |
+| `module linear`             | 坐标变换(X/Y 互换、旋转等)            |
 
 部署到自己做的根文件系统时:`bin/` 下可执行文件拷到 `/usr/bin`,`ts.conf` 拷到 `/etc`,`lib/` 下库文件拷到 `/usr/lib`。运行时依赖以下环境变量(出厂系统已在 `/etc/profile` 中配置):
 
-| 变量 | 含义 |
-| ---- | ---- |
-| `TSLIB_CONSOLEDEVICE` | 控制台设备名,直接为 `none` |
-| `TSLIB_FBDEVICE` | 显示设备节点(画线测试要在 LCD 显示) |
-| `TSLIB_TSDEVICE` | 触摸屏设备节点,按实际配置 |
-| `TSLIB_CONFFILE` | `ts.conf` 路径 |
-| `TSLIB_PLUGINDIR` | 插件路径 |
+| 变量                  | 含义                                  |
+| --------------------- | ------------------------------------- |
+| `TSLIB_CONSOLEDEVICE` | 控制台设备名,直接为 `none`           |
+| `TSLIB_FBDEVICE`      | 显示设备节点(画线测试要在 LCD 显示) |
+| `TSLIB_TSDEVICE`      | 触摸屏设备节点,按实际配置            |
+| `TSLIB_CONFFILE`      | `ts.conf` 路径                        |
+| `TSLIB_PLUGINDIR`     | 插件路径                              |
 
 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
 >
@@ -547,14 +558,14 @@ make install
 
 使用需包含 `#include <tslib.h>`,步骤为:打开设备 → 配置设备 → 读取数据。
 
-| 函数 | 说明 |
-| ---- | ---- |
-| `ts_open` | 打开触摸屏设备;成功返回句柄,失败返回 `NULL` |
-| `ts_setup` | 打开**并配置**设备;`dev_name` 传 `NULL` 时读取 `TSLIB_TSDEVICE` 环境变量 |
-| `ts_config` | 解析 `ts.conf`、加载插件;成功 0、失败 -1 |
-| `ts_close` | 关闭设备 |
-| `ts_read` | 读单点触摸数据 |
-| `ts_read_mt` | 读多点触摸数据;`max_slots` 为最大触摸点数 |
+| 函数         | 说明                                                                      |
+| ------------ | ------------------------------------------------------------------------- |
+| `ts_open`    | 打开触摸屏设备;成功返回句柄,失败返回 `NULL`                             |
+| `ts_setup`   | 打开**并配置**设备;`dev_name` 传 `NULL` 时读取 `TSLIB_TSDEVICE` 环境变量 |
+| `ts_config`  | 解析 `ts.conf`、加载插件;成功 0、失败 -1                                 |
+| `ts_close`   | 关闭设备                                                                  |
+| `ts_read`    | 读单点触摸数据                                                            |
+| `ts_read_mt` | 读多点触摸数据;`max_slots` 为最大触摸点数                                |
 
 `nonblock`:0 阻塞、非 0 非阻塞。`nr` 为对一个触摸点的采样数,设为 1 即可;出错时 `ts_read` 返回负数。
 单点样本结构体:
@@ -568,18 +579,19 @@ struct ts_sample {
 };
 
 ```
+
 多点样本结构体 `struct ts_sample_mt` 常用字段:
 
-| 字段 | 含义 |
-| ---- | ---- |
-| `x` / `y` | X / Y 坐标 |
-| `pressure` | 按压力大小 |
-| `slot` | 触摸点 slot |
-| `tracking_id` | 触摸点 ID |
-| `tv` | 时间 |
-| `pen_down` | `BTN_TOUCH` 状态 |
-| `valid` | 本次样本是否有效(数据是否更新) |
-其余字段(`tool_type`、`tool_x/y`、`touch_major/minor`、`width_major/minor`、`orientation`、`distance`、`blob_id`)为更细的描述,一般可忽略。
+| 字段                                                                                                                                          | 含义                             |
+| --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
+| `x` / `y`                                                                                                                                     | X / Y 坐标                       |
+| `pressure`                                                                                                                                    | 按压力大小                       |
+| `slot`                                                                                                                                        | 触摸点 slot                      |
+| `tracking_id`                                                                                                                                 | 触摸点 ID                        |
+| `tv`                                                                                                                                          | 时间                             |
+| `pen_down`                                                                                                                                    | `BTN_TOUCH` 状态                 |
+| `valid`                                                                                                                                       | 本次样本是否有效(数据是否更新) |
+| 其余字段(`tool_type`、`tool_x/y`、`touch_major/minor`、`width_major/minor`、`orientation`、`distance`、`blob_id`)为更细的描述,一般可忽略。 |
 
 ## 11. 基于 tslib 的单点触摸程序(ts_read.c)
 
@@ -628,6 +640,7 @@ int main(int argc, char *argv[])
 }
 
 ```
+
 `ts_setup(NULL, 0)` 内部读取 `TSLIB_TSDEVICE` 得到设备节点并完成配置;通过**按压力**判断状态:`pressure=0` 表示松开,大于 0 时再根据上一次按压力判断"按下/移动"。
 
 ```bash
@@ -636,10 +649,10 @@ scp testApp root@192.168.1.10:/home/root/
 ./testApp
 ```
 
-| 选项 | 含义 |
-| ---- | ---- |
-| `-I` | 头文件路径(tslib 的 `include`) |
-| `-L` | 库文件路径(tslib 的 `lib`) |
+| 选项   | 含义                                                 |
+| ------ | ---------------------------------------------------- |
+| `-I`   | 头文件路径(tslib 的 `include`)                     |
+| `-L`   | 库文件路径(tslib 的 `lib`)                         |
 | `-lts` | 链接 `libts.so`(Linux 动态库命名 `lib`+名字+`.so`) |
 
 ---
@@ -720,22 +733,22 @@ arm-linux-gnueabihf-gcc -I /home/dt/tools/tslib/include -L /home/dt/tools/tslib/
 ### 13.1 实验步骤
 
 1. 将 LCD 屏连接到开发板 LCD 接口,上电启动出厂系统;点击屏幕进入设置页面,点击退出按钮退出出厂系统 GUI 应用;
-3. `cat /proc/bus/input/devices` 确认触摸屏设备节点(如 `goodix-ts`);
-4. 用 `read_input.c` 打印原始数据,对照第 6.2 节分析事件规则;
-5. 用 `read_slot.c` 查看最大触摸点数,再运行 `read_ts.c`、`read_mt.c`;
-6. 运行 `ts_print`/`ts_print_mt`/`ts_test`/`ts_test_mt` 验证 tslib;
-7. 交叉编译 `ts_read.c`/`ts_read_mt.c`(带 `-I`、`-L`、`-lts`),拷到开发板运行。
+2. `cat /proc/bus/input/devices` 确认触摸屏设备节点(如 `goodix-ts`);
+3. 用 `read_input.c` 打印原始数据,对照第 6.2 节分析事件规则;
+4. 用 `read_slot.c` 查看最大触摸点数,再运行 `read_ts.c`、`read_mt.c`;
+5. 运行 `ts_print`/`ts_print_mt`/`ts_test`/`ts_test_mt` 验证 tslib;
+6. 交叉编译 `ts_read.c`/`ts_read_mt.c`(带 `-I`、`-L`、`-lts`),拷到开发板运行。
 
 ### 13.2 调试方法
 
-| 现象 | 可能原因 | 排查手段 |
-| ---- | -------- | -------- |
-| `open error: No such file or directory` | 设备节点写错 | `cat /proc/bus/input/devices` 确认 `eventX` |
-| `open error: Permission denied` | 权限不足 | 用 `root` 或检查节点权限 |
-| `read` 阻塞无输出 | 该设备无事件(正常) | 触发按键/触摸;或改用非阻塞 |
-| 坐标与屏幕不符 | 未做坐标变换/校准 | 用 tslib `module linear` 或校准 |
-| 编译找不到 `tslib.h` | 未指定头文件路径 | 加 `-I <tslib>/include` |
-| 链接找不到 `libts` | 未指定库路径或库名 | 加 `-L <tslib>/lib -lts` |
+| 现象                                    | 可能原因             | 排查手段                                    |
+| --------------------------------------- | -------------------- | ------------------------------------------- |
+| `open error: No such file or directory` | 设备节点写错         | `cat /proc/bus/input/devices` 确认 `eventX` |
+| `open error: Permission denied`         | 权限不足             | 用 `root` 或检查节点权限                    |
+| `read` 阻塞无输出                       | 该设备无事件(正常) | 触发按键/触摸;或改用非阻塞                 |
+| 坐标与屏幕不符                          | 未做坐标变换/校准    | 用 tslib `module linear` 或校准             |
+| 编译找不到 `tslib.h`                    | 未指定头文件路径     | 加 `-I <tslib>/include`                     |
+| 链接找不到 `libts`                      | 未指定库路径或库名   | 加 `-L <tslib>/lib -lts`                    |
 
 ```bash
 cat /proc/bus/input/devices      # 查看所有输入设备及 eventX
@@ -749,13 +762,13 @@ ts_finddev                       # 查看 tslib 设备与版本
 
 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
 
-| 维度 | I.MX6ULL(本教程) | STM32(裸机/RTOS) | RK3568 |
-| ---- | ------------------ | ------------------ | ------ |
-| 输入框架 | Linux input 子系统,`/dev/input/eventX` | 无统一框架,直接读寄存器或自写驱动 | Linux input 子系统,接口一致 |
-| 数据单元 | `struct input_event` | 自定义结构/寄存器值 | `struct input_event` |
-| 多点触摸 | MT Type B,slot + tracking_id | 触摸 IC 自定义报文 | MT 协议,接口一致 |
-| 用户态库 | tslib(去噪/校准/坐标变换,Qt 插件) | 通常无,需自行滤波校准 | tslib 同样可用 |
-| 交叉工具链 | `arm-linux-gnueabihf-gcc` | `arm-none-eabi-gcc` | `aarch64-linux-gnu-gcc` |
+| 维度       | I.MX6ULL(本教程)                      | STM32(裸机/RTOS)                 | RK3568                       |
+| ---------- | --------------------------------------- | ---------------------------------- | ---------------------------- |
+| 输入框架   | Linux input 子系统,`/dev/input/eventX` | 无统一框架,直接读寄存器或自写驱动 | Linux input 子系统,接口一致 |
+| 数据单元   | `struct input_event`                    | 自定义结构/寄存器值                | `struct input_event`         |
+| 多点触摸   | MT Type B,slot + tracking_id           | 触摸 IC 自定义报文                 | MT 协议,接口一致            |
+| 用户态库   | tslib(去噪/校准/坐标变换,Qt 插件)    | 通常无,需自行滤波校准             | tslib 同样可用               |
+| 交叉工具链 | `arm-linux-gnueabihf-gcc`               | `arm-none-eabi-gcc`                | `aarch64-linux-gnu-gcc`      |
 
 I.MX6ULL 与 RK3568 同属 Linux 应用编程,input 子系统与 tslib 用法基本通用;STM32 裸机没有设备节点与 `input_event`,不能套用本篇模型。
 

+ 173 - 159
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/04-FrameBuffer与LCD应用编程.md

@@ -1,6 +1,20 @@
 ---
 title: FrameBuffer 与 LCD 应用编程
-tags: [嵌入式Linux, Linux应用编程, FrameBuffer, LCD, fb0, mmap, RGB565, libjpeg, libpng, FreeType, 竖屏, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    Linux应用编程,
+    FrameBuffer,
+    LCD,
+    fb0,
+    mmap,
+    RGB565,
+    libjpeg,
+    libpng,
+    FreeType,
+    竖屏,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十九章 FrameBuffer应用编程;第二十章 在LCD上显示jpeg图像;第二十一章 在LCD上显示png图片;第二十二章 LCD横屏切换为竖屏;第二十三章 在LCD上显示字符"
@@ -77,11 +91,11 @@ FrameBuffer 应用编程绕不开 `<linux/fb.h>` 里的两个结构体:可变
 
 ### 2.1 ioctl request 速查
 
-| request 宏 | 值 | 作用 | 第三参数 |
-| ---------- | -- | ---- | -------- |
-| `FBIOGET_VSCREENINFO` | `0x4600` | 获取**可变**参数信息 | `struct fb_var_screeninfo *` |
+| request 宏            | 值       | 作用                                     | 第三参数                     |
+| --------------------- | -------- | ---------------------------------------- | ---------------------------- |
+| `FBIOGET_VSCREENINFO` | `0x4600` | 获取**可变**参数信息                     | `struct fb_var_screeninfo *` |
 | `FBIOPUT_VSCREENINFO` | `0x4601` | 设置**可变**参数信息(驱动支持时才能改) | `struct fb_var_screeninfo *` |
-| `FBIOGET_FSCREENINFO` | `0x4602` | 获取**固定**参数信息(应用不可改) | `struct fb_fix_screeninfo *` |
+| `FBIOGET_FSCREENINFO` | `0x4602` | 获取**固定**参数信息(应用不可改)       | `struct fb_fix_screeninfo *` |
 
 ```c
 struct fb_var_screeninfo fb_var;
@@ -188,15 +202,15 @@ struct fb_fix_screeninfo {
 
 ### 2.5 字段速查表
 
-| 结构体 | 字段 | 含义 | 典型用途 |
-| ------ | ---- | ---- | -------- |
-| fb_var | `xres` / `yres` | 可视分辨率 | 计算 `width`、`height` |
-| fb_var | `xres_virtual` / `yres_virtual` | 虚拟分辨率 | 双缓冲 / panning |
-| fb_var | `xoffset` / `yoffset` | 可视窗口在虚拟显存中的偏移 | 平移显示 |
-| fb_var | `bits_per_pixel` | 像素深度 bpp | 一行有效字节数 = `width * bpp / 8` |
-| fb_var | `red/green/blue` | 通道位域 | 判定 RGB565 / RGB888 |
-| fb_fix | `line_length` | 一行字节数 | 显存大小 = `line_length * yres` |
-| fb_fix | `smem_start` / `smem_len` | 显存物理地址/长度 | 物理信息,应用不可直接用 |
+| 结构体 | 字段                            | 含义                       | 典型用途                           |
+| ------ | ------------------------------- | -------------------------- | ---------------------------------- |
+| fb_var | `xres` / `yres`                 | 可视分辨率                 | 计算 `width`、`height`             |
+| fb_var | `xres_virtual` / `yres_virtual` | 虚拟分辨率                 | 双缓冲 / panning                   |
+| fb_var | `xoffset` / `yoffset`           | 可视窗口在虚拟显存中的偏移 | 平移显示                           |
+| fb_var | `bits_per_pixel`                | 像素深度 bpp               | 一行有效字节数 = `width * bpp / 8` |
+| fb_var | `red/green/blue`                | 通道位域                   | 判定 RGB565 / RGB888               |
+| fb_fix | `line_length`                   | 一行字节数                 | 显存大小 = `line_length * yres`    |
+| fb_fix | `smem_start` / `smem_len`       | 显存物理地址/长度          | 物理信息,应用不可直接用           |
 
 ### 2.6 像素格式与 RGB565
 
@@ -212,6 +226,7 @@ RGB565 的位域掩码为 `0xF800`、`0x07E0`、`0x001F`。从 ARGB8888 的颜
             ((temp & 0xF8UL) >> 3); \
             })
 ```
+
 ---
 
 ## 3. 例程一:读取屏幕参数 lcd_info.c
@@ -477,12 +492,12 @@ base + (y * width + x) * pix_bytes
 
 ### 4.2 四个函数职责
 
-| 函数 | 作用 | 关键参数 |
-| ---- | ---- | -------- |
-| `lcd_draw_point` | 打点 | `x, y, color` |
-| `lcd_draw_line` | 画水平/垂直线(不支持斜线) | `dir != 0` 水平,`dir == 0` 垂直;`length` 像素长度 |
-| `lcd_draw_rectangle` | 画矩形边框(4 条线拼成) | 左上 `(start_x,start_y)`、右下 `(end_x,end_y)` |
-| `lcd_fill` | 矩形区域填充 | 同上 |
+| 函数                 | 作用                        | 关键参数                                            |
+| -------------------- | --------------------------- | --------------------------------------------------- |
+| `lcd_draw_point`     | 打点                        | `x, y, color`                                       |
+| `lcd_draw_line`      | 画水平/垂直线(不支持斜线) | `dir != 0` 水平,`dir == 0` 垂直;`length` 像素长度 |
+| `lcd_draw_rectangle` | 画矩形边框(4 条线拼成)    | 左上 `(start_x,start_y)`、右下 `(end_x,end_y)`      |
+| `lcd_fill`           | 矩形区域填充                | 同上                                                |
 
 `lcd_draw_rectangle` 的内边长度 `y_len = end_y - start_y - 1`,左右两条竖线各画 `y_len` 个点;`lcd_fill` 用双层循环逐行填充。
 
@@ -506,40 +521,40 @@ base + (y * width + x) * pix_bytes
 
 ### 5.1 BMP 文件由四部分组成
 
-| 数据段 | 大小(Byte) | 说明 |
-| ------ | ------------ | ---- |
-| BMP 文件头(bmp file header) | 14 | 文件格式、大小、到位图数据的偏移量 |
-| 位图信息头(bitmap information) | 通常 40 或 56 | 头大小、图像尺寸、图像大小、位平面数、压缩方式、颜色索引 |
-| 调色板(color palette) | 由颜色索引数决定 | 可选;索引色图像才有 |
-| 位图数据(bitmap data) | 由图像尺寸决定 | 图像数据本体 |
+| 数据段                           | 大小(Byte)     | 说明                                                     |
+| -------------------------------- | ---------------- | -------------------------------------------------------- |
+| BMP 文件头(bmp file header)    | 14               | 文件格式、大小、到位图数据的偏移量                       |
+| 位图信息头(bitmap information) | 通常 40 或 56    | 头大小、图像尺寸、图像大小、位平面数、压缩方式、颜色索引 |
+| 调色板(color palette)          | 由颜色索引数决定 | 可选;索引色图像才有                                     |
+| 位图数据(bitmap data)          | 由图像尺寸决定   | 图像数据本体                                             |
 
 16 位(R5/G6/B5)、24 位(R8/G8/B8)真彩色图像**不需要调色板**,位图信息头后紧跟位图数据。
 
 ### 5.2 BMP 文件头成员
 
-| 变量名 | 地址偏移 | 大小 | 作用 |
-| ------ | -------- | ---- | ---- |
-| `bfType` | 00H | 2 bytes | 文件类型,`BM` 表示 Windows 位图 |
-| `bfSize` | 02H | 4 bytes | 文件大小 |
-| `bfReserved1` | 06H | 2 bytes | 保留,必须为 0 |
-| `bfReserved2` | 08H | 2 bytes | 保留,必须为 0 |
-| `bfOffBits` | 0AH | 4 bytes | 从文件头到图像数据的字节偏移量,用它快速定位图像数据 |
+| 变量名        | 地址偏移 | 大小    | 作用                                                 |
+| ------------- | -------- | ------- | ---------------------------------------------------- |
+| `bfType`      | 00H      | 2 bytes | 文件类型,`BM` 表示 Windows 位图                     |
+| `bfSize`      | 02H      | 4 bytes | 文件大小                                             |
+| `bfReserved1` | 06H      | 2 bytes | 保留,必须为 0                                       |
+| `bfReserved2` | 08H      | 2 bytes | 保留,必须为 0                                       |
+| `bfOffBits`   | 0AH      | 4 bytes | 从文件头到图像数据的字节偏移量,用它快速定位图像数据 |
 
 ### 5.3 位图信息头成员
 
-| 变量名 | 地址偏移 | 大小 | 作用 |
-| ------ | -------- | ---- | ---- |
-| `biSize` | 0EH | 4 bytes | 位图信息头大小 |
-| `biWidth` | 12H | 4 bytes | 图像宽度(像素) |
-| `biHeight` | 16H | 4 bytes | 图像高度(像素);**正数 = 倒向位图,负数 = 正向位图** |
-| `biPlanes` | 1AH | 2 bytes | 色彩平面数,总为 1 |
-| `biBitCount` | 1CH | 2 bytes | 像素深度,可为 1、4、8、16、24、32 |
-| `biCompression` | 1EH | 4 bytes | 压缩方式:0=RGB,1=8bpp RLE,2=4bpp RLE,3=Bit-fields,4/5=打印机 |
-| `biSizeImage` | 22H | 4 bytes | 图像数据大小(BI_RGB 时可设为 0) |
-| `biXPelsPerMeter` | 26H | 4 bytes | 水平分辨率(像素/米) |
-| `biYPelsPerMeter` | 2AH | 4 bytes | 垂直分辨率(像素/米) |
-| `biClrUsed` | 2EH | 4 bytes | 实际使用的调色板颜色索引数 |
-| `biClrImportant` | 32H | 4 bytes | 重要颜色索引数,0 表示都重要 |
+| 变量名            | 地址偏移 | 大小    | 作用                                                              |
+| ----------------- | -------- | ------- | ----------------------------------------------------------------- |
+| `biSize`          | 0EH      | 4 bytes | 位图信息头大小                                                    |
+| `biWidth`         | 12H      | 4 bytes | 图像宽度(像素)                                                  |
+| `biHeight`        | 16H      | 4 bytes | 图像高度(像素);**正数 = 倒向位图,负数 = 正向位图**            |
+| `biPlanes`        | 1AH      | 2 bytes | 色彩平面数,总为 1                                                |
+| `biBitCount`      | 1CH      | 2 bytes | 像素深度,可为 1、4、8、16、24、32                                |
+| `biCompression`   | 1EH      | 4 bytes | 压缩方式:0=RGB,1=8bpp RLE,2=4bpp RLE,3=Bit-fields,4/5=打印机 |
+| `biSizeImage`     | 22H      | 4 bytes | 图像数据大小(BI_RGB 时可设为 0)                                 |
+| `biXPelsPerMeter` | 26H      | 4 bytes | 水平分辨率(像素/米)                                             |
+| `biYPelsPerMeter` | 2AH      | 4 bytes | 垂直分辨率(像素/米)                                             |
+| `biClrUsed`       | 2EH      | 4 bytes | 实际使用的调色板颜色索引数                                        |
+| `biClrImportant`  | 32H      | 4 bytes | 重要颜色索引数,0 表示都重要                                      |
 
 只有压缩方式为 Bit-fields(0x3)时,位图信息头才是 56 字节,否则为 40 字节;多出的 16 字节是 R、G、B、A 四个 32bit 位域掩码。RGB565 的位域掩码是 `0xF800`、`0x07E0`、`0x001F`。
 
@@ -785,10 +800,10 @@ flowchart TD
 
 ### 6.3 核心数据结构
 
-| 结构体 | 说明 |
-| ------ | ---- |
+| 结构体                                | 说明                                       |
+| ------------------------------------- | ------------------------------------------ |
 | `struct jpeg_decompress_struct cinfo` | 解码对象,记录 JPEG 详细信息与解码输出信息 |
-| `struct jpeg_error_mgr jerr` | 错误处理对象 |
+| `struct jpeg_error_mgr jerr`          | 错误处理对象                               |
 
 调用 `jpeg_read_header()` 后可读到的信息:
 
@@ -812,17 +827,17 @@ cinfo.output_scanline     /* 接下来要读取的行索引 */
 
 ### 6.4 API 速查
 
-| 函数 | 作用 |
-| ---- | ---- |
-| `jpeg_std_error(&jerr)` | 绑定 libjpeg 默认错误处理 |
-| `jpeg_create_decompress(&cinfo)` | 创建解码对象 |
-| `jpeg_stdio_src(&cinfo, fp)` | 以标准 I/O 文件流作为数据源 |
-| `jpeg_read_header(&cinfo, TRUE)` | 读取 JPEG 头信息(约定必须调用) |
-| `jpeg_calc_output_dimensions(&cinfo)` | 在 start 前提前计算输出尺寸 |
-| `jpeg_start_decompress(&cinfo)` | 开始解码 |
+| 函数                                  | 作用                                    |
+| ------------------------------------- | --------------------------------------- |
+| `jpeg_std_error(&jerr)`               | 绑定 libjpeg 默认错误处理               |
+| `jpeg_create_decompress(&cinfo)`      | 创建解码对象                            |
+| `jpeg_stdio_src(&cinfo, fp)`          | 以标准 I/O 文件流作为数据源             |
+| `jpeg_read_header(&cinfo, TRUE)`      | 读取 JPEG 头信息(约定必须调用)        |
+| `jpeg_calc_output_dimensions(&cinfo)` | 在 start 前提前计算输出尺寸             |
+| `jpeg_start_decompress(&cinfo)`       | 开始解码                                |
 | `jpeg_read_scanlines(&cinfo, buf, 1)` | 逐行读取解码数据(当前一次只支持 1 行) |
-| `jpeg_finish_decompress(&cinfo)` | 完成解码 |
-| `jpeg_destroy_decompress(&cinfo)` | 销毁解码对象、释放资源 |
+| `jpeg_finish_decompress(&cinfo)`      | 完成解码                                |
+| `jpeg_destroy_decompress(&cinfo)`     | 销毁解码对象、释放资源                  |
 
 ### 6.5 错误处理
 
@@ -1052,10 +1067,10 @@ int main(int argc, char *argv[])
 
 ### 7.2 两个核心数据结构
 
-| 结构体 | 说明 |
-| ------ | ---- |
+| 结构体                                     | 说明                                                                         |
+| ------------------------------------------ | ---------------------------------------------------------------------------- |
 | `png_struct`(句柄 `png_structp png_ptr`) | libpng 内部使用,几乎所有库函数的第一个参数;`png_create_read_struct()` 创建 |
-| `png_info`(句柄 `png_infop info_ptr`) | 描述 PNG 图像信息;新版本通过 `png_get_XXX` / `png_set_XXX` 访问成员 |
+| `png_info`(句柄 `png_infop info_ptr`)    | 描述 PNG 图像信息;新版本通过 `png_get_XXX` / `png_set_XXX` 访问成员         |
 
 ### 7.3 setjmp / longjmp 与错误返回点
 
@@ -1111,28 +1126,28 @@ int main(void)
 
 ### 7.4 high-level 与 low-level 接口
 
-| 方式 | 函数 | 特点 | 适用条件 |
-| ---- | ---- | ---- | -------- |
-| high-level | `png_read_png()` | 一个函数一次性解码全部数据,内部自动分配缓冲区 | 内存足够大、且输出格式限定为 libpng 预定义转换 |
-| low-level | `png_read_info` + `png_set_xxx` + `png_read_update_info` + `png_read_image`/`png_read_rows` | 灵活,需用户分配缓冲区 | 需要自定义转换或逐行处理 |
+| 方式       | 函数                                                                                        | 特点                                           | 适用条件                                       |
+| ---------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------- | ---------------------------------------------- |
+| high-level | `png_read_png()`                                                                            | 一个函数一次性解码全部数据,内部自动分配缓冲区 | 内存足够大、且输出格式限定为 libpng 预定义转换 |
+| low-level  | `png_read_info` + `png_set_xxx` + `png_read_update_info` + `png_read_image`/`png_read_rows` | 灵活,需用户分配缓冲区                         | 需要自定义转换或逐行处理                       |
 
 libpng 预定义数据转换类型(可 `|` 组合,作为 `png_read_png` 第三参数):
 
-| 转换宏 | 说明 |
-| ------ | ---- |
-| `PNG_TRANSFORM_IDENTITY` | No transformation |
-| `PNG_TRANSFORM_STRIP_16` | Strip 16-bit samples to 8 bits |
-| `PNG_TRANSFORM_STRIP_ALPHA` | Discard the alpha channel |
-| `PNG_TRANSFORM_PACKING` | Expand 1, 2 and 4-bit samples to bytes |
-| `PNG_TRANSFORM_PACKSWAP` | Change order of packed pixels to LSB first |
-| `PNG_TRANSFORM_EXPAND` | Perform set_expand() |
-| `PNG_TRANSFORM_INVERT_MONO` | Invert monochrome images |
-| `PNG_TRANSFORM_SHIFT` | Normalize pixels to the sBIT depth |
-| `PNG_TRANSFORM_BGR` | Flip RGB to BGR, RGBA to BGRA |
-| `PNG_TRANSFORM_SWAP_ALPHA` | Flip RGBA to ARGB or GA to AG |
-| `PNG_TRANSFORM_INVERT_ALPHA` | Change alpha from opacity to transparency |
-| `PNG_TRANSFORM_SWAP_ENDIAN` | Byte-swap 16-bit samples |
-| `PNG_TRANSFORM_GRAY_TO_RGB` | Expand grayscale samples to RGB |
+| 转换宏                       | 说明                                       |
+| ---------------------------- | ------------------------------------------ |
+| `PNG_TRANSFORM_IDENTITY`     | No transformation                          |
+| `PNG_TRANSFORM_STRIP_16`     | Strip 16-bit samples to 8 bits             |
+| `PNG_TRANSFORM_STRIP_ALPHA`  | Discard the alpha channel                  |
+| `PNG_TRANSFORM_PACKING`      | Expand 1, 2 and 4-bit samples to bytes     |
+| `PNG_TRANSFORM_PACKSWAP`     | Change order of packed pixels to LSB first |
+| `PNG_TRANSFORM_EXPAND`       | Perform set_expand()                       |
+| `PNG_TRANSFORM_INVERT_MONO`  | Invert monochrome images                   |
+| `PNG_TRANSFORM_SHIFT`        | Normalize pixels to the sBIT depth         |
+| `PNG_TRANSFORM_BGR`          | Flip RGB to BGR, RGBA to BGRA              |
+| `PNG_TRANSFORM_SWAP_ALPHA`   | Flip RGBA to ARGB or GA to AG              |
+| `PNG_TRANSFORM_INVERT_ALPHA` | Change alpha from opacity to transparency  |
+| `PNG_TRANSFORM_SWAP_ENDIAN`  | Byte-swap 16-bit samples                   |
+| `PNG_TRANSFORM_GRAY_TO_RGB`  | Expand grayscale samples to RGB            |
 
 `png_read_png()` 等价于依次执行:`png_read_info` → 按 `transforms` 设置转换 → `png_read_image` → `png_read_end`。
 
@@ -1148,23 +1163,23 @@ libpng 预定义数据转换类型(可 `|` 组合,作为 `png_read_png` 第
 
 ### 7.5 API 速查
 
-| 函数 | 作用 |
-| ---- | ---- |
-| `png_create_read_struct(PNG_LIBPNG_VER_STRING, ...)` | 创建解码 `png_struct`,失败返回 NULL |
-| `png_create_info_struct(png_ptr)` | 创建 `png_info`,失败返回 NULL |
-| `png_jmpbuf(png_ptr)` | 获取错误跳转点 `jmp_buf` |
-| `png_init_io(png_ptr, fp)` | 以标准 I/O 文件流指定数据源 |
-| `png_read_png(png_ptr, info_ptr, transforms, NULL)` | high-level 一次性解码 |
-| `png_read_info` / `png_read_update_info` / `png_read_end` | low-level 读取信息 / 更新信息 / 结束 |
-| `png_read_image(png_ptr, row_pointers)` | low-level 一次性读全部数据 |
-| `png_read_rows(png_ptr, &row_buf, NULL, 1)` | 每次读 1 行或多行 |
-| `png_get_image_width` / `png_get_image_height` | 获取宽 / 高 |
-| `png_get_bit_depth` / `png_get_color_type` | 获取位深 / 颜色类型 |
-| `png_get_rows(png_ptr, info_ptr)` | 获取指向每一行数据缓冲区的指针数组(high-level 内部缓冲区) |
-| `png_get_rowbytes` | 每行数据字节数 |
-| `png_set_strip_16` / `png_set_expand` / `png_set_gray_to_rgb` | 16→8 位 / 低位深扩展 / 灰度转 RGB |
-| `png_malloc(png_ptr, size)` | libpng 提供的内存分配(等价 malloc) |
-| `png_destroy_read_struct(&png_ptr, &info_ptr, NULL)` | 销毁 `png_struct`(及其关联资源) |
+| 函数                                                          | 作用                                                        |
+| ------------------------------------------------------------- | ----------------------------------------------------------- |
+| `png_create_read_struct(PNG_LIBPNG_VER_STRING, ...)`          | 创建解码 `png_struct`,失败返回 NULL                        |
+| `png_create_info_struct(png_ptr)`                             | 创建 `png_info`,失败返回 NULL                              |
+| `png_jmpbuf(png_ptr)`                                         | 获取错误跳转点 `jmp_buf`                                    |
+| `png_init_io(png_ptr, fp)`                                    | 以标准 I/O 文件流指定数据源                                 |
+| `png_read_png(png_ptr, info_ptr, transforms, NULL)`           | high-level 一次性解码                                       |
+| `png_read_info` / `png_read_update_info` / `png_read_end`     | low-level 读取信息 / 更新信息 / 结束                        |
+| `png_read_image(png_ptr, row_pointers)`                       | low-level 一次性读全部数据                                  |
+| `png_read_rows(png_ptr, &row_buf, NULL, 1)`                   | 每次读 1 行或多行                                           |
+| `png_get_image_width` / `png_get_image_height`                | 获取宽 / 高                                                 |
+| `png_get_bit_depth` / `png_get_color_type`                    | 获取位深 / 颜色类型                                         |
+| `png_get_rows(png_ptr, info_ptr)`                             | 获取指向每一行数据缓冲区的指针数组(high-level 内部缓冲区) |
+| `png_get_rowbytes`                                            | 每行数据字节数                                              |
+| `png_set_strip_16` / `png_set_expand` / `png_set_gray_to_rgb` | 16→8 位 / 低位深扩展 / 灰度转 RGB                           |
+| `png_malloc(png_ptr, size)`                                   | libpng 提供的内存分配(等价 malloc)                        |
+| `png_destroy_read_struct(&png_ptr, &info_ptr, NULL)`          | 销毁 `png_struct`(及其关联资源)                           |
 
 ### 7.6 libpng 应用编程完整源码
 
@@ -1368,15 +1383,15 @@ rm -rf /lib/libpng* /usr/lib/libpng*
 
 ### 7.8 libpng 与 libjpeg 对比
 
-| 维度 | libjpeg | libpng |
-| ---- | ------- | ------ |
-| 压缩 | 有损 | 无损(LZ77 派生) |
-| 依赖 | 无 | 依赖 zlib |
-| 主要对象 | `jpeg_decompress_struct` + `jpeg_error_mgr` | `png_struct` + `png_info` |
-| 读取方式 | 逐行 `jpeg_read_scanlines` | high-level 一次性 `png_read_png` 或 low-level 逐行 |
-| 输出格式 | 默认 BGR888 | 由转换参数决定,`STRIP_ALPHA` 后为 RGB888 |
-| 错误处理 | `jpeg_std_error` 默认 `exit`,可自定义 `error_exit` | 默认 `longjmp`,用 `setjmp(png_jmpbuf())` 设返回点 |
-| 结束 | `jpeg_finish_decompress` + `jpeg_destroy_decompress` | `png_destroy_read_struct` |
+| 维度     | libjpeg                                              | libpng                                             |
+| -------- | ---------------------------------------------------- | -------------------------------------------------- |
+| 压缩     | 有损                                                 | 无损(LZ77 派生)                                  |
+| 依赖     | 无                                                   | 依赖 zlib                                          |
+| 主要对象 | `jpeg_decompress_struct` + `jpeg_error_mgr`          | `png_struct` + `png_info`                          |
+| 读取方式 | 逐行 `jpeg_read_scanlines`                           | high-level 一次性 `png_read_png` 或 low-level 逐行 |
+| 输出格式 | 默认 BGR888                                          | 由转换参数决定,`STRIP_ALPHA` 后为 RGB888          |
+| 错误处理 | `jpeg_std_error` 默认 `exit`,可自定义 `error_exit`  | 默认 `longjmp`,用 `setjmp(png_jmpbuf())` 设返回点 |
+| 结束     | `jpeg_finish_decompress` + `jpeg_destroy_decompress` | `png_destroy_read_struct`                          |
 
 ---
 
@@ -1640,10 +1655,10 @@ int main(int argc, char *argv[])
 
 ### 9.1 两种方式
 
-| 方式 | 原理 | 优点 | 缺点 |
-| ---- | ---- | ---- | ---- |
-| 取模显示 | 用取模软件生成字符点阵二维数组,逐 bit 打点 | 简单、无需库 | 只能显示固定几个字符,很“low” |
-| 字体引擎 | 解析 `.ttf`/`.otf`/`.ttc` 等字体文件,读取字形位图 | 通用、支持任意字符与字号 | 需移植 FreeType 等库 |
+| 方式     | 原理                                               | 优点                     | 缺点                          |
+| -------- | -------------------------------------------------- | ------------------------ | ----------------------------- |
+| 取模显示 | 用取模软件生成字符点阵二维数组,逐 bit 打点        | 简单、无需库             | 只能显示固定几个字符,很“low” |
+| 字体引擎 | 解析 `.ttf`/`.otf`/`.ttc` 等字体文件,读取字形位图 | 通用、支持任意字符与字号 | 需移植 FreeType 等库          |
 
 取模原理:字符点阵中每个小方块对应一个 bit,填充用 1、不填充用 0。例如“正”是 64×86 点阵,用 `unsigned char arr[86][8]` 存储(宽 64 点 = 8 字节/行)。取模时选择“从左到右、从上到下”。
 
@@ -1695,16 +1710,16 @@ rm -rf /usr/lib/libfreetype.*
 
 字形布局参数(水平布局为例):
 
-| 参数 | 含义 |
-| ---- | ---- |
-| `origin` / 基准线 | 水平基线与垂直基线用于定位与对齐字形 |
-| `width` / `height` | 字形轮廓最左到最右、最上到最下的距离 |
-| `bearingX`(`bitmap_left`) | 垂直基线到字形轮廓最左边的距离;水平布局为正 |
-| `bearingY`(`bitmap_top`) | 水平基线到字形轮廓最上边的距离;轮廓在基线上方为正 |
-| `xMin/xMax`、`yMin/yMax` | 字形轮廓四边位置,构成边界框(bbox) |
-| `advance`(`advance.x`) | 步进宽度/字间距,相邻两原点距离;26.6 固定浮点格式 |
-| `bitmap.buffer` | 字形位图,每点 1 字节;值为 0 不填充,大于 0 填充 |
-| `bitmap.rows` / `bitmap.width` | 位图行数 / 列数 |
+| 参数                           | 含义                                               |
+| ------------------------------ | -------------------------------------------------- |
+| `origin` / 基准线              | 水平基线与垂直基线用于定位与对齐字形               |
+| `width` / `height`             | 字形轮廓最左到最右、最上到最下的距离               |
+| `bearingX`(`bitmap_left`)    | 垂直基线到字形轮廓最左边的距离;水平布局为正       |
+| `bearingY`(`bitmap_top`)     | 水平基线到字形轮廓最上边的距离;轮廓在基线上方为正 |
+| `xMin/xMax`、`yMin/yMax`       | 字形轮廓四边位置,构成边界框(bbox)               |
+| `advance`(`advance.x`)       | 步进宽度/字间距,相邻两原点距离;26.6 固定浮点格式 |
+| `bitmap.buffer`                | 字形位图,每点 1 字节;值为 0 不填充,大于 0 填充  |
+| `bitmap.rows` / `bitmap.width` | 位图行数 / 列数                                    |
 
 对齐:水平基线负责上下对齐,垂直基线负责左右对齐。画字形先定位左上角:若原点为 `(100,100)`,则左上角为 `(100+bearingX, 100-bearingY)`。
 
@@ -1712,18 +1727,18 @@ rm -rf /usr/lib/libfreetype.*
 
 ### 9.4 API 速查
 
-| 函数 | 作用 |
-| ---- | ---- |
-| `FT_Init_FreeType(&library)` | 初始化 FreeType 库对象,成功返回 0 |
-| `FT_New_Face(library, path, face_index, &face)` | 加载字体文件,创建 face;`face_index` 通常为 0,`face->num_faces` 指示文件内 face 数 |
-| `FT_Set_Pixel_Sizes(face, w, h)` | 以像素为单位设置字体宽高;某一位为 0 则等于另一位 |
-| `FT_Set_Char_Size(face, w, h, hdpi, vdpi)` | 以 1/64 点为单位的宽高 + dpi 设置字号 |
-| `FT_Set_Transform(face, &matrix, &pen)` | 设置 2×2 变换矩阵与原点,实现旋转/斜体 |
-| `FT_Get_Char_Index(face, code)` | 字符编码转字形索引 |
-| `FT_Load_Char(face, char, flags)` | 加载字符字形;`FT_LOAD_RENDER` 直接渲染为位图 |
-| `FT_Load_Glyph(face, index, flags)` | 按字形索引加载 |
-| `FT_Render_Glyph(slot, mode)` | 把已加载的字形轮廓渲染为位图 |
-| `FT_Done_Face(face)` / `FT_Done_FreeType(library)` | 释放 face / 库 |
+| 函数                                               | 作用                                                                                 |
+| -------------------------------------------------- | ------------------------------------------------------------------------------------ |
+| `FT_Init_FreeType(&library)`                       | 初始化 FreeType 库对象,成功返回 0                                                   |
+| `FT_New_Face(library, path, face_index, &face)`    | 加载字体文件,创建 face;`face_index` 通常为 0,`face->num_faces` 指示文件内 face 数 |
+| `FT_Set_Pixel_Sizes(face, w, h)`                   | 以像素为单位设置字体宽高;某一位为 0 则等于另一位                                    |
+| `FT_Set_Char_Size(face, w, h, hdpi, vdpi)`         | 以 1/64 点为单位的宽高 + dpi 设置字号                                                |
+| `FT_Set_Transform(face, &matrix, &pen)`            | 设置 2×2 变换矩阵与原点,实现旋转/斜体                                               |
+| `FT_Get_Char_Index(face, code)`                    | 字符编码转字形索引                                                                   |
+| `FT_Load_Char(face, char, flags)`                  | 加载字符字形;`FT_LOAD_RENDER` 直接渲染为位图                                        |
+| `FT_Load_Glyph(face, index, flags)`                | 按字形索引加载                                                                       |
+| `FT_Render_Glyph(slot, mode)`                      | 把已加载的字形轮廓渲染为位图                                                         |
+| `FT_Done_Face(face)` / `FT_Done_FreeType(library)` | 释放 face / 库                                                                       |
 
 ### 9.5 FreeType 显示字符完整源码
 
@@ -2034,12 +2049,12 @@ export CC=arm-poky-linux-gnueabi-gcc     # 或用环境初始化脚本导出的
 source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
 ```
 
-| 例程 | 依赖库 | 交叉编译命令 |
-| ---- | ------ | ------------ |
-| `lcd_info.c` / `lcd_test.c` / `bmp_show.c` / `lcd_vertical_display.c` | 无 | `${CC} -o testApp xxx.c` |
-| `show_jpeg_image.c` | libjpeg | `${CC} -o testApp show_jpeg_image.c -I /home/dt/tools/jpeg/include -L /home/dt/tools/jpeg/lib -ljpeg` |
-| `show_png_image.c` | libpng + zlib | `${CC} -o testApp show_png_image.c -I/home/dt/tools/png/include -L/home/dt/tools/png/lib -L/home/dt/tools/zlib/lib -lpng -lz` |
-| `freetype_test.c` | freetype + zlib + libpng + libm | `${CC} -o testApp freetype_test.c -I/home/dt/tools/freetype/include/freetype2 -L/home/dt/tools/freetype/lib -lfreetype -L/home/dt/tools/zlib/lib -lz -L/home/dt/tools/png/lib -lpng -lm` |
+| 例程                                                                  | 依赖库                          | 交叉编译命令                                                                                                                                                                             |
+| --------------------------------------------------------------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `lcd_info.c` / `lcd_test.c` / `bmp_show.c` / `lcd_vertical_display.c` | 无                              | `${CC} -o testApp xxx.c`                                                                                                                                                                 |
+| `show_jpeg_image.c`                                                   | libjpeg                         | `${CC} -o testApp show_jpeg_image.c -I /home/dt/tools/jpeg/include -L /home/dt/tools/jpeg/lib -ljpeg`                                                                                    |
+| `show_png_image.c`                                                    | libpng + zlib                   | `${CC} -o testApp show_png_image.c -I/home/dt/tools/png/include -L/home/dt/tools/png/lib -L/home/dt/tools/zlib/lib -lpng -lz`                                                            |
+| `freetype_test.c`                                                     | freetype + zlib + libpng + libm | `${CC} -o testApp freetype_test.c -I/home/dt/tools/freetype/include/freetype2 -L/home/dt/tools/freetype/lib -lfreetype -L/home/dt/tools/zlib/lib -lz -L/home/dt/tools/png/lib -lpng -lm` |
 
 编译选项说明:
 
@@ -2073,18 +2088,18 @@ source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linu
 
 ### 11.2 常见问题与调试
 
-| 现象 | 可能原因 | 处理 |
-| ---- | -------- | ---- |
-| `open error` | 设备节点不存在 / 权限不足 | 确认 `/dev/fb0` 存在;用 root 或加权限 |
-| `mmap error` | `screen_size` 计算错误或参数非法 | 检查 `line_length * yres`;先跑 `lcd_info` 确认参数 |
-| 花屏 / 颜色错乱 | 把 RGB565 当 RGB888(或反之) | 用 `lcd_info` 打印像素格式,按实际位域转换 |
-| 图片上下颠倒 | BMP 正/倒向位图处理错 | 按 `biHeight` 正负分别处理 |
-| 竖屏画面方向不对 | 坐标变换公式与预期布局不一致 | 确认原点定义(左下角/左上角),套用对应公式 |
-| 屏幕内容被 Qt 覆盖 | 出厂 Qt GUI 未退出 | 先退出 Qt 程序再运行 |
-| 运行报缺库 | 目标板 `/usr/lib` 无对应 `.so` | 拷贝库文件并保持符号链接 |
-| FreeType 显示乱码 | 字体文件与程序不匹配 | 换一个 `.ttf/.ttc` 字体(如宋体) |
-| 删除旧库后 Qt 图片空白 | 出厂 Qt 依赖旧版 libjpeg/libpng 等 | 属预期现象,学习阶段知道即可 |
-| 编译报缺 `-lz/-lpng` | 未链接依赖库 | 按第 10 节补全 `-L`/`-l` |
+| 现象                   | 可能原因                           | 处理                                                |
+| ---------------------- | ---------------------------------- | --------------------------------------------------- |
+| `open error`           | 设备节点不存在 / 权限不足          | 确认 `/dev/fb0` 存在;用 root 或加权限              |
+| `mmap error`           | `screen_size` 计算错误或参数非法   | 检查 `line_length * yres`;先跑 `lcd_info` 确认参数 |
+| 花屏 / 颜色错乱        | 把 RGB565 当 RGB888(或反之)      | 用 `lcd_info` 打印像素格式,按实际位域转换          |
+| 图片上下颠倒           | BMP 正/倒向位图处理错              | 按 `biHeight` 正负分别处理                          |
+| 竖屏画面方向不对       | 坐标变换公式与预期布局不一致       | 确认原点定义(左下角/左上角),套用对应公式         |
+| 屏幕内容被 Qt 覆盖     | 出厂 Qt GUI 未退出                 | 先退出 Qt 程序再运行                                |
+| 运行报缺库             | 目标板 `/usr/lib` 无对应 `.so`     | 拷贝库文件并保持符号链接                            |
+| FreeType 显示乱码      | 字体文件与程序不匹配               | 换一个 `.ttf/.ttc` 字体(如宋体)                   |
+| 删除旧库后 Qt 图片空白 | 出厂 Qt 依赖旧版 libjpeg/libpng 等 | 属预期现象,学习阶段知道即可                        |
+| 编译报缺 `-lz/-lpng`   | 未链接依赖库                       | 按第 10 节补全 `-L`/`-l`                            |
 
 **常用辅助命令:**
 
@@ -2098,13 +2113,13 @@ cat /proc/fb                                       # 查看已注册的 fb 设
 
 ## 12. 跨平台对比
 
-| 平台 / 场景 | 显示抽象 | 上屏方式 | 特点 |
-| ----------- | -------- | -------- | ---- |
-| 嵌入式 Linux(FrameBuffer) | `/dev/fb0` + 显存 | `mmap` 后直接写显存 | 接口简单、稳定,本书主线 |
-| 嵌入式 Linux(DRM/KMS) | `/dev/dri/card0` + plane | `drmMode*` 提交 framebuffer | 现代方案,支持多图层/硬件合成 |
-| Windows | GDI / DIB | 位图 API、绘图函数 | 图形子系统封装,不直接暴露显存 |
-| 裸机 MCU(如 STM32 + 并口屏) | LCD 控制器寄存器 + FSMC/SDRAM | 写帧缓存地址,靠 DMA/控制器扫描 | 无操作系统,需自己配时序 |
-| Android | SurfaceFlinger + Gralloc | Surface/Canvas,合成后送显示 | 多层合成,应用不碰显存 |
+| 平台 / 场景                   | 显示抽象                      | 上屏方式                        | 特点                           |
+| ----------------------------- | ----------------------------- | ------------------------------- | ------------------------------ |
+| 嵌入式 Linux(FrameBuffer)   | `/dev/fb0` + 显存             | `mmap` 后直接写显存             | 接口简单、稳定,本书主线       |
+| 嵌入式 Linux(DRM/KMS)       | `/dev/dri/card0` + plane      | `drmMode*` 提交 framebuffer     | 现代方案,支持多图层/硬件合成  |
+| Windows                       | GDI / DIB                     | 位图 API、绘图函数              | 图形子系统封装,不直接暴露显存 |
+| 裸机 MCU(如 STM32 + 并口屏) | LCD 控制器寄存器 + FSMC/SDRAM | 写帧缓存地址,靠 DMA/控制器扫描 | 无操作系统,需自己配时序       |
+| Android                       | SurfaceFlinger + Gralloc      | Surface/Canvas,合成后送显示    | 多层合成,应用不碰显存         |
 
 > ⚠️ **来源说明**:DRM/KMS、Android SurfaceFlinger、Windows GDI 的对比属于教材之外的扩展知识,用于建立全局认知;教材只讲解 Linux FrameBuffer。
 
@@ -2150,4 +2165,3 @@ libpng 默认错误处理会调用 `longjmp()` 跳转,需要用 `setjmp(png_jm
   - 第二十三章 在 LCD 上显示字符
 - 例程源码:`11、Linux C 应用编程例程源码` → `19_lcd`、`20_libjpeg`、`21_libpng`、`22_lcd_vertical_display`、`23_freetype`
 - 开发板:正点原子 ALPHA / Mini I.MX6U(7 寸 800×480 RGB LCD,出厂系统 RGB565)
-

+ 63 - 63
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/05-PWM与看门狗.md

@@ -20,12 +20,12 @@ pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第二十四章 PWM
 
 PWM(Pulse Width Modulation,脉冲宽度调制)是一种通过**数字信号占空比**来模拟不同输出的技术。一个 PWM 波形由三个要素描述:
 
-| 要素 | 含义 | 单位 |
-| ---- | ---- | ---- |
-| 周期 `period` | 一个完整波形的时间长度 | ns(纳秒) |
-| 频率 `f` | 周期的倒数,`f = 1 / period` | Hz |
-| 占空比 `duty_cycle` | 一个周期内高电平的时间长度 | ns(纳秒) |
-| 极性 `polarity` | 高电平有效(normal)或低电平有效(inversed) | — |
+| 要素                | 含义                                         | 单位       |
+| ------------------- | -------------------------------------------- | ---------- |
+| 周期 `period`       | 一个完整波形的时间长度                       | ns(纳秒) |
+| 频率 `f`            | 周期的倒数,`f = 1 / period`                 | Hz         |
+| 占空比 `duty_cycle` | 一个周期内高电平的时间长度                   | ns(纳秒) |
+| 极性 `polarity`     | 高电平有效(normal)或低电平有效(inversed) | —          |
 
 占空比比例 = `duty_cycle / period`。例如周期 `500000 ns = 500 us`,对应频率 `1 / 500us = 2 kHz`;占空比 `250000 ns` 占周期一半,即 50%。
 
@@ -53,10 +53,10 @@ ls /sys/class/pwm
 
 进入某个控制器目录(如 `pwmchip0`),重点关注 `npwm`、`export`、`unexport`:
 
-| 属性文件 | 读写 | 说明 |
-| -------- | ---- | ---- |
-| `npwm` | 只读 | 该 PWM 控制器下共有几路 PWM 输出 |
-| `export` | 只写 | 导出 PWM,写入编号(从 0 开始) |
+| 属性文件   | 读写 | 说明                                     |
+| ---------- | ---- | ---------------------------------------- |
+| `npwm`     | 只读 | 该 PWM 控制器下共有几路 PWM 输出         |
+| `export`   | 只写 | 导出 PWM,写入编号(从 0 开始)          |
 | `unexport` | 只写 | 删除已导出的 PWM,写入编号与 export 对应 |
 
 I.MX6U 每个 PWM 控制器只有 1 路输出,因此 `npwm` 读出来是 1,导出时编号都用 0。
@@ -78,12 +78,12 @@ ls /sys/class/pwm/pwmchip0/pwm0
 # duty_cycle  enable  period  polarity
 ```
 
-| 属性文件 | 读写 | 说明 |
-| -------- | ---- | ---- |
-| `enable` | 可读写 | 写 `1` 使能 PWM 输出,写 `0` 禁止;读可获取当前状态 |
-| `polarity` | 可读写 | `normal` 普通极性、`inversed` 反转极性 |
-| `period` | 可读写 | 配置 PWM 周期,单位 ns |
-| `duty_cycle` | 可读写 | 配置 PWM 占空比,单位 ns |
+| 属性文件     | 读写   | 说明                                                |
+| ------------ | ------ | --------------------------------------------------- |
+| `enable`     | 可读写 | 写 `1` 使能 PWM 输出,写 `0` 禁止;读可获取当前状态 |
+| `polarity`   | 可读写 | `normal` 普通极性、`inversed` 反转极性              |
+| `period`     | 可读写 | 配置 PWM 周期,单位 ns                              |
+| `duty_cycle` | 可读写 | 配置 PWM 占空比,单位 ns                            |
 
 ```bash
 echo 0 > enable           # 禁止 PWM 输出
@@ -191,16 +191,16 @@ int main(int argc, char *argv[])
 
 逐段解释:
 
-| 位置 | 代码 | 说明 |
-| ---- | ---- | ---- |
-| `pwm_config()` | 通用属性写函数 | 拼接 `pwm_path/attr` 路径,以只写方式打开并写入值,是操作 sysfs 属性的标准写法 |
-| 传参校验 | `4 != argc` | 需传入 `id`、`period`、`duty` 三个参数 |
-| `sprintf(pwm_path, ...)` | 构造 `pwm0` 路径 | 如 `id=0` 时为 `/sys/class/pwm/pwmchip0/pwm0` |
-| `access(pwm_path, F_OK)` | 判断是否已导出 | 目录不存在说明未导出,需要写 `export` |
-| 写 `export` | `write(fd, "0", 1)` | 导出编号 0 的 PWM |
-| `pwm_config("period", ...)` | 配置周期 | 单位 ns |
-| `pwm_config("duty_cycle", ...)` | 配置占空比 | 单位 ns |
-| `pwm_config("enable", "1")` | 使能输出 | 配置完成后最后一步 |
+| 位置                            | 代码                | 说明                                                                           |
+| ------------------------------- | ------------------- | ------------------------------------------------------------------------------ |
+| `pwm_config()`                  | 通用属性写函数      | 拼接 `pwm_path/attr` 路径,以只写方式打开并写入值,是操作 sysfs 属性的标准写法 |
+| 传参校验                        | `4 != argc`         | 需传入 `id`、`period`、`duty` 三个参数                                         |
+| `sprintf(pwm_path, ...)`        | 构造 `pwm0` 路径    | 如 `id=0` 时为 `/sys/class/pwm/pwmchip0/pwm0`                                  |
+| `access(pwm_path, F_OK)`        | 判断是否已导出      | 目录不存在说明未导出,需要写 `export`                                          |
+| 写 `export`                     | `write(fd, "0", 1)` | 导出编号 0 的 PWM                                                              |
+| `pwm_config("period", ...)`     | 配置周期            | 单位 ns                                                                        |
+| `pwm_config("duty_cycle", ...)` | 配置占空比          | 单位 ns                                                                        |
+| `pwm_config("enable", "1")`     | 使能输出            | 配置完成后最后一步                                                             |
 
 编译:
 
@@ -250,13 +250,13 @@ setenv bootcmd 'nand read ${loadaddr} 0x620000 0x800000;nand read ${fdt_addr} ${
 
 ### 4.3 调试方法
 
-| 现象 | 可能原因 | 排查手段 |
-| ---- | -------- | -------- |
-| `open error: No such file or directory` | 没进 u-boot 禁用 LCD/backlight,或路径/编号写错 | 确认 `ls /sys/class/pwm/pwmchip0/pwm0` |
-| 写属性报错 | 未先导出、或写入格式不对 | 先 `echo 0 > export`,写入的值不带单位 |
-| 引脚无波形 | PWM 未使能 / 被其它功能占用 | `cat enable` 应为 1;确认 PWM1 已腾出 |
-| 占空比大于周期 | 参数不合法 | `duty_cycle` 必须不超过 `period` |
-| 无 `pwm0` 目录 | 未导出 | 程序会自动导出,也可手动 `echo 0 > export` |
+| 现象                                    | 可能原因                                        | 排查手段                                   |
+| --------------------------------------- | ----------------------------------------------- | ------------------------------------------ |
+| `open error: No such file or directory` | 没进 u-boot 禁用 LCD/backlight,或路径/编号写错 | 确认 `ls /sys/class/pwm/pwmchip0/pwm0`     |
+| 写属性报错                              | 未先导出、或写入格式不对                        | 先 `echo 0 > export`,写入的值不带单位     |
+| 引脚无波形                              | PWM 未使能 / 被其它功能占用                     | `cat enable` 应为 1;确认 PWM1 已腾出      |
+| 占空比大于周期                          | 参数不合法                                      | `duty_cycle` 必须不超过 `period`           |
+| 无 `pwm0` 目录                          | 未导出                                          | 程序会自动导出,也可手动 `echo 0 > export` |
 
 ### 4.4 PWM 蜂鸣器/背光实验
 
@@ -336,13 +336,13 @@ ls /dev/watchdog*
 
 常用指令:
 
-| ioctl 指令 | 作用 |
-| ---------- | ---- |
+| ioctl 指令         | 作用                   |
+| ------------------ | ---------------------- |
 | `WDIOC_GETSUPPORT` | 获取看门狗支持哪些功能 |
-| `WDIOC_SETOPTIONS` | 开启或关闭看门狗 |
-| `WDIOC_KEEPALIVE` | 喂狗操作 |
-| `WDIOC_SETTIMEOUT` | 设置看门狗超时时间 |
-| `WDIOC_GETTIMEOUT` | 获取看门狗超时时间 |
+| `WDIOC_SETOPTIONS` | 开启或关闭看门狗       |
+| `WDIOC_KEEPALIVE`  | 喂狗操作               |
+| `WDIOC_SETTIMEOUT` | 设置看门狗超时时间     |
+| `WDIOC_GETTIMEOUT` | 获取看门狗超时时间     |
 
 ### 7.1 打开设备
 
@@ -558,16 +558,16 @@ int main(int argc, char *argv[])
 
 逐段解释:
 
-| 位置 | 代码 | 说明 |
-| ---- | ---- | ---- |
-| 参数校验 | `2 != argc` | 需传入看门狗超时时间(秒) |
-| `open(WDOG_DEV, O_RDWR)` | 打开看门狗 | 以读写方式打开 `/dev/watchdog` |
-| `WDIOC_SETOPTIONS` + `WDIOS_DISABLECARD` | 先停止计时 | 因为打开设备后计时器会开启 |
-| `atoi(argv[1])` | 解析超时时间 | 小于 1 秒则强制为 1 |
-| `WDIOC_SETTIMEOUT` | 设置超时 | 单位秒,不能超过最大值 |
-| `WDIOC_SETOPTIONS` + `WDIOS_ENABLECARD` | 开启计时 | 开始看门狗工作 |
-| `time = (timeout*1000 - 100)*1000` | 计算喂狗间隔 | `usleep` 以微秒为单位,在超时前 100ms 喂狗 |
-| `WDIOC_KEEPALIVE` | 循环喂狗 | 不断重置计时器,避免系统复位 |
+| 位置                                     | 代码         | 说明                                       |
+| ---------------------------------------- | ------------ | ------------------------------------------ |
+| 参数校验                                 | `2 != argc`  | 需传入看门狗超时时间(秒)                 |
+| `open(WDOG_DEV, O_RDWR)`                 | 打开看门狗   | 以读写方式打开 `/dev/watchdog`             |
+| `WDIOC_SETOPTIONS` + `WDIOS_DISABLECARD` | 先停止计时   | 因为打开设备后计时器会开启                 |
+| `atoi(argv[1])`                          | 解析超时时间 | 小于 1 秒则强制为 1                        |
+| `WDIOC_SETTIMEOUT`                       | 设置超时     | 单位秒,不能超过最大值                     |
+| `WDIOC_SETOPTIONS` + `WDIOS_ENABLECARD`  | 开启计时     | 开始看门狗工作                             |
+| `time = (timeout*1000 - 100)*1000`       | 计算喂狗间隔 | `usleep` 以微秒为单位,在超时前 100ms 喂狗 |
+| `WDIOC_KEEPALIVE`                        | 循环喂狗     | 不断重置计时器,避免系统复位               |
 
 编译:
 
@@ -635,13 +635,13 @@ watchdog watchdog0: watchdog did not stop!     <- 内核打印:计时器仍在
 
 ### 9.3 调试方法
 
-| 现象 | 可能原因 | 排查手段 |
-| ---- | -------- | -------- |
-| `Device or resource busy` | 看门狗已被系统占用 | 在 `/etc/init.d/watchdog.sh` 开头加 `exit 0` 后重启 |
-| 设置超时失败 | 超过设备支持的最大值 | 用 `WDIOC_GETTIMEOUT` 读取,或改小参数 |
-| 程序退出后系统立即复位 | `close` 会自动启动看门狗,且退出后无人喂狗 | 正常现象,用于验证;测试完恢复 `watchdog.sh` |
-| 设备不支持喂狗/设置超时 | 硬件/驱动不支持 | 用 `WDIOC_GETSUPPORT` 检查 `options` 位 |
-| `open` 失败无权限 | 非 root 运行 | 用 `root` 执行 |
+| 现象                      | 可能原因                                   | 排查手段                                            |
+| ------------------------- | ------------------------------------------ | --------------------------------------------------- |
+| `Device or resource busy` | 看门狗已被系统占用                         | 在 `/etc/init.d/watchdog.sh` 开头加 `exit 0` 后重启 |
+| 设置超时失败              | 超过设备支持的最大值                       | 用 `WDIOC_GETTIMEOUT` 读取,或改小参数              |
+| 程序退出后系统立即复位    | `close` 会自动启动看门狗,且退出后无人喂狗 | 正常现象,用于验证;测试完恢复 `watchdog.sh`        |
+| 设备不支持喂狗/设置超时   | 硬件/驱动不支持                            | 用 `WDIOC_GETSUPPORT` 检查 `options` 位             |
+| `open` 失败无权限         | 非 root 运行                               | 用 `root` 执行                                      |
 
 ---
 
@@ -649,13 +649,13 @@ watchdog watchdog0: watchdog did not stop!     <- 内核打印:计时器仍在
 
 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
 
-| 维度 | I.MX6ULL(本教程) | STM32(裸机/RTOS) | RK3568 |
-| ---- | ------------------ | ------------------ | ------ |
-| PWM 接口 | sysfs `/sys/class/pwm/pwmchipN` | 定时器寄存器 / HAL_TIM_PWM | sysfs,接口一致 |
-| PWM 配置 | 写 `period`/`duty_cycle`/`enable`(ns) | 配置 ARR/PSC/CCR 寄存器 | 写 `period`/`duty_cycle`/`enable`(ns) |
-| 看门狗接口 | `/dev/watchdog` + `ioctl` | IWDG/WWDG 寄存器直接喂狗 | `/dev/watchdog` + `ioctl`,接口一致 |
-| 看门狗喂狗 | `WDIOC_KEEPALIVE` | 写 IWDG_KR 重装载键 | `WDIOC_KEEPALIVE` |
-| 交叉工具链 | `arm-linux-gnueabihf-gcc` | `arm-none-eabi-gcc` | `aarch64-linux-gnu-gcc` |
+| 维度       | I.MX6ULL(本教程)                      | STM32(裸机/RTOS)         | RK3568                                  |
+| ---------- | --------------------------------------- | -------------------------- | --------------------------------------- |
+| PWM 接口   | sysfs `/sys/class/pwm/pwmchipN`         | 定时器寄存器 / HAL_TIM_PWM | sysfs,接口一致                         |
+| PWM 配置   | 写 `period`/`duty_cycle`/`enable`(ns) | 配置 ARR/PSC/CCR 寄存器    | 写 `period`/`duty_cycle`/`enable`(ns) |
+| 看门狗接口 | `/dev/watchdog` + `ioctl`               | IWDG/WWDG 寄存器直接喂狗   | `/dev/watchdog` + `ioctl`,接口一致     |
+| 看门狗喂狗 | `WDIOC_KEEPALIVE`                       | 写 IWDG_KR 重装载键        | `WDIOC_KEEPALIVE`                       |
+| 交叉工具链 | `arm-linux-gnueabihf-gcc`               | `arm-none-eabi-gcc`        | `aarch64-linux-gnu-gcc`                 |
 
 I.MX6ULL 与 RK3568 的 sysfs PWM 与 ioctl 看门狗用法基本通用;STM32 裸机没有 sysfs 与设备节点,需直接操作寄存器。
 

+ 268 - 247
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/06-摄像头串口与音频.md

@@ -1,6 +1,22 @@
 ---
 title: 摄像头、串口与音频应用编程
-tags: [嵌入式Linux, Linux应用编程, V4L2, 摄像头, 视频采集, 串口, UART, termios, ALSA, alsa-lib, PCM, WAV, 音频, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    Linux应用编程,
+    V4L2,
+    摄像头,
+    视频采集,
+    串口,
+    UART,
+    termios,
+    ALSA,
+    alsa-lib,
+    PCM,
+    WAV,
+    音频,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第二十五章 V4L2摄像头应用编程、第二十六章 串口应用编程、第二十八章 音频应用编程"
@@ -53,23 +69,23 @@ flowchart TB
 
 所有指令定义在头文件 `<linux/videodev2.h>` 中,以宏 `VIDIOC_XXX` 形式提供,每个宏还携带一个 `struct` 类型——即调用 `ioctl()` 时第三个参数的类型,传入该类型变量的指针。
 
-| V4L2 指令 | 描述 |
-| --------- | ---- |
-| `VIDIOC_QUERYCAP` | 查询设备的属性/能力/功能 |
-| `VIDIOC_ENUM_FMT` | 枚举设备支持的像素格式 |
-| `VIDIOC_G_FMT` | 获取设备当前的帧格式信息 |
-| `VIDIOC_S_FMT` | 设置帧格式信息 |
-| `VIDIOC_REQBUFS` | 申请帧缓冲 |
-| `VIDIOC_QUERYBUF` | 查询帧缓冲 |
-| `VIDIOC_QBUF` | 帧缓冲入队操作 |
-| `VIDIOC_DQBUF` | 帧缓冲出队操作 |
-| `VIDIOC_STREAMON` | 开启视频采集 |
-| `VIDIOC_STREAMOFF` | 关闭视频采集 |
-| `VIDIOC_G_PARM` | 获取设备的流类型参数 |
-| `VIDIOC_S_PARM` | 设置流类型参数(帧率) |
-| `VIDIOC_TRY_FMT` | 尝试设置帧格式,用于判断设备是否支持该格式 |
-| `VIDIOC_ENUM_FRAMESIZES` | 枚举设备支持的视频采集分辨率 |
-| `VIDIOC_ENUM_FRAMEINTERVALS` | 枚举设备支持的视频采集帧率 |
+| V4L2 指令                    | 描述                                       |
+| ---------------------------- | ------------------------------------------ |
+| `VIDIOC_QUERYCAP`            | 查询设备的属性/能力/功能                   |
+| `VIDIOC_ENUM_FMT`            | 枚举设备支持的像素格式                     |
+| `VIDIOC_G_FMT`               | 获取设备当前的帧格式信息                   |
+| `VIDIOC_S_FMT`               | 设置帧格式信息                             |
+| `VIDIOC_REQBUFS`             | 申请帧缓冲                                 |
+| `VIDIOC_QUERYBUF`            | 查询帧缓冲                                 |
+| `VIDIOC_QBUF`                | 帧缓冲入队操作                             |
+| `VIDIOC_DQBUF`               | 帧缓冲出队操作                             |
+| `VIDIOC_STREAMON`            | 开启视频采集                               |
+| `VIDIOC_STREAMOFF`           | 关闭视频采集                               |
+| `VIDIOC_G_PARM`              | 获取设备的流类型参数                       |
+| `VIDIOC_S_PARM`              | 设置流类型参数(帧率)                     |
+| `VIDIOC_TRY_FMT`             | 尝试设置帧格式,用于判断设备是否支持该格式 |
+| `VIDIOC_ENUM_FRAMESIZES`     | 枚举设备支持的视频采集分辨率               |
+| `VIDIOC_ENUM_FRAMEINTERVALS` | 枚举设备支持的视频采集帧率                 |
 
 ## 1.4 关键数据结构
 
@@ -89,13 +105,13 @@ struct v4l2_capability {
 
 重点是 `capabilities`,它描述设备能力,可为多个值的位或。摄像头设备必须包含 `V4L2_CAP_VIDEO_CAPTURE`(0x00000001,视频采集)。其它常用值:
 
-| 能力宏 | 值 | 含义 |
-| ------ | -- | ---- |
-| `V4L2_CAP_VIDEO_CAPTURE` | 0x00000001 | 是视频采集设备 |
-| `V4L2_CAP_VIDEO_OUTPUT` | 0x00000002 | 是视频输出设备 |
-| `V4L2_CAP_VIDEO_OVERLAY` | 0x00000004 | 支持视频叠加 |
-| `V4L2_CAP_READWRITE` | 0x01000000 | 支持 read/write 方式读写 |
-| `V4L2_CAP_STREAMING` | 0x04000000 | 支持 streaming I/O 方式 |
+| 能力宏                   | 值         | 含义                     |
+| ------------------------ | ---------- | ------------------------ |
+| `V4L2_CAP_VIDEO_CAPTURE` | 0x00000001 | 是视频采集设备           |
+| `V4L2_CAP_VIDEO_OUTPUT`  | 0x00000002 | 是视频输出设备           |
+| `V4L2_CAP_VIDEO_OVERLAY` | 0x00000004 | 支持视频叠加             |
+| `V4L2_CAP_READWRITE`     | 0x01000000 | 支持 read/write 方式读写 |
+| `V4L2_CAP_STREAMING`     | 0x04000000 | 支持 streaming I/O 方式  |
 
 ### 1.4.2 struct v4l2_fmtdesc / frmsizeenum / frmivalenum
 
@@ -150,21 +166,21 @@ struct v4l2_fract {
 
 常用像素格式(由 `v4l2_fourcc` 宏合成 32 位数据):
 
-| 像素格式宏 | fourcc | 含义 |
-| ---------- | ------ | ---- |
+| 像素格式宏            | fourcc | 含义            |
+| --------------------- | ------ | --------------- |
 | `V4L2_PIX_FMT_RGB565` | 'RGBP' | 16 位 RGB 5-6-5 |
 | `V4L2_PIX_FMT_RGB555` | 'RGBO' | 16 位 RGB 5-5-5 |
-| `V4L2_PIX_FMT_YUYV` | 'YUYV' | 16 位 YUV 4:2:2 |
-| `V4L2_PIX_FMT_MJPEG` | 'MJPG' | Motion-JPEG |
-| `V4L2_PIX_FMT_GREY` | 'GREY' | 8 位灰度 |
+| `V4L2_PIX_FMT_YUYV`   | 'YUYV' | 16 位 YUV 4:2:2 |
+| `V4L2_PIX_FMT_MJPEG`  | 'MJPG' | Motion-JPEG     |
+| `V4L2_PIX_FMT_GREY`   | 'GREY' | 8 位灰度        |
 
 `v4l2_buf_type` 常用取值:
 
-| 取值 | 含义 |
-| ---- | ---- |
-| `V4L2_BUF_TYPE_VIDEO_CAPTURE`(1) | 视频采集 |
-| `V4L2_BUF_TYPE_VIDEO_OUTPUT`(2) | 视频输出 |
-| `V4L2_BUF_TYPE_VIDEO_OVERLAY`(3) | 视频叠加 |
+| 取值                                    | 含义           |
+| --------------------------------------- | -------------- |
+| `V4L2_BUF_TYPE_VIDEO_CAPTURE`(1)        | 视频采集       |
+| `V4L2_BUF_TYPE_VIDEO_OUTPUT`(2)         | 视频输出       |
+| `V4L2_BUF_TYPE_VIDEO_OVERLAY`(3)        | 视频叠加       |
 | `V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE`(9) | 多平面视频采集 |
 
 ### 1.4.3 struct v4l2_format / v4l2_pix_format
@@ -221,9 +237,9 @@ struct v4l2_captureparm {
 
 `capability` 字段标志:
 
-| 标志 | 值 | 含义 |
-| ---- | -- | ---- |
-| `V4L2_MODE_HIGHQUALITY` | 0x0001 | 高品质成像模式 |
+| 标志                    | 值     | 含义                         |
+| ----------------------- | ------ | ---------------------------- |
+| `V4L2_MODE_HIGHQUALITY` | 0x0001 | 高品质成像模式               |
 | `V4L2_CAP_TIMEPERFRAME` | 0x1000 | 支持设置 `timeperframe` 字段 |
 
 只有 `capability` 包含 `V4L2_CAP_TIMEPERFRAME` 时,应用层才能通过 `VIDIOC_S_PARM` 设置帧率。
@@ -262,12 +278,12 @@ struct v4l2_buffer {
 
 `enum v4l2_memory`:
 
-| 取值 | 值 | 含义 |
-| ---- | -- | ---- |
-| `V4L2_MEMORY_MMAP` | 1 | 内存映射(最常用) |
-| `V4L2_MEMORY_USERPTR` | 2 | 用户指针 |
-| `V4L2_MEMORY_OVERLAY` | 3 | 叠加 |
-| `V4L2_MEMORY_DMABUF` | 4 | DMA 缓冲 |
+| 取值                  | 值  | 含义               |
+| --------------------- | --- | ------------------ |
+| `V4L2_MEMORY_MMAP`    | 1   | 内存映射(最常用) |
+| `V4L2_MEMORY_USERPTR` | 2   | 用户指针           |
+| `V4L2_MEMORY_OVERLAY` | 3   | 叠加               |
+| `V4L2_MEMORY_DMABUF`  | 4   | DMA 缓冲           |
 
 `length` 是帧缓冲长度,`m.offset` 是帧缓冲在内核申请的那块大内存中的偏移量;`VIDIOC_REQBUFS` 时内核申请"数量 × 单个大小"的连续内存,每个帧缓冲对应其中一段。
 
@@ -704,11 +720,11 @@ static void yuyv_to_rgb565(const unsigned char *src, unsigned short *dst, int pi
 
 串口在 Linux 中就是一个**终端(Terminal)**。终端是"处理主机输入、输出的一套设备",能接受输入、能显示输出就是终端。终端分类:
 
-| 类型 | 说明 |
-| ---- | ---- |
-| 本地终端 | 连接本机键盘显示器,如 `/dev/tty1`~`/dev/tty63` |
-| 串口远程终端 | 开发板最常见,通过串口线连接 PC,运行 putty/MobaXterm/SecureCRT |
-| 网络远程终端(伪终端) | ssh/Telnet 登录,节点在 `/dev/pts/X` |
+| 类型                   | 说明                                                            |
+| ---------------------- | --------------------------------------------------------------- |
+| 本地终端               | 连接本机键盘显示器,如 `/dev/tty1`~`/dev/tty63`                 |
+| 串口远程终端           | 开发板最常见,通过串口线连接 PC,运行 putty/MobaXterm/SecureCRT |
+| 网络远程终端(伪终端) | ssh/Telnet 登录,节点在 `/dev/pts/X`                            |
 
 设备节点:
 
@@ -743,53 +759,53 @@ struct termios {
 
 控制输入数据(驱动从串口/键盘收到的字符)在交给应用前的处理方式。
 
-| 标志 | 含义 |
-| ---- | ---- |
-| `IGNBRK` | 忽略输入终止条件 |
-| `BRKINT` | 检测到输入终止条件时发送 `SIGINT` 信号 |
-| `IGNPAR` | 忽略帧错误和奇偶校验错误 |
-| `PARMRK` | 对奇偶校验错误做出标记 |
-| `INPCK` | 对接收到的数据执行奇偶校验 |
+| 标志     | 含义                                        |
+| -------- | ------------------------------------------- |
+| `IGNBRK` | 忽略输入终止条件                            |
+| `BRKINT` | 检测到输入终止条件时发送 `SIGINT` 信号      |
+| `IGNPAR` | 忽略帧错误和奇偶校验错误                    |
+| `PARMRK` | 对奇偶校验错误做出标记                      |
+| `INPCK`  | 对接收到的数据执行奇偶校验                  |
 | `ISTRIP` | 将所有接收数据裁剪为 7 比特位(去掉第八位) |
-| `INLCR` | 将接收到的 NL(换行符)转换为 CR(回车符) |
-| `IGNCR` | 忽略接收到的 CR(回车符) |
-| `ICRNL` | 将接收到的 CR(回车符)转换为 NL(换行符) |
-| `IUCLC` | 将接收到的大写字符映射为小写字符 |
-| `IXON` | 启动输出软件流控 |
-| `IXOFF` | 启动输入软件流控 |
+| `INLCR`  | 将接收到的 NL(换行符)转换为 CR(回车符)  |
+| `IGNCR`  | 忽略接收到的 CR(回车符)                   |
+| `ICRNL`  | 将接收到的 CR(回车符)转换为 NL(换行符)  |
+| `IUCLC`  | 将接收到的大写字符映射为小写字符            |
+| `IXON`   | 启动输出软件流控                            |
+| `IXOFF`  | 启动输入软件流控                            |
 
 ### 2.2.2 输出模式 c_oflag
 
 控制输出字符在传递到串口/屏幕前的处理方式。
 
-| 标志 | 含义 |
-| ---- | ---- |
-| `OPOST` | 启用输出处理功能;不设置该标志则其它标志都被忽略 |
-| `OLCUC` | 将输出字符中的大写字符转换成小写字符 |
-| `ONLCR` | 将输出中的换行符 NL(`\n`)转换成回车符 CR(`\r`) |
-| `OCRNL` | 将输出中的回车符 CR(`\r`)转换成换行符 NL(`\n`) |
-| `ONOCR` | 在第 0 列不输出回车符 CR |
-| `ONLRET` | 不输出回车符 |
-| `OFILL` | 发送填充字符以提供延时 |
-| `OFDEL` | 若设置该标志,填充字符为 DEL 字符,否则为 NULL 字符 |
+| 标志     | 含义                                                |
+| -------- | --------------------------------------------------- |
+| `OPOST`  | 启用输出处理功能;不设置该标志则其它标志都被忽略    |
+| `OLCUC`  | 将输出字符中的大写字符转换成小写字符                |
+| `ONLCR`  | 将输出中的换行符 NL(`\n`)转换成回车符 CR(`\r`)  |
+| `OCRNL`  | 将输出中的回车符 CR(`\r`)转换成换行符 NL(`\n`)  |
+| `ONOCR`  | 在第 0 列不输出回车符 CR                            |
+| `ONLRET` | 不输出回车符                                        |
+| `OFILL`  | 发送填充字符以提供延时                              |
+| `OFDEL`  | 若设置该标志,填充字符为 DEL 字符,否则为 NULL 字符 |
 
 ### 2.2.3 控制模式 c_cflag
 
 控制终端硬件特性,对串口最重要:波特率、数据位、校验位、停止位等。
 
-| 标志 | 含义 |
-| ---- | ---- |
-| `CBAUD` | 波特率的位掩码 |
-| `B0`/`B1200`/`B1800`/`B2400`/`B4800`/`B9600`/`B19200`/`B38400`/`B57600`/`B115200`/`B230400`/`B460800`/`B500000`/`B576000`/`B921600`/`B1000000`/`B1152000`/`B1500000`/`B2000000`/`B2500000`/`B3000000` | 对应波特率 |
-| `CSIZE` | 数据位的位掩码 |
-| `CS5`/`CS6`/`CS7`/`CS8` | 5/6/7/8 个数据位 |
-| `CSTOPB` | 2 个停止位;不设置则默认 1 个停止位 |
-| `CREAD` | 接收使能 |
-| `PARENB` | 使能奇偶校验 |
-| `PARODD` | 使用奇校验,而不是偶校验 |
-| `HUPCL` | 关闭时挂断调制解调器 |
-| `CLOCAL` | 忽略调制解调器控制线 |
-| `CRTSCTS` | 使能硬件流控 |
+| 标志                                                                                                                                                                                                  | 含义                                |
+| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
+| `CBAUD`                                                                                                                                                                                               | 波特率的位掩码                      |
+| `B0`/`B1200`/`B1800`/`B2400`/`B4800`/`B9600`/`B19200`/`B38400`/`B57600`/`B115200`/`B230400`/`B460800`/`B500000`/`B576000`/`B921600`/`B1000000`/`B1152000`/`B1500000`/`B2000000`/`B2500000`/`B3000000` | 对应波特率                          |
+| `CSIZE`                                                                                                                                                                                               | 数据位的位掩码                      |
+| `CS5`/`CS6`/`CS7`/`CS8`                                                                                                                                                                               | 5/6/7/8 个数据位                    |
+| `CSTOPB`                                                                                                                                                                                              | 2 个停止位;不设置则默认 1 个停止位 |
+| `CREAD`                                                                                                                                                                                               | 接收使能                            |
+| `PARENB`                                                                                                                                                                                              | 使能奇偶校验                        |
+| `PARODD`                                                                                                                                                                                              | 使用奇校验,而不是偶校验            |
+| `HUPCL`                                                                                                                                                                                               | 关闭时挂断调制解调器                |
+| `CLOCAL`                                                                                                                                                                                              | 忽略调制解调器控制线                |
+| `CRTSCTS`                                                                                                                                                                                             | 使能硬件流控                        |
 
 Linux 下波特率由 `CBAUD` 位掩码的若干 bit 指定,并提供 `cfgetispeed()`/`cfsetispeed()`/`cfsetospeed()`/`cfsetspeed()` 函数获取与设置。
 
@@ -797,37 +813,37 @@ Linux 下波特率由 `CBAUD` 位掩码的若干 bit 指定,并提供 `cfgetis
 
 控制终端的本地数据处理和工作模式。
 
-| 标志 | 含义 |
-| ---- | ---- |
-| `ISIG` | 收到信号字符(INTR、QUIT 等)则产生相应信号 |
-| `ICANON` | 启用规范模式 |
-| `ECHO` | 启用输入字符的本地回显功能 |
-| `ECHOE` | 若设置 `ICANON`,则允许退格操作 |
-| `ECHOK` | 若设置 `ICANON`,则 KILL 字符会删除当前行 |
-| `ECHONL` | 若设置 `ICANON`,则允许回显换行符 |
-| `ECHOCTL` | 若设置 `ECHO`,控制字符会显示成 `^X` |
-| `ECHOPRT` | 若设置 `ICANON` 和 `IECHO`,删除字符和被删除字符都显示 |
-| `ECHOKE` | 若设置 `ICANON`,允许回显 `ECHOE`/`ECHOPRT` 中设定的 KILL 字符 |
-| `NOFLSH` | 通常收到 INTR/QUIT/SUSP 会清空输入输出队列;设置该标志则不清空 |
-| `TOSTOP` | 后台进程写控制终端时,系统向该进程组发送 `SIGTTOU` |
-| `IEXTEN` | 启用输入处理功能 |
+| 标志      | 含义                                                           |
+| --------- | -------------------------------------------------------------- |
+| `ISIG`    | 收到信号字符(INTR、QUIT 等)则产生相应信号                    |
+| `ICANON`  | 启用规范模式                                                   |
+| `ECHO`    | 启用输入字符的本地回显功能                                     |
+| `ECHOE`   | 若设置 `ICANON`,则允许退格操作                                |
+| `ECHOK`   | 若设置 `ICANON`,则 KILL 字符会删除当前行                      |
+| `ECHONL`  | 若设置 `ICANON`,则允许回显换行符                              |
+| `ECHOCTL` | 若设置 `ECHO`,控制字符会显示成 `^X`                           |
+| `ECHOPRT` | 若设置 `ICANON` 和 `IECHO`,删除字符和被删除字符都显示         |
+| `ECHOKE`  | 若设置 `ICANON`,允许回显 `ECHOE`/`ECHOPRT` 中设定的 KILL 字符 |
+| `NOFLSH`  | 通常收到 INTR/QUIT/SUSP 会清空输入输出队列;设置该标志则不清空 |
+| `TOSTOP`  | 后台进程写控制终端时,系统向该进程组发送 `SIGTTOU`             |
+| `IEXTEN`  | 启用输入处理功能                                               |
 
 ### 2.2.5 特殊控制字符 c_cc
 
-| 宏 | 对应键 | 作用 |
-| -- | ------ | ---- |
-| `VEOF` | Ctrl+D | 文件结尾符 EOF;read 返回 0 表示文件结束 |
-| `VEOL` | CR | 附加行结尾符 |
-| `VEOL2` | LF | 第二行结尾符 |
-| `VERASE` | Backspace | 删除输入行最后一个字符 |
-| `VINTR` | Ctrl+C | 向与终端相连的进程发送 `SIGINT` |
-| `VKILL` | Ctrl+U | 删除整个输入行 |
-| `VMIN` | — | 非规范模式下,最少读取的字符数 MIN |
-| `VQUIT` | Ctrl+Z | 发送 `SIGQUIT` |
-| `VSTART` | Ctrl+Q | 重新启动被 STOP 暂停的输出 |
-| `VSTOP` | Ctrl+S | 停止向终端的进一步输出(XON/XOFF 流控) |
-| `VSUSP` | Ctrl+Z | 发送 `SIGSUSP`,挂起当前应用程序 |
-| `VTIME` | — | 非规范模式下,读取每个字符之间的超时(单位:十分之一秒) |
+| 宏       | 对应键    | 作用                                                     |
+| -------- | --------- | -------------------------------------------------------- |
+| `VEOF`   | Ctrl+D    | 文件结尾符 EOF;read 返回 0 表示文件结束                 |
+| `VEOL`   | CR        | 附加行结尾符                                             |
+| `VEOL2`  | LF        | 第二行结尾符                                             |
+| `VERASE` | Backspace | 删除输入行最后一个字符                                   |
+| `VINTR`  | Ctrl+C    | 向与终端相连的进程发送 `SIGINT`                          |
+| `VKILL`  | Ctrl+U    | 删除整个输入行                                           |
+| `VMIN`   | —         | 非规范模式下,最少读取的字符数 MIN                       |
+| `VQUIT`  | Ctrl+Z    | 发送 `SIGQUIT`                                           |
+| `VSTART` | Ctrl+Q    | 重新启动被 STOP 暂停的输出                               |
+| `VSTOP`  | Ctrl+S    | 停止向终端的进一步输出(XON/XOFF 流控)                  |
+| `VSUSP`  | Ctrl+Z    | 发送 `SIGSUSP`,挂起当前应用程序                         |
+| `VTIME`  | —         | 非规范模式下,读取每个字符之间的超时(单位:十分之一秒) |
 
 ## 2.3 终端的三种工作模式
 
@@ -836,12 +852,12 @@ Linux 下波特率由 `CBAUD` 位掩码的若干 bit 指定,并提供 `cfgetis
 - **规范模式(canonical)**:所有输入基于行处理。用户输入行结束符(回车、EOF 等)之前,`read()` 读不到任何字符;除 EOF 外的行结束符与普通字符一样被读入缓冲区;支持行编辑,一次 `read()` 最多读一行。
 - **非规范模式(non-canonical)**:所有输入即时有效,不需要行结束符,不可行编辑。由 `MIN`(`c_cc[VMIN]`)与 `TIME`(`c_cc[VTIME]`)决定 `read()` 行为:
 
-| MIN | TIME | read() 行为 |
-| --- | ---- | ----------- |
-| 0 | 0 | 总是立即返回:有数据则读并返回字节数,否则返回 0 |
-| >0 | 0 | 阻塞直到有 MIN 个字符可读才返回;到达文件尾返回 0 |
-| 0 | >0 | 只要有数据可读,或经过 TIME 个十分之一秒,立即返回;超时无数据返回 0 |
-| >0 | >0 | 有 MIN 个字节可读,或两字符间隔超过 TIME 个十分之一秒才返回;至少读一个字节 |
+| MIN | TIME | read() 行为                                                                 |
+| --- | ---- | --------------------------------------------------------------------------- |
+| 0   | 0    | 总是立即返回:有数据则读并返回字节数,否则返回 0                            |
+| >0  | 0    | 阻塞直到有 MIN 个字符可读才返回;到达文件尾返回 0                           |
+| 0   | >0   | 只要有数据可读,或经过 TIME 个十分之一秒,立即返回;超时无数据返回 0        |
+| >0  | >0   | 有 MIN 个字节可读,或两字符间隔超过 TIME 个十分之一秒才返回;至少读一个字节 |
 
 - **原始模式(raw)**:严格说是一种特殊的非规范模式。所有输入数据以字节为单位处理,终端不回显,禁用输入输出字符的所有特殊处理。通过 `cfmakeraw()` 设置,其内部等价于:
 
@@ -858,42 +874,42 @@ termios_p->c_cflag |= CS8;
 
 ## 2.4 串口编程 API 汇总
 
-| 函数 | 原型 | 作用 |
-| ---- | ---- | ---- |
-| `tcgetattr` | `int tcgetattr(int fd, struct termios *termios_p)` | 获取当前配置(便于事后恢复) |
-| `cfmakeraw` | `void cfmakeraw(struct termios *termios_p)` | 配置为原始模式 |
-| `cfsetispeed` | `int cfsetispeed(struct termios *, speed_t)` | 设置输入波特率 |
-| `cfsetospeed` | `int cfsetospeed(struct termios *, speed_t)` | 设置输出波特率 |
-| `cfsetspeed` | `int cfsetspeed(struct termios *, speed_t)` | 一次设置输入输出波特率 |
-| `tcflush` | `int tcflush(int fd, int queue_selector)` | 清空输入/输出缓冲区 |
-| `tcdrain` | `int tcdrain(int fd)` | 阻塞直到输出缓冲区数据全部发送完毕 |
-| `tcflow` | `int tcflow(int fd, int action)` | 暂停/重启数据收发 |
-| `tcsetattr` | `int tcsetattr(int fd, int optional_actions, const struct termios *)` | 写入配置使其生效 |
-| `read`/`write` | — | 读写数据 |
+| 函数           | 原型                                                                  | 作用                               |
+| -------------- | --------------------------------------------------------------------- | ---------------------------------- |
+| `tcgetattr`    | `int tcgetattr(int fd, struct termios *termios_p)`                    | 获取当前配置(便于事后恢复)       |
+| `cfmakeraw`    | `void cfmakeraw(struct termios *termios_p)`                           | 配置为原始模式                     |
+| `cfsetispeed`  | `int cfsetispeed(struct termios *, speed_t)`                          | 设置输入波特率                     |
+| `cfsetospeed`  | `int cfsetospeed(struct termios *, speed_t)`                          | 设置输出波特率                     |
+| `cfsetspeed`   | `int cfsetspeed(struct termios *, speed_t)`                           | 一次设置输入输出波特率             |
+| `tcflush`      | `int tcflush(int fd, int queue_selector)`                             | 清空输入/输出缓冲区                |
+| `tcdrain`      | `int tcdrain(int fd)`                                                 | 阻塞直到输出缓冲区数据全部发送完毕 |
+| `tcflow`       | `int tcflow(int fd, int action)`                                      | 暂停/重启数据收发                  |
+| `tcsetattr`    | `int tcsetattr(int fd, int optional_actions, const struct termios *)` | 写入配置使其生效                   |
+| `read`/`write` | —                                                                     | 读写数据                           |
 
 `tcflush()` 的 `queue_selector`:
 
-| 取值 | 含义 |
-| ---- | ---- |
-| `TCIFLUSH` | 清空接收到但未被读取的数据 |
-| `TCOFLUSH` | 清空尚未传输成功的输出数据 |
-| `TCIOFLUSH` | 前两种都清空 |
+| 取值        | 含义                       |
+| ----------- | -------------------------- |
+| `TCIFLUSH`  | 清空接收到但未被读取的数据 |
+| `TCOFLUSH`  | 清空尚未传输成功的输出数据 |
+| `TCIOFLUSH` | 前两种都清空               |
 
 `tcflow()` 的 `action`:
 
-| 取值 | 含义 |
-| ---- | ---- |
-| `TCOOFF` | 暂停数据输出 |
-| `TCOON` | 重新启动暂停的输出 |
-| `TCIOFF` | 发送 STOP 字符,停止终端设备向系统发送数据 |
-| `TCION` | 发送 START 字符,启动终端设备向系统发送数据 |
+| 取值     | 含义                                        |
+| -------- | ------------------------------------------- |
+| `TCOOFF` | 暂停数据输出                                |
+| `TCOON`  | 重新启动暂停的输出                          |
+| `TCIOFF` | 发送 STOP 字符,停止终端设备向系统发送数据  |
+| `TCION`  | 发送 START 字符,启动终端设备向系统发送数据 |
 
 `tcsetattr()` 的 `optional_actions`(生效时机):
 
-| 取值 | 含义 |
-| ---- | ---- |
-| `TCSANOW` | 配置立即生效 |
-| `TCSADRAIN` | 在所有写入 fd 的输出传输完毕之后生效 |
+| 取值        | 含义                                       |
+| ----------- | ------------------------------------------ |
+| `TCSANOW`   | 配置立即生效                               |
+| `TCSADRAIN` | 在所有写入 fd 的输出传输完毕之后生效       |
 | `TCSAFLUSH` | 所有已接收但未读取的输入在配置生效前被丢弃 |
 
 **配置步骤**(以原始模式为例):
@@ -1343,35 +1359,35 @@ flowchart TB
 
 `/dev/snd` 下常见设备节点:
 
-| 设备节点 | 含义 |
-| -------- | ---- |
+| 设备节点    | 含义                                                        |
+| ----------- | ----------------------------------------------------------- |
 | `controlC0` | 声卡控制设备(通道选择、混音器、麦克风控制等),C0 = 声卡 0 |
-| `pcmC0D0c` | 声卡 0、设备 0 的**录音** PCM 设备(c = capture) |
-| `pcmC0D0p` | 声卡 0、设备 0 的**播放** PCM 设备(p = playback) |
-| `pcmC0D1c` | 声卡 0、设备 1 的录音 PCM 设备 |
-| `pcmC0D1p` | 声卡 0、设备 1 的播放 PCM 设备 |
-| `timer` | 定时器 |
+| `pcmC0D0c`  | 声卡 0、设备 0 的**录音** PCM 设备(c = capture)           |
+| `pcmC0D0p`  | 声卡 0、设备 0 的**播放** PCM 设备(p = playback)          |
+| `pcmC0D1c`  | 声卡 0、设备 1 的录音 PCM 设备                              |
+| `pcmC0D1p`  | 声卡 0、设备 1 的播放 PCM 设备                              |
+| `timer`     | 定时器                                                      |
 
 `/proc/asound` 目录记录声卡信息:
 
-| 文件/命令 | 作用 |
-| --------- | ---- |
-| `cat /proc/asound/cards` | 列出系统中注册的所有声卡 |
+| 文件/命令                  | 作用                                                  |
+| -------------------------- | ----------------------------------------------------- |
+| `cat /proc/asound/cards`   | 列出系统中注册的所有声卡                              |
 | `cat /proc/asound/devices` | 列出所有声卡注册的设备(control、pcm、timer、seq 等) |
-| `cat /proc/asound/pcm` | 列出所有 PCM 设备(playback 与 capture) |
+| `cat /proc/asound/pcm`     | 列出所有 PCM 设备(playback 与 capture)              |
 
 ## 3.3 音频基本概念
 
-| 概念 | 说明 |
-| ---- | ---- |
-| 样本长度(Sample) | 记录音频数据最基本的单元;即采样位数/位深度,如 8bit、16bit、24bit |
-| 声道数(channel) | 单声道 Mono=1,双声道/立体声 Stereo=2 |
-| 帧(frame) | 一个声音单元,长度 = 样本长度 × 声道数。16bit 双声道一帧 = 16×2/8 = 4 字节 |
-| 采样率(Sample rate) | 每秒采样次数(针对帧)。8KHz 电话、22.05KHz FM、44.1KHz CD、48KHz 数字电视 |
-| 交错模式(interleaved) | 数据以连续帧存放:先帧 1 左、右声道样本,再帧 2 左、右……多数情况使用交错模式 |
-| 周期(period) | 音频设备读写数据的单位,一个周期包含若干帧,如 1024 帧 |
-| 缓冲区(buffer) | 由若干周期组成的一块空间,如 16 个周期 |
-| XRUN | 录音时应用读得慢导致数据被覆盖叫 **overrun**;播放时应用写得慢导致缓冲"饿死"叫 **underrun**,统称 XRUN |
+| 概念                    | 说明                                                                                                   |
+| ----------------------- | ------------------------------------------------------------------------------------------------------ |
+| 样本长度(Sample)      | 记录音频数据最基本的单元;即采样位数/位深度,如 8bit、16bit、24bit                                     |
+| 声道数(channel)       | 单声道 Mono=1,双声道/立体声 Stereo=2                                                                  |
+| 帧(frame)             | 一个声音单元,长度 = 样本长度 × 声道数。16bit 双声道一帧 = 16×2/8 = 4 字节                             |
+| 采样率(Sample rate)   | 每秒采样次数(针对帧)。8KHz 电话、22.05KHz FM、44.1KHz CD、48KHz 数字电视                             |
+| 交错模式(interleaved) | 数据以连续帧存放:先帧 1 左、右声道样本,再帧 2 左、右……多数情况使用交错模式                           |
+| 周期(period)          | 音频设备读写数据的单位,一个周期包含若干帧,如 1024 帧                                                 |
+| 缓冲区(buffer)        | 由若干周期组成的一块空间,如 16 个周期                                                                 |
+| XRUN                    | 录音时应用读得慢导致数据被覆盖叫 **overrun**;播放时应用写得慢导致缓冲"饿死"叫 **underrun**,统称 XRUN |
 
 **为什么要把 buffer 拆成多个 period?** 底层驱动用 DMA 搬运数据,每搬完一个 period 触发一次中断。若一次性搬完整个 buffer,数据量越大延迟越高。周期越小延迟越低,但中断越频繁、CPU 效率越低。因此在延迟可接受的前提下,周期尽量大一些,具体依应用场合而定。
 
@@ -1407,12 +1423,12 @@ int snd_pcm_open(snd_pcm_t **pcmp, const char *name,
 int snd_pcm_close(snd_pcm_t *pcm);
 ```
 
-| 参数 | 说明 |
-| ---- | ---- |
-| `pcmp` | 返回 PCM 设备句柄(`snd_pcm_t *`) |
-| `name` | 逻辑设备名,非设备文件名。`"hw:0,0"` = 声卡 0 的 PCM 设备 0,播放对应 `/dev/snd/pcmC0D0p`,录音对应 `pcmC0D0c`;还有 `"plughw:i,j"`、`"default"` 等 |
-| `stream` | `SND_PCM_STREAM_PLAYBACK`(播放)或 `SND_PCM_STREAM_CAPTURE`(采集) |
-| `mode` | 通常为 0(默认,阻塞方式);`SND_PCM_NONBLOCK` 为非阻塞 |
+| 参数     | 说明                                                                                                                                                |
+| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `pcmp`   | 返回 PCM 设备句柄(`snd_pcm_t *`)                                                                                                                  |
+| `name`   | 逻辑设备名,非设备文件名。`"hw:0,0"` = 声卡 0 的 PCM 设备 0,播放对应 `/dev/snd/pcmC0D0p`,录音对应 `pcmC0D0c`;还有 `"plughw:i,j"`、`"default"` 等 |
+| `stream` | `SND_PCM_STREAM_PLAYBACK`(播放)或 `SND_PCM_STREAM_CAPTURE`(采集)                                                                                |
+| `mode`   | 通常为 0(默认,阻塞方式);`SND_PCM_NONBLOCK` 为非阻塞                                                                                             |
 
 ```c
 ret = snd_pcm_open(&pcm_handle, "hw:0,0", SND_PCM_STREAM_PLAYBACK, 0);
@@ -1433,15 +1449,15 @@ snd_pcm_hw_params(pcm_handle, hwparams);        /* 加载生效 */
 snd_pcm_hw_params_free(hwparams);
 ```
 
-| 设置函数 | 作用 | 关键说明 |
-| -------- | ---- | -------- |
-| `snd_pcm_hw_params_set_access()` | 访问类型 | `SND_PCM_ACCESS_RW_INTERLEAVED`(交错,配 `readi/writei`)、`..._RW_NONINTERLEAVED`(配 `readn/writen`) |
-| `snd_pcm_hw_params_set_format()` | 数据格式 | 常用 `SND_PCM_FORMAT_S16_LE`(有符号 16 位小端) |
-| `snd_pcm_hw_params_set_channels()` | 声道数 | 2 = 双声道 |
-| `snd_pcm_hw_params_set_rate()` | 采样率 | `val` 如 44100;`dir`:-1 实际≤val、0 实际=val、1 实际≥val |
-| `snd_pcm_hw_params_set_period_size()` | 周期大小(帧) | 如 1024 |
-| `snd_pcm_hw_params_set_buffer_size()` | buffer 大小(帧) | 如 16×1024 |
-| `snd_pcm_hw_params_set_periods()` | buffer 大小(周期数) | 如 16 |
+| 设置函数                              | 作用                  | 关键说明                                                                                                 |
+| ------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------- |
+| `snd_pcm_hw_params_set_access()`      | 访问类型              | `SND_PCM_ACCESS_RW_INTERLEAVED`(交错,配 `readi/writei`)、`..._RW_NONINTERLEAVED`(配 `readn/writen`) |
+| `snd_pcm_hw_params_set_format()`      | 数据格式              | 常用 `SND_PCM_FORMAT_S16_LE`(有符号 16 位小端)                                                         |
+| `snd_pcm_hw_params_set_channels()`    | 声道数                | 2 = 双声道                                                                                               |
+| `snd_pcm_hw_params_set_rate()`        | 采样率                | `val` 如 44100;`dir`:-1 实际≤val、0 实际=val、1 实际≥val                                               |
+| `snd_pcm_hw_params_set_period_size()` | 周期大小(帧)        | 如 1024                                                                                                  |
+| `snd_pcm_hw_params_set_buffer_size()` | buffer 大小(帧)     | 如 16×1024                                                                                               |
+| `snd_pcm_hw_params_set_periods()`     | buffer 大小(周期数) | 如 16                                                                                                    |
 
 > `snd_pcm_hw_params_set_period_size()` 的 `val` 单位是**帧**不是字节;`snd_pcm_hw_params_set_periods()` 的 `val` 单位是**周期**。`snd_pcm_hw_params()` 内部会自动调用 `snd_pcm_prepare()`,使设备进入 `SND_PCM_STATE_PREPARED`。
 
@@ -1464,17 +1480,17 @@ snd_pcm_sframes_t snd_pcm_readi(snd_pcm_t *pcm, void *buffer, snd_pcm_uframes_t
 
 `snd_pcm_state_t snd_pcm_state(snd_pcm_t *pcm)` 获取当前状态:
 
-| 状态 | 含义 |
-| ---- | ---- |
-| `SND_PCM_STATE_OPEN` | 设备已打开(`snd_pcm_open()` 后) |
-| `SND_PCM_STATE_SETUP` | 设备已初始化、参数已配置好 |
-| `SND_PCM_STATE_PREPARED` | 设备已就绪,可开始播放/录音 |
-| `SND_PCM_STATE_RUNNING` | 正在运行(播放/录音中) |
-| `SND_PCM_STATE_XRUN` | 发生 XRUN;可调 `snd_pcm_prepare()` 恢复 |
-| `SND_PCM_STATE_DRAINING` | Draining(播放中/采集停止) |
-| `SND_PCM_STATE_PAUSED` | 暂停 |
-| `SND_PCM_STATE_SUSPENDED` | 硬件挂起;可用 `snd_pcm_resume()` 精细恢复 |
-| `SND_PCM_STATE_DISCONNECTED` | 硬件已断开 |
+| 状态                         | 含义                                       |
+| ---------------------------- | ------------------------------------------ |
+| `SND_PCM_STATE_OPEN`         | 设备已打开(`snd_pcm_open()` 后)          |
+| `SND_PCM_STATE_SETUP`        | 设备已初始化、参数已配置好                 |
+| `SND_PCM_STATE_PREPARED`     | 设备已就绪,可开始播放/录音                |
+| `SND_PCM_STATE_RUNNING`      | 正在运行(播放/录音中)                    |
+| `SND_PCM_STATE_XRUN`         | 发生 XRUN;可调 `snd_pcm_prepare()` 恢复   |
+| `SND_PCM_STATE_DRAINING`     | Draining(播放中/采集停止)                |
+| `SND_PCM_STATE_PAUSED`       | 暂停                                       |
+| `SND_PCM_STATE_SUSPENDED`    | 硬件挂起;可用 `snd_pcm_resume()` 精细恢复 |
+| `SND_PCM_STATE_DISCONNECTED` | 硬件已断开                                 |
 
 相关函数:
 
@@ -1495,10 +1511,10 @@ int snd_pcm_hw_params_can_resume(const snd_pcm_hw_params_t *params);
 
 `snd_pcm_readi/writei` 返回负错误码时用 `snd_strerror()` 获取描述:
 
-| 返回值 | 含义 | 处理建议 |
-| ------ | ---- | -------- |
-| `-EBADFD` | PCM 设备状态不对 | 需处于 PREPARED 或 RUNNING,检查状态转换 |
-| `-EPIPE` | 发生 XRUN | `snd_pcm_drop()` 停止,或 `snd_pcm_prepare()` 恢复 |
+| 返回值      | 含义                  | 处理建议                                                  |
+| ----------- | --------------------- | --------------------------------------------------------- |
+| `-EBADFD`   | PCM 设备状态不对      | 需处于 PREPARED 或 RUNNING,检查状态转换                  |
+| `-EPIPE`    | 发生 XRUN             | `snd_pcm_drop()` 停止,或 `snd_pcm_prepare()` 恢复        |
 | `-ESTRPIPE` | 硬件挂起(SUSPENDED) | `snd_pcm_resume()` 精细恢复;不支持则 `snd_pcm_prepare()` |
 
 ## 3.8 混音器 API
@@ -1516,20 +1532,20 @@ snd_mixer_load(mixer);                           /* 加载 */
 
 遍历与取值:
 
-| 函数 | 作用 |
-| ---- | ---- |
-| `snd_mixer_first_elem(mixer)` / `snd_mixer_last_elem(mixer)` | 第一个/最后一个元素 |
-| `snd_mixer_elem_next(elem)` / `snd_mixer_elem_prev(elem)` | 下一个/上一个元素 |
-| `snd_mixer_selem_get_name(elem)` | 获取元素名字(如 `"Headphone"`、`"Playback"`) |
-| `snd_mixer_selem_has_playback_volume(elem)` / `..._has_capture_volume(elem)` | 是否 volume 类型 |
-| `snd_mixer_selem_has_playback_switch(elem)` / `..._has_capture_switch(elem)` | 是否 switch(bool)类型 |
-| `snd_mixer_selem_has_playback_channel(elem, ch)` / `..._has_capture_channel(elem, ch)` | 是否含指定通道 |
-| `snd_mixer_selem_is_playback_mono(elem)` / `..._is_capture_mono(elem)` | 是否单声道 |
-| `snd_mixer_selem_get_playback_volume_range(elem, &min, &max)` | 获取音量范围 |
-| `snd_mixer_selem_get_playback_volume(elem, ch, &value)` | 获取某声道音量 |
-| `snd_mixer_selem_set_playback_volume(elem, ch, value)` | 设置某声道音量 |
-| `snd_mixer_selem_set_playback_volume_all(elem, value)` | 设置所有声道音量 |
-| `snd_mixer_close(mixer)` | 关闭混音器 |
+| 函数                                                                                   | 作用                                           |
+| -------------------------------------------------------------------------------------- | ---------------------------------------------- |
+| `snd_mixer_first_elem(mixer)` / `snd_mixer_last_elem(mixer)`                           | 第一个/最后一个元素                            |
+| `snd_mixer_elem_next(elem)` / `snd_mixer_elem_prev(elem)`                              | 下一个/上一个元素                              |
+| `snd_mixer_selem_get_name(elem)`                                                       | 获取元素名字(如 `"Headphone"`、`"Playback"`) |
+| `snd_mixer_selem_has_playback_volume(elem)` / `..._has_capture_volume(elem)`           | 是否 volume 类型                               |
+| `snd_mixer_selem_has_playback_switch(elem)` / `..._has_capture_switch(elem)`           | 是否 switch(bool)类型                        |
+| `snd_mixer_selem_has_playback_channel(elem, ch)` / `..._has_capture_channel(elem, ch)` | 是否含指定通道                                 |
+| `snd_mixer_selem_is_playback_mono(elem)` / `..._is_capture_mono(elem)`                 | 是否单声道                                     |
+| `snd_mixer_selem_get_playback_volume_range(elem, &min, &max)`                          | 获取音量范围                                   |
+| `snd_mixer_selem_get_playback_volume(elem, ch, &value)`                                | 获取某声道音量                                 |
+| `snd_mixer_selem_set_playback_volume(elem, ch, value)`                                 | 设置某声道音量                                 |
+| `snd_mixer_selem_set_playback_volume_all(elem, value)`                                 | 设置所有声道音量                               |
+| `snd_mixer_close(mixer)`                                                               | 关闭混音器                                     |
 
 声道枚举 `snd_mixer_selem_channel_id_t`:`SND_MIXER_SCHN_FRONT_LEFT`(0,左前)、`SND_MIXER_SCHN_FRONT_RIGHT`(右前)、`SND_MIXER_SCHN_REAR_LEFT`、`SND_MIXER_SCHN_REAR_RIGHT`、`SND_MIXER_SCHN_FRONT_CENTER`、`SND_MIXER_SCHN_WOOFER`、`SND_MIXER_SCHN_MONO`(= 左前)。
 
@@ -1537,16 +1553,16 @@ snd_mixer_load(mixer);                           /* 加载 */
 
 出厂系统已移植 alsa-lib 与 alsa-utils,可直接使用:
 
-| 工具 | 用途 |
-| ---- | ---- |
-| `aplay xxx.wav` | 播放 wav(不支持 mp3 解码) |
-| `arecord -f cd -d 10 test.wav` | 录音 10 秒,`-f cd` = 16bit little endian 44100 stereo |
-| `alsamixer` | 字符图形化混音器配置界面(F3 Playback / F4 Capture / F5 All,F6 选声卡) |
-| `amixer scontrols` / `amixer scontents` | 列出配置项 / 查看配置说明 |
-| `amixer sset Headphone 100,100` | 设置耳机音量左右声道 |
-| `amixer sset "Headphone Playback ZC" off` | 开关某项 |
-| `alsactl -f /var/lib/alsa/asound.state store` | 保存声卡配置 |
-| `alsactl -f /var/lib/alsa/asound.state restore` | 加载声卡配置 |
+| 工具                                            | 用途                                                                     |
+| ----------------------------------------------- | ------------------------------------------------------------------------ |
+| `aplay xxx.wav`                                 | 播放 wav(不支持 mp3 解码)                                              |
+| `arecord -f cd -d 10 test.wav`                  | 录音 10 秒,`-f cd` = 16bit little endian 44100 stereo                   |
+| `alsamixer`                                     | 字符图形化混音器配置界面(F3 Playback / F4 Capture / F5 All,F6 选声卡) |
+| `amixer scontrols` / `amixer scontents`         | 列出配置项 / 查看配置说明                                                |
+| `amixer sset Headphone 100,100`                 | 设置耳机音量左右声道                                                     |
+| `amixer sset "Headphone Playback ZC" off`       | 开关某项                                                                 |
+| `alsactl -f /var/lib/alsa/asound.state store`   | 保存声卡配置                                                             |
+| `alsactl -f /var/lib/alsa/asound.state restore` | 加载声卡配置                                                             |
 
 > 开机自动从 `/var/lib/alsa/asound.state` 加载配置,关机时保存。此文件即 WM8960 声卡配置文件,由 `alsactl` 完成。
 
@@ -1578,17 +1594,17 @@ typedef struct WAV_DATA {
 } __attribute__ ((packed)) DATA_t;
 ```
 
-| 字段 | 含义 |
-| ---- | ---- |
-| `ChunkID` / `Format` | 固定 `"RIFF"` / `"WAVE"` |
-| `Subchunk1ID` | 固定 `"fmt "` |
-| `AudioFormat` | `1` 表示 PCM |
-| `NumChannels` | 声道数 |
-| `SampleRate` | 采样率 |
-| `ByteRate` | 每秒字节数 = 采样率 × 声道数 × 位深/8 |
-| `BlockAlign` | 一帧字节数 = 声道数 × 位深/8(即 frame_bytes) |
-| `BitsPerSample` | 位深 |
-| `Subchunk2ID` / `Subchunk2Size` | 数据块标识 `"data"` 与大小 |
+| 字段                            | 含义                                           |
+| ------------------------------- | ---------------------------------------------- |
+| `ChunkID` / `Format`            | 固定 `"RIFF"` / `"WAVE"`                       |
+| `Subchunk1ID`                   | 固定 `"fmt "`                                  |
+| `AudioFormat`                   | `1` 表示 PCM                                   |
+| `NumChannels`                   | 声道数                                         |
+| `SampleRate`                    | 采样率                                         |
+| `ByteRate`                      | 每秒字节数 = 采样率 × 声道数 × 位深/8          |
+| `BlockAlign`                    | 一帧字节数 = 声道数 × 位深/8(即 frame_bytes) |
+| `BitsPerSample`                 | 位深                                           |
+| `Subchunk2ID` / `Subchunk2Size` | 数据块标识 `"data"` 与大小                     |
 
 **解析思路**:读 RIFF 头并校验 `"RIFF"`/`"WAVE"`;读 `fmt ` 子块并校验 `"fmt "`,同时拿到 `SampleRate`、`NumChannels`、`BlockAlign`(用于配置 ALSA 参数与缓冲区);用 `lseek` 跳过 `sizeof(RIFF_t)+8+Subchunk1Size`,循环读 `DATA_t` 找 `"data"` 块(不是则 `lseek` 跳过该块),找到后文件位置即 PCM 数据起点。
 
@@ -1667,7 +1683,7 @@ static int snd_pcm_init(void)
         goto err2;
     }
 
-    /************** 
+    /**************
      设置参数
     ***************/
     /* 设置访问类型: 交错模式 */
@@ -1923,7 +1939,7 @@ static int snd_pcm_init(void)
         goto err2;
     }
 
-    /************** 
+    /**************
      设置参数
     ***************/
     /* 设置访问类型: 交错模式 */
@@ -2223,12 +2239,12 @@ ${CC} -o pcm_playback_mixer pcm_playback_mixer.c -lasound
 
 `user 空间的 alsa-lib` 使用**逻辑设备名**而非设备节点。`snd_pcm_open()` 会加载 `/usr/share/alsa/alsa.conf` 并解析,其中又会加载 `/etc/asound.conf` 与 `~/.asoundrc`。每个 `pcm.name { ... }` 定义了一个插件,`type` 字段指定插件类型:
 
-| 插件 | 作用 |
-| ---- | ---- |
-| `hw` | 直接与 ALSA 内核驱动通信,无任何转换的原始通信 |
-| `plughw` | 提供采样率转换等软件特性(硬件不支持时可用) |
-| `dmix` | 混音,把多个应用程序的音频数据混合 |
-| `softvol` | 软件音量 |
+| 插件      | 作用                                           |
+| --------- | ---------------------------------------------- |
+| `hw`      | 直接与 ALSA 内核驱动通信,无任何转换的原始通信 |
+| `plughw`  | 提供采样率转换等软件特性(硬件不支持时可用)   |
+| `dmix`    | 混音,把多个应用程序的音频数据混合             |
+| `softvol` | 软件音量                                       |
 
 `"hw:i,j"` 就是名为 `hw` 的插件,`i` 是声卡号、`j` 是设备号。
 
@@ -2236,13 +2252,13 @@ ${CC} -o pcm_playback_mixer pcm_playback_mixer.c -lasound
 
 # 跨平台对比
 
-| 主题 | I.MX6ULL(本教程) | STM32(裸机/HAL) | RK3568(Linux) |
-| ---- | ------------------ | ----------------- | --------------- |
-| 摄像头 | V4L2 `/dev/videoX`,ov5640/ov2640/ov7725/UVC,ioctl + mmap 队列 | DCMI + DMA,直接操作寄存器/LL 库 | V4L2 + Media Controller/V4L2 subdev,Rockchip ISP |
-| 串口 | termios API,`/dev/ttymxcX` | USART,HAL_UART_Transmit/Receive + 中断/DMA | termios API,`/dev/ttySX` |
-| 音频 | ALSA + alsa-lib,WM8960/ES8388 | I2S + 外部 codec,寄存器配置 | ALSA + alsa-lib,通常走 GStreamer/ASoC |
-| 编程层 | 用户态,统一设备框架 | 裸机/RTOS,直接寄存器 | 用户态,统一设备框架 |
-| 关键差异 | 需掌握框架接口与设备节点 | 需手工配置时钟/引脚/DMA | 同 V4L2/ALSA,但多平面、复杂管线常见 |
+| 主题     | I.MX6ULL(本教程)                                              | STM32(裸机/HAL)                           | RK3568(Linux)                                   |
+| -------- | --------------------------------------------------------------- | ------------------------------------------- | ------------------------------------------------- |
+| 摄像头   | V4L2 `/dev/videoX`,ov5640/ov2640/ov7725/UVC,ioctl + mmap 队列 | DCMI + DMA,直接操作寄存器/LL 库            | V4L2 + Media Controller/V4L2 subdev,Rockchip ISP |
+| 串口     | termios API,`/dev/ttymxcX`                                     | USART,HAL_UART_Transmit/Receive + 中断/DMA | termios API,`/dev/ttySX`                         |
+| 音频     | ALSA + alsa-lib,WM8960/ES8388                                  | I2S + 外部 codec,寄存器配置                | ALSA + alsa-lib,通常走 GStreamer/ASoC            |
+| 编程层   | 用户态,统一设备框架                                            | 裸机/RTOS,直接寄存器                       | 用户态,统一设备框架                              |
+| 关键差异 | 需掌握框架接口与设备节点                                        | 需手工配置时钟/引脚/DMA                     | 同 V4L2/ALSA,但多平面、复杂管线常见              |
 
 ---
 
@@ -2255,6 +2271,7 @@ ${CC} -o pcm_playback_mixer pcm_playback_mixer.c -lasound
 **详细解答**:内核维护一个帧缓冲队列。应用 `VIDIOC_QBUF` 把空闲缓冲交给驱动,驱动填充一帧后缓冲变为"满";应用 `VIDIOC_DQBUF` 取走满缓冲(出队),处理完再 `VIDIOC_QBUF` 归还(入队)。这样避免了每帧内存拷贝——`mmap` 后应用直接读内核缓冲,即零拷贝。`count` 一般取 3~4,太多浪费内存、太少可能丢帧。
 
 **追问**
+
 1. `VIDIOC_DQBUF` 在缓冲为空时会怎样?(阻塞直到驱动填入一帧;非阻塞打开则立即返回 `EAGAIN`)
 2. `V4L2_MEMORY_MMAP` 与 `V4L2_MEMORY_USERPTR` 区别?(MMAP 由内核分配、应用映射;USERPTR 由应用提供缓冲指针)
 
@@ -2275,6 +2292,7 @@ if (V4L2_PIX_FMT_RGB565 != fmt.fmt.pix.pixelformat) { /* 不支持,退化为 Y
 注意 `VIDIOC_TRY_FMT` 只试不改,可在正式设置前探测。
 
 **追问**
+
 1. 为什么 USB 摄像头常不支持 RGB565?(UVC 标准输出以 YUYV/MJPEG 为主)
 2. `fmt.fmt.pix.sizeimage` 有什么用?(一帧图像数据的总字节数,用于分配缓冲/校验)
 
@@ -2300,6 +2318,7 @@ tcsetattr(fd, TCSANOW, &new_cfg);
 原始模式会清除 `ICANON/ECHO/ISIG` 等本地标志,并关闭 `OPOST` 输出处理——这是二进制通信必需的。
 
 **追问**
+
 1. `VMIN=VTIME=0` 时 `read()` 行为?(立即返回:有数据返回字节数,无数据返回 0)
 2. 为什么打开串口要加 `O_NOCTTY`?(避免该终端成为本进程的控制终端)
 
@@ -2310,6 +2329,7 @@ tcsetattr(fd, TCSANOW, &new_cfg);
 **详细解答**:以 16bit 双声道为例,一帧 = 16/8 × 2 = 4 字节。`period_size=1024` 帧即 4096 字节;`periods=16` 则 buffer = 16×1024 帧。DMA 每搬完一个 period 触发一次中断。整块搬运延迟大,period 太小则中断频繁、CPU 开销高。`buf_bytes = period_size × BlockAlign` 即每次 `snd_pcm_writei/readi` 的数据量。
 
 **追问**
+
 1. overrun 和 underrun 分别发生在播放还是录音?(录音读太慢 → overrun;播放写太慢 → underrun,统称 XRUN)
 2. 发生 XRUN 后如何恢复?(`snd_pcm_prepare()` 回到 PREPARED,或 `snd_pcm_drop()` 停止)
 
@@ -2320,6 +2340,7 @@ tcsetattr(fd, TCSANOW, &new_cfg);
 **详细解答**:阻塞方式下,录音无数据可读、播放缓冲满时调用会阻塞。错误码:`-EBADFD`(状态不对,需 PREPARED/RUNNING)、`-EPIPE`(XRUN,用 `prepare` 恢复)、`-ESTRPIPE`(硬件挂起,用 `resume` 或 `prepare`)。写入帧数小于请求值时,需回退文件读位置重发,见 `pcm_playback.c` 的 `lseek(fd, (ret-period_size)*BlockAlign, SEEK_CUR)`。
 
 **追问**
+
 1. 非交错模式该用什么函数?(`snd_pcm_readn` / `snd_pcm_writen`)
 2. 为什么 `snd_pcm_hw_params()` 后设备就是 PREPARED?(其内部自动调用 `snd_pcm_prepare()`)
 

+ 147 - 90
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/03-外设与高级IO编程/面试-外设与高级IO编程.md

@@ -1,6 +1,33 @@
 ---
 title: 面试-外设与高级IO编程
-tags: [嵌入式Linux, Linux应用编程, 面试, 高级IO, 非阻塞IO, IO多路复用, select, poll, 异步IO, mmap, GPIO, LED, input子系统, tslib, FrameBuffer, LCD, libjpeg, libpng, FreeType, PWM, 看门狗, V4L2, 串口, termios, ALSA]
+tags:
+  [
+    嵌入式Linux,
+    Linux应用编程,
+    面试,
+    高级IO,
+    非阻塞IO,
+    IO多路复用,
+    select,
+    poll,
+    异步IO,
+    mmap,
+    GPIO,
+    LED,
+    input子系统,
+    tslib,
+    FrameBuffer,
+    LCD,
+    libjpeg,
+    libpng,
+    FreeType,
+    PWM,
+    看门狗,
+    V4L2,
+    串口,
+    termios,
+    ALSA,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第十三章 高级I_O、第十五~二十八章 外设应用编程"
@@ -30,12 +57,13 @@ flag |= O_NONBLOCK;
 fcntl(0, F_SETFL, flag);
 ```
 
-| 方式 | 数据不可用时 | CPU 占用 | 适用场景 |
-| ---- | ------------ | -------- | -------- |
-| 阻塞 I/O | 休眠等待,让出 CPU | 几乎为 0 | 单一数据源、无需同时处理多路 |
-| 非阻塞 I/O | 立即返回错误 | 轮询时高 | 需同时处理多路,配合轮询/多路复用 |
+| 方式       | 数据不可用时       | CPU 占用 | 适用场景                          |
+| ---------- | ------------------ | -------- | --------------------------------- |
+| 阻塞 I/O   | 休眠等待,让出 CPU | 几乎为 0 | 单一数据源、无需同时处理多路      |
+| 非阻塞 I/O | 立即返回错误       | 轮询时高 | 需同时处理多路,配合轮询/多路复用 |
 
 **追问**
+
 1. 阻塞式 I/O 的困境是什么?(同时读多个设备时,先读的那个没数据就卡住,后面的读不到,无法并发读取)
 2. 非阻塞 + 轮询有什么缺点?(CPU 空转、占用率高、效率低)
 
@@ -56,13 +84,14 @@ if (ret > 0 && FD_ISSET(mouse_fd, &rdfds)) { /* 鼠标可读 */ }
 
 三个 fd_set 是**值-结果参数**:调用前放入关心的 fd,返回后只保留就绪的 fd。因此在循环调用中**每轮都要重新初始化并设置集合**。
 
-| 返回值 | 含义 |
-| ------ | ---- |
-| `>0` | 就绪 fd 的个数(同一 fd 同时就绪多个事件会计多次) |
-| `0` | 超时,没有任何 fd 就绪;集合被清空 |
-| `-1` | 出错,设置 errno |
+| 返回值 | 含义                                               |
+| ------ | -------------------------------------------------- |
+| `>0`   | 就绪 fd 的个数(同一 fd 同时就绪多个事件会计多次) |
+| `0`    | 超时,没有任何 fd 就绪;集合被清空                 |
+| `-1`   | 出错,设置 errno                                   |
 
 **追问**
+
 1. `timeout` 传 `NULL`、`0`、非 0 分别代表什么?(一直阻塞 / 不阻塞仅轮询一次 / 阻塞上限)
 2. 为什么每次循环都要重新 `FD_ZERO`+`FD_SET`?(select 返回时修改了集合,只保留就绪 fd)
 
@@ -87,6 +116,7 @@ struct pollfd {
 常用事件:`POLLIN`(普通优先级数据可读)、`POLLOUT`(可写)、`POLLPRI`(高优先级数据可读,常用于中断)、`POLLERR`(错误)。判断用 `pfd.revents & POLLIN`。
 
 **追问**
+
 1. `events` 和 `revents` 有什么区别?(events 由调用者设置、poll 不改动;revents 由 poll 填写,表示实际发生的事件)
 2. 监测到可读后如果不读走数据会怎样?(就绪状态一直存在,下次 select/poll 仍立即返回,需读取以清除就绪态)
 
@@ -113,6 +143,7 @@ sigaction(SIGIO, &sa, NULL);         // 注册信号处理函数
 默认通知信号是 `SIGIO`,处理函数可读取 `siginfo_t.si_code`(与 poll 的 revents 一致)、`si_band`。串口例程 `uart_test.c` 正是用 `SIGRTMIN` + `SA_SIGINFO` 读取数据。
 
 **追问**
+
 1. 异步 I/O 相比 select/poll 的优势?(select/poll 内部是轮询,fd 很多时性能低;异步 I/O 由内核主动通知,适合大量 fd)
 2. fd 很多时更好的选择是什么?(epoll,性能类似甚至优于异步 I/O)
 
@@ -122,17 +153,18 @@ sigaction(SIGIO, &sa, NULL);         // 注册信号处理函数
 
 **详细解答**:原型 `void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)`。
 
-| 参数 | 说明 |
-| ---- | ---- |
-| `addr` | 期望映射地址,通常传 `NULL` 由内核选择 |
-| `length` | 映射长度(字节),内核按页对齐 |
-| `prot` | `PROT_READ`/`PROT_WRITE`/`PROT_EXEC`/`PROT_NONE` |
-| `flags` | `MAP_SHARED`(修改写回文件、进程间共享)或 `MAP_PRIVATE`(写时复制、修改不写回) |
-| `fd`/`offset` | 被映射文件描述符与偏移(通常要求页对齐) |
+| 参数          | 说明                                                                             |
+| ------------- | -------------------------------------------------------------------------------- |
+| `addr`        | 期望映射地址,通常传 `NULL` 由内核选择                                           |
+| `length`      | 映射长度(字节),内核按页对齐                                                   |
+| `prot`        | `PROT_READ`/`PROT_WRITE`/`PROT_EXEC`/`PROT_NONE`                                 |
+| `flags`       | `MAP_SHARED`(修改写回文件、进程间共享)或 `MAP_PRIVATE`(写时复制、修改不写回) |
+| `fd`/`offset` | 被映射文件描述符与偏移(通常要求页对齐)                                         |
 
 解除映射用 `int munmap(void *addr, size_t length)`。映射失效会触发 `SIGSEGV`(写只读映射)或 `SIGBUS`(访问已不存在的部分)。LCD 显存、摄像头帧缓冲都靠 mmap 实现"零拷贝"。
 
 **追问**
+
 1. `msync()` 和 `mprotect()` 有什么用?(`msync` 把映射区修改同步到文件/磁盘;`mprotect` 修改映射区保护权限)
 2. 文件锁 `flock()` 与 `fcntl()` 锁有何区别?(`flock` 是 BSD 风格的整文件锁;`fcntl` 支持记录锁/字节范围锁,功能更强)
 
@@ -146,16 +178,17 @@ sigaction(SIGIO, &sa, NULL);         // 注册信号处理函数
 
 **详细解答**:Linux"一切皆文件"。设备文件是硬件向应用层提供的接口,在 `/dev` 下,通过 `open/read/write/ioctl` 操作。另一种是 sysfs:内核对象 → 目录,对象属性 → 文件,对象关系 → 链接。读取属性文件即读设备属性,写属性文件即设置设备状态。
 
-| /sys 子目录 | 作用 |
-| ----------- | ---- |
-| `/sys/devices` | 所有设备,最重要 |
-| `/sys/bus` | 按总线类型分类 |
-| `/sys/class` | 按功能分类(`/sys/class/leds`、`/sys/class/gpio`、`/sys/class/input`、`/sys/class/pwm`) |
-| `/sys/dev` | 按设备号(major:minor)分类 |
+| /sys 子目录    | 作用                                                                                     |
+| -------------- | ---------------------------------------------------------------------------------------- |
+| `/sys/devices` | 所有设备,最重要                                                                         |
+| `/sys/bus`     | 按总线类型分类                                                                           |
+| `/sys/class`   | 按功能分类(`/sys/class/leds`、`/sys/class/gpio`、`/sys/class/input`、`/sys/class/pwm`) |
+| `/sys/dev`     | 按设备号(major:minor)分类                                                              |
 
 简单设备(LED、GPIO)多用 sysfs;复杂设备(LCD、触摸屏、摄像头)多用设备节点。sysfs 属性文件读写的是**字符串(ASCII)**。
 
 **追问**
+
 1. 通常哪些设备用设备节点方式?(较复杂的设备,如 LCD、触摸屏、摄像头)
 2. 杂项设备(misc)为什么叫"杂散"?(无法归入既有设备分类的外设,接口通常非标准化)
 
@@ -165,11 +198,11 @@ sigaction(SIGIO, &sa, NULL);         // 注册信号处理函数
 
 **详细解答**:
 
-| 属性文件 | 读写 | 说明 |
-| -------- | ---- | ---- |
-| `brightness` | 可读可写 | 设置/获取亮度,0 灭、正整数亮(PWM LED 有等级,GPIO LED 只有亮灭) |
-| `max_brightness` | 只读 | 最大亮度等级 |
-| `trigger` | 可读可写 | 触发模式,读可获取支持的所有模式及当前模式(方括号内) |
+| 属性文件         | 读写     | 说明                                                               |
+| ---------------- | -------- | ------------------------------------------------------------------ |
+| `brightness`     | 可读可写 | 设置/获取亮度,0 灭、正整数亮(PWM LED 有等级,GPIO LED 只有亮灭) |
+| `max_brightness` | 只读     | 最大亮度等级                                                       |
+| `trigger`        | 可读可写 | 触发模式,读可获取支持的所有模式及当前模式(方括号内)             |
 
 常用触发模式:`none`(无触发)、`mmc0`(读写 mmc0 时闪烁)、`timer`(定时器规律亮灭)、`heartbeat`(心跳呼吸)。
 
@@ -182,6 +215,7 @@ echo 0 > brightness      # 熄灭
 控制 LED 的程序需先写 `trigger` 为 `none`,再写 `brightness`。
 
 **追问**
+
 1. 为什么 GPIO 控制的 LED 没有亮度等级?(只能输出高低电平,不存在占空比)
 2. 应用程序向属性文件写入的数据格式是什么?(字符串,如 `"1"`,不能直接写二进制整数)
 
@@ -201,6 +235,7 @@ echo 112 > /sys/class/gpio/unexport  # 删除
 若引脚已被内核驱动占用,则导出失败。导出后 `gpioX` 目录下关注 `direction`、`value`、`active_low`、`edge`。
 
 **追问**
+
 1. `active_low` 有什么用?(控制极性:=1 时 `value` 写 1 输出低、写 0 输出高)
 2. 为什么有些 GPIO 导出失败?(已被内核中的驱动使用,无法再由 sysfs 接管)
 
@@ -224,6 +259,7 @@ fprintf(fd, "out");
 输入时写 `direction=in`、`edge=none`,再以 `O_RDONLY` 打开 `value` 读取 1 字节。注意:**只有输入模式下才能用 `cat value` 读电平**,输出模式读不到实际电平。
 
 **追问**
+
 1. GPIO 输出模式下如何验证电平?(外接 LED 或用万用表测量)
 2. `access(path, F_OK)` 的作用?(判断文件/目录是否存在,用于决定是否需要导出)
 
@@ -251,6 +287,7 @@ for (;;) {
 `POLLPRI` 表示有高优先级数据可读,中断即高优先级事件。直接 `read()` 在无中断时会阻塞、且难以区分事件;`poll()` 可同时监视多个 fd 的 I/O 状态变化,也可改用异步 I/O。
 
 **追问**
+
 1. 进入循环前为什么要先 `read()` 一次?(清除可能存在的旧状态,避免 poll 立即返回误报)
 2. `edge` 配置前需要什么前提?(引脚必须配置为输入模式)
 
@@ -276,6 +313,7 @@ struct input_event {
 其中 `type`、`code`、`value` 是解析的核心。
 
 **追问**
+
 1. 输入设备节点在 `/sys` 下如何分类?(`/sys/class/input`)
 2. 一次 `read()` 能读多个事件吗?(通常只读一个;可按结构体大小循环读)
 
@@ -285,11 +323,11 @@ struct input_event {
 
 **详细解答**:
 
-| type | 含义 |
-| ---- | ---- |
-| `EV_SYN`(0x00) | 同步类事件 |
-| `EV_KEY`(0x01) | 按键类事件 |
-| `EV_REL`(0x02) | 相对位移类(鼠标) |
+| type           | 含义                 |
+| -------------- | -------------------- |
+| `EV_SYN`(0x00) | 同步类事件           |
+| `EV_KEY`(0x01) | 按键类事件           |
+| `EV_REL`(0x02) | 相对位移类(鼠标)   |
 | `EV_ABS`(0x03) | 绝对位移类(触摸屏) |
 
 - `code` 指定具体事件,如按键 `KEY_A`、`KEY_VOLUMEDOWN`,绝对位移 `ABS_X`、`ABS_Y`、`ABS_MT_SLOT` 等。
@@ -298,6 +336,7 @@ struct input_event {
 所有输入设备都要上报同步事件(通常 `SYN_REPORT`,value=0),告知本轮数据已完整。
 
 **追问**
+
 1. `value=2` 表示什么?哪些事件不支持?(长按;`BTN_TOUCH` 不支持长按,故 value 不为 2)
 2. 为什么要上报 `EV_SYN`?(一次触摸会产生多个事件,靠 SYN_REPORT 标记一组数据完整)
 
@@ -331,6 +370,7 @@ case EV_SYN:
 手指点击时先上报 `BTN_TOUCH`(value=1),再上报 `ABS_X`/`ABS_Y`,最后 `SYN_REPORT`;滑动时坐标不断更新但不重复上报 `BTN_TOUCH`。
 
 **追问**
+
 1. 为什么不能只靠 `ABS_X`/`ABS_Y` 判断按下?(需配合 BTN_TOUCH 或 TRACKING_ID 判断触摸状态)
 2. 单点触摸需要处理 slot 吗?(不需要,多点才涉及 slot)
 
@@ -363,6 +403,7 @@ case EV_SYN:
 新增点流程:`ABS_MT_TRACKING_ID`(ID≥0) → `ABS_MT_POSITION_X` → `ABS_MT_POSITION_Y` → `BTN_TOUCH`(1) → `ABS_X`/`ABS_Y` → `SYN_REPORT`;松开时上报 `ABS_MT_TRACKING_ID`=-1。
 
 **追问**
+
 1. `input_absinfo` 里有哪些字段?(`value` 最新值、`minimum`/`maximum` 范围、`fuzz`、`flat`、`resolution`)
 2. 为什么有的屏幕没有 `BTN_TOUCH`?(触摸芯片不同,可能只上报 ABS 事件)
 
@@ -382,18 +423,19 @@ ts_read(ts, &samp, 1);                   /* 单点 */
 ts_close(ts);
 ```
 
-| 函数 | 作用 |
-| ---- | ---- |
-| `ts_open(dev, nonblock)` | 打开触摸屏设备 |
-| `ts_setup(dev, nonblock)` | 打开并配置(内部读环境变量、解析 ts.conf) |
-| `ts_config(ts)` | 解析 ts.conf、加载插件 |
-| `ts_read(ts, &samp, nr)` | 读单点触摸 |
-| `ts_read_mt(ts, &samp, max_slots, nr)` | 读多点触摸 |
-| `ts_close(ts)` | 关闭 |
+| 函数                                   | 作用                                       |
+| -------------------------------------- | ------------------------------------------ |
+| `ts_open(dev, nonblock)`               | 打开触摸屏设备                             |
+| `ts_setup(dev, nonblock)`              | 打开并配置(内部读环境变量、解析 ts.conf) |
+| `ts_config(ts)`                        | 解析 ts.conf、加载插件                     |
+| `ts_read(ts, &samp, nr)`               | 读单点触摸                                 |
+| `ts_read_mt(ts, &samp, max_slots, nr)` | 读多点触摸                                 |
+| `ts_close(ts)`                         | 关闭                                       |
 
 关键环境变量:`TSLIB_TSDEVICE`、`TSLIB_CONFFILE`、`TSLIB_PLUGINDIR`、`TSLIB_FBDEVICE`。编译:`${CC} -I .../tslib/include -L .../tslib/lib -lts -o testApp testApp.c`。
 
 **追问**
+
 1. `ts_setup` 相比 `ts_open` 多了什么?(额外完成了设备配置、插件加载)
 2. tslib 提供了哪些测试工具?(`ts_print`、`ts_print_mt`、`ts_test`、`ts_test_mt`)
 
@@ -407,15 +449,16 @@ ts_close(ts);
 
 **详细解答**:应用层操作 `/dev/fbX` 五步:打开 → `FBIOGET_VSCREENINFO` 取可变参数 → `FBIOGET_FSCREENINFO` 取固定参数 → `mmap` 映射显存 → 读写显存。
 
-| ioctl | 第三个参数 | 说明 |
-| ----- | ---------- | ---- |
+| ioctl                         | 第三个参数                   | 说明             |
+| ----------------------------- | ---------------------------- | ---------------- |
 | `FBIOGET_VSCREENINFO`(0x4600) | `struct fb_var_screeninfo *` | 获取可变参数信息 |
-| `FBIOPUT_VSCREENINFO` | `struct fb_var_screeninfo *` | 设置可变参数 |
+| `FBIOPUT_VSCREENINFO`         | `struct fb_var_screeninfo *` | 设置可变参数     |
 | `FBIOGET_FSCREENINFO`(0x4602) | `struct fb_fix_screeninfo *` | 获取固定参数信息 |
 
 `fb_var_screeninfo` 关键字段:`xres`/`yres`(可视区 X/Y 分辨率)、`xres_virtual`/`yres_virtual`、`bits_per_pixel`(bpp)、`red/green/blue`(`struct fb_bitfield`,含 `offset`/`length`)。`fb_fix_screeninfo` 关键字段:`smem_len`(显存长度)、`line_length`(一行字节数)。显存大小可用 `xres * yres * bpp / 8` 或 `line_length * yres`。
 
 **追问**
+
 1. `fb_var_screeninfo` 与 `fb_fix_screeninfo` 有何区别?(前者可修改的运行参数,后者驱动程序设定、只读的固定参数)
 2. 为什么显存大小通常用 `line_length * yres`?(可能存在行填充,`line_length` 才是真实一行字节数)
 
@@ -434,6 +477,7 @@ unsigned short *p = (unsigned short *)base + y * (line_length / 2) + x;
 若 `line_length == xres * pix_bytes`,可简化为 `base + (y * xres + x) * pix_bytes`。
 
 **追问**
+
 1. LCD 横屏转竖屏为什么是应用层的事?(硬件坐标固定,竖屏只是应用"假装"的坐标系,需换算显存地址)
 2. 竖屏 `(x, y)` 对应横屏物理坐标是什么?(如 `(x, 480 - 1 - y)` 等,取决于旋转方向)
 
@@ -455,6 +499,7 @@ unsigned short *p = (unsigned short *)base + y * (line_length / 2) + x;
 RGB565 用 5 位 R、6 位 G、5 位 B。若把 RGB888 数据直接写入 RGB565 显存,颜色会错乱(颜色分量错位、图像"花屏")。LCD 出厂系统通常是 RGB565。
 
 **追问**
+
 1. 为什么 G 分量多 1 位?(人眼对绿色更敏感)
 2. 如何判断屏幕像素格式?(读 `fb_var_screeninfo` 的 `bits_per_pixel` 与 `red/green/blue` 的 `offset`/`length`)
 
@@ -482,6 +527,7 @@ jpeg_destroy_decompress(&cinfo);
 读到的数据在内存中按 B、G、R 字节序排列,故称 BGR888;转 RGB565 时需取对应字节。`cinfo.scale_num/scale_denom` 可缩放解码尺寸。
 
 **追问**
+
 1. libjpeg 的错误处理怎么设置?(`cinfo.err = jpeg_std_error(&jerr)`,可自定义 `error_exit` 回调)
 2. `jpeg_read_scanlines` 的第二个参数类型?(`unsigned char **`,逐行读取)
 
@@ -501,14 +547,15 @@ png_init_io(png_ptr, png_file);
 png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_STRIP_ALPHA, NULL);  // high-level
 ```
 
-| 接口 | 函数 | 特点 |
-| ---- | ---- | ---- |
-| high-level | `png_read_png()` | 一次性解码整图,内部自动分配缓冲,可用 `png_get_rows()` 取数据 |
-| low-level | `png_read_info`→`png_set_*`→`png_read_update_info`→`png_read_image` | 需用户自行分配行缓冲,可控性强 |
+| 接口       | 函数                                                                | 特点                                                           |
+| ---------- | ------------------------------------------------------------------- | -------------------------------------------------------------- |
+| high-level | `png_read_png()`                                                    | 一次性解码整图,内部自动分配缓冲,可用 `png_get_rows()` 取数据 |
+| low-level  | `png_read_info`→`png_set_*`→`png_read_update_info`→`png_read_image` | 需用户自行分配行缓冲,可控性强                                 |
 
 libpng 依赖 zlib,编译需 `-lpng -lz`。
 
 **追问**
+
 1. `png_set_expand`、`png_set_strip_16`、`png_set_gray_to_rgb` 分别做什么?(位深扩展、16→8 位、灰度转 RGB)
 2. FreeType 中 `bearingY` 与 `advance` 的作用?(`bearingY` 用于确定字形顶部 y 坐标,`advance` 是下一个字符的水平步进)
 
@@ -522,14 +569,14 @@ libpng 依赖 zlib,编译需 `-lpng -lz`。
 
 **详细解答**:I.MX6U 有 8 个 PWM 控制器(`pwmchip0`~`pwmchip7`),每个控制器 1 路 PWM,编号都是 0。
 
-| 文件 | 读写 | 说明 |
-| ---- | ---- | ---- |
-| `npwm` | 只读 | 该控制器下 PWM 路数 |
-| `export` / `unexport` | 只写 | 导出/删除 PWM(写编号,从 0 开始) |
-| `period` | 可读写 | PWM 周期,单位 ns |
-| `duty_cycle` | 可读写 | 占空比,单位 ns |
-| `enable` | 可读写 | `1` 使能、`0` 禁止 |
-| `polarity` | 可读写 | `normal`/`inversed`(硬件不支持则无此接口) |
+| 文件                  | 读写   | 说明                                        |
+| --------------------- | ------ | ------------------------------------------- |
+| `npwm`                | 只读   | 该控制器下 PWM 路数                         |
+| `export` / `unexport` | 只写   | 导出/删除 PWM(写编号,从 0 开始)          |
+| `period`              | 可读写 | PWM 周期,单位 ns                           |
+| `duty_cycle`          | 可读写 | 占空比,单位 ns                             |
+| `enable`              | 可读写 | `1` 使能、`0` 禁止                          |
+| `polarity`            | 可读写 | `normal`/`inversed`(硬件不支持则无此接口) |
 
 ```bash
 echo 0 > /sys/class/pwm/pwmchip0/export
@@ -539,6 +586,7 @@ echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable
 ```
 
 **追问**
+
 1. 上面配置的 PWM 频率是多少?(周期 500us → 频率 2KHz,占空比 50%)
 2. 为什么 I.MX6U 出厂系统用不了 PWM1?(PWM1 已被用作 LCD 背光,需在设备树禁用 LCD/backlight 才能释放)
 
@@ -549,6 +597,7 @@ echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable
 **详细解答**:频率 = 1e9 / period(ns)。例如 `period=500000ns` → 2KHz;`duty_cycle=250000ns` → 占空比 50%。`polarity=normal` 时占空比对应高电平时间,`inversed` 则反转。周期单位是 **ns**,写入的是**字符串数字**。
 
 **追问**
+
 1. `period=10000` 对应频率多少?(100KHz)
 2. 很多 SoC 不支持极性配置,表现在哪?(Sysfs 中无 `polarity` 接口,写会失败)
 
@@ -569,6 +618,7 @@ pwm_config("enable", "1");
 测试 PWM1 时输出绑定到 `GPIO1_IO08`(LCD 背光引脚),可用示波器测波形。
 
 **追问**
+
 1. 配置 PWM 为什么要先 `enable=0` 再改参数?(周期/占空比存在约束关系,运行中改可能失败;部分驱动要求先停)
 2. 如何在不重编设备树的情况下临时释放 PWM1?(在 u-boot 里用 `fdt set` 把 backlight、lcdif 的 status 设为 disabled,仅本次启动有效)
 
@@ -581,6 +631,7 @@ pwm_config("enable", "1");
 Linux 中看门狗设备节点为 `/dev/watchdogX`,`/dev/watchdog` 是默认看门狗(通常即 `watchdog0`)。
 
 **追问**
+
 1. 看门狗如何提升系统可靠性?(程序异常/死锁时自动复位,避免系统永久挂死)
 2. 打开看门狗设备后即使没主动开启,关闭时会怎样?(`close()` 时看门狗会自动启动,因此打开后通常先用 `WDIOC_SETOPTIONS` 停止)
 
@@ -590,13 +641,13 @@ Linux 中看门狗设备节点为 `/dev/watchdogX`,`/dev/watchdog` 是默认
 
 **详细解答**:
 
-| ioctl | 参数 | 作用 |
-| ----- | ---- | ---- |
-| `WDIOC_GETSUPPORT` | `struct watchdog_info *` | 获取支持的功能(`options` 字段) |
-| `WDIOC_GETTIMEOUT` | `int *` | 获取当前超时时间(秒) |
-| `WDIOC_SETTIMEOUT` | `int *` | 设置超时时间(秒),不能超过最大值 |
-| `WDIOC_SETOPTIONS` | `int *` | `WDIOS_ENABLECARD` 开启 / `WDIOS_DISABLECARD` 停止 |
-| `WDIOC_KEEPALIVE` | `NULL` | 喂狗 |
+| ioctl              | 参数                     | 作用                                               |
+| ------------------ | ------------------------ | -------------------------------------------------- |
+| `WDIOC_GETSUPPORT` | `struct watchdog_info *` | 获取支持的功能(`options` 字段)                   |
+| `WDIOC_GETTIMEOUT` | `int *`                  | 获取当前超时时间(秒)                             |
+| `WDIOC_SETTIMEOUT` | `int *`                  | 设置超时时间(秒),不能超过最大值                 |
+| `WDIOC_SETOPTIONS` | `int *`                  | `WDIOS_ENABLECARD` 开启 / `WDIOS_DISABLECARD` 停止 |
+| `WDIOC_KEEPALIVE`  | `NULL`                   | 喂狗                                               |
 
 `watchdog_info.options` 常用标志:`WDIOF_SETTIMEOUT`(支持设置超时)、`WDIOF_KEEPALIVEPING`(支持喂狗)。流程:
 
@@ -611,6 +662,7 @@ for (;;) { usleep(...); ioctl(fd, WDIOC_KEEPALIVE, NULL); }      // 喂狗
 代码里打开设备后**立即停止**看门狗,全部设置完再开启;喂狗时间设在超时前(如超时前 100ms)。
 
 **追问**
+
 1. 测试时提示 `Device or resource busy` 怎么办?(看门狗已被系统服务占用,需注释掉 `/etc/init.d/watchdog.sh`)
 2. 程序被 `Ctrl+C` 终止后会怎样?(停止喂狗但看门狗仍在计时,超时后系统复位,内核打印 `watchdog did not stop!`)
 
@@ -624,20 +676,21 @@ for (;;) { usleep(...); ioctl(fd, WDIOC_KEEPALIVE, NULL); }      // 喂狗
 
 **详细解答**:
 
-| 步骤 | ioctl / 操作 |
-| ---- | ------------ |
-| 打开 | `open("/dev/video0", O_RDWR)` |
-| 查询能力 | `VIDIOC_QUERYCAP`,校验含 `V4L2_CAP_VIDEO_CAPTURE` |
+| 步骤     | ioctl / 操作                                                              |
+| -------- | ------------------------------------------------------------------------- |
+| 打开     | `open("/dev/video0", O_RDWR)`                                             |
+| 查询能力 | `VIDIOC_QUERYCAP`,校验含 `V4L2_CAP_VIDEO_CAPTURE`                        |
 | 枚举格式 | `VIDIOC_ENUM_FMT`、`VIDIOC_ENUM_FRAMESIZES`、`VIDIOC_ENUM_FRAMEINTERVALS` |
-| 设置格式 | `VIDIOC_S_FMT`(回读确认),`VIDIOC_G_PARM`/`VIDIOC_S_PARM` 设帧率 |
-| 申请缓冲 | `VIDIOC_REQBUFS`(`V4L2_MEMORY_MMAP`) |
-| 映射 | `VIDIOC_QUERYBUF` + `mmap` |
-| 入队 | `VIDIOC_QBUF` |
-| 开采集 | `VIDIOC_STREAMON` |
-| 取帧 | 循环 `VIDIOC_DQBUF` → 处理 → `VIDIOC_QBUF` |
-| 结束 | `VIDIOC_STREAMOFF` |
+| 设置格式 | `VIDIOC_S_FMT`(回读确认),`VIDIOC_G_PARM`/`VIDIOC_S_PARM` 设帧率        |
+| 申请缓冲 | `VIDIOC_REQBUFS`(`V4L2_MEMORY_MMAP`)                                    |
+| 映射     | `VIDIOC_QUERYBUF` + `mmap`                                                |
+| 入队     | `VIDIOC_QBUF`                                                             |
+| 开采集   | `VIDIOC_STREAMON`                                                         |
+| 取帧     | 循环 `VIDIOC_DQBUF` → 处理 → `VIDIOC_QBUF`                                |
+| 结束     | `VIDIOC_STREAMOFF`                                                        |
 
 **追问**
+
 1. 为什么 `VIDIOC_S_FMT` 后必须回读?(驱动会按硬件能力修改参数,实际值未必等于请求值)
 2. 设备能力里 `V4L2_CAP_STREAMING` 与 `V4L2_CAP_READWRITE` 区别?(前者支持流式 I/O(缓冲队列),后者支持 read/write 方式)
 
@@ -647,20 +700,21 @@ for (;;) { usleep(...); ioctl(fd, WDIOC_KEEPALIVE, NULL); }      // 喂狗
 
 **详细解答**:
 
-| 结构体 | 对应指令 | 关键字段 |
-| ------ | -------- | -------- |
-| `v4l2_capability` | `VIDIOC_QUERYCAP` | `capabilities` |
-| `v4l2_fmtdesc` | `VIDIOC_ENUM_FMT` | `index`、`pixelformat`、`description`、`type` |
-| `v4l2_frmsizeenum` | `VIDIOC_ENUM_FRAMESIZES` | `index`、`pixel_format`、`discrete.width/height` |
-| `v4l2_frmivalenum` | `VIDIOC_ENUM_FRAMEINTERVALS` | `index`、`width/height`、`discrete.numerator/denominator` |
-| `v4l2_format` | `VIDIOC_G_FMT`/`S_FMT` | `type`、`fmt.pix.width/height/pixelformat` |
-| `v4l2_streamparm` | `VIDIOC_G_PARM`/`S_PARM` | `parm.capture.capability`、`timeperframe` |
-| `v4l2_requestbuffers` | `VIDIOC_REQBUFS` | `count`、`type`、`memory` |
-| `v4l2_buffer` | `QUERYBUF`/`QBUF`/`DQBUF` | `index`、`type`、`memory`、`length`、`m.offset` |
+| 结构体                | 对应指令                     | 关键字段                                                  |
+| --------------------- | ---------------------------- | --------------------------------------------------------- |
+| `v4l2_capability`     | `VIDIOC_QUERYCAP`            | `capabilities`                                            |
+| `v4l2_fmtdesc`        | `VIDIOC_ENUM_FMT`            | `index`、`pixelformat`、`description`、`type`             |
+| `v4l2_frmsizeenum`    | `VIDIOC_ENUM_FRAMESIZES`     | `index`、`pixel_format`、`discrete.width/height`          |
+| `v4l2_frmivalenum`    | `VIDIOC_ENUM_FRAMEINTERVALS` | `index`、`width/height`、`discrete.numerator/denominator` |
+| `v4l2_format`         | `VIDIOC_G_FMT`/`S_FMT`       | `type`、`fmt.pix.width/height/pixelformat`                |
+| `v4l2_streamparm`     | `VIDIOC_G_PARM`/`S_PARM`     | `parm.capture.capability`、`timeperframe`                 |
+| `v4l2_requestbuffers` | `VIDIOC_REQBUFS`             | `count`、`type`、`memory`                                 |
+| `v4l2_buffer`         | `QUERYBUF`/`QBUF`/`DQBUF`    | `index`、`type`、`memory`、`length`、`m.offset`           |
 
 `v4l2_fract` 中 `numerator/denominator` 表示采集周期,帧率 = `denominator/numerator`。`pixelformat` 由 `v4l2_fourcc` 合成,如 `V4L2_PIX_FMT_RGB565`、`V4L2_PIX_FMT_YUYV`。
 
 **追问**
+
 1. `v4l2_format` 是 union,`VIDEO_CAPTURE` 时哪个成员生效?(`fmt.pix`)
 2. `V4L2_BUF_TYPE_VIDEO_CAPTURE` 与 `..._MPLANE` 区别?(后者用于多平面格式,`fmt.pix_mp` 生效)
 
@@ -671,6 +725,7 @@ for (;;) { usleep(...); ioctl(fd, WDIOC_KEEPALIVE, NULL); }      // 喂狗
 **详细解答**:`VIDIOC_QBUF`(入队)把缓冲交给驱动,驱动采集一帧填入后缓冲变"满";`VIDIOC_DQBUF`(出队)取走满缓冲,处理完再入队,往复循环。`v4l2_buffer` 的 `index` 标识缓冲编号,`m.offset` 是在内核申请的整块内存中的偏移,配合 `mmap(fd, m.offset)` 得到用户空间地址。处理时"出队→读映射区→处理→入队"。
 
 **追问**
+
 1. 帧缓冲数量为什么不能太多?(嵌入式内存紧张,缓冲多会占用过多系统内存;太少又可能丢帧)
 2. USB 摄像头为什么常不能直接设为 RGB565?(UVC 标准通常输出 YUYV/MJPEG,需应用自行转换)
 
@@ -680,17 +735,18 @@ for (;;) { usleep(...); ioctl(fd, WDIOC_KEEPALIVE, NULL); }      // 喂狗
 
 **详细解答**:
 
-| 字段 | 作用 |
-| ---- | ---- |
-| `c_iflag` | 输入模式(IGNBRK、IGNPAR、INPCK、ICRNL、IXON 等) |
-| `c_oflag` | 输出模式(OPOST、ONLCR 等) |
+| 字段      | 作用                                                                      |
+| --------- | ------------------------------------------------------------------------- |
+| `c_iflag` | 输入模式(IGNBRK、IGNPAR、INPCK、ICRNL、IXON 等)                         |
+| `c_oflag` | 输出模式(OPOST、ONLCR 等)                                               |
 | `c_cflag` | 控制模式(波特率、CSIZE/CS5~CS8、CSTOPB、CREAD、PARENB、PARODD、CRTSCTS) |
-| `c_lflag` | 本地模式(ICANON、ECHO、ISIG 等) |
-| `c_cc[]` | 特殊字符(VMIN、VTIME、VINTR、VEOF 等) |
+| `c_lflag` | 本地模式(ICANON、ECHO、ISIG 等)                                         |
+| `c_cc[]`  | 特殊字符(VMIN、VTIME、VINTR、VEOF 等)                                   |
 
 必须用"按位与/或"增删标志,不能整体赋值。终端三种模式:规范、非规范、原始;串口与传感器做二进制通信时用**原始模式**(`cfmakeraw`)。`VMIN`/`TIME` 仅对非规范/原始模式有效:都置 0 则 `read()` 立即返回。
 
 **追问**
+
 1. 串口为什么常用原始模式?(二进制数据不应被终端解析成 ASCII 或做特殊处理)
 2. `tcsetattr` 的 `TCSANOW`/`TCSADRAIN`/`TCSAFLUSH` 区别?(立即生效 / 输出传完生效 / 生效前丢弃未读输入)
 
@@ -705,6 +761,7 @@ for (;;) { usleep(...); ioctl(fd, WDIOC_KEEPALIVE, NULL); }      // 喂狗
 `snd_pcm_writei/readi` 返回**实际帧数**,失败返回负错误码(`-EPIPE`=XRUN、`-EBADFD`=状态错、`-ESTRPIPE`=挂起);`snd_pcm_hw_params()` 内部会调 `snd_pcm_prepare()`,设备进入 `SND_PCM_STATE_PREPARED`。混音器用 `snd_mixer_open/attach/selem_register/load` 后遍历元素,用 `snd_mixer_selem_set_playback_volume_all` 调音量。
 
 **追问**
+
 1. overrun 与 underrun 分别指什么?(录音应用读太慢导致数据被覆盖=overrun;播放应用写太慢导致缓冲饿死=underrun,统称 XRUN)
 2. 为什么示例没有调用 `snd_pcm_start()`?(PREPARED 状态下首次 `writei/readi` 会自动调用 `snd_pcm_start()`)
 

+ 193 - 178
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/04-网络编程与项目实战/01-网络基础与socket编程.md

@@ -1,6 +1,21 @@
 ---
 title: 网络基础与socket编程
-tags: [嵌入式Linux, Linux应用编程, 网络编程, socket, TCP, UDP, OSI, IP地址, 端口号, 字节序, htonl, inet_pton, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    Linux应用编程,
+    网络编程,
+    socket,
+    TCP,
+    UDP,
+    OSI,
+    IP地址,
+    端口号,
+    字节序,
+    htonl,
+    inet_pton,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第二十九章 网络基础知识、第三十章 socket编程基础"
@@ -18,10 +33,10 @@ pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第二十九章 网
 
 网络通信分三个层次:
 
-| 层次 | 角色 | 说明 |
-| ---- | ---- | ---- |
-| 硬件层 | 网卡设备 | 收发网络数据的物理基础 |
-| 驱动层 | 网卡驱动 | 内核中的网卡驱动,向应用层提供 socket 接口 |
+| 层次   | 角色     | 说明                                                  |
+| ------ | -------- | ----------------------------------------------------- |
+| 硬件层 | 网卡设备 | 收发网络数据的物理基础                                |
+| 驱动层 | 网卡驱动 | 内核中的网卡驱动,向应用层提供 socket 接口            |
 | 应用层 | 应用程序 | 基于 socket 接口编程,或使用 http、网络控件等更高封装 |
 
 硬件上两台主机各有网卡,通过传输媒介相连(有线:双绞线、光纤;无线:WiFi、蓝牙、ZigBee、4G/5G)。内核驱动底层网卡并暴露 socket 接口;应用层调用 socket 接口即所谓网络编程。
@@ -41,15 +56,15 @@ flowchart TB
 
 OSI(Open System Interconnection)是国际标准化组织(ISO)制定的网络互连标准体系,从上到下依次为:
 
-| 层 | 名称 | 主要功能 | 典型协议 / 设备 |
-| -- | ---- | -------- | --------------- |
-| 7 | 应用层 | 为用户提供应用接口与网络服务 | HTTP、FTP、TFTP、SMTP、SNMP、DNS、TELNET、HTTPS、POP3、DHCP |
-| 6 | 表示层 | 数据编码/转换、压缩/解压缩、加密/解密 | — |
-| 5 | 会话层 | 建立、管理和终止通信会话 | — |
-| 4 | 传输层 | 定义端口号、端到端流控和差错校验 | TCP、UDP |
-| 3 | 网络层 | 逻辑地址寻址、路由选择(IP 层) | IP(IPv4/IPv6)、ICMP、IGMP |
-| 2 | 数据链路层 | 成帧、硬件地址寻址、差错检测(分 LLC / MAC 子层) | 以太网、MAC |
-| 1 | 物理层 | 为数据链路层提供物理连接,透明传输比特流 | 集线器、中继器、网线、双绞线、同轴电缆 |
+| 层  | 名称       | 主要功能                                          | 典型协议 / 设备                                             |
+| --- | ---------- | ------------------------------------------------- | ----------------------------------------------------------- |
+| 7   | 应用层     | 为用户提供应用接口与网络服务                      | HTTP、FTP、TFTP、SMTP、SNMP、DNS、TELNET、HTTPS、POP3、DHCP |
+| 6   | 表示层     | 数据编码/转换、压缩/解压缩、加密/解密             | —                                                           |
+| 5   | 会话层     | 建立、管理和终止通信会话                          | —                                                           |
+| 4   | 传输层     | 定义端口号、端到端流控和差错校验                  | TCP、UDP                                                    |
+| 3   | 网络层     | 逻辑地址寻址、路由选择(IP 层)                   | IP(IPv4/IPv6)、ICMP、IGMP                                   |
+| 2   | 数据链路层 | 成帧、硬件地址寻址、差错检测(分 LLC / MAC 子层) | 以太网、MAC                                                 |
+| 1   | 物理层     | 为数据链路层提供物理连接,透明传输比特流          | 集线器、中继器、网线、双绞线、同轴电缆                      |
 
 分层的意义:网络通信极其复杂,分层后每层只解决一类问题,层间通过接口协作,便于设计、实现与排错。
 
@@ -57,12 +72,12 @@ OSI(Open System Interconnection)是国际标准化组织(ISO)制定的
 
 TCP/IP 模型是 OSI 的简化版本:
 
-| OSI 七层 | TCP/IP 五层 | TCP/IP 四层 |
-| -------- | ----------- | ----------- |
-| 应用层、表示层、会话层 | 应用层 | 应用层 |
-| 传输层 | 传输层 | 传输层 |
-| 网络层 | 网络层 | 网络层 |
-| 数据链路层 + 物理层 | 数据链路层、物理层 | 网络接口层 |
+| OSI 七层               | TCP/IP 五层        | TCP/IP 四层 |
+| ---------------------- | ------------------ | ----------- |
+| 应用层、表示层、会话层 | 应用层             | 应用层      |
+| 传输层                 | 传输层             | 传输层      |
+| 网络层                 | 网络层             | 网络层      |
+| 数据链路层 + 物理层    | 数据链路层、物理层 | 网络接口层  |
 
 五层模型专为介绍网络原理而设计;四层模型(应用层、传输层、网络层、网络接口层)是实际工程所用。
 
@@ -110,25 +125,25 @@ Internet 上每个节点依靠唯一的 **IP 地址**相互区分。IP 是**软
 
 ### 3.1 IP 地址分类
 
-| 类别 | 第一字节范围 | 网络/主机地址 | 默认子网掩码 | 高位特征 | 用途 |
-| ---- | ------------ | ------------- | ------------ | -------- | ---- |
-| A | 1 ~ 126 | 1 字节网络 + 3 字节主机 | 255.0.0.0 | 最高位 0 | 特大网络 |
-| B | 128 ~ 191 | 2 字节网络 + 2 字节主机 | 255.255.0.0 | 最高位 10 | 中型网络 |
-| C | 192 ~ 223 | 3 字节网络 + 1 字节主机 | 255.255.255.0 | 最高位 110 | 小型网络、局域网 |
-| D | 224 ~ 239 | 不分网络/主机 | — | 高四位 1110 | 多播 Multicast |
-| E | 240 ~ 255 | 不分网络/主机 | — | 高五位 11110 | 保留,暂未使用 |
+| 类别 | 第一字节范围 | 网络/主机地址           | 默认子网掩码  | 高位特征     | 用途             |
+| ---- | ------------ | ----------------------- | ------------- | ------------ | ---------------- |
+| A    | 1 ~ 126      | 1 字节网络 + 3 字节主机 | 255.0.0.0     | 最高位 0     | 特大网络         |
+| B    | 128 ~ 191    | 2 字节网络 + 2 字节主机 | 255.255.0.0   | 最高位 10    | 中型网络         |
+| C    | 192 ~ 223    | 3 字节网络 + 1 字节主机 | 255.255.255.0 | 最高位 110   | 小型网络、局域网 |
+| D    | 224 ~ 239    | 不分网络/主机           | —             | 高四位 1110  | 多播 Multicast   |
+| E    | 240 ~ 255    | 不分网络/主机           | —             | 高五位 11110 | 保留,暂未使用   |
 
 三类的私有地址(公网不可用,仅局域网):A 类 `10.0.0.0 ~ 10.255.255.255`;B 类 `172.16.0.0 ~ 172.31.255.255`;C 类 `192.168.0.0 ~ 192.168.255.255`。另外 A 类 `127.X.X.X` 为环回保留地址,B 类 `169.254.X.X` 为自动获取 IP 失败时的保留地址。
 
 ### 3.2 特殊 IP 地址
 
-| 名称 | 地址形式 | 说明 |
-| ---- | -------- | ---- |
-| 直接广播地址 | 主机号全 1,如 `192.168.0.255` | 向某网络内所有主机发报文 |
-| 受限广播地址 | `255.255.255.255`(32 位全 1) | 本网络内部广播,启动时不知网络号也可用 |
-| 多播地址 | D 类 `224.0.0.1 ~ 239.255.255.254` | 一对多通信,只能作目的地址 |
-| 环回地址 | `127.X.X.X` | 测试网络协议、本机进程通信;不能作主机 IP |
-| `0.0.0.0` | 32 位全 0 | 表示本网络上的本主机,只能作源地址;不能被 ping 通 |
+| 名称         | 地址形式                           | 说明                                               |
+| ------------ | ---------------------------------- | -------------------------------------------------- |
+| 直接广播地址 | 主机号全 1,如 `192.168.0.255`     | 向某网络内所有主机发报文                           |
+| 受限广播地址 | `255.255.255.255`(32 位全 1)     | 本网络内部广播,启动时不知网络号也可用             |
+| 多播地址     | D 类 `224.0.0.1 ~ 239.255.255.254` | 一对多通信,只能作目的地址                         |
+| 环回地址     | `127.X.X.X`                        | 测试网络协议、本机进程通信;不能作主机 IP          |
+| `0.0.0.0`    | 32 位全 0                          | 表示本网络上的本主机,只能作源地址;不能被 ping 通 |
 
 服务器中 `0.0.0.0`(`INADDR_ANY`)表示监听本机所有 IPv4 地址的该端口。
 
@@ -147,12 +162,12 @@ Internet 上每个节点依靠唯一的 **IP 地址**相互区分。IP 是**软
 
 一台主机通常只有一个 IP,但会运行多个联网进程。IP 只能区分主机,无法区分进程,于是引入端口号:它是用来在主机中**唯一标识一个能上网进程**的数字编号,取值范围 **0 ~ 65535**,通过「IP + 端口号」区分主机内不同进程。
 
-| 服务 | 端口 | 说明 | 服务 | 端口 | 说明 |
-| ---- | ---- | ---- | ---- | ---- | ---- |
-| HTTP | 80 | 超文本传输协议 | SSH | 22 | 安全外壳协议 |
-| FTP | 21 | 文件传输协议 | Telnet | 23 | 终端远程登录 |
-| SMTP | 25 | 简单邮件传输协议 | POP3 | 110 | 邮局协议 v3 |
-| TFTP | 69 | 简单文件传输协议 | — | — | — |
+| 服务 | 端口 | 说明             | 服务   | 端口 | 说明         |
+| ---- | ---- | ---------------- | ------ | ---- | ------------ |
+| HTTP | 80   | 超文本传输协议   | SSH    | 22   | 安全外壳协议 |
+| FTP  | 21   | 文件传输协议     | Telnet | 23   | 终端远程登录 |
+| SMTP | 25   | 简单邮件传输协议 | POP3   | 110  | 邮局协议 v3  |
+| TFTP | 69   | 简单文件传输协议 | —      | —    | —            |
 
 > 自编服务器端口不能与其它服务冲突,**不常用端口通常大于 5000**。
 
@@ -162,20 +177,20 @@ Internet 上每个节点依靠唯一的 **IP 地址**相互区分。IP 是**软
 
 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。(原书仅在 `bind()` 示例中说明 `htons`/`htonl` 用于避免大小端问题。)
 
-| 概念 | 含义 |
-| ---- | ---- |
-| 大端序 Big-Endian | 低地址存放**高位**字节(网络字节序即大端) |
+| 概念                 | 含义                                                 |
+| -------------------- | ---------------------------------------------------- |
+| 大端序 Big-Endian    | 低地址存放**高位**字节(网络字节序即大端)           |
 | 小端序 Little-Endian | 低地址存放**低位**字节(x86、多数 ARM 的主机字节序) |
-| 网络字节序 | TCP/IP 规定的统一字节序,等同于大端序 |
+| 网络字节序           | TCP/IP 规定的统一字节序,等同于大端序                |
 
 以 32 位数 `0x12345678` 为例,内存低地址到高地址:大端为 `12 34 56 78`,小端为 `78 56 34 12`。
 
-| 函数 | 含义 | 方向 | 宽度 |
-| ---- | ---- | ---- | ---- |
-| `htons` | host to network short | 主机 → 网络 | 16 位(端口) |
-| `htonl` | host to network long | 主机 → 网络 | 32 位(IP/整数) |
-| `ntohs` | network to host short | 网络 → 主机 | 16 位 |
-| `ntohl` | network to host long | 网络 → 主机 | 32 位 |
+| 函数    | 含义                  | 方向        | 宽度             |
+| ------- | --------------------- | ----------- | ---------------- |
+| `htons` | host to network short | 主机 → 网络 | 16 位(端口)    |
+| `htonl` | host to network long  | 主机 → 网络 | 32 位(IP/整数) |
+| `ntohs` | network to host short | 网络 → 主机 | 16 位            |
+| `ntohl` | network to host long  | 网络 → 主机 | 32 位            |
 
 使用要点:`sin_port` 必须用 `htons()`;`inet_pton()`/`inet_addr()` 得到的地址**已是网络字节序**,无需再 `htonl`;若手工给 `s_addr` 赋主机字节序整数则需 `htonl()`;`INADDR_ANY` 为 0,转换后仍为 0。
 
@@ -192,15 +207,15 @@ TCP(Transmission Control Protocol,传输控制协议)是**面向连接的
 
 ### 6.1 核心特点
 
-| 特点 | 说明 |
-| ---- | ---- |
-| 面向连接 | 通信前必须三次握手建立连接 |
-| 确认与重传 | 每个报文段需对端 ACK;超时未收到应答则重传 |
-| 全双工 | 连接建立后数据双向独立流通 |
-| 基于字节流 | 按字节编号,接收端用 ACK 确认,保证有序完整 |
-| 流量控制 | 滑动窗口:接收端告知发送端自己的缓存大小 |
-| 差错控制 | 校验和检验有效性,丢弃重复、重组乱序、请求重传 |
-| 拥塞控制 | 慢启动、拥塞避免、快重传、快恢复 |
+| 特点       | 说明                                           |
+| ---------- | ---------------------------------------------- |
+| 面向连接   | 通信前必须三次握手建立连接                     |
+| 确认与重传 | 每个报文段需对端 ACK;超时未收到应答则重传     |
+| 全双工     | 连接建立后数据双向独立流通                     |
+| 基于字节流 | 按字节编号,接收端用 ACK 确认,保证有序完整    |
+| 流量控制   | 滑动窗口:接收端告知发送端自己的缓存大小       |
+| 差错控制   | 校验和检验有效性,丢弃重复、重组乱序、请求重传 |
+| 拥塞控制   | 慢启动、拥塞避免、快重传、快恢复               |
 
 **保证可靠传输的三个机制**:① 发送应答机制——每个报文段须得到接收方应答;② 超时重传机制——发送后启动定时器,超时未应答就重发;③ 重排整理机制——IP 数据报可能乱序、重复,TCP 重排去重后再交付应用层。
 
@@ -210,27 +225,27 @@ TCP(Transmission Control Protocol,传输控制协议)是**面向连接的
 
 TCP 报文 = TCP 首部 + 数据区,首部通常 **20 字节**。
 
-| 字段 | 长度 | 说明 |
-| ---- | ---- | ---- |
-| 源端口号 / 目标端口号 | 各 2 字节 | 标识收发端应用进程 |
-| 序号 Sequence Number | 4 字节 | 本报文段第一个数据字节的编号 |
-| 确认序号 Acknowledge Number | 4 字节 | 期望收到的下一个字节编号,仅 ACK=1 时有效 |
-| 首部长度 | 4 bit | 以 4 字节为单位,最大 15×4=60 字节 |
-| 保留位 | 6 bit | 目前置 0 |
-| 标志位 | 6 bit | URG / ACK / PSH / RST / SYN / FIN |
-| 窗口大小 | 2 字节 | 接收窗口大小,用于流量控制 |
-| 校验和 | 2 字节 | 覆盖首部与数据,强制字段 |
-| 紧急指针 | 2 字节 | 紧急数据最后一个字节的序号 |
-| 选项 | 最多 40 字节 | 可选 |
-
-| 标志 | 置位含义 |
-| ---- | -------- |
-| URG | 紧急指针字段有效 |
-| ACK | 确认序号字段有效 |
-| PSH | 接收方应尽快把本报文段交给应用层 |
-| RST | 连接出现错误,必须释放连接后重建 |
-| SYN | 请求建立连接 |
-| FIN | 发送方无数据要发送,要求释放连接 |
+| 字段                        | 长度         | 说明                                      |
+| --------------------------- | ------------ | ----------------------------------------- |
+| 源端口号 / 目标端口号       | 各 2 字节    | 标识收发端应用进程                        |
+| 序号 Sequence Number        | 4 字节       | 本报文段第一个数据字节的编号              |
+| 确认序号 Acknowledge Number | 4 字节       | 期望收到的下一个字节编号,仅 ACK=1 时有效 |
+| 首部长度                    | 4 bit        | 以 4 字节为单位,最大 15×4=60 字节        |
+| 保留位                      | 6 bit        | 目前置 0                                  |
+| 标志位                      | 6 bit        | URG / ACK / PSH / RST / SYN / FIN         |
+| 窗口大小                    | 2 字节       | 接收窗口大小,用于流量控制                |
+| 校验和                      | 2 字节       | 覆盖首部与数据,强制字段                  |
+| 紧急指针                    | 2 字节       | 紧急数据最后一个字节的序号                |
+| 选项                        | 最多 40 字节 | 可选                                      |
+
+| 标志 | 置位含义                         |
+| ---- | -------------------------------- |
+| URG  | 紧急指针字段有效                 |
+| ACK  | 确认序号字段有效                 |
+| PSH  | 接收方应尽快把本报文段交给应用层 |
+| RST  | 连接出现错误,必须释放连接后重建 |
+| SYN  | 请求建立连接                     |
+| FIN  | 发送方无数据要发送,要求释放连接 |
 
 > 小写 **ack** 指首部中的「确认序号」,大写 **ACK** 指标志位,二者不同。
 
@@ -249,11 +264,11 @@ sequenceDiagram
     Note over C,S: 双方进入 ESTABLISHED,连接建立成功
 ```
 
-| 握手 | 发送方 | 内容 | 状态变化 |
-| ---- | ------ | ---- | -------- |
-| 第一次 | 客户端 | `SYN=1, seq=J` | 客户端 → SYN_SENT |
-| 第二次 | 服务器 | `SYN=1, ACK=1, ack=J+1, seq=K` | 服务器 → SYN_RCVD |
-| 第三次 | 客户端 | `ACK=1, ack=K+1` | 双方 → ESTABLISHED |
+| 握手   | 发送方 | 内容                           | 状态变化           |
+| ------ | ------ | ------------------------------ | ------------------ |
+| 第一次 | 客户端 | `SYN=1, seq=J`                 | 客户端 → SYN_SENT  |
+| 第二次 | 服务器 | `SYN=1, ACK=1, ack=J+1, seq=K` | 服务器 → SYN_RCVD  |
+| 第三次 | 客户端 | `ACK=1, ack=K+1`               | 双方 → ESTABLISHED |
 
 握手完成后双方都获得彼此的窗口大小、序列号;传输阶段 SYN 为 0(只用于发起连接、同步序号)。**为什么需要三次握手**:类比打电话——A 问「你能听到我吗?」,B 答「我听到了,你能听到我吗?」,A 再答「我能听到」。三次互相确认后,双方才确信「对方能听到自己」,才能开始可靠通信。
 
@@ -278,12 +293,12 @@ sequenceDiagram
     Note over C,S: 服务器收到 ACK 后 CLOSED;客户端 2MSL 后 CLOSED
 ```
 
-| 挥手 | 发送方 | 内容 | 状态变化 |
-| ---- | ------ | ---- | -------- |
-| 第一次 | 主动方 | `FIN=1` | 主动方 → FIN_WAIT_1 |
+| 挥手   | 发送方 | 内容            | 状态变化                                 |
+| ------ | ------ | --------------- | ---------------------------------------- |
+| 第一次 | 主动方 | `FIN=1`         | 主动方 → FIN_WAIT_1                      |
 | 第二次 | 被动方 | `ACK=1, ack=+1` | 被动方 → CLOSE_WAIT,主动方 → FIN_WAIT_2 |
-| 第三次 | 被动方 | `FIN=1` | 被动方 → LAST_ACK |
-| 第四次 | 主动方 | `ACK=1, ack=+1` | 主动方 → TIME_WAIT,等待 2MSL 后 CLOSED |
+| 第三次 | 被动方 | `FIN=1`         | 被动方 → LAST_ACK                        |
+| 第四次 | 主动方 | `ACK=1, ack=+1` | 主动方 → TIME_WAIT,等待 2MSL 后 CLOSED  |
 
 ---
 
@@ -291,23 +306,23 @@ sequenceDiagram
 
 UDP(User Datagram Protocol,用户数据报协议)是**无连接、不可靠**的传输层协议。发送端把上层数据加 UDP 首部、简单校验后交给 IP 层;接收端简单校验,出错即丢弃且不通知源主机。到达顺序无法预知,需应用层自行排序。
 
-| 特点 | 说明 |
-| ---- | ---- |
+| 特点           | 说明                               |
+| -------------- | ---------------------------------- |
 | 无连接、不可靠 | 不需握手,报文可能丢失、乱序、重复 |
-| 无反馈 | 出现差错直接丢弃,不产生应答 |
-| 面向报文 | 直接添加 UDP 首部,不拆分合并 |
-| 速度快 | 无握手、确认、窗口、重传、拥塞控制 |
+| 无反馈         | 出现差错直接丢弃,不产生应答       |
+| 面向报文       | 直接添加 UDP 首部,不拆分合并      |
+| 速度快         | 无握手、确认、窗口、重传、拥塞控制 |
 
 UDP 适用于**实时性要求高、可容忍少量差错**的场景,如直播、网络电话、实时视频。
 
-| 维度 | TCP | UDP |
-| ---- | --- | --- |
-| 连接 | 面向连接(三次握手) | 无连接 |
-| 可靠性 | 可靠(确认、重传、排序) | 不可靠 |
-| 流量/拥塞控制 | 有 | 无 |
-| 传输单位 | 字节流 | 数据报 |
-| 速度 | 较慢 | 快 |
-| 典型应用 | 文件传输、Web、SSH | 直播、VoIP、DNS、广播/多播 |
+| 维度          | TCP                      | UDP                        |
+| ------------- | ------------------------ | -------------------------- |
+| 连接          | 面向连接(三次握手)     | 无连接                     |
+| 可靠性        | 可靠(确认、重传、排序) | 不可靠                     |
+| 流量/拥塞控制 | 有                       | 无                         |
+| 传输单位      | 字节流                   | 数据报                     |
+| 速度          | 较慢                     | 快                         |
+| 典型应用      | 文件传输、Web、SSH       | 直播、VoIP、DNS、广播/多播 |
 
 ---
 
@@ -336,25 +351,25 @@ int socket(int domain, int type, int protocol);
 
 常用协议族 `domain`:
 
-| domain | 说明 |
-| ------ | ---- |
-| `AF_UNIX`/`AF_LOCAL` | 本地通信 |
-| `AF_INET` | IPv4 |
-| `AF_INET6` | IPv6 |
-| `AF_NETLINK` | 内核与用户空间接口 |
-| `AF_PACKET` | 底层数据包接口 |
-| `AF_ALG` | 内核加密 API 接口 |
+| domain               | 说明               |
+| -------------------- | ------------------ |
+| `AF_UNIX`/`AF_LOCAL` | 本地通信           |
+| `AF_INET`            | IPv4               |
+| `AF_INET6`           | IPv6               |
+| `AF_NETLINK`         | 内核与用户空间接口 |
+| `AF_PACKET`          | 底层数据包接口     |
+| `AF_ALG`             | 内核加密 API 接口  |
 
 套接字类型 `type`:
 
-| type | 说明 |
-| ---- | ---- |
-| `SOCK_STREAM` | 有序可靠双向、基于连接的字节流,用于 TCP |
-| `SOCK_DGRAM` | 固定长度、无连接、不可靠报文,用于 UDP |
-| `SOCK_SEQPACKET` | 固定长度、有序、可靠、面向连接的报文 |
-| `SOCK_RAW` | 原始套接字,可访问网络层原始数据包 |
-| `SOCK_RDM` | 不保证排序的可靠数据报层 |
-| `SOCK_PACKET` | 已过时,不应使用 |
+| type             | 说明                                     |
+| ---------------- | ---------------------------------------- |
+| `SOCK_STREAM`    | 有序可靠双向、基于连接的字节流,用于 TCP |
+| `SOCK_DGRAM`     | 固定长度、无连接、不可靠报文,用于 UDP   |
+| `SOCK_SEQPACKET` | 固定长度、有序、可靠、面向连接的报文     |
+| `SOCK_RAW`       | 原始套接字,可访问网络层原始数据包       |
+| `SOCK_RDM`       | 不保证排序的可靠数据报层                 |
+| `SOCK_PACKET`    | 已过时,不应使用                         |
 
 `protocol` 通常为 0,表示使用默认协议(`AF_INET`+`SOCK_STREAM` 默认 TCP,`AF_INET`+`SOCK_DGRAM` 默认 UDP)。
 
@@ -440,27 +455,27 @@ ssize_t send(int sockfd, const void *buf, size_t len, int flags);
 
 `recv` 标志:
 
-| recv 标志 | 描述 |
-| --------- | ---- |
+| recv 标志          | 描述                                               |
+| ------------------ | -------------------------------------------------- |
 | `MSG_CMSG_CLOEXEC` | 为 UNIX 域套接字接收的文件描述符设置执行时关闭标志 |
-| `MSG_DONTWAIT` | 非阻塞操作(相当于 `O_NONBLOCK`) |
-| `MSG_ERRQUEUE` | 接收错误信息作为辅助数据 |
-| `MSG_OOB` | 如果协议支持,获取带外数据 |
-| `MSG_PEEK` | 返回数据包内容但不真正取走 |
-| `MSG_TRUNC` | 即使数据包被截断也返回其长度 |
-| `MSG_WAITALL` | 等待所有数据可用(仅 `SOCK_STREAM`) |
+| `MSG_DONTWAIT`     | 非阻塞操作(相当于 `O_NONBLOCK`)                  |
+| `MSG_ERRQUEUE`     | 接收错误信息作为辅助数据                           |
+| `MSG_OOB`          | 如果协议支持,获取带外数据                         |
+| `MSG_PEEK`         | 返回数据包内容但不真正取走                         |
+| `MSG_TRUNC`        | 即使数据包被截断也返回其长度                       |
+| `MSG_WAITALL`      | 等待所有数据可用(仅 `SOCK_STREAM`)               |
 
 `send` 标志:
 
-| send 标志 | 描述 |
-| --------- | ---- |
-| `MSG_CONFIRM` | 提供链路层反馈以保持地址映射有效 |
-| `MSG_DONTROUTE` | 勿将数据包路由出本地网络 |
-| `MSG_DONTWAIT` | 允许非阻塞操作 |
-| `MSG_EOR` | 如果协议支持,标志记录结束 |
-| `MSG_MORE` | 延迟发送数据包,允许写更多数据 |
-| `MSG_NOSIGNAL` | 写无连接套接字时不产生 `SIGPIPE` |
-| `MSG_OOB` | 如果协议支持,发送带外数据 |
+| send 标志       | 描述                             |
+| --------------- | -------------------------------- |
+| `MSG_CONFIRM`   | 提供链路层反馈以保持地址映射有效 |
+| `MSG_DONTROUTE` | 勿将数据包路由出本地网络         |
+| `MSG_DONTWAIT`  | 允许非阻塞操作                   |
+| `MSG_EOR`       | 如果协议支持,标志记录结束       |
+| `MSG_MORE`      | 延迟发送数据包,允许写更多数据   |
+| `MSG_NOSIGNAL`  | 写无连接套接字时不产生 `SIGPIPE` |
+| `MSG_OOB`       | 如果协议支持,发送带外数据       |
 
 要点:通常 `flags` 为 0;对 `SOCK_STREAM` 一次 `recv` 可能少于请求字节数,`MSG_WAITALL` 可阻止;对 `SOCK_DGRAM`/`SOCK_SEQPACKET` 一次读取返回整个报文;发送者关闭后数据收完 `recv` 返回 0;`send()` 成功不代表对方进程已收到,只保证数据无错误地发到网络驱动。
 
@@ -472,11 +487,11 @@ ssize_t send(int sockfd, const void *buf, size_t len, int flags);
 
 需包含 `<sys/socket.h>`、`<arpa/inet.h>`、`<netinet/in.h>`。
 
-| 函数 | 作用 | 说明 |
-| ---- | ---- | ---- |
-| `inet_aton` | 字符串 → 二进制 | 已废弃,旧代码可见 |
-| `inet_addr` | 字符串 → 二进制 | 已废弃 |
-| `inet_ntoa` | 二进制 → 字符串 | 已废弃 |
+| 函数        | 作用            | 说明                 |
+| ----------- | --------------- | -------------------- |
+| `inet_aton` | 字符串 → 二进制 | 已废弃,旧代码可见   |
+| `inet_addr` | 字符串 → 二进制 | 已废弃               |
+| `inet_ntoa` | 二进制 → 字符串 | 已废弃               |
 | `inet_pton` | 字符串 → 二进制 | 支持 IPv4/IPv6,推荐 |
 | `inet_ntop` | 二进制 → 字符串 | 支持 IPv4/IPv6,推荐 |
 
@@ -602,19 +617,19 @@ int main(void)
 }
 ```
 
-| 代码段 | 作用 |
-| ------ | ---- |
-| `socket(AF_INET, SOCK_STREAM, 0)` | 创建 IPv4 TCP 套接字,返回监听套接字 `sockfd` |
-| `sin_family = AF_INET` | 协议族设为 IPv4 |
-| `sin_addr.s_addr = htonl(INADDR_ANY)` | 绑定本机所有 IPv4 地址 |
-| `sin_port = htons(8888)` | 绑定端口 8888,注意字节序转换 |
-| `bind()` | 把套接字与地址、端口关联 |
-| `listen(sockfd, 50)` | 进入监听状态,等待队列上限 50 |
-| `accept()` | 阻塞等待客户端,返回代表该连接的 `connfd` |
-| `inet_ntop()` | 把客户端 IP 从二进制转为点分十进制字符串 |
-| `recv()` 循环 | 不断读取客户端数据;返回 ≤0 表示连接关闭或出错 |
-| `strncmp("exit", recvbuf, 4)` | 收到 `exit` 时结束服务 |
-| `close(connfd)` / `close(sockfd)` | 关闭连接套接字与监听套接字 |
+| 代码段                                | 作用                                           |
+| ------------------------------------- | ---------------------------------------------- |
+| `socket(AF_INET, SOCK_STREAM, 0)`     | 创建 IPv4 TCP 套接字,返回监听套接字 `sockfd`  |
+| `sin_family = AF_INET`                | 协议族设为 IPv4                                |
+| `sin_addr.s_addr = htonl(INADDR_ANY)` | 绑定本机所有 IPv4 地址                         |
+| `sin_port = htons(8888)`              | 绑定端口 8888,注意字节序转换                  |
+| `bind()`                              | 把套接字与地址、端口关联                       |
+| `listen(sockfd, 50)`                  | 进入监听状态,等待队列上限 50                  |
+| `accept()`                            | 阻塞等待客户端,返回代表该连接的 `connfd`      |
+| `inet_ntop()`                         | 把客户端 IP 从二进制转为点分十进制字符串       |
+| `recv()` 循环                         | 不断读取客户端数据;返回 ≤0 表示连接关闭或出错 |
+| `strncmp("exit", recvbuf, 4)`         | 收到 `exit` 时结束服务                         |
+| `close(connfd)` / `close(sockfd)`     | 关闭连接套接字与监听套接字                     |
 
 > 服务器用 `%d` 直接打印 `client_addr.sin_port` 时得到的仍是**网络字节序**,要显示正确端口应先 `ntohs()`。
 
@@ -676,14 +691,14 @@ int main(void)
 }
 ```
 
-| 代码段 | 作用 |
-| ------ | ---- |
-| `socket(AF_INET, SOCK_STREAM, 0)` | 创建 TCP 套接字 |
-| `sin_port = htons(SERVER_PORT)` | 服务器端口,需网络字节序 |
+| 代码段                               | 作用                             |
+| ------------------------------------ | -------------------------------- |
+| `socket(AF_INET, SOCK_STREAM, 0)`    | 创建 TCP 套接字                  |
+| `sin_port = htons(SERVER_PORT)`      | 服务器端口,需网络字节序         |
 | `inet_pton(AF_INET, SERVER_IP, ...)` | 把点分十进制字符串转为二进制地址 |
-| `connect()` | 发起三次握手连接服务器 |
-| `fgets()` + `send()` | 读取用户输入并发送给服务器 |
-| `strncmp("exit", buf, 4)` | 输入 `exit` 时结束 |
+| `connect()`                          | 发起三次握手连接服务器           |
+| `fgets()` + `send()`                 | 读取用户输入并发送给服务器       |
+| `strncmp("exit", buf, 4)`            | 输入 `exit` 时结束               |
 
 > `SERVER_IP` 需按实际修改;服务器绑定 8888 端口,客户端也必须用 8888。
 
@@ -1009,15 +1024,15 @@ setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
 
 ## 15. 网络调试工具
 
-| 工具 | 用途 | 示例 |
-| ---- | ---- | ---- |
-| `ifconfig` | 查看/配置网络接口 IP、状态 | `ifconfig eth0` |
-| `ip addr` | 查看/配置地址(`ifconfig` 的现代替代) | `ip addr show eth0` |
-| `ip link` | 查看/配置链路层(启用、关闭接口) | `ip link set eth0 up` |
-| `ping` | 测试与目标主机连通性 | `ping 192.168.1.150`、`ping 127.0.0.1` |
-| `netstat` | 查看 TCP/UDP 连接、监听状态、路由 | `netstat -anp`、`netstat -tlnp` |
-| `ss` | 更现代的 socket 统计工具 | `ss -tlnp` |
-| `tcpdump` | 抓包分析网络流量 | `tcpdump -i eth0 port 8888` |
+| 工具       | 用途                                   | 示例                                   |
+| ---------- | -------------------------------------- | -------------------------------------- |
+| `ifconfig` | 查看/配置网络接口 IP、状态             | `ifconfig eth0`                        |
+| `ip addr`  | 查看/配置地址(`ifconfig` 的现代替代) | `ip addr show eth0`                    |
+| `ip link`  | 查看/配置链路层(启用、关闭接口)      | `ip link set eth0 up`                  |
+| `ping`     | 测试与目标主机连通性                   | `ping 192.168.1.150`、`ping 127.0.0.1` |
+| `netstat`  | 查看 TCP/UDP 连接、监听状态、路由      | `netstat -anp`、`netstat -tlnp`        |
+| `ss`       | 更现代的 socket 统计工具               | `ss -tlnp`                             |
+| `tcpdump`  | 抓包分析网络流量                       | `tcpdump -i eth0 port 8888`            |
 
 `ping 127.0.0.1` 可测试本机 TCP/IP 协议栈是否正常;用 `netstat -an` 可在握手/挥手过程中观察 `SYN_SENT`、`ESTABLISHED`、`FIN_WAIT_2`、`CLOSE_WAIT`、`TIME_WAIT` 等状态。
 
@@ -1027,14 +1042,14 @@ setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
 
 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
 
-| 维度 | Linux(本教程) | Windows Winsock | RT-Thread / LwIP |
-| ---- | --------------- | --------------- | ---------------- |
-| 头文件 | `<sys/socket.h>`、`<netinet/in.h>` | `<winsock2.h>` | `<sys/socket.h>` |
-| 初始化 | 无需 | 需 `WSAStartup()` | 无需 |
-| 关闭 | `close()` | `closesocket()` | `close()` |
-| 错误码 | `errno` | `WSAGetLastError()` | `errno` |
-| 接口风格 | BSD socket | BSD socket(略有差异) | BSD socket(兼容) |
-| 非阻塞 | `fcntl(O_NONBLOCK)` | `ioctlsocket(FIONBIO)` | `fcntl` |
+| 维度     | Linux(本教程)                    | Windows Winsock        | RT-Thread / LwIP   |
+| -------- | ---------------------------------- | ---------------------- | ------------------ |
+| 头文件   | `<sys/socket.h>`、`<netinet/in.h>` | `<winsock2.h>`         | `<sys/socket.h>`   |
+| 初始化   | 无需                               | 需 `WSAStartup()`      | 无需               |
+| 关闭     | `close()`                          | `closesocket()`        | `close()`          |
+| 错误码   | `errno`                            | `WSAGetLastError()`    | `errno`            |
+| 接口风格 | BSD socket                         | BSD socket(略有差异) | BSD socket(兼容) |
+| 非阻塞   | `fcntl(O_NONBLOCK)`                | `ioctlsocket(FIONBIO)` | `fcntl`            |
 
 BSD socket 是事实标准,跨平台移植时主要差异集中在初始化、关闭函数与错误码获取方式上。
 

+ 121 - 110
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/04-网络编程与项目实战/02-CAN应用编程.md

@@ -1,6 +1,17 @@
 ---
 title: CAN应用编程
-tags: [嵌入式Linux, Linux应用编程, CAN, SocketCAN, CAN总线, can_frame, can_filter, can-utils, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    Linux应用编程,
+    CAN,
+    SocketCAN,
+    CAN总线,
+    can_frame,
+    can_filter,
+    can-utils,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第三十一章 CAN应用编程基础"
@@ -18,11 +29,11 @@ CAN(Controller Area Network,控制器局域网络)是目前应用非常广
 
 CAN 是 ISO 国际标准化的串行通信协议:
 
-| 时间/标准 | 内容 |
-| --------- | ---- |
-| 1986 年 | 德国博世公司开发出面向汽车的 CAN 通信协议,动机是解决汽车中庞大电子控制系统之间的通讯、减少信号线束 |
-| ISO11898 / ISO11519 | CAN 标准化 |
-| 现状 | 欧洲汽车网络标准协议,当前应用最广泛的现场总线之一 |
+| 时间/标准           | 内容                                                                                                |
+| ------------------- | --------------------------------------------------------------------------------------------------- |
+| 1986 年             | 德国博世公司开发出面向汽车的 CAN 通信协议,动机是解决汽车中庞大电子控制系统之间的通讯、减少信号线束 |
+| ISO11898 / ISO11519 | CAN 标准化                                                                                          |
+| 现状                | 欧洲汽车网络标准协议,当前应用最广泛的现场总线之一                                                  |
 
 CAN 是一种**多主方式的串行通讯总线**,基本设计规范要求高比特率、高抗电磁干扰性,并且能检测产生的任何错误。经过几十年发展,其高性能、高可靠性和高实时性已被广泛认可,应用于工业自动化、船舶、医疗设备、工业设备等方面。汽车上的空调、车门、发动机、大量传感器等部件、模块都通过 CAN 总线连成一个网络。
 
@@ -30,16 +41,16 @@ CAN 是一种**多主方式的串行通讯总线**,基本设计规范要求高
 
 ## 2. CAN 的特点
 
-| 特点 | 说明 |
-| ---- | ---- |
-| 多主控制 | 总线空闲时所有单元都可开始发送(CSMA/CA);最先访问总线者获得发送权;多个单元同时发送时,**高优先级 ID** 的单元获得发送权 |
-| 消息发送 | 所有消息以固定格式发送;两个以上单元同时发送时,根据标识符 ID 决定优先级;ID 不表示目的地址,而表示访问总线的消息优先级。逐位仲裁比较,仲裁获胜者继续发送,失利者立即停止发送转为接收 |
-| 系统柔软性 | 连接在总线上的单元没有类似「地址」的信息;增加单元时其它单元的软硬件及应用层无需改变 |
-| 通信速度 | 可按网络规模设定合适速度;同一网络中所有单元必须设定**统一速度**,否则会输出错误信号妨碍整个网络;不同网络间可不同 |
-| 远程数据请求 | 可通过发送「遥控帧」请求其它单元发送数据 |
-| 错误检测/通知/恢复 | 所有单元都可检测错误;检测到错误的单元立即通知其它所有单元;发送单元检测出错误会强制结束当前发送并反复重发直到成功 |
-| 故障封闭 | 可判断是总线暂时性数据错误(外部噪声)还是持续性错误(单元内部故障、驱动器故障、断线);持续错误时可将故障单元从总线隔离 |
-| 连接 | 可同时连接多个单元,理论上数量无限制;实际受总线时间延迟及电气负载限制。降低速度可增加单元数,提高速度则减少 |
+| 特点               | 说明                                                                                                                                                                                  |
+| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 多主控制           | 总线空闲时所有单元都可开始发送(CSMA/CA);最先访问总线者获得发送权;多个单元同时发送时,**高优先级 ID** 的单元获得发送权                                                             |
+| 消息发送           | 所有消息以固定格式发送;两个以上单元同时发送时,根据标识符 ID 决定优先级;ID 不表示目的地址,而表示访问总线的消息优先级。逐位仲裁比较,仲裁获胜者继续发送,失利者立即停止发送转为接收 |
+| 系统柔软性         | 连接在总线上的单元没有类似「地址」的信息;增加单元时其它单元的软硬件及应用层无需改变                                                                                                  |
+| 通信速度           | 可按网络规模设定合适速度;同一网络中所有单元必须设定**统一速度**,否则会输出错误信号妨碍整个网络;不同网络间可不同                                                                    |
+| 远程数据请求       | 可通过发送「遥控帧」请求其它单元发送数据                                                                                                                                              |
+| 错误检测/通知/恢复 | 所有单元都可检测错误;检测到错误的单元立即通知其它所有单元;发送单元检测出错误会强制结束当前发送并反复重发直到成功                                                                    |
+| 故障封闭           | 可判断是总线暂时性数据错误(外部噪声)还是持续性错误(单元内部故障、驱动器故障、断线);持续错误时可将故障单元从总线隔离                                                              |
+| 连接               | 可同时连接多个单元,理论上数量无限制;实际受总线时间延迟及电气负载限制。降低速度可增加单元数,提高速度则减少                                                                          |
 
 ---
 
@@ -47,10 +58,10 @@ CAN 是一种**多主方式的串行通讯总线**,基本设计规范要求高
 
 CAN 总线用两根线连接各单元:**CAN_H** 和 **CAN_L**。CAN 控制器通过判断两根线上的**电位差**得到总线电平,分为**显性电平**和**隐性电平**:
 
-| 电平 | 逻辑值 | CAN_H | CAN_L | 电位差 |
-| ---- | ------ | ----- | ----- | ------ |
-| 显性电平 | 逻辑 0 | 约 3.5V | 约 1.5V | 2V |
-| 隐性电平 | 逻辑 1 | 约 2.5V | 约 2.5V | 0V |
+| 电平     | 逻辑值 | CAN_H   | CAN_L   | 电位差 |
+| -------- | ------ | ------- | ------- | ------ |
+| 显性电平 | 逻辑 0 | 约 3.5V | 约 1.5V | 2V     |
+| 隐性电平 | 逻辑 1 | 约 2.5V | 约 2.5V | 0V     |
 
 CAN 总线上没有节点传输数据时一直处于隐性状态(总线空闲即隐性)。CAN 总线就通过显性、隐性电平的变化把数据发送出去。**显性电平覆盖隐性电平**,这是仲裁机制的基础。
 
@@ -94,11 +105,11 @@ flowchart TB
 
 CAN 传输协议参考了 OSI 开放系统互连模型,但实际只定义了**传输层、数据链路层、物理层**三层,应用层协议可以由用户定义成适合特定工业领域的方案。
 
-| 层 | CAN 的内容 |
-| -- | ---------- |
-| 应用层 | 用户自定义;常见标准:DeviceNet(工业控制/制造业,为 PLC 和智能传感器设计)、CANopen(工业控制)、OBD / UDS(统一诊断服务, ISO14229,乘用车诊断)、SAEJ1939(商用车) |
+| 层         | CAN 的内容                                                                                                                                                                               |
+| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| 应用层     | 用户自定义;常见标准:DeviceNet(工业控制/制造业,为 PLC 和智能传感器设计)、CANopen(工业控制)、OBD / UDS(统一诊断服务, ISO14229,乘用车诊断)、SAEJ1939(商用车)                      |
 | 数据链路层 | 分 LLC 子层和 MAC 子层,**MAC 子层是 CAN 协议的核心**;负责把物理层信号组织成有意义的消息,提供传送错误控制等流程:消息的帧化、仲裁、应答、错误的检测或报告,通常在 CAN 控制器硬件中执行 |
-| 物理层 | 定义信号实际的发送方式、位时序、位的编码方式及同步步骤;信号电平、通信速度、采样点、驱动器/总线电气特性、连接器形态等由用户根据系统需求确定 |
+| 物理层     | 定义信号实际的发送方式、位时序、位的编码方式及同步步骤;信号电平、通信速度、采样点、驱动器/总线电气特性、连接器形态等由用户根据系统需求确定                                              |
 
 ```mermaid
 flowchart TB
@@ -112,13 +123,13 @@ flowchart TB
 
 CAN 定义了 **5 种类型**的报文帧:
 
-| 帧 | 用途 |
-| -- | ---- |
-| 数据帧 | 发送单元向接收单元传送数据的帧 |
+| 帧     | 用途                                         |
+| ------ | -------------------------------------------- |
+| 数据帧 | 发送单元向接收单元传送数据的帧               |
 | 遥控帧 | 接收单元向具有相同 ID 的发送单元请求数据的帧 |
-| 错误帧 | 检测出错误时向其它单元通知错误的帧 |
-| 过载帧 | 接收单元通知其尚未做好接收准备的帧 |
-| 间隔帧 | 将数据帧及遥控帧与前面的帧分离开来的帧 |
+| 错误帧 | 检测出错误时向其它单元通知错误的帧           |
+| 过载帧 | 接收单元通知其尚未做好接收准备的帧           |
+| 间隔帧 | 将数据帧及遥控帧与前面的帧分离开来的帧       |
 
 其中数据帧和遥控帧都有**标准格式**(11 位标识符 ID)和**扩展格式**(29 位标识符 ID)。数据帧是使用最多的帧类型。
 
@@ -126,15 +137,15 @@ CAN 定义了 **5 种类型**的报文帧:
 
 数据帧由 **7 个段**构成:
 
-| 段 | 说明 |
-| -- | ---- |
-| 帧起始 | 表示数据帧开始的段 |
-| 仲裁段 | 表示该帧优先级的段 |
-| 控制段 | 表示数据的字节数及保留位的段 |
+| 段     | 说明                              |
+| ------ | --------------------------------- |
+| 帧起始 | 表示数据帧开始的段                |
+| 仲裁段 | 表示该帧优先级的段                |
+| 控制段 | 表示数据的字节数及保留位的段      |
 | 数据段 | 数据的内容,可发送 **0~8 个字节** |
-| CRC 段 | 检查帧的传输错误的段 |
-| ACK 段 | 表示确认正常接收的段 |
-| 帧结束 | 表示数据帧结束的段 |
+| CRC 段 | 检查帧的传输错误的段              |
+| ACK 段 | 表示确认正常接收的段              |
+| 帧结束 | 表示数据帧结束的段                |
 
 ```mermaid
 flowchart LR
@@ -150,18 +161,18 @@ flowchart LR
 
 标准帧与扩展帧的位级构成对比:
 
-| 字段 | 标准帧 | 扩展帧 | 说明 |
-| ---- | ------ | ------ | ---- |
-| 帧起始 SOF | 1 位 | 1 位 | 显性,标志帧开始 |
-| 标识符 ID | 11 位 | 11 位基本 ID | 决定优先级,ID 越小优先级越高 |
-| SRR / IDE | IDE(1 位,显性) | SRR(1 位,隐性)+ IDE(1 位,隐性) | 区分标准/扩展格式 |
-| 扩展 ID | — | 18 位 | 与基本 ID 组成 29 位扩展 ID |
-| RTR | 1 位 | 1 位 | 数据帧为显性,遥控帧为隐性 |
-| 控制段 | IDE + r0 + DLC(4 位) | 同左 | DLC 表示数据字节数 0~8 |
-| 数据段 | 0~8 字节 | 0~8 字节 | 实际数据 |
-| CRC 段 | 15 位 + 界定符 | 15 位 + 界定符 | 校验传输错误 |
-| ACK 段 | ACK 槽 + 界定符 | 同左 | 发送方发隐性,接收方正确接收后拉为显性 |
-| 帧结束 EOF | 7 位 | 7 位 | 隐性 |
+| 字段       | 标准帧               | 扩展帧                               | 说明                                   |
+| ---------- | -------------------- | ------------------------------------ | -------------------------------------- |
+| 帧起始 SOF | 1 位                 | 1 位                                 | 显性,标志帧开始                       |
+| 标识符 ID  | 11 位                | 11 位基本 ID                         | 决定优先级,ID 越小优先级越高          |
+| SRR / IDE  | IDE(1 位,显性)    | SRR(1 位,隐性)+ IDE(1 位,隐性) | 区分标准/扩展格式                      |
+| 扩展 ID    | —                    | 18 位                                | 与基本 ID 组成 29 位扩展 ID            |
+| RTR        | 1 位                 | 1 位                                 | 数据帧为显性,遥控帧为隐性             |
+| 控制段     | IDE + r0 + DLC(4 位) | 同左                                 | DLC 表示数据字节数 0~8                 |
+| 数据段     | 0~8 字节             | 0~8 字节                             | 实际数据                               |
+| CRC 段     | 15 位 + 界定符       | 15 位 + 界定符                       | 校验传输错误                           |
+| ACK 段     | ACK 槽 + 界定符      | 同左                                 | 发送方发隐性,接收方正确接收后拉为显性 |
+| 帧结束 EOF | 7 位                 | 7 位                                 | 隐性                                   |
 
 ### 6.2 仲裁机制
 
@@ -181,12 +192,12 @@ flowchart LR
 
 CAN 是异步串行总线,收发双方靠位时序同步。一个位时间(Bit Time)被划分为若干时间段,每段由若干个最小时间单位 **Tq(Time Quantum)** 组成:
 
-| 段 | 作用 |
-| -- | ---- |
-| 同步段 SYNC_SEG | 1 个 Tq,用于硬同步,期望信号跳变发生在此段 |
-| 传播时间段 PROP_SEG | 补偿总线上的物理传播延迟和收发器延迟 |
-| 相位缓冲段 1 PHASE_SEG1 | 补偿正相位误差,可通过重同步延长 |
-| 相位缓冲段 2 PHASE_SEG2 | 补偿负相位误差,可通过重同步缩短 |
+| 段                      | 作用                                        |
+| ----------------------- | ------------------------------------------- |
+| 同步段 SYNC_SEG         | 1 个 Tq,用于硬同步,期望信号跳变发生在此段 |
+| 传播时间段 PROP_SEG     | 补偿总线上的物理传播延迟和收发器延迟        |
+| 相位缓冲段 1 PHASE_SEG1 | 补偿正相位误差,可通过重同步延长            |
+| 相位缓冲段 2 PHASE_SEG2 | 补偿负相位误差,可通过重同步缩短            |
 
 **采样点**通常位于 PHASE_SEG1 结束处,大致在位时间的 75%~87.5% 位置(常用 87.5%)。配置 bitrate 时,采样点、波特率分频(brp)、各段长度需匹配晶振与网络延迟。
 
@@ -227,11 +238,11 @@ if (0 > sockfd) {
 }
 ```
 
-| 参数 | 取值 | 说明 |
-| ---- | ---- | ---- |
-| domain | `PF_CAN` | 指定 CAN 通信协议 |
-| type | `SOCK_RAW` | 原始套接字类型 |
-| protocol | `CAN_RAW` | CAN 原始协议 |
+| 参数     | 取值       | 说明              |
+| -------- | ---------- | ----------------- |
+| domain   | `PF_CAN`   | 指定 CAN 通信协议 |
+| type     | `SOCK_RAW` | 原始套接字类型    |
+| protocol | `CAN_RAW`  | CAN 原始协议      |
 
 ### 7.2 将套接字与 CAN 设备绑定
 
@@ -256,9 +267,9 @@ if (0 > ret) {
 }
 ```
 
-| 结构体 | 头文件 | 关键成员 |
-| ------ | ------ | -------- |
-| `struct ifreq` | `<net/if.h>` | `ifr_name`(接口名)、`ifr_ifindex`(接口索引) |
+| 结构体                | 头文件          | 关键成员                                            |
+| --------------------- | --------------- | --------------------------------------------------- |
+| `struct ifreq`        | `<net/if.h>`    | `ifr_name`(接口名)、`ifr_ifindex`(接口索引)     |
 | `struct sockaddr_can` | `<linux/can.h>` | `can_family`(`AF_CAN`)、`can_ifindex`(接口索引) |
 
 > ⚠️ **来源说明**:以下 `struct sockaddr_can` 完整定义不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识(原书未展开该结构体)。
@@ -392,14 +403,14 @@ setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_LOOPBACK, &loopback, sizeof(loopback));
 
 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
 
-| 选项 | 层级 | 作用 |
-| ---- | ---- | ---- |
-| `CAN_RAW_FILTER` | `SOL_CAN_RAW` | 设置接收过滤规则 |
-| `CAN_RAW_ERR_FILTER` | `SOL_CAN_RAW` | 设置要接收的错误帧类型掩码 |
-| `CAN_RAW_LOOPBACK` | `SOL_CAN_RAW` | 本地回环开关,默认开启 |
+| 选项                    | 层级          | 作用                                                   |
+| ----------------------- | ------------- | ------------------------------------------------------ |
+| `CAN_RAW_FILTER`        | `SOL_CAN_RAW` | 设置接收过滤规则                                       |
+| `CAN_RAW_ERR_FILTER`    | `SOL_CAN_RAW` | 设置要接收的错误帧类型掩码                             |
+| `CAN_RAW_LOOPBACK`      | `SOL_CAN_RAW` | 本地回环开关,默认开启                                 |
 | `CAN_RAW_RECV_OWN_MSGS` | `SOL_CAN_RAW` | 是否接收自己发送的报文(回环开启后本 socket 是否收到) |
-| `CAN_RAW_JOIN_FILTERS` | `SOL_CAN_RAW` | 多个过滤器之间改为「或」的并集关系 |
-| `CAN_RAW_FD_FRAMES` | `SOL_CAN_RAW` | 使能 CAN-FD 帧(`struct canfd_frame`) |
+| `CAN_RAW_JOIN_FILTERS`  | `SOL_CAN_RAW` | 多个过滤器之间改为「或」的并集关系                     |
+| `CAN_RAW_FD_FRAMES`     | `SOL_CAN_RAW` | 使能 CAN-FD 帧(`struct canfd_frame`)                 |
 
 ---
 
@@ -523,18 +534,18 @@ out:
 
 逐段解释:
 
-| 代码段 | 作用 |
-| ------ | ---- |
-| `socket(PF_CAN, SOCK_RAW, CAN_RAW)` | 创建 CAN 原始套接字 |
-| `strcpy(ifr.ifr_name, "can0")` | 指定 CAN 设备名为 can0 |
-| `ioctl(SIOCGIFINDEX, &ifr)` | 取得 can0 的接口索引,存入 `ifr.ifr_ifindex` |
-| `can_addr.can_family/can_ifindex` | 填充 CAN 套接字地址 |
-| `bind()` | 把套接字与 can0 绑定 |
-| `CAN_RAW_FILTER, NULL, 0` | 只发不收,省略内核接收队列 |
-| `frame.can_id = 0x123` | 标准帧 ID,低 11 位有效 |
-| `frame.can_dlc = 6` | 数据长度 6 字节 |
-| `write()` | 发送一帧,返回值为 `sizeof(frame)` 表示成功 |
-| `sleep(1)` | 每秒发送一次 |
+| 代码段                              | 作用                                         |
+| ----------------------------------- | -------------------------------------------- |
+| `socket(PF_CAN, SOCK_RAW, CAN_RAW)` | 创建 CAN 原始套接字                          |
+| `strcpy(ifr.ifr_name, "can0")`      | 指定 CAN 设备名为 can0                       |
+| `ioctl(SIOCGIFINDEX, &ifr)`         | 取得 can0 的接口索引,存入 `ifr.ifr_ifindex` |
+| `can_addr.can_family/can_ifindex`   | 填充 CAN 套接字地址                          |
+| `bind()`                            | 把套接字与 can0 绑定                         |
+| `CAN_RAW_FILTER, NULL, 0`           | 只发不收,省略内核接收队列                   |
+| `frame.can_id = 0x123`              | 标准帧 ID,低 11 位有效                      |
+| `frame.can_dlc = 6`                 | 数据长度 6 字节                              |
+| `write()`                           | 发送一帧,返回值为 `sizeof(frame)` 表示成功  |
+| `sleep(1)`                          | 每秒发送一次                                 |
 
 ### 9.2 CAN 数据接收实例(can_read.c)
 
@@ -624,16 +635,16 @@ int main(void)
 
 逐段解释:
 
-| 代码段 | 作用 |
-| ------ | ---- |
-| `socket()` / `ioctl()` / `bind()` | 与发送端完全相同的创建与绑定流程 |
-| 注释掉的 `setsockopt` | 若打开,则「不接受任何报文」;默认不设置即接收所有 ID |
-| `read()` | 阻塞读取一帧 CAN 报文,存入 `frame` |
-| `can_id & CAN_ERR_FLAG` | 判断是否为错误帧,是则退出 |
-| `can_id & CAN_EFF_FLAG` | 判断扩展帧,用 `CAN_EFF_MASK` 取出 29 位 ID |
-| `can_id & CAN_SFF_MASK` | 标准帧取 11 位 ID |
-| `can_id & CAN_RTR_FLAG` | 判断是否为远程帧(遥控帧) |
-| `can_dlc` + `data[]` 循环 | 按长度打印数据字节 |
+| 代码段                            | 作用                                                  |
+| --------------------------------- | ----------------------------------------------------- |
+| `socket()` / `ioctl()` / `bind()` | 与发送端完全相同的创建与绑定流程                      |
+| 注释掉的 `setsockopt`             | 若打开,则「不接受任何报文」;默认不设置即接收所有 ID |
+| `read()`                          | 阻塞读取一帧 CAN 报文,存入 `frame`                   |
+| `can_id & CAN_ERR_FLAG`           | 判断是否为错误帧,是则退出                            |
+| `can_id & CAN_EFF_FLAG`           | 判断扩展帧,用 `CAN_EFF_MASK` 取出 29 位 ID           |
+| `can_id & CAN_SFF_MASK`           | 标准帧取 11 位 ID                                     |
+| `can_id & CAN_RTR_FLAG`           | 判断是否为远程帧(遥控帧)                            |
+| `can_dlc` + `data[]` 循环         | 按长度打印数据字节                                    |
 
 ### 9.3 编译、部署与测试步骤
 
@@ -681,14 +692,14 @@ cansend can0 123#01.02.03.04.05.06.07.08
 
 ### 9.4 调试方法
 
-| 现象 | 可能原因 | 排查手段 |
-| ---- | -------- | -------- |
-| `socket error` | 内核不支持 SocketCAN 或未加载 CAN 驱动 | `ifconfig -a` 看是否有 can0;`dmesg` 看驱动加载日志 |
-| `bind error` | 设备名写错或 can0 未 up | `ifconfig can0` 检查状态,`ip link set can0 up ...` |
-| 收不到数据 | 双端波特率不一致、接线错误、缺端接电阻 | `ip -details link show can0` 核对 bitrate;检查 CAN_H/CAN_L;两端接 120Ω |
-| 收到 `Error frame!` | 总线错误 / bus-off | 查看 `dmesg` 内核 CAN 报错,检查布线、终端电阻、波特率 |
-| `write error` | 接口 down 或被过滤 | 确认 `ifconfig can0 up`,返回值是否等于 `sizeof(frame)` |
-| 单板回环收不到 | 回环被关闭 | 用 `CAN_RAW_LOOPBACK` 设为 1(默认即 1) |
+| 现象                | 可能原因                               | 排查手段                                                                 |
+| ------------------- | -------------------------------------- | ------------------------------------------------------------------------ |
+| `socket error`      | 内核不支持 SocketCAN 或未加载 CAN 驱动 | `ifconfig -a` 看是否有 can0;`dmesg` 看驱动加载日志                      |
+| `bind error`        | 设备名写错或 can0 未 up                | `ifconfig can0` 检查状态,`ip link set can0 up ...`                      |
+| 收不到数据          | 双端波特率不一致、接线错误、缺端接电阻 | `ip -details link show can0` 核对 bitrate;检查 CAN_H/CAN_L;两端接 120Ω |
+| 收到 `Error frame!` | 总线错误 / bus-off                     | 查看 `dmesg` 内核 CAN 报错,检查布线、终端电阻、波特率                   |
+| `write error`       | 接口 down 或被过滤                     | 确认 `ifconfig can0 up`,返回值是否等于 `sizeof(frame)`                  |
+| 单板回环收不到      | 回环被关闭                             | 用 `CAN_RAW_LOOPBACK` 设为 1(默认即 1)                                 |
 
 ---
 
@@ -696,15 +707,15 @@ cansend can0 123#01.02.03.04.05.06.07.08
 
 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
 
-| 维度 | Linux SocketCAN(本教程) | STM32(HAL/裸机) | 说明 |
-| ---- | ------------------------- | ----------------- | ---- |
-| 接口层次 | 标准 socket(`PF_CAN`) | 直接操作 bxCAN 寄存器 | Linux 把 CAN 当网络设备 |
-| 发送 | `write(sockfd, &frame, sizeof(frame))` | 填充发送邮箱 / `HAL_CAN_AddTxMessage` | 数据都封装为帧 |
-| 接收 | `read()` + `can_frame` | 中断/轮询接收邮箱 | Linux 由内核管理接收队列 |
-| 过滤 | `can_filter` + `CAN_RAW_FILTER` | 配置筛选器组(Filter Bank) | 硬件/内核过滤 |
-| 帧格式 | `struct can_frame`(`can_id/can_dlc/data[]`) | `CAN_TxHeaderTypeDef` 等 | 字段概念一致 |
-| 配置波特率 | `ip link set can0 up type can bitrate ...` | 配置位时序寄存器 | 都需匹配对端 |
-| 工具 | `candump` / `cansend` | 无,需自行实现 | SocketCAN 生态完善 |
+| 维度       | Linux SocketCAN(本教程)                     | STM32(HAL/裸机)                     | 说明                     |
+| ---------- | --------------------------------------------- | ------------------------------------- | ------------------------ |
+| 接口层次   | 标准 socket(`PF_CAN`)                       | 直接操作 bxCAN 寄存器                 | Linux 把 CAN 当网络设备  |
+| 发送       | `write(sockfd, &frame, sizeof(frame))`        | 填充发送邮箱 / `HAL_CAN_AddTxMessage` | 数据都封装为帧           |
+| 接收       | `read()` + `can_frame`                        | 中断/轮询接收邮箱                     | Linux 由内核管理接收队列 |
+| 过滤       | `can_filter` + `CAN_RAW_FILTER`               | 配置筛选器组(Filter Bank)           | 硬件/内核过滤            |
+| 帧格式     | `struct can_frame`(`can_id/can_dlc/data[]`) | `CAN_TxHeaderTypeDef` 等              | 字段概念一致             |
+| 配置波特率 | `ip link set can0 up type can bitrate ...`    | 配置位时序寄存器                      | 都需匹配对端             |
+| 工具       | `candump` / `cansend`                         | 无,需自行实现                        | SocketCAN 生态完善       |
 
 ---
 

+ 177 - 153
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/04-网络编程与项目实战/03-CMake入门与进阶.md

@@ -1,6 +1,25 @@
 ---
 title: CMake入门与进阶
-tags: [嵌入式Linux, Linux应用编程, CMake, CMakeLists, Makefile, 构建系统, add_executable, add_library, target_link_libraries, target_include_directories, 静态库, 动态库, 交叉编译, 工具链文件, arm-linux-setup.cmake, install, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    Linux应用编程,
+    CMake,
+    CMakeLists,
+    Makefile,
+    构建系统,
+    add_executable,
+    add_library,
+    target_link_libraries,
+    target_include_directories,
+    静态库,
+    动态库,
+    交叉编译,
+    工具链文件,
+    arm-linux-setup.cmake,
+    install,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第三十二章 CMake入门与进阶"
@@ -20,11 +39,11 @@ pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第三十二章 CMak
 
 ### 1.1 构建系统的演进
 
-| 阶段 | 工具 | 说明 |
-| ---- | ---- | ---- |
-| 手动编译 | `gcc main.c hello.c -o hello` | 文件一多就要手敲一长串,改一个文件全量重编 |
-| 脚本化 | Makefile + `make` | 描述依赖关系与规则,支持增量编译;但语法复杂、各平台不通用 |
-| 跨平台构建 | CMake | 写与平台无关的 `CMakeLists.txt`,自动生成本地化 Makefile / 工程文件 |
+| 阶段       | 工具                          | 说明                                                                |
+| ---------- | ----------------------------- | ------------------------------------------------------------------- |
+| 手动编译   | `gcc main.c hello.c -o hello` | 文件一多就要手敲一长串,改一个文件全量重编                          |
+| 脚本化     | Makefile + `make`             | 描述依赖关系与规则,支持增量编译;但语法复杂、各平台不通用          |
+| 跨平台构建 | CMake                         | 写与平台无关的 `CMakeLists.txt`,自动生成本地化 Makefile / 工程文件 |
 
 CMake 的主要优点(教材总结):
 
@@ -34,14 +53,14 @@ CMake 的主要优点(教材总结):
 
 ### 1.2 CMake 与 Makefile 对比
 
-| 维度 | Makefile | CMake |
-| ---- | -------- | ----- |
-| 编写者 | 开发者手工编写 | 开发者写 `CMakeLists.txt`,Makefile 由 CMake 生成 |
-| 语法 | 复杂、易错,制表符敏感 | 命令 + 变量,类脚本,简单 |
-| 跨平台 | 各平台规则往往不同 | `CMakeLists.txt` 与平台无关 |
-| 依赖扫描 | 需手工维护或配合工具 | 自动处理头文件依赖 |
-| 产物 | 直接给 `make` 用 | 中间层,再生成 Makefile |
-| 适合规模 | 小工程、学习原理 | 中大型工程、跨平台工程 |
+| 维度     | Makefile               | CMake                                             |
+| -------- | ---------------------- | ------------------------------------------------- |
+| 编写者   | 开发者手工编写         | 开发者写 `CMakeLists.txt`,Makefile 由 CMake 生成 |
+| 语法     | 复杂、易错,制表符敏感 | 命令 + 变量,类脚本,简单                         |
+| 跨平台   | 各平台规则往往不同     | `CMakeLists.txt` 与平台无关                       |
+| 依赖扫描 | 需手工维护或配合工具   | 自动处理头文件依赖                                |
+| 产物     | 直接给 `make` 用       | 中间层,再生成 Makefile                           |
+| 适合规模 | 小工程、学习原理       | 中大型工程、跨平台工程                            |
 
 ```mermaid
 flowchart LR
@@ -248,10 +267,10 @@ add_library(libhello hello.c)
 set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")
 ```
 
-| 变量 | 作用 |
-| ---- | ---- |
+| 变量                     | 作用                   |
+| ------------------------ | ---------------------- |
 | `EXECUTABLE_OUTPUT_PATH` | 控制可执行文件输出目录 |
-| `LIBRARY_OUTPUT_PATH` | 控制库文件输出目录 |
+| `LIBRARY_OUTPUT_PATH`    | 控制库文件输出目录     |
 
 最终目录结构:
 
@@ -282,26 +301,26 @@ message(${MY_VAL})           # 用 ${} 引用变量
 
 ### 3.2 常用命令一览
 
-| 命令 | 说明 |
-| ---- | ---- |
-| `add_executable` | 定义可执行程序目标 |
-| `add_library` | 定义库文件目标 |
-| `add_subdirectory` | 去指定目录中寻找新的 `CMakeLists.txt` |
-| `aux_source_directory` | 收集目录中的源文件名并赋值给变量 |
-| `cmake_minimum_required` | 设置 CMake 最低版本要求 |
-| `get_target_property` | 获取目标属性 |
-| `include_directories` | 设置**所有目标**的头文件搜索路径(≈ `gcc -I`) |
-| `link_directories` | 设置**所有目标**的库文件搜索路径(≈ `gcc -L`) |
-| `link_libraries` | 设置**所有目标**需要链接的库(≈ `gcc -l`) |
-| `list` | 列表相关操作 |
-| `message` | 打印输出信息 |
-| `project` | 设置工程名称 |
-| `set` | 设置变量 |
-| `set_target_properties` | 设置目标属性 |
-| `target_include_directories` | 设置**指定目标**的头文件搜索路径 |
-| `target_link_libraries` | 设置**指定目标**链接的库 |
-| `target_sources` | 设置指定目标所需的源文件 |
-| `target_link_directories` | 设置**指定目标**的库搜索路径 |
+| 命令                         | 说明                                           |
+| ---------------------------- | ---------------------------------------------- |
+| `add_executable`             | 定义可执行程序目标                             |
+| `add_library`                | 定义库文件目标                                 |
+| `add_subdirectory`           | 去指定目录中寻找新的 `CMakeLists.txt`          |
+| `aux_source_directory`       | 收集目录中的源文件名并赋值给变量               |
+| `cmake_minimum_required`     | 设置 CMake 最低版本要求                        |
+| `get_target_property`        | 获取目标属性                                   |
+| `include_directories`        | 设置**所有目标**的头文件搜索路径(≈ `gcc -I`) |
+| `link_directories`           | 设置**所有目标**的库文件搜索路径(≈ `gcc -L`) |
+| `link_libraries`             | 设置**所有目标**需要链接的库(≈ `gcc -l`)     |
+| `list`                       | 列表相关操作                                   |
+| `message`                    | 打印输出信息                                   |
+| `project`                    | 设置工程名称                                   |
+| `set`                        | 设置变量                                       |
+| `set_target_properties`      | 设置目标属性                                   |
+| `target_include_directories` | 设置**指定目标**的头文件搜索路径               |
+| `target_link_libraries`      | 设置**指定目标**链接的库                       |
+| `target_sources`             | 设置指定目标所需的源文件                       |
+| `target_link_directories`    | 设置**指定目标**的库搜索路径                   |
 
 ### 3.3 常用命令详解
 
@@ -420,15 +439,15 @@ list(SORT SRC_LIST)                   # 排序
 message([<mode>] "message to display" ...)
 ```
 
-| mode | 说明 |
-| ---- | ---- |
-| 无 | 重要信息、普通信息 |
-| `STATUS` | 附带信息 |
-| `WARNING` | CMake 警告,继续处理 |
-| `AUTHOR_WARNING` | CMake 警告(开发),继续处理 |
-| `SEND_ERROR` | CMake 错误,继续处理,但跳过生成 |
-| `FATAL_ERROR` | CMake 错误,停止处理和生成 |
-| `DEPRECATION` | 弃用错误/警告 |
+| mode             | 说明                             |
+| ---------------- | -------------------------------- |
+| 无               | 重要信息、普通信息               |
+| `STATUS`         | 附带信息                         |
+| `WARNING`        | CMake 警告,继续处理             |
+| `AUTHOR_WARNING` | CMake 警告(开发),继续处理     |
+| `SEND_ERROR`     | CMake 错误,继续处理,但跳过生成 |
+| `FATAL_ERROR`    | CMake 错误,停止处理和生成       |
+| `DEPRECATION`    | 弃用错误/警告                    |
 
 ```cmake
 message("Hello World!")
@@ -464,11 +483,11 @@ target_link_libraries(<target>
 
 它们与 `include_directories`/`link_libraries` 功能相同,但作用范围可控,只影响**指定目标**。关键是三个作用域关键字:
 
-| 关键字 | 当前目标是否使用 | 是否传递给依赖目标 | 等价关系 |
-| ------ | ---------------- | ------------------ | -------- |
-| `PRIVATE` | 使用 | 不传递 | 私有 |
-| `INTERFACE` | 不使用 | 传递 | 只给依赖者用 |
-| `PUBLIC` | 使用 | 传递 | `PRIVATE` + `INTERFACE` |
+| 关键字      | 当前目标是否使用 | 是否传递给依赖目标 | 等价关系                |
+| ----------- | ---------------- | ------------------ | ----------------------- |
+| `PRIVATE`   | 使用             | 不传递             | 私有                    |
+| `INTERFACE` | 不使用           | 传递               | 只给依赖者用            |
+| `PUBLIC`    | 使用             | 传递               | `PRIVATE` + `INTERFACE` |
 
 ```cmake
 # hello_world 内部用 hello,且 hello_world.h 也对外暴露 hello.h
@@ -489,31 +508,31 @@ target_link_libraries(mqttClient PRIVATE paho-mqtt3c)
 
 #### 3.4.1 提供信息的变量
 
-| 变量 | 说明 |
-| ---- | ---- |
-| `PROJECT_SOURCE_DIR` | 工程顶层目录(顶层 `CMakeLists.txt` 所在目录) |
-| `PROJECT_BINARY_DIR` | 工程 `BINARY_DIR`(顶层源码的输出目录) |
-| `CMAKE_SOURCE_DIR` | 与 `PROJECT_SOURCE_DIR` 等价 |
-| `CMAKE_BINARY_DIR` | 与 `PROJECT_BINARY_DIR` 等价 |
-| `CMAKE_CURRENT_SOURCE_DIR` | 当前源码所在路径 |
-| `CMAKE_CURRENT_BINARY_DIR` | 当前源码的 `BINARY_DIR` |
-| `CMAKE_MAJOR_VERSION` / `CMAKE_MINOR_VERSION` / `CMAKE_VERSION` | CMake 主/次/完整版本号 |
-| `PROJECT_VERSION` / `PROJECT_VERSION_MAJOR` / `PROJECT_VERSION_MINOR` | 工程版本号 |
-| `CMAKE_PROJECT_NAME` / `PROJECT_NAME` | 工程名(二者等价) |
+| 变量                                                                  | 说明                                           |
+| --------------------------------------------------------------------- | ---------------------------------------------- |
+| `PROJECT_SOURCE_DIR`                                                  | 工程顶层目录(顶层 `CMakeLists.txt` 所在目录) |
+| `PROJECT_BINARY_DIR`                                                  | 工程 `BINARY_DIR`(顶层源码的输出目录)        |
+| `CMAKE_SOURCE_DIR`                                                    | 与 `PROJECT_SOURCE_DIR` 等价                   |
+| `CMAKE_BINARY_DIR`                                                    | 与 `PROJECT_BINARY_DIR` 等价                   |
+| `CMAKE_CURRENT_SOURCE_DIR`                                            | 当前源码所在路径                               |
+| `CMAKE_CURRENT_BINARY_DIR`                                            | 当前源码的 `BINARY_DIR`                        |
+| `CMAKE_MAJOR_VERSION` / `CMAKE_MINOR_VERSION` / `CMAKE_VERSION`       | CMake 主/次/完整版本号                         |
+| `PROJECT_VERSION` / `PROJECT_VERSION_MAJOR` / `PROJECT_VERSION_MINOR` | 工程版本号                                     |
+| `CMAKE_PROJECT_NAME` / `PROJECT_NAME`                                 | 工程名(二者等价)                             |
 
 #### 3.4.2 改变行为的变量
 
-| 变量 | 说明 |
-| ---- | ---- |
-| `BUILD_SHARED_LIBS` | 控制 `add_library` 未显式指定时是否生成动态库 |
-| `CMAKE_BUILD_TYPE` | 构建类型,`Debug` 或 `Release` |
-| `CMAKE_SYSROOT` | 对应编译器 `--sysroot` 选项,交叉编译时使用 |
-| `CMAKE_IGNORE_PATH` | 被 `find_xxx` 忽略的目录列表 |
-| `CMAKE_INCLUDE_PATH` | `find_file()` / `find_path()` 的搜索路径 |
-| `CMAKE_INCLUDE_DIRECTORIES_BEFORE` | 控制 `include_directories()` 默认行为 |
-| `CMAKE_LIBRARY_PATH` | `find_library()` 的搜索路径 |
-| `CMAKE_MODULE_PATH` | `include()` / `find_package()` 加载模块的搜索路径 |
-| `CMAKE_PROGRAM_PATH` | `find_program()` 的搜索路径 |
+| 变量                               | 说明                                              |
+| ---------------------------------- | ------------------------------------------------- |
+| `BUILD_SHARED_LIBS`                | 控制 `add_library` 未显式指定时是否生成动态库     |
+| `CMAKE_BUILD_TYPE`                 | 构建类型,`Debug` 或 `Release`                    |
+| `CMAKE_SYSROOT`                    | 对应编译器 `--sysroot` 选项,交叉编译时使用       |
+| `CMAKE_IGNORE_PATH`                | 被 `find_xxx` 忽略的目录列表                      |
+| `CMAKE_INCLUDE_PATH`               | `find_file()` / `find_path()` 的搜索路径          |
+| `CMAKE_INCLUDE_DIRECTORIES_BEFORE` | 控制 `include_directories()` 默认行为             |
+| `CMAKE_LIBRARY_PATH`               | `find_library()` 的搜索路径                       |
+| `CMAKE_MODULE_PATH`                | `include()` / `find_package()` 加载模块的搜索路径 |
+| `CMAKE_PROGRAM_PATH`               | `find_program()` 的搜索路径                       |
 
 ```cmake
 set(BUILD_SHARED_LIBS on)        # add_library 默认生成动态库
@@ -531,18 +550,18 @@ message(${P_VAR})
 
 #### 3.4.3 描述系统的变量
 
-| 变量 | 说明 |
-| ---- | ---- |
-| `CMAKE_HOST_SYSTEM_NAME` | 运行 CMake 的操作系统名(`uname -s`) |
-| `CMAKE_HOST_SYSTEM_PROCESSOR` | 运行 CMake 的处理器名(`uname -p`) |
-| `CMAKE_HOST_SYSTEM` | 运行 CMake 的系统(复合信息) |
-| `CMAKE_HOST_SYSTEM_VERSION` | 运行 CMake 的系统版本(`uname -r`) |
-| `CMAKE_HOST_UNIX` / `UNIX` | 主机是类 UNIX 时为真 |
-| `CMAKE_HOST_WIN32` / `WIN32` | 主机是 Windows 时为真 |
-| `CMAKE_SYSTEM_NAME` | 目标主机的操作系统名 |
-| `CMAKE_SYSTEM_PROCESSOR` | 目标主机的处理器名 |
-| `CMAKE_SYSTEM` / `CMAKE_SYSTEM_VERSION` | 目标主机的复合信息 / 版本号 |
-| `ENV` | 访问环境变量,用法 `$ENV{VAR}` |
+| 变量                                    | 说明                                  |
+| --------------------------------------- | ------------------------------------- |
+| `CMAKE_HOST_SYSTEM_NAME`                | 运行 CMake 的操作系统名(`uname -s`) |
+| `CMAKE_HOST_SYSTEM_PROCESSOR`           | 运行 CMake 的处理器名(`uname -p`)   |
+| `CMAKE_HOST_SYSTEM`                     | 运行 CMake 的系统(复合信息)         |
+| `CMAKE_HOST_SYSTEM_VERSION`             | 运行 CMake 的系统版本(`uname -r`)   |
+| `CMAKE_HOST_UNIX` / `UNIX`              | 主机是类 UNIX 时为真                  |
+| `CMAKE_HOST_WIN32` / `WIN32`            | 主机是 Windows 时为真                 |
+| `CMAKE_SYSTEM_NAME`                     | 目标主机的操作系统名                  |
+| `CMAKE_SYSTEM_PROCESSOR`                | 目标主机的处理器名                    |
+| `CMAKE_SYSTEM` / `CMAKE_SYSTEM_VERSION` | 目标主机的复合信息 / 版本号           |
+| `ENV`                                   | 访问环境变量,用法 `$ENV{VAR}`        |
 
 ```cmake
 message(${CMAKE_HOST_SYSTEM_NAME})
@@ -552,10 +571,10 @@ message($ENV{XXX})              # 读取环境变量 XXX
 
 #### 3.4.4 控制编译的变量
 
-| 变量 | 说明 |
-| ---- | ---- |
+| 变量                     | 说明                 |
+| ------------------------ | -------------------- |
 | `EXECUTABLE_OUTPUT_PATH` | 可执行程序的输出路径 |
-| `LIBRARY_OUTPUT_PATH` | 库文件的输出路径 |
+| `LIBRARY_OUTPUT_PATH`    | 库文件的输出路径     |
 
 默认情况下,最终目标文件的输出目录就是源码的 `BINARY_DIR`。
 
@@ -590,22 +609,22 @@ endif()
 
 `else` / `endif` 括号中的表达式可写可不写,写了必须与 `if` 一致。常用表达式:
 
-| 表达式 | 为真条件 | 常用写法示例 |
-| ------ | -------- | ------------ |
-| `<constant>` | `1`/`ON`/`YES`/`TRUE`/`Y`/非零数字 | `if(ON)` |
-| `<variable\|string>` | 变量已定义且不为假常量 | `if(GG)` |
-| `NOT <expr>` | expr 为假 | `if(NOT GG)` |
-| `<e1> AND <e2>` | 两者同为真 | `if(yes AND on)` |
-| `<e1> OR <e2>` | 至少一个为真 | `if(yes OR no)` |
-| `COMMAND name` | name 是已定义的命令/宏/函数 | `if(COMMAND project)` |
-| `TARGET name` | name 是已定义的目标 | `if(TARGET hello)` |
-| `EXISTS path` | 文件或目录存在(需绝对路径) | `if(EXISTS ${PROJECT_BINARY_DIR})` |
-| `IS_DIRECTORY path` | path 是目录 | `if(IS_DIRECTORY ${PROJECT_BINARY_DIR}/hello)` |
-| `IS_ABSOLUTE path` | path 是绝对路径 | `if(IS_ABSOLUTE ${PROJECT_BINARY_DIR})` |
-| `<var\|str> MATCHES regex` | 正则匹配成功 | `if(MY_STR MATCHES "Hello World")` |
-| `<var\|str> IN_LIST <var>` | 元素在列表中 | `if(Hello IN_LIST MY_LIST)` |
-| `DEFINED <var>` | 变量已定义(值真假无关) | `if(DEFINED yyds)` |
-| `<a> LESS/GREATER/EQUAL <b>` | 数值比较 | `if(20 LESS 100)` |
+| 表达式                       | 为真条件                           | 常用写法示例                                   |
+| ---------------------------- | ---------------------------------- | ---------------------------------------------- |
+| `<constant>`                 | `1`/`ON`/`YES`/`TRUE`/`Y`/非零数字 | `if(ON)`                                       |
+| `<variable\|string>`         | 变量已定义且不为假常量             | `if(GG)`                                       |
+| `NOT <expr>`                 | expr 为假                          | `if(NOT GG)`                                   |
+| `<e1> AND <e2>`              | 两者同为真                         | `if(yes AND on)`                               |
+| `<e1> OR <e2>`               | 至少一个为真                       | `if(yes OR no)`                                |
+| `COMMAND name`               | name 是已定义的命令/宏/函数        | `if(COMMAND project)`                          |
+| `TARGET name`                | name 是已定义的目标                | `if(TARGET hello)`                             |
+| `EXISTS path`                | 文件或目录存在(需绝对路径)       | `if(EXISTS ${PROJECT_BINARY_DIR})`             |
+| `IS_DIRECTORY path`          | path 是目录                        | `if(IS_DIRECTORY ${PROJECT_BINARY_DIR}/hello)` |
+| `IS_ABSOLUTE path`           | path 是绝对路径                    | `if(IS_ABSOLUTE ${PROJECT_BINARY_DIR})`        |
+| `<var\|str> MATCHES regex`   | 正则匹配成功                       | `if(MY_STR MATCHES "Hello World")`             |
+| `<var\|str> IN_LIST <var>`   | 元素在列表中                       | `if(Hello IN_LIST MY_LIST)`                    |
+| `DEFINED <var>`              | 变量已定义(值真假无关)           | `if(DEFINED yyds)`                             |
+| `<a> LESS/GREATER/EQUAL <b>` | 数值比较                           | `if(20 LESS 100)`                              |
 
 常量真假规则:`1`、`ON`、`YES`、`TRUE`、`Y`、非零数字为真;`0`、`OFF`、`NO`、`FALSE`、`N`、`IGNORE`、`NOTFOUND`、空字符串、以 `-NOTFOUND` 结尾为假,大小写不敏感。不匹配这些常量时,才当作变量或字符串处理。
 
@@ -678,12 +697,12 @@ endfunction()
 
 调用函数时实际参数个数**可以多于**定义个数,甚至定义 0 个也行。函数内部内置变量:
 
-| 内部变量 | 说明 |
-| -------- | ---- |
-| `ARGVX` | 第 X 个参数,如 `ARGV0`、`ARGV1` |
-| `ARGV` | 实际传入的所有参数(列表) |
-| `ARGN` | 超出形参个数的剩余参数(列表) |
-| `ARGC` | 实际传入的参数个数 |
+| 内部变量 | 说明                             |
+| -------- | -------------------------------- |
+| `ARGVX`  | 第 X 个参数,如 `ARGV0`、`ARGV1` |
+| `ARGV`   | 实际传入的所有参数(列表)       |
+| `ARGN`   | 超出形参个数的剩余参数(列表)   |
+| `ARGC`   | 实际传入的参数个数               |
 
 ```cmake
 function(xyz arg1 arg2)
@@ -806,17 +825,17 @@ message("${type}")     # STATIC_LIBRARY
 
 ### 3.11 `file()` 文件操作
 
-| 用法 | 说明 |
-| ---- | ---- |
-| `file(WRITE <file> <content>...)` | 写文件,存在则覆盖 |
-| `file(APPEND <file> <content>...)` | 追加到文件末尾 |
-| `file(GENERATE OUTPUT <out> INPUT <in> \| CONTENT <c> [CONDITION <expr>])` | 由内容/输入文件生成文件 |
-| `file(READ <file> <var> [OFFSET <o>] [LIMIT <n>] [HEX])` | 按字节读取 |
-| `file(STRINGS <file> <var> [options...])` | 按字符串列表读取(忽略二进制、CR) |
-| `file(<MD5\|SHA1\|SHA256\|...> <file> <var>)` | 计算 hash 值 |
-| `file(RENAME <old> <new>)` | 重命名 |
-| `file(REMOVE <files>...)` | 删除文件 |
-| `file(REMOVE_RECURSE <files>...)` | 删除文件或目录(含非空目录) |
+| 用法                                                                       | 说明                               |
+| -------------------------------------------------------------------------- | ---------------------------------- |
+| `file(WRITE <file> <content>...)`                                          | 写文件,存在则覆盖                 |
+| `file(APPEND <file> <content>...)`                                         | 追加到文件末尾                     |
+| `file(GENERATE OUTPUT <out> INPUT <in> \| CONTENT <c> [CONDITION <expr>])` | 由内容/输入文件生成文件            |
+| `file(READ <file> <var> [OFFSET <o>] [LIMIT <n>] [HEX])`                   | 按字节读取                         |
+| `file(STRINGS <file> <var> [options...])`                                  | 按字符串列表读取(忽略二进制、CR) |
+| `file(<MD5\|SHA1\|SHA256\|...> <file> <var>)`                              | 计算 hash 值                       |
+| `file(RENAME <old> <new>)`                                                 | 重命名                             |
+| `file(REMOVE <files>...)`                                                  | 删除文件                           |
+| `file(REMOVE_RECURSE <files>...)`                                          | 删除文件或目录(含非空目录)       |
 
 ```cmake
 file(WRITE wtest.txt "Hello World!")
@@ -851,11 +870,11 @@ add_library(hello hello/hello.c)        # 生成 libhello.so
 add_library(world world/world.c)        # 生成 libworld.so
 ```
 
-| 库类型 | 选项 | 产物前缀/后缀 | 链接方式 |
-| ------ | ---- | ------------- | -------- |
-| 静态库 | `STATIC`(默认) | `lib` + `.a` | 链接时把代码复制进可执行文件 |
-| 动态库 | `SHARED` | `lib` + `.so` | 运行时加载,需保证目标机有对应 `.so` |
-| 模块库 | `MODULE` | 视平台而定 | 供运行时动态加载,一般不参与链接 |
+| 库类型 | 选项             | 产物前缀/后缀 | 链接方式                             |
+| ------ | ---------------- | ------------- | ------------------------------------ |
+| 静态库 | `STATIC`(默认) | `lib` + `.a`  | 链接时把代码复制进可执行文件         |
+| 动态库 | `SHARED`         | `lib` + `.so` | 运行时加载,需保证目标机有对应 `.so` |
+| 模块库 | `MODULE`         | 视平台而定    | 供运行时动态加载,一般不参与链接     |
 
 想要自定义输出文件名,用属性而非目标名:
 
@@ -905,17 +924,17 @@ set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
 
 各变量含义:
 
-| 变量 | 含义 |
-| ---- | ---- |
-| `CMAKE_SYSTEM_NAME` | 目标主机操作系统名,`Linux` 表示目标是 Linux 系统 |
-| `CMAKE_SYSTEM_PROCESSOR` | 目标架构名,`arm` |
-| `CMAKE_SYSROOT` | 传给 gcc 的 `--sysroot` 选项,编译时去该目录找标准库与头文件 |
-| `CMAKE_C_COMPILER` | C 编译器(交叉编译时指向 arm-gcc) |
-| `CMAKE_CXX_COMPILER` | C++ 编译器(交叉编译时指向 arm-g++) |
-| `CMAKE_C_FLAGS` / `CMAKE_CXX_FLAGS` | 分别为 C/C++ 编译器追加编译选项 |
+| 变量                                | 含义                                                                                        |
+| ----------------------------------- | ------------------------------------------------------------------------------------------- |
+| `CMAKE_SYSTEM_NAME`                 | 目标主机操作系统名,`Linux` 表示目标是 Linux 系统                                           |
+| `CMAKE_SYSTEM_PROCESSOR`            | 目标架构名,`arm`                                                                           |
+| `CMAKE_SYSROOT`                     | 传给 gcc 的 `--sysroot` 选项,编译时去该目录找标准库与头文件                                |
+| `CMAKE_C_COMPILER`                  | C 编译器(交叉编译时指向 arm-gcc)                                                          |
+| `CMAKE_CXX_COMPILER`                | C++ 编译器(交叉编译时指向 arm-g++)                                                        |
+| `CMAKE_C_FLAGS` / `CMAKE_CXX_FLAGS` | 分别为 C/C++ 编译器追加编译选项                                                             |
 | `CMAKE_FIND_ROOT_PATH_MODE_LIBRARY` | `ONLY` 表示 `find_library()` 只在 `CMAKE_SYSROOT` 中搜索;`NEVER` 只用主机路径;`BOTH` 都搜 |
-| `CMAKE_FIND_ROOT_PATH_MODE_INCLUDE` | 控制 `find_file()`/`find_path()` 是否使用 `CMAKE_SYSROOT`,取值同上 |
-| `CMAKE_FIND_ROOT_PATH_MODE_PROGRAM` | 控制查找**可执行程序**时使用主机还是 sysroot 路径;通常设为 `NEVER`,即用主机工具 |
+| `CMAKE_FIND_ROOT_PATH_MODE_INCLUDE` | 控制 `find_file()`/`find_path()` 是否使用 `CMAKE_SYSROOT`,取值同上                         |
+| `CMAKE_FIND_ROOT_PATH_MODE_PROGRAM` | 控制查找**可执行程序**时使用主机还是 sysroot 路径;通常设为 `NEVER`,即用主机工具           |
 
 ### 5.2 两种使用方式
 
@@ -1145,15 +1164,15 @@ file(GLOB SRC_FILES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/src/*.c)
 
 ## 7. 实验步骤与调试
 
-| 现象 | 原因 | 处理 |
-| ---- | ---- | ---- |
-| `cmake` 报交叉编译配置相关错误 | Ubuntu 自带 CMake 版本太旧(如 3.5.1) | 换用 CMake 3.16.0(解压即用的二进制包) |
-| 交叉编译不生效 | 配置写进了 `CMakeLists.txt` 但放在了 `project()` 之后 | 交叉编译配置必须在 `project()` 之前,或改用工具链文件 |
-| `add_subdirectory(../lib)` 报错 | 加载平级/上级目录未指定 `binary_dir` | 显式写 `add_subdirectory(../lib output)` |
-| 找不到头文件 | 未设置头文件搜索路径,或未向下/向上传递 | 用 `target_include_directories(... PUBLIC ...)` |
-| 生成的库名是 `liblibhello.a` | 库目标名与 `OUTPUT_NAME` 混用 | 用 `set_target_properties(... PROPERTIES OUTPUT_NAME "hello")` |
-| `make` 找不到目标 | 没在 `build` 目录执行 `cmake` | 回到 `build` 目录,先 `cmake ..` 再 `make` |
-| 想彻底重来 | 缓存了旧变量 | 删除 `build` 目录(或 `CMakeCache.txt`)后重新 `cmake` |
+| 现象                            | 原因                                                  | 处理                                                           |
+| ------------------------------- | ----------------------------------------------------- | -------------------------------------------------------------- |
+| `cmake` 报交叉编译配置相关错误  | Ubuntu 自带 CMake 版本太旧(如 3.5.1)                | 换用 CMake 3.16.0(解压即用的二进制包)                        |
+| 交叉编译不生效                  | 配置写进了 `CMakeLists.txt` 但放在了 `project()` 之后 | 交叉编译配置必须在 `project()` 之前,或改用工具链文件          |
+| `add_subdirectory(../lib)` 报错 | 加载平级/上级目录未指定 `binary_dir`                  | 显式写 `add_subdirectory(../lib output)`                       |
+| 找不到头文件                    | 未设置头文件搜索路径,或未向下/向上传递               | 用 `target_include_directories(... PUBLIC ...)`                |
+| 生成的库名是 `liblibhello.a`    | 库目标名与 `OUTPUT_NAME` 混用                         | 用 `set_target_properties(... PROPERTIES OUTPUT_NAME "hello")` |
+| `make` 找不到目标               | 没在 `build` 目录执行 `cmake`                         | 回到 `build` 目录,先 `cmake ..` 再 `make`                     |
+| 想彻底重来                      | 缓存了旧变量                                          | 删除 `build` 目录(或 `CMakeCache.txt`)后重新 `cmake`         |
 
 推荐构建流程:
 
@@ -1167,14 +1186,14 @@ make
 
 ## 8. 跨平台 / 工具对比
 
-| 维度 | 手写 gcc 命令 | Makefile | CMake | Qt qmake |
-| ---- | ------------- | -------- | ----- | -------- |
-| 跨平台 | 差 | 一般 | 好 | 好 |
-| 学习成本 | 低 | 中 | 中 | 低(Qt 专用) |
-| 依赖管理 | 无 | 手工 | 自动扫描 + 目标属性 | `QT +=` 模块 |
-| 大型工程 | 不可行 | 尚可 | 推荐 | Qt 工程推荐 |
-| 与 IDE 集成 | 差 | 一般 | 好(生成工程文件) | 好 |
-| 典型场景 | 单文件验证 | 小型 C 工程 | 中大型跨平台 C/C++ | Qt 应用 |
+| 维度        | 手写 gcc 命令 | Makefile    | CMake               | Qt qmake      |
+| ----------- | ------------- | ----------- | ------------------- | ------------- |
+| 跨平台      | 差            | 一般        | 好                  | 好            |
+| 学习成本    | 低            | 中          | 中                  | 低(Qt 专用) |
+| 依赖管理    | 无            | 手工        | 自动扫描 + 目标属性 | `QT +=` 模块  |
+| 大型工程    | 不可行        | 尚可        | 推荐                | Qt 工程推荐   |
+| 与 IDE 集成 | 差            | 一般        | 好(生成工程文件)  | 好            |
+| 典型场景    | 单文件验证    | 小型 C 工程 | 中大型跨平台 C/C++  | Qt 应用       |
 
 对同一份多文件源码,三种方式的对比:
 
@@ -1201,6 +1220,7 @@ cmake -B build && cmake --build build
 **详解**:`CMakeLists.txt` 与平台无关,CMake 根据当前平台/工具链生成对应构建脚本;Makefile 的语法和规则在不同平台往往不同,不能通用。因此跨平台的本质是"用统一描述文件 + 生成器适配各平台",而真正干活的仍是 `make`。
 
 **追问**:
+
 1. 为什么不直接写 Makefile?(语法复杂、跨平台差、大工程依赖难维护)
 2. `cmake` 和 `make` 分别负责什么?(cmake 生成构建文件,make 依据构建文件执行编译)
 
@@ -1211,6 +1231,7 @@ cmake -B build && cmake --build build
 **详解**:`target_include_directories(t PRIVATE d)` 只给 `t` 用;`INTERFACE` 只传给依赖 `t` 的目标;`PUBLIC` 两者都传。全局命令在大工程里容易污染其它目标、产生隐蔽错误,因此推荐目标级命令以保持目录清晰。
 
 **追问**:
+
 1. `PRIVATE` 和 `PUBLIC` 的差别在哪?(头文件目录是否传递给依赖该目标的目标)
 2. 静态库的 `PUBLIC` 头文件目录会怎样传给可执行文件?(通过链接关系自动带入)
 
@@ -1221,6 +1242,7 @@ cmake -B build && cmake --build build
 **详解**:`add_library` 默认生成静态库,`SHARED` 生成动态库;库名不含 `lib` 前缀与 `.a/.so` 后缀,由 CMake 自动补齐。目标名工程内必须唯一,所以不能用目标名去改文件名,只能用 `OUTPUT_NAME` 属性。
 
 **追问**:
+
 1. `BUILD_SHARED_LIBS` 的作用?(设为 `on` 时 `add_library` 默认生成动态库)
 2. 动态库拷到开发板后程序运行报 "cannot open shared object file" 怎么办?(把 `.so` 放到 `/usr/lib` 或设置 `LD_LIBRARY_PATH`)
 
@@ -1231,6 +1253,7 @@ cmake -B build && cmake --build build
 **详解**:配置不要写进 `CMakeLists.txt`,而是放到独立工具链文件,且必须在 `project()` 之前生效。示例见本篇第 5.1 节。`CMAKE_SYSROOT` 会作为 `--sysroot` 传给 gcc,`CMAKE_FIND_ROOT_PATH_MODE_LIBRARY/INCLUDE` 设为 `ONLY` 可保证只搜 sysroot。
 
 **追问**:
+
 1. `-D` 选项创建的是什么变量?(缓存变量,全局生效,覆盖同名普通变量)
 2. 怎么验证交叉编译成功?(`file` 命令查看产物架构,应为 ARM 而非 x86-64)
 
@@ -1241,6 +1264,7 @@ cmake -B build && cmake --build build
 **详解**:函数内 `set` 默认创建局部变量,不改变外层同名变量;加 `PARENT_SCOPE` 后设置到上一层作用域,可借"传入变量名"的技巧实现返回值。目录之间是值拷贝,子目录改父目录变量无效。`-D` 定义的缓存变量全局有效。
 
 **追问**:
+
 1. `PARENT_SCOPE` 在嵌套函数中写到哪一层?(调用者的作用域,即上一层)
 2. 为什么每次 `cmake` 都要清理 `build`?(缓存变量会保留并覆盖新配置,可能导致旧设置残留)
 

+ 147 - 120
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/04-网络编程与项目实战/04-实战项目MQTT与视频监控.md

@@ -1,6 +1,28 @@
 ---
 title: 实战项目MQTT与视频监控
-tags: [嵌入式Linux, 项目实战, MQTT, mosquitto, paho.mqtt.c, MQTTClient, 发布订阅, QoS, 遗嘱, 保留消息, 物联网, RTMP, Nginx, FFmpeg, VLC, 视频监控, V4L2, MJPEG, Qt, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    项目实战,
+    MQTT,
+    mosquitto,
+    paho.mqtt.c,
+    MQTTClient,
+    发布订阅,
+    QoS,
+    遗嘱,
+    保留消息,
+    物联网,
+    RTMP,
+    Nginx,
+    FFmpeg,
+    VLC,
+    视频监控,
+    V4L2,
+    MJPEG,
+    Qt,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第三十三章 实战小项目之MQTT物联网、第三十四章 实战小项目之视频监控;《嵌入式Qt开发指南》视频监控例程"
@@ -22,15 +44,15 @@ MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是一
 
 ### 1.1 主要特性
 
-| 特性 | 说明 |
-| ---- | ---- |
-| 发布/订阅模式 | 一对多的消息发布,解除应用程序耦合 |
-| 基于 TCP/IP | 主流 MQTT 基于 TCP;也有基于 UDP 的 MQTT-SN |
-| 支持 QoS | 按消息重要性设置不同服务质量等级(0/1/2) |
-| 小型传输 | 开销很小,协议交换最小化,降低网络流量 |
-| 遗嘱机制(will) | 客户端异常断线时通知其它客户端 |
-| 主题寻址 | 基于主题发布/订阅,对负载内容屏蔽 |
-| 心跳机制 | 检测客户端是否在线 |
+| 特性             | 说明                                        |
+| ---------------- | ------------------------------------------- |
+| 发布/订阅模式    | 一对多的消息发布,解除应用程序耦合          |
+| 基于 TCP/IP      | 主流 MQTT 基于 TCP;也有基于 UDP 的 MQTT-SN |
+| 支持 QoS         | 按消息重要性设置不同服务质量等级(0/1/2)   |
+| 小型传输         | 开销很小,协议交换最小化,降低网络流量      |
+| 遗嘱机制(will) | 客户端异常断线时通知其它客户端              |
+| 主题寻址         | 基于主题发布/订阅,对负载内容屏蔽           |
+| 心跳机制         | 检测客户端是否在线                          |
 
 ### 1.2 历史与版本
 
@@ -43,11 +65,11 @@ MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是一
 
 ### 2.1 三个核心角色
 
-| 角色 | 说明 |
-| ---- | ---- |
+| 角色             | 说明                                                                      |
+| ---------------- | ------------------------------------------------------------------------- |
 | 服务端(Broker) | MQTT 信息传输的枢纽,负责把客户端发来的信息转发给其它客户端,并管理客户端 |
-| 客户端 | 可以发布消息,也可以订阅主题后接收消息;角色不固定,随主题切换 |
-| 主题(Topic) | 发布/订阅的纽带,服务端按主题决定消息转发给哪些订阅者 |
+| 客户端           | 可以发布消息,也可以订阅主题后接收消息;角色不固定,随主题切换            |
+| 主题(Topic)    | 发布/订阅的纽带,服务端按主题决定消息转发给哪些订阅者                     |
 
 客户端发布消息时要指定主题;订阅者订阅主题后,服务端就把该主题的新消息推送给订阅者。一个客户端对不同主题可以既是发布者又是订阅者。
 
@@ -92,11 +114,11 @@ flowchart LR
 
 CONNECT 报文关键字段:
 
-| 字段 | 说明 |
-| ---- | ---- |
-| `clientId` | 客户端标识,必须唯一;两个客户端用同一 clientId 会被当成同一个客户端 |
-| `keepAlive` | 心跳时间间隔,如 60 表示约每 60 秒发一次心跳包 |
-| `cleanSession` | 清除会话标志 |
+| 字段           | 说明                                                                 |
+| -------------- | -------------------------------------------------------------------- |
+| `clientId`     | 客户端标识,必须唯一;两个客户端用同一 clientId 会被当成同一个客户端 |
+| `keepAlive`    | 心跳时间间隔,如 60 表示约每 60 秒发一次心跳包                       |
+| `cleanSession` | 清除会话标志                                                         |
 
 `cleanSession` 的作用:
 
@@ -105,35 +127,35 @@ CONNECT 报文关键字段:
 
 CONNACK 报文包含 `returnCode`(连接返回码)与 `sessionPresent`:
 
-| returnCode | 说明 |
-| ---------- | ---- |
-| 0 | 连接成功 |
-| 1 | 拒绝:不支持的 MQTT 协议版本 |
-| 2 | 拒绝:不支持客户端标识符的编码 |
-| 3 | 拒绝:服务端不可用 |
-| 4 | 拒绝:用户名或密码无效 |
-| 5 | 拒绝:客户端未授权 |
-| 6-255 | 保留备用 |
+| returnCode | 说明                           |
+| ---------- | ------------------------------ |
+| 0          | 连接成功                       |
+| 1          | 拒绝:不支持的 MQTT 协议版本   |
+| 2          | 拒绝:不支持客户端标识符的编码 |
+| 3          | 拒绝:服务端不可用             |
+| 4          | 拒绝:用户名或密码无效         |
+| 5          | 拒绝:客户端未授权             |
+| 6-255      | 保留备用                       |
 
 `sessionPresent` 与 `cleanSession` 配合:`cleanSession=0` 时,若服务端保存了上次会话状态则返回 1,否则 0;`cleanSession=1` 时恒为 0。
 
 ### 2.3 发布、订阅、取消订阅
 
-| 报文 | 作用 |
-| ---- | ---- |
-| PUBLISH | 发布消息(含 packetId、topicName、payload、qos、retain、dup) |
-| SUBSCRIBE / SUBACK | 订阅主题 / 订阅确认(一个 SUBSCRIBE 可含多个主题) |
-| UNSUBSCRIBE / UNSUBACK | 取消订阅 / 取消订阅确认 |
-| DISCONNECT | 客户端主动断开连接 |
+| 报文                   | 作用                                                          |
+| ---------------------- | ------------------------------------------------------------- |
+| PUBLISH                | 发布消息(含 packetId、topicName、payload、qos、retain、dup) |
+| SUBSCRIBE / SUBACK     | 订阅主题 / 订阅确认(一个 SUBSCRIBE 可含多个主题)            |
+| UNSUBSCRIBE / UNSUBACK | 取消订阅 / 取消订阅确认                                       |
+| DISCONNECT             | 客户端主动断开连接                                            |
 
 SUBACK 的订阅返回码:
 
-| 返回码 | 说明 |
-| ------ | ---- |
-| 0x00 | 订阅成功——QoS0 |
-| 0x01 | 订阅成功——QoS1 |
-| 0x02 | 订阅成功——QoS2 |
-| 0x80 | 订阅失败 |
+| 返回码 | 说明           |
+| ------ | -------------- |
+| 0x00   | 订阅成功——QoS0 |
+| 0x01   | 订阅成功——QoS1 |
+| 0x02   | 订阅成功——QoS2 |
+| 0x80   | 订阅失败       |
 
 注意事项:
 
@@ -152,11 +174,11 @@ SUBACK 的订阅返回码:
 
 ### 2.5 QoS 服务质量
 
-| QoS | 语义 | 传输流程 |
-| --- | ---- | -------- |
-| 0 | 最多发一次(最多一次) | 发送即结束,不确认、不重传,靠 TCP 保证 |
-| 1 | 至少发一次(最少一次) | PUBLISH → PUBACK,超时未收到 PUBACK 则重发,可能重复 |
-| 2 | 保证收一次 | PUBLISH → PUBREC → PUBREL → PUBCOMP,最安全也最慢 |
+| QoS | 语义                   | 传输流程                                             |
+| --- | ---------------------- | ---------------------------------------------------- |
+| 0   | 最多发一次(最多一次) | 发送即结束,不确认、不重传,靠 TCP 保证              |
+| 1   | 至少发一次(最少一次) | PUBLISH → PUBACK,超时未收到 PUBACK 则重发,可能重复 |
+| 2   | 保证收一次             | PUBLISH → PUBREC → PUBREL → PUBCOMP,最安全也最慢    |
 
 ```mermaid
 sequenceDiagram
@@ -198,12 +220,12 @@ sequenceDiagram
 
 客户端在"活着"时通过 CONNECT 报文预先写好遗嘱,一旦**意外断线**(非主动发 DISCONNECT,如断网、断电),服务端就把遗嘱消息发布出去。主动断开不会触发遗嘱。
 
-| 字段 | 说明 |
-| ---- | ---- |
-| `willTopic` | 遗嘱主题,订阅该主题的客户端才会收到遗嘱消息 |
-| `willMessage` | 遗嘱内容 |
-| `willRetain` | 遗嘱的保留标志 |
-| `willQoS` | 遗嘱的 QoS 等级(0/1/2) |
+| 字段          | 说明                                         |
+| ------------- | -------------------------------------------- |
+| `willTopic`   | 遗嘱主题,订阅该主题的客户端才会收到遗嘱消息 |
+| `willMessage` | 遗嘱内容                                     |
+| `willRetain`  | 遗嘱的保留标志                               |
+| `willQoS`     | 遗嘱的 QoS 等级(0/1/2)                     |
 
 小技巧:客户端上线时主动向自己的遗嘱主题发一条"在线"消息,订阅者即可据此实现上线通知。
 
@@ -215,12 +237,12 @@ CONNECT 报文中的 `username`、`password` 是可选字段。服务端若开
 
 ### 3.1 公用 MQTT 服务器
 
-| 服务器 | 地址 | 端口 |
-| ------ | ---- | ---- |
+| 服务器                     | 地址                 | 端口                                        |
+| -------------------------- | -------------------- | ------------------------------------------- |
 | test.mosquitto.org(国外) | `test.mosquitto.org` | 1883(TCP)、8883(TLS)、8080(WebSocket) |
-| broker.hivemq.com(国外) | `broker.hivemq.com` | 1883、8000(WebSocket) |
-| 然也物联(国内) | `test.ranye-iot.net` | 1883、8883(TLS) |
-| 通信猫(国内) | `mq.tongxinmao.com` | 1883 |
+| broker.hivemq.com(国外)  | `broker.hivemq.com`  | 1883、8000(WebSocket)                     |
+| 然也物联(国内)           | `test.ranye-iot.net` | 1883、8883(TLS)                           |
+| 通信猫(国内)             | `mq.tongxinmao.com`  | 1883                                        |
 
 > 公用服务器仅供学习测试,不可商用;国内服务器连接快、延迟低。
 
@@ -334,12 +356,12 @@ make install
 
 `install/` 下有 `bin`、`include`、`lib`、`share`:
 
-| 库文件 | 说明 |
-| ------ | ---- |
-| `libpaho-mqtt3a.so` | 异步模式,不支持 SSL |
-| `libpaho-mqtt3as.so` | 异步模式,支持 SSL |
-| `libpaho-mqtt3c.so` | 同步模式,不支持 SSL |
-| `libpaho-mqtt3cs.so` | 同步模式,支持 SSL |
+| 库文件               | 说明                 |
+| -------------------- | -------------------- |
+| `libpaho-mqtt3a.so`  | 异步模式,不支持 SSL |
+| `libpaho-mqtt3as.so` | 异步模式,支持 SSL   |
+| `libpaho-mqtt3c.so`  | 同步模式,不支持 SSL |
+| `libpaho-mqtt3cs.so` | 同步模式,支持 SSL   |
 
 头文件:同步模式包含 `MQTTClient.h`,异步模式包含 `MQTTAsync.h`(其它头文件被这两个包含)。本项目使用**同步模式** `libpaho-mqtt3c.so` + `MQTTClient.h`。
 
@@ -386,19 +408,19 @@ int MQTTClient_create(MQTTClient *handle,
                       void *persistence_context);
 ```
 
-| 参数 | 说明 |
-| ---- | ---- |
-| `handle` | MQTT 客户端句柄 |
-| `serverURI` | MQTT 服务器地址 |
-| `clientId` | 客户端 ID |
-| `persistence_type` | 持久化类型,见下表 |
+| 参数                  | 说明                                                                   |
+| --------------------- | ---------------------------------------------------------------------- |
+| `handle`              | MQTT 客户端句柄                                                        |
+| `serverURI`           | MQTT 服务器地址                                                        |
+| `clientId`            | 客户端 ID                                                              |
+| `persistence_type`    | 持久化类型,见下表                                                     |
 | `persistence_context` | `NONE` 时传 `NULL`;`DEFAULT` 时为持久化目录(传 `NULL` 表示工作目录) |
 
-| persistence_type | 说明 |
-| ---------------- | ---- |
-| `MQTTCLIENT_PERSISTENCE_NONE` | 内存持久化,系统故障时传输中消息状态可能丢失 |
+| persistence_type                 | 说明                                           |
+| -------------------------------- | ---------------------------------------------- |
+| `MQTTCLIENT_PERSISTENCE_NONE`    | 内存持久化,系统故障时传输中消息状态可能丢失   |
 | `MQTTCLIENT_PERSISTENCE_DEFAULT` | 默认(基于文件系统)持久化,提供一定防丢失保护 |
-| `MQTTCLIENT_PERSISTENCE_USER` | 应用自定义持久化实现 |
+| `MQTTCLIENT_PERSISTENCE_USER`    | 应用自定义持久化实现                           |
 
 返回值:成功 `MQTTCLIENT_SUCCESS`,失败返回错误码。
 
@@ -474,11 +496,11 @@ int MQTTClient_setCallbacks(MQTTClient handle,
                             MQTTClient_deliveryComplete *dc);
 ```
 
-| 回调 | 类型 | 说明 |
-| ---- | ---- | ---- |
-| `cl` | `void (void *context, char *cause)` | 断线时执行;设 `NULL` 表示不处理断线 |
-| `ma` | `int (void *context, char *topicName, int topicLen, MQTTClient_message *message)` | 收到消息时执行,**必须设置** |
-| `dc` | `void (void *context, MQTTClient_deliveryToken dt)` | 发布确认时执行,可设 `NULL` |
+| 回调 | 类型                                                                              | 说明                                 |
+| ---- | --------------------------------------------------------------------------------- | ------------------------------------ |
+| `cl` | `void (void *context, char *cause)`                                               | 断线时执行;设 `NULL` 表示不处理断线 |
+| `ma` | `int (void *context, char *topicName, int topicLen, MQTTClient_message *message)` | 收到消息时执行,**必须设置**         |
+| `dc` | `void (void *context, MQTTClient_deliveryToken dt)`                               | 发布确认时执行,可设 `NULL`          |
 
 `ma` 回调的两个关键点:
 
@@ -813,10 +835,10 @@ exit:
 
 三个主题的作用:
 
-| 宏 | 主题 | 作用 |
-| -- | ---- | ---- |
-| `WILL_TOPIC` | `dt_mqtt/will` | 遗嘱主题,正常上线时发布 "Online",意外断线时由服务端发布遗嘱消息 |
-| `LED_TOPIC` | `dt_mqtt/led` | 开发板订阅;收到 "0"/"1"/"2" 分别控制 LED 熄灭/常亮/呼吸灯 |
+| 宏           | 主题                  | 作用                                                                          |
+| ------------ | --------------------- | ----------------------------------------------------------------------------- |
+| `WILL_TOPIC` | `dt_mqtt/will`        | 遗嘱主题,正常上线时发布 "Online",意外断线时由服务端发布遗嘱消息             |
+| `LED_TOPIC`  | `dt_mqtt/led`         | 开发板订阅;收到 "0"/"1"/"2" 分别控制 LED 熄灭/常亮/呼吸灯                    |
 | `TEMP_TOPIC` | `dt_mqtt/temperature` | 开发板每 30 秒发布一次 SoC 温度(读 `/sys/class/thermal/thermal_zone0/temp`) |
 
 ### 6.5 构建与编译
@@ -871,10 +893,10 @@ cJSON_Delete(root);
 
 常见视频监控/直播使用 **RTMP** 和 **RTSP** 流媒体传输协议:
 
-| 协议 | 提出方 | 特点 | 适用 |
-| ---- | ------ | ---- | ---- |
-| RTSP(Real-Time Stream Protocol) | Real Networks 与 Netscape | 基于文本的多媒体播放控制协议,定义流格式,流数据经 RTP 传输;实时性最好 | 视频聊天、视频监控 |
-| RTMP(Real Time Message Protocol) | Adobe | 解决流媒体多路复用与分包,低延迟、稳定性高,支持所有摄像头格式,浏览器加载 flash 插件可直接播放 | 直播、推流 |
+| 协议                               | 提出方                    | 特点                                                                                            | 适用               |
+| ---------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------- | ------------------ |
+| RTSP(Real-Time Stream Protocol)  | Real Networks 与 Netscape | 基于文本的多媒体播放控制协议,定义流格式,流数据经 RTP 传输;实时性最好                         | 视频聊天、视频监控 |
+| RTMP(Real Time Message Protocol) | Adobe                     | 解决流媒体多路复用与分包,低延迟、稳定性高,支持所有摄像头格式,浏览器加载 flash 插件可直接播放 | 直播、推流         |
 
 选择:RTSP 实时性最好但实现复杂;RTMP 浏览器支持好、生态成熟。本章用 **FFmpeg + Nginx** 通过 RTMP 推流实现视频监控/直播。
 
@@ -916,11 +938,11 @@ source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linu
 
 修改两处源码,否则配置通不过:
 
-| 文件 | 位置 | 修改 |
-| ---- | ---- | ---- |
-| `auto/cc/name` | 第 21 行 | 注释掉 `exit 1` |
+| 文件                | 位置     | 修改                          |
+| ------------------- | -------- | ----------------------------- |
+| `auto/cc/name`      | 第 21 行 | 注释掉 `exit 1`               |
 | `auto/types/sizeof` | 第 15 行 | `ngx_size=` 改为 `ngx_size=4` |
-| `auto/types/sizeof` | 第 36 行 | `$CC` 改为 `gcc` |
+| `auto/types/sizeof` | 第 36 行 | `$CC` 改为 `gcc`              |
 
 ### 9.3 配置、编译、安装
 
@@ -1289,16 +1311,16 @@ void CaptureThread::run()
 
 V4L2 采集流程(与 C 应用编程 V4L2 章节一致):
 
-| 步骤 | ioctl / 操作 | 说明 |
-| ---- | ------------ | ---- |
-| 打开设备 | `open(VIDEO_DEV, O_RDWR)` | `/dev/video1` 为摄像头节点(按实际确认) |
-| 设置格式 | `VIDIOC_S_FMT` | 640×480、`V4L2_PIX_FMT_RGB565` |
-| 申请缓冲 | `VIDIOC_REQBUFS` | 3 个缓冲区,`V4L2_MEMORY_MMAP` |
-| 查询并映射 | `VIDIOC_QUERYBUF` + `mmap` | 得到每个缓冲区的地址与长度 |
-| 入队 | `VIDIOC_QBUF` | 把所有缓冲区放入采集队列 |
-| 启动流 | `VIDIOC_STREAMON` | 开始采集 |
-| 循环取帧 | `VIDIOC_DQBUF` → 处理 → `VIDIOC_QBUF` | 取出一帧、转 `QImage`、处理完再入队 |
-| 停止 | 循环退出后 `munmap` + `close` | `while(startFlag)` 控制 |
+| 步骤       | ioctl / 操作                          | 说明                                     |
+| ---------- | ------------------------------------- | ---------------------------------------- |
+| 打开设备   | `open(VIDEO_DEV, O_RDWR)`             | `/dev/video1` 为摄像头节点(按实际确认) |
+| 设置格式   | `VIDIOC_S_FMT`                        | 640×480、`V4L2_PIX_FMT_RGB565`           |
+| 申请缓冲   | `VIDIOC_REQBUFS`                      | 3 个缓冲区,`V4L2_MEMORY_MMAP`           |
+| 查询并映射 | `VIDIOC_QUERYBUF` + `mmap`            | 得到每个缓冲区的地址与长度               |
+| 入队       | `VIDIOC_QBUF`                         | 把所有缓冲区放入采集队列                 |
+| 启动流     | `VIDIOC_STREAMON`                     | 开始采集                                 |
+| 循环取帧   | `VIDIOC_DQBUF` → 处理 → `VIDIOC_QBUF` | 取出一帧、转 `QImage`、处理完再入队      |
+| 停止       | 循环退出后 `munmap` + `close`         | `while(startFlag)` 控制                  |
 
 广播处理链:`QImage` → `QBuffer`(内存 IO)→ `save(..., "JPEG", -1)` 编码为 JPEG → `toBase64()` → `QUdpSocket::writeDatagram(..., QHostAddress::Broadcast, 8888)`。
 
@@ -1459,25 +1481,25 @@ RTMP 推流常见编码为 H.264;而上面 Qt 方案用 JPEG 逐帧广播(MJ
 
 ## 13. 实验步骤与调试
 
-| 现象 | 原因 | 处理 |
-| ---- | ---- | ---- |
-| 开发板连不上 MQTT | 服务器 IP/端口错、用户名密码错、不在同一网段 | 确认 `BROKER_ADDRESS`、`ping` 通、`mosquitto` 已启动 |
-| 连接返回码 4/5 | 用户名或密码无效 / 未授权 | 用 `mosquitto_passwd` 重新注册,核对 `myconfig.conf` |
-| 编译 mqttClient 找不到 `MQTTClient.h` | 头文件路径未指定 | `target_include_directories` 指向 install/include |
-| 链接时报找不到 `paho-mqtt3c` | 库路径未指定或库未安装 | `target_link_directories` + `target_link_libraries`,并 `make install` |
-| 开发板运行报找不到 `libpaho-mqtt3c.so` | 动态库未拷入 | 打包 `.so` 解压到 `/usr/lib`,或设置 `LD_LIBRARY_PATH` |
-| VLC 拉不到 RTMP 流 | nginx 未启动/未 reload、端口被占、推流地址错 | `ps` 查进程、`nginx -s reload`、核对 `1935` 与流名 |
-| 视频延迟大 | 软编解码 + 服务器与推流端同板 | 降低分辨率/帧率,或换高性能平台 |
-| ARM 上采集线程直接返回 | 采集代码用 `#ifndef __arm__ return;` 限定 ARM | 确认交叉编译宏 `__arm__`,PC 上不采集 |
+| 现象                                   | 原因                                          | 处理                                                                   |
+| -------------------------------------- | --------------------------------------------- | ---------------------------------------------------------------------- |
+| 开发板连不上 MQTT                      | 服务器 IP/端口错、用户名密码错、不在同一网段  | 确认 `BROKER_ADDRESS`、`ping` 通、`mosquitto` 已启动                   |
+| 连接返回码 4/5                         | 用户名或密码无效 / 未授权                     | 用 `mosquitto_passwd` 重新注册,核对 `myconfig.conf`                   |
+| 编译 mqttClient 找不到 `MQTTClient.h`  | 头文件路径未指定                              | `target_include_directories` 指向 install/include                      |
+| 链接时报找不到 `paho-mqtt3c`           | 库路径未指定或库未安装                        | `target_link_directories` + `target_link_libraries`,并 `make install` |
+| 开发板运行报找不到 `libpaho-mqtt3c.so` | 动态库未拷入                                  | 打包 `.so` 解压到 `/usr/lib`,或设置 `LD_LIBRARY_PATH`                 |
+| VLC 拉不到 RTMP 流                     | nginx 未启动/未 reload、端口被占、推流地址错  | `ps` 查进程、`nginx -s reload`、核对 `1935` 与流名                     |
+| 视频延迟大                             | 软编解码 + 服务器与推流端同板                 | 降低分辨率/帧率,或换高性能平台                                        |
+| ARM 上采集线程直接返回                 | 采集代码用 `#ifndef __arm__ return;` 限定 ARM | 确认交叉编译宏 `__arm__`,PC 上不采集                                  |
 
 ## 14. 跨平台对比
 
-| 项目 | IMX6U(本方案) | STM32(如 F4/F7 + LwIP) | 高性能 ARM Linux(RK3568 等) |
-| ---- | --------------- | ------------------------ | ----------------------------- |
-| MQTT 客户端 | paho.mqtt.c(POSIX,同步/异步) | MQTT 嵌入式库(如 Paho Embedded / coreMQTT) | 同 paho / 各云 SDK |
-| 视频方案 | Nginx+FFmpeg(软编) 或 V4L2+JPEG 广播 | 一般无 Linux,靠专用编码/传输 | 硬件 H.264 编码 + RTSP/RTMP,低延迟 |
-| 网络栈 | 内核 TCP/IP + BSD socket | LwIP | 内核 TCP/IP |
-| 构建 | CMake + 交叉工具链 | Keil/IAR/Makefile | CMake/Yocto/Buildroot |
+| 项目        | IMX6U(本方案)                      | STM32(如 F4/F7 + LwIP)                     | 高性能 ARM Linux(RK3568 等)       |
+| ----------- | ------------------------------------ | -------------------------------------------- | ----------------------------------- |
+| MQTT 客户端 | paho.mqtt.c(POSIX,同步/异步)      | MQTT 嵌入式库(如 Paho Embedded / coreMQTT) | 同 paho / 各云 SDK                  |
+| 视频方案    | Nginx+FFmpeg(软编) 或 V4L2+JPEG 广播 | 一般无 Linux,靠专用编码/传输                | 硬件 H.264 编码 + RTSP/RTMP,低延迟 |
+| 网络栈      | 内核 TCP/IP + BSD socket             | LwIP                                         | 内核 TCP/IP                         |
+| 构建        | CMake + 交叉工具链                   | Keil/IAR/Makefile                            | CMake/Yocto/Buildroot               |
 
 ## 15. 面试精选(5 题)
 
@@ -1487,14 +1509,15 @@ RTMP 推流常见编码为 H.264;而上面 Qt 方案用 JPEG 逐帧广播(MJ
 
 **详解**:发布者把消息发到某个主题,Broker 按订阅关系转发给订阅者;客户端角色随主题切换,彼此无需知道对方存在。相比 HTTP 的请求-响应、短连接,MQTT 保持长连接、低开销、支持一对多推送与离线消息,更适合物联网。区别:
 
-| 维度 | MQTT | HTTP |
-| ---- | ---- | ---- |
-| 模型 | 发布/订阅 | 请求/响应 |
-| 连接 | 长连接 + 心跳 | 多为短连接 |
-| 开销 | 极小 | 头部冗余大 |
+| 维度 | MQTT             | HTTP             |
+| ---- | ---------------- | ---------------- |
+| 模型 | 发布/订阅        | 请求/响应        |
+| 连接 | 长连接 + 心跳    | 多为短连接       |
+| 开销 | 极小             | 头部冗余大       |
 | 推送 | 服务端可主动推送 | 需轮询/长轮询/WS |
 
 **追问**:
+
 1. 发布/订阅的"三个可"是什么?(客户端相互独立、空间可分离、时间可异步)
 2. Broker 的职责有哪些?(接收、存储、转发消息,管理客户端与会话)
 
@@ -1505,6 +1528,7 @@ RTMP 推流常见编码为 H.264;而上面 Qt 方案用 JPEG 逐帧广播(MJ
 **详解**:QoS0 发完即弃,靠 TCP;QoS1 用 PUBACK 确认,超时重发,可能重复(协议不去重,需按 dup 处理);QoS2 经 PUBREC/PUBREL/PUBCOMP 四次交互,保证接收端只收一次。实现 QoS>0 必须 `cleanSession=false`。发布与订阅 QoS 不同时,服务端按较低值降级。
 
 **追问**:
+
 1. 为什么 QoS1 需要应用自己去重?(协议只保证至少一次,重复由重发引起)
 2. QoS2 的报文顺序?(PUBLISH → PUBREC → PUBREL → PUBCOMP)
 
@@ -1515,6 +1539,7 @@ RTMP 推流常见编码为 H.264;而上面 Qt 方案用 JPEG 逐帧广播(MJ
 **详解**:`cleanSession=1` 每次连接都是全新会话,不保存订阅、不收离线消息,`sessionPresent` 恒为 0;`cleanSession=0` 时服务端保存会话,`sessionPresent` 表示是否恢复。保留消息针对"发布在前、订阅在后"的场景,每个主题只保留一条最新值,发布空保留消息可删除。
 
 **追问**:
+
 1. 想接收离线 QoS1/QoS2 消息该怎样设置?(`cleanSession=false`)
 2. 如何删除某主题的保留消息?(向该主题发布一条 payload 为空的保留消息)
 
@@ -1525,6 +1550,7 @@ RTMP 推流常见编码为 H.264;而上面 Qt 方案用 JPEG 逐帧广播(MJ
 **详解**:客户端空闲时按 `keepAlive` 定时发 PINGREQ,服务端回 PINGRESP,长时间收不到即判定掉线;客户端发 PINGREQ 收不到响应则认为自身断线。遗嘱在 CONNECT 时设置(willTopic/willMessage/willRetain/willQoS),仅**意外断线**触发,主动 DISCONNECT 不触发。
 
 **追问**:
+
 1. 遗嘱在什么情况下不会发布?(客户端主动发送 DISCONNECT 断开)
 2. 如何用遗嘱实现上线通知?(上线时主动向自己的遗嘱主题发布一条"在线"消息)
 
@@ -1535,6 +1561,7 @@ RTMP 推流常见编码为 H.264;而上面 Qt 方案用 JPEG 逐帧广播(MJ
 **详解**:`ffmpeg -re -i ... -f flv rtmp://.../live/xxx` 推流,VLC 拉流;Nginx 加 `nginx-rtmp-module` 并配置 `rtmp { server { application live {...} } }`。延迟原因:I.MX6U 无硬件视频解码,全靠软件处理,且服务器与推流端在同一弱性能板子上。另一条 V4L2+JPEG 广播路线(Qt 例程)无需流媒体服务器:采集 RGB565 → JPEG → base64 → UDP 广播,客户端解码显示;实现简单、适合同网段,但带宽大、本地显示与广播互相制约。
 
 **追问**:
+
 1. RTMP 与 RTSP 的主要区别?(RTSP 实时性最好但实现复杂;RTMP 浏览器支持好、低延迟、生态成熟)
 2. 为什么 Qt 方案选择 UDP 广播而不是 TCP?(一对多、无连接、实现简单;代价是可能丢包)
 

+ 147 - 95
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/04-网络编程与项目实战/面试-网络编程与项目实战.md

@@ -1,6 +1,33 @@
 ---
 title: 面试-网络编程与项目实战
-tags: [嵌入式Linux, 面试, 网络编程, socket, TCP, UDP, OSI, IP地址, 端口号, CAN, SocketCAN, can_frame, CMake, 交叉编译, MQTT, QoS, 遗嘱, paho, RTMP, RTSP, Nginx, FFmpeg, V4L2, 视频监控, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    面试,
+    网络编程,
+    socket,
+    TCP,
+    UDP,
+    OSI,
+    IP地址,
+    端口号,
+    CAN,
+    SocketCAN,
+    can_frame,
+    CMake,
+    交叉编译,
+    MQTT,
+    QoS,
+    遗嘱,
+    paho,
+    RTMP,
+    RTSP,
+    Nginx,
+    FFmpeg,
+    V4L2,
+    视频监控,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第二十九章~第三十四章:网络基础知识、socket编程基础、CAN应用编程基础、CMake入门与进阶、实战小项目之MQTT物联网、实战小项目之视频监控"
@@ -28,19 +55,20 @@ pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第二十九章~第
 
 网络通信本质是不同主机上进程之间的通信,属于 IPC 的一种,称为 socket IPC。socket 是应用层与 TCP/IP 协议之间的软件抽象层(门面模式),把复杂的协议隐藏在简单接口后面,便于跨平台(BSD socket 标准)。
 
-| OSI 七层 | 作用 | TCP/IP 四层 |
-| -------- | ---- | ----------- |
-| 应用层 | HTTP/FTP/MQTT 等 | 应用层 |
-| 表示层 | 编码转换、压缩/加密 | 应用层 |
-| 会话层 | 建立/管理/终止会话 | 应用层 |
-| 传输层 | 端到端、端口号、TCP/UDP | 传输层 |
-| 网络层 | IP 寻址、路由 | 网络层 |
-| 数据链路层 | MAC、成帧、差错检测 | 网络接口层 |
-| 物理层 | 比特流、电平、介质 | 网络接口层 |
+| OSI 七层   | 作用                    | TCP/IP 四层 |
+| ---------- | ----------------------- | ----------- |
+| 应用层     | HTTP/FTP/MQTT 等        | 应用层      |
+| 表示层     | 编码转换、压缩/加密     | 应用层      |
+| 会话层     | 建立/管理/终止会话      | 应用层      |
+| 传输层     | 端到端、端口号、TCP/UDP | 传输层      |
+| 网络层     | IP 寻址、路由           | 网络层      |
+| 数据链路层 | MAC、成帧、差错检测     | 网络接口层  |
+| 物理层     | 比特流、电平、介质      | 网络接口层  |
 
 数据发送时逐层加首部(封装),接收时逐层去首部(拆封)。
 
 **追问**
+
 1. socket 属于哪一层?(应用层与传输层之间的接口层,把 TCP/IP 隐藏在接口后面)
 2. 为什么说网络通信也是 IPC?(它是不同主机上进程之间的通信,属于进程间通信范畴)
 
@@ -50,14 +78,14 @@ pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第二十九章~第
 
 **详细解答**:
 
-| 维度 | TCP | UDP |
-| ---- | --- | --- |
-| 连接 | 面向连接(三次握手) | 无连接 |
-| 可靠性 | 可靠,不丢不乱 | 不可靠,出错丢弃无反馈 |
-| 传输单位 | 字节流 | 报文 |
-| 速度/开销 | 慢、开销大 | 快、无状态 |
-| 流控/拥塞控制 | 有(滑动窗口、慢启动等) | 无 |
-| 典型应用 | 文件传输、网页、MQTT、RTSP | 直播、网络电话、视频、DNS |
+| 维度          | TCP                        | UDP                       |
+| ------------- | -------------------------- | ------------------------- |
+| 连接          | 面向连接(三次握手)       | 无连接                    |
+| 可靠性        | 可靠,不丢不乱             | 不可靠,出错丢弃无反馈    |
+| 传输单位      | 字节流                     | 报文                      |
+| 速度/开销     | 慢、开销大                 | 快、无状态                |
+| 流控/拥塞控制 | 有(滑动窗口、慢启动等)   | 无                        |
+| 典型应用      | 文件传输、网页、MQTT、RTSP | 直播、网络电话、视频、DNS |
 
 TCP 可靠传输机制:
 
@@ -67,6 +95,7 @@ TCP 可靠传输机制:
 4. **校验和**:检测数据有效性;此外还有**窗口流量控制**与**拥塞控制**。
 
 **追问**
+
 1. TCP 流量控制与拥塞控制分别由谁控制?(流量控制由接收方通过窗口控制;拥塞控制由发送方控制)
 2. 为什么视频直播常用 UDP?(实时性要求高、可容忍少量丢包,无重传带来的延迟)
 
@@ -94,6 +123,7 @@ TCP 可靠传输机制:
 因为 TCP 全双工,每个方向要单独关闭:一方发 FIN 只表示该方向没有数据了,另一方向仍可继续发送,所以确认和 FIN 不能像握手那样合并,需四次。
 
 **追问**
+
 1. TIME_WAIT 为什么等 2MSL?(确保最后的 ACK 能到达对端,并让本连接的旧报文在网络中消失)
 2. 为什么握手需要三次而不能两次?(两次无法让服务端确认客户端已收到自己的 SYN/ACK,且可防止历史失效连接请求)
 
@@ -135,6 +165,7 @@ close(sockfd);
 关键点:`socket()` 返回的是**监听套接字**;`accept()` 返回的是**已连接套接字**,服务器用它和对应客户端收发电数据。`accept()` 在无连接请求时阻塞;若对客户端地址不关心,`addr`/`addrlen` 可传 `NULL`。`bind` 不是必需:客户端可让内核自动选址。
 
 **追问**
+
 1. `listen()` 的 `backlog` 是什么?(等待连接队列的最大值,队列满时新连接请求可能被丢弃/报错)
 2. UDP 客户端调用 `connect()` 会发生握手吗?(不会,只在 sockfd 中记录服务器地址端口,不发送数据)
 
@@ -144,13 +175,13 @@ close(sockfd);
 
 **详细解答**:
 
-| 类别 | 首字节范围 | 默认掩码 | 私有地址 |
-| ---- | ---------- | -------- | -------- |
-| A | 1~126 | 255.0.0.0 | 10.0.0.0~10.255.255.255 |
-| B | 128~191 | 255.255.0.0 | 172.16.0.0~172.31.255.255 |
-| C | 192~223 | 255.255.255.0 | 192.168.0.0~192.168.255.255 |
-| D | 224~239(多播) | — | — |
-| E | 240~255(保留) | — | — |
+| 类别 | 首字节范围      | 默认掩码      | 私有地址                    |
+| ---- | --------------- | ------------- | --------------------------- |
+| A    | 1~126           | 255.0.0.0     | 10.0.0.0~10.255.255.255     |
+| B    | 128~191         | 255.255.0.0   | 172.16.0.0~172.31.255.255   |
+| C    | 192~223         | 255.255.255.0 | 192.168.0.0~192.168.255.255 |
+| D    | 224~239(多播) | —             | —                           |
+| E    | 240~255(保留) | —             | —                           |
 
 特殊地址:
 
@@ -179,6 +210,7 @@ inet_ntop(AF_INET, &addr, buf, sizeof(buf)); /* 二进制 -> 点分十进制字
 `inet_aton/inet_addr/inet_ntoa` 已废弃,且不支持 IPv6;新代码用 `inet_pton/inet_ntop`。
 
 **追问**
+
 1. `INADDR_ANY` 是什么?(值为 0,表示绑定本机所有网络接口地址)
 2. 端口号为什么不能随便选?(要避免与已有服务冲突;自定义服务通常选大于 5000 的不常用端口)
 
@@ -208,6 +240,7 @@ inet_ntop(AF_INET, &addr, buf, sizeof(buf)); /* 二进制 -> 点分十进制字
 网络拓扑:每个节点由 MCU + CAN 控制器 + CAN 收发器构成,通过 CAN_H/CAN_L 连成总线;总线两端各接一个 **120Ω 端接电阻**,匹配阻抗、吸收反射。CAN 速度可达 1Mbps(CAN-FD 更高),速度与总线距离相关。
 
 **追问**
+
 1. 为什么总线两端要接 120Ω 电阻?(匹配总线阻抗,吸收信号反射,提高抗干扰能力与可靠性)
 2. CAN 的 ID 是目的地址吗?(不是,ID 表示消息访问总线的优先级)
 
@@ -217,19 +250,20 @@ inet_ntop(AF_INET, &addr, buf, sizeof(buf)); /* 二进制 -> 点分十进制字
 
 **详细解答**:
 
-| 帧 | 用途 |
-| -- | ---- |
-| 数据帧 | 发送单元向接收单元传送数据 |
+| 帧     | 用途                                     |
+| ------ | ---------------------------------------- |
+| 数据帧 | 发送单元向接收单元传送数据               |
 | 遥控帧 | 接收单元向具有相同 ID 的发送单元请求数据 |
-| 错误帧 | 检测出错误时向其它单元通知错误 |
-| 过载帧 | 接收单元通知尚未做好接收准备 |
-| 间隔帧 | 将数据帧/遥控帧与前面的帧分离开 |
+| 错误帧 | 检测出错误时向其它单元通知错误           |
+| 过载帧 | 接收单元通知尚未做好接收准备             |
+| 间隔帧 | 将数据帧/遥控帧与前面的帧分离开          |
 
 数据帧有标准格式(11 位 ID)和扩展格式(29 位 ID)两种,由 7 段构成:帧起始、仲裁段、控制段(含数据字节数与保留位)、数据段(0~8 字节)、CRC 段、ACK 段、帧结束。图中 D 表示显性(0)、R 表示隐性(1)。
 
 仲裁:多个单元同时发送时,对 ID 的每个位逐位比较,显性(0)优先,仲裁获胜者继续发送,失利者立即停止改为接收。因此 ID 数值越小优先级越高。
 
 **追问**
+
 1. 标准帧和扩展帧的 ID 位宽分别是多少?(标准 11 位,扩展 29 位)
 2. 数据段最多发送多少字节?(8 字节)
 
@@ -273,6 +307,7 @@ setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter));
 仅发送、不接收时,可用 `setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0)` 省略内核接收队列以省 CPU。不设置过滤规则则默认接收所有 ID 报文。
 
 **追问**
+
 1. `can_filter` 的两个成员是什么?(`can_id` 与 `can_mask`,满足 `can_id & can_mask` 的帧被接收)
 2. 仅发送的应用如何减少 CPU 消耗?(设置 CAN_RAW_FILTER 为 NULL、长度为 0,省略内核接收队列)
 
@@ -295,11 +330,11 @@ struct can_frame {
 
 `can_id` 低 11 位为标准帧 ID,0~28 位为扩展帧 ID;第 29/30/31 位是帧类型标志:
 
-| 宏 | 值 | 含义 |
-| -- | -- | ---- |
-| `CAN_EFF_FLAG` | 0x80000000 | 扩展帧标识 |
-| `CAN_RTR_FLAG` | 0x40000000 | 远程帧标识 |
-| `CAN_ERR_FLAG` | 0x20000000 | 错误帧标识 |
+| 宏             | 值         | 含义        |
+| -------------- | ---------- | ----------- |
+| `CAN_EFF_FLAG` | 0x80000000 | 扩展帧标识  |
+| `CAN_RTR_FLAG` | 0x40000000 | 远程帧标识  |
+| `CAN_ERR_FLAG` | 0x20000000 | 错误帧标识  |
 | `CAN_SFF_MASK` | 0x000007FF | 取标准帧 ID |
 | `CAN_EFF_MASK` | 0x1FFFFFFF | 取扩展帧 ID |
 
@@ -336,6 +371,7 @@ for (i = 0; i < frame.can_dlc; i++) printf("%02x ", frame.data[i]);
 错误帧的具体原因通过 `can_id` 的其他符号位判断,定义在 `<linux/can/error.h>`,如 `CAN_ERR_TX_TIMEOUT`、`CAN_ERR_LOSTARB`、`CAN_ERR_BUSOFF`、`CAN_ERR_ACK` 等。
 
 **追问**
+
 1. 发送数据后如何判断是否成功?(比较 `write` 返回值与 `sizeof(frame)`,不等即发送失败)
 2. 如何判断收到的是扩展帧还是标准帧?(看 `can_id & CAN_EFF_FLAG`,再用对应 MASK 取 ID)
 
@@ -362,6 +398,7 @@ candump -ta can0                                              # 接收并显示
 `#` 前的 `123` 是帧 ID,后面是数据。测试时要用 CAN 分析仪或另一块开发板对接,且**分析仪波特率必须与开发板一致**(示例为 1000000)。
 
 **追问**
+
 1. 回环功能默认是开还是关?(默认开启)
 2. 两块开发板对接时,CAN_H/CAN_L 如何连接?(CAN_H 接 CAN_H、CAN_L 接 CAN_L,且需 120Ω 端接)
 
@@ -390,6 +427,7 @@ cmake .. && make
 清理时直接删除 `build` 目录。
 
 **追问**
+
 1. out-of-source 构建有什么好处?(中间文件和产物与源码分离,清理方便,不污染源码树)
 2. `cmake` 生成的中间文件有哪些?(`CMakeCache.txt`、`CMakeFiles/`、`cmake_install.cmake`、`Makefile` 等)
 
@@ -399,30 +437,31 @@ cmake .. && make
 
 **详细解答**:
 
-| 命令 | 说明 |
-| ---- | ---- |
-| `add_executable` | 可执行程序目标 |
-| `add_library` | 库文件目标 |
-| `add_subdirectory` | 加载子目录的 CMakeLists.txt |
-| `aux_source_directory` | 收集目录源文件到变量 |
-| `include_directories` / `link_directories` / `link_libraries` | 全局头文件/库搜索路径、链接库 |
+| 命令                                                                      | 说明                           |
+| ------------------------------------------------------------------------- | ------------------------------ |
+| `add_executable`                                                          | 可执行程序目标                 |
+| `add_library`                                                             | 库文件目标                     |
+| `add_subdirectory`                                                        | 加载子目录的 CMakeLists.txt    |
+| `aux_source_directory`                                                    | 收集目录源文件到变量           |
+| `include_directories` / `link_directories` / `link_libraries`             | 全局头文件/库搜索路径、链接库  |
 | `target_include_directories` / `target_link_libraries` / `target_sources` | 针对指定目标的头文件/库/源文件 |
-| `cmake_minimum_required` | 最低版本要求 |
-| `project` | 工程名(可带 VERSION) |
-| `set` | 设置变量 |
-| `set_target_properties` / `get_target_property` | 设置/获取目标属性 |
-| `list` | 列表操作 |
-| `message` | 打印信息 |
+| `cmake_minimum_required`                                                  | 最低版本要求                   |
+| `project`                                                                 | 工程名(可带 VERSION)         |
+| `set`                                                                     | 设置变量                       |
+| `set_target_properties` / `get_target_property`                           | 设置/获取目标属性              |
+| `list`                                                                    | 列表操作                       |
+| `message`                                                                 | 打印信息                       |
 
 关键区别:`include_directories()`/`link_libraries()` 针对当前源码中**所有目标**并向下传递(经 `add_subdirectory` 传递给子源码),大工程易混乱;`target_*` 只影响指定目标,传播范围由关键字决定:
 
-| 关键字 | 当前目标使用 | 传给依赖目标 |
-| ------ | ------------ | ------------ |
-| PRIVATE | 是 | 否 |
-| INTERFACE | 否 | 是 |
-| PUBLIC | 是 | 是 |
+| 关键字    | 当前目标使用 | 传给依赖目标 |
+| --------- | ------------ | ------------ |
+| PRIVATE   | 是           | 否           |
+| INTERFACE | 否           | 是           |
+| PUBLIC    | 是           | 是           |
 
 **追问**
+
 1. 为什么推荐 `target_*` 系列?(作用范围明确,避免全局污染,保持工程目录清晰)
 2. `PUBLIC` 等于什么?(`PRIVATE` + `INTERFACE`)
 
@@ -447,6 +486,7 @@ set_target_properties(libhello PROPERTIES OUTPUT_NAME "hello")  # libhello.a / .
 Linux 下库名自动加 `lib` 前缀和 `.a`/`.so` 后缀,`add_library` 的第一个参数是**不含前后缀**的库名。
 
 **追问**
+
 1. 为什么 `add_library(hello hello.c)` 不能用来把库改名?(目标名工程内唯一,已存在同名目标)
 2. 可执行文件与库文件的输出路径由什么变量控制?(`EXECUTABLE_OUTPUT_PATH`、`LIBRARY_OUTPUT_PATH`)
 
@@ -484,6 +524,7 @@ file main      # 应显示 ARM 架构
 工具链方式的优点:配置与业务解耦,不污染 `CMakeLists.txt`;若直接写进 `CMakeLists.txt`,必须放在 `project()` 之前否则不生效。`-D` 创建的是缓存变量(全局,覆盖同名普通变量)。
 
 **追问**
+
 1. `CMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY` 的含义?(`find_library()` 只在 `CMAKE_SYSROOT` 中搜索)
 2. 交叉编译后如何验证产物架构?(`file` 命令,应为 ARM 而非 x86-64)
 
@@ -520,6 +561,7 @@ message("${out_var}")   # 15
 ```
 
 **追问**
+
 1. 嵌套函数中 `PARENT_SCOPE` 写到哪一层?(写到调用者的作用域,即上一层)
 2. `-D` 定义的变量属于哪种作用域?(全局缓存变量)
 
@@ -540,6 +582,7 @@ message("${out_var}")   # 15
 应用场景:车联网、智能家居、即时聊天、工业物联网;因需长连接和心跳,不适合低功耗场合。
 
 **追问**
+
 1. MQTT 工作在 TCP/IP 的哪一层?(应用层,构建于 TCP/IP 之上)
 2. 为什么说 MQTT 不适合低功耗场合?(需保持长连接、定时发心跳包,比较耗电)
 
@@ -549,17 +592,18 @@ message("${out_var}")   # 15
 
 **详细解答**:
 
-| QoS | 语义 | 流程 |
-| --- | ---- | ---- |
-| 0 | 最多发一次 | PUBLISH,不确认、不重传 |
-| 1 | 至少发一次 | PUBLISH → PUBACK,超时重发,可能重复 |
-| 2 | 保证收一次 | PUBLISH → PUBREC → PUBREL → PUBCOMP |
+| QoS | 语义       | 流程                                 |
+| --- | ---------- | ------------------------------------ |
+| 0   | 最多发一次 | PUBLISH,不确认、不重传              |
+| 1   | 至少发一次 | PUBLISH → PUBACK,超时重发,可能重复 |
+| 2   | 保证收一次 | PUBLISH → PUBREC → PUBREL → PUBCOMP  |
 
 - 实现 QoS>0 必须 `cleanSession=false`,否则收不到离线消息;
 - QoS1 协议本身不去重,需应用根据 `dup` 标志处理;
 - 降级:客户端 A 以 QoS2 发布、客户端 B 以 QoS1 订阅,则服务端对 B 采用 QoS1。
 
 **追问**
+
 1. QoS1 为什么可能重复投递?(发送方未及时收到 PUBACK 会重发,协议不做去重)
 2. QoS2 为什么最慢?(需要两次确认、四次报文交互)
 
@@ -577,6 +621,7 @@ CONNACK 的 `sessionPresent` 与 `cleanSession` 配合:`cleanSession=0` 时返
 保留消息:发布时 `retain=true`,服务端保存该主题**最新一条**消息,任何客户端订阅后立即收到,无需等下次发布。每个主题只有一条保留消息,新保留消息覆盖旧的;发布一条空的保留消息即可删除。
 
 **追问**
+
 1. 想接收离线消息要怎样设置连接参数?(`cleanSession=false`)
 2. 如何删除保留消息?(发布一条 payload 为空的保留消息)
 
@@ -591,6 +636,7 @@ CONNACK 的 `sessionPresent` 与 `cleanSession` 配合:`cleanSession=0` 时返
 - **认证**:CONNECT 报文的 `username`/`password` 可选;服务端开启认证时客户端必须提供正确凭据,有些服务端还用其管理私人主题权限。
 
 **追问**
+
 1. 主动断开连接会触发遗嘱吗?(不会,只有意外断线才触发)
 2. 心跳间隔由哪个参数决定?(CONNECT 报文的 `keepAlive`)
 
@@ -651,6 +697,7 @@ static int msgarrvd(void *context, char *topicName, int topicLen,
 - 库分同步(`MQTTClient.h` / `libpaho-mqtt3c.so`)与异步(`MQTTAsync.h` / `libpaho-mqtt3a.so`)。
 
 **追问**
+
 1. 为什么回调里要调用 `MQTTClient_freeMessage` 和 `MQTTClient_free`?(消息与主题名的内存由库分配,需应用释放)
 2. 同步与异步模式的区别体现在哪里?(是否在发布后等待确认;由 `reliable` 等控制,默认同步)
 
@@ -664,14 +711,15 @@ static int msgarrvd(void *context, char *topicName, int topicLen,
 
 **详细解答**:
 
-| 协议 | 特点 | 适用 |
-| ---- | ---- | ---- |
-| RTSP | 基于文本的多媒体播放控制协议,定义流格式,流经 RTP 传输;实时性最好,实现复杂 | 视频聊天、视频监控 |
-| RTMP | Adobe 提出,解决流媒体多路复用与分包;低延迟、稳定性高、支持所有摄像头格式,加载 flash 插件即可播放 | 直播、推流 |
+| 协议 | 特点                                                                                                | 适用               |
+| ---- | --------------------------------------------------------------------------------------------------- | ------------------ |
+| RTSP | 基于文本的多媒体播放控制协议,定义流格式,流经 RTP 传输;实时性最好,实现复杂                       | 视频聊天、视频监控 |
+| RTMP | Adobe 提出,解决流媒体多路复用与分包;低延迟、稳定性高、支持所有摄像头格式,加载 flash 插件即可播放 | 直播、推流         |
 
 方案结构:推流端负责把视频数据经 RTMP 传给流媒体服务器;服务器接收并转发给拉流客户端;拉流端从服务器获取数据。实现上推流用 FFmpeg、服务器用 Nginx、拉流用 VLC。
 
 **追问**
+
 1. 为什么在浏览器里播放 RTSP 很困难?(浏览器原生不支持 RTSP,RTMP 可通过 flash 插件播放而流行)
 2. 本方案中开发板扮演哪些角色?(既是流媒体服务器,也是推流端)
 
@@ -690,11 +738,11 @@ source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linu
 
 源码修改:
 
-| 文件 | 位置 | 修改 |
-| ---- | ---- | ---- |
-| `auto/cc/name` | 第 21 行 | 注释掉 `exit 1` |
+| 文件                | 位置     | 修改                          |
+| ------------------- | -------- | ----------------------------- |
+| `auto/cc/name`      | 第 21 行 | 注释掉 `exit 1`               |
 | `auto/types/sizeof` | 第 15 行 | `ngx_size=` 改为 `ngx_size=4` |
-| `auto/types/sizeof` | 第 36 行 | `$CC` 改为 `gcc` |
+| `auto/types/sizeof` | 第 36 行 | `$CC` 改为 `gcc`              |
 
 ```bash
 ./configure --prefix=/home/dt/tools/nginx-1.20.0/install \
@@ -716,6 +764,7 @@ arm-poky-linux-gnueabi-strip --strip-debug nginx   # 可选,去调试信息
 `nginx.conf` 加 RTMP 配置:`rtmp { server { listen 1935; application live { live on; record off; } } }`。
 
 **追问**
+
 1. `--add-module` 的作用?(添加第三方模块,此处指向 nginx-rtmp-module 源码路径使 Nginx 支持 RTMP)
 2. 为什么编译前要改 `auto/types/sizeof`?(交叉编译时 size 探测无法运行目标程序,需固定 `ngx_size=4`、用主机 gcc 探测)
 
@@ -733,20 +782,21 @@ ffmpeg -re -i /run/media/mmcblk0p1/testVideo.mp4 -c:av copy -f flv rtmp://127.0.
 ffmpeg -f v4l2 -video_size 320x240 -framerate 15 -i /dev/video2 -q 10 -f flv rtmp://127.0.0.1/live/mytest
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
-| `-re` | 按原始帧率读取,实时推流 |
-| `-i` | 输入(视频文件或 V4L2 设备) |
-| `-c:av copy` | 音视频直接拷贝(不重新编码) |
-| `-f v4l2` | 使用 V4L2 采集摄像头 |
-| `-video_size` / `-framerate` | 分辨率 / 帧率 |
-| `-q 10` | 视频质量参数 |
-| `-f flv` | 输出 FLV 封装(RTMP 传输) |
+| 参数                           | 含义                            |
+| ------------------------------ | ------------------------------- |
+| `-re`                          | 按原始帧率读取,实时推流        |
+| `-i`                           | 输入(视频文件或 V4L2 设备)    |
+| `-c:av copy`                   | 音视频直接拷贝(不重新编码)    |
+| `-f v4l2`                      | 使用 V4L2 采集摄像头            |
+| `-video_size` / `-framerate`   | 分辨率 / 帧率                   |
+| `-q 10`                        | 视频质量参数                    |
+| `-f flv`                       | 输出 FLV 封装(RTMP 传输)      |
 | `rtmp://127.0.0.1/live/mytest` | 推流地址:服务器/live 应用/流名 |
 
 延迟原因:I.MX6U 没有硬件视频解码,FFmpeg 的音视频处理全靠软件,耗时大;服务器和推流端都在同一块弱性能开发板上。
 
 **追问**
+
 1. 拉流端用什么工具?(VLC,媒体 → 打开网络串流,输入 `rtmp://<开发板IP>/live/mytest`)
 2. 如何降低延迟?(降低分辨率/帧率,或换带硬件编解码的高性能平台)
 
@@ -756,16 +806,16 @@ ffmpeg -f v4l2 -video_size 320x240 -framerate 15 -i /dev/video2 -q 10 -f flv rtm
 
 **详细解答**(与 Qt 视频监控例程 `capture_thread.cpp` 一致):
 
-| 步骤 | 操作 | 说明 |
-| ---- | ---- | ---- |
-| 打开设备 | `open("/dev/video1", O_RDWR)` | 摄像头节点按实际确认 |
-| 设置格式 | `VIDIOC_S_FMT` | 如 640×480、`V4L2_PIX_FMT_RGB565` |
-| 申请缓冲 | `VIDIOC_REQBUFS` | 如 3 个缓冲、`V4L2_MEMORY_MMAP` |
-| 查询映射 | `VIDIOC_QUERYBUF` + `mmap` | 得到缓冲地址与长度 |
-| 入队 | `VIDIOC_QBUF` | 所有缓冲放入采集队列 |
-| 启动 | `VIDIOC_STREAMON` | 开始采集 |
-| 循环取帧 | `VIDIOC_DQBUF` → 处理 → `VIDIOC_QBUF` | 取出、处理、再入队 |
-| 结束 | `munmap` + `close` | 释放映射与设备 |
+| 步骤     | 操作                                  | 说明                              |
+| -------- | ------------------------------------- | --------------------------------- |
+| 打开设备 | `open("/dev/video1", O_RDWR)`         | 摄像头节点按实际确认              |
+| 设置格式 | `VIDIOC_S_FMT`                        | 如 640×480、`V4L2_PIX_FMT_RGB565` |
+| 申请缓冲 | `VIDIOC_REQBUFS`                      | 如 3 个缓冲、`V4L2_MEMORY_MMAP`   |
+| 查询映射 | `VIDIOC_QUERYBUF` + `mmap`            | 得到缓冲地址与长度                |
+| 入队     | `VIDIOC_QBUF`                         | 所有缓冲放入采集队列              |
+| 启动     | `VIDIOC_STREAMON`                     | 开始采集                          |
+| 循环取帧 | `VIDIOC_DQBUF` → 处理 → `VIDIOC_QBUF` | 取出、处理、再入队                |
+| 结束     | `munmap` + `close`                    | 释放映射与设备                    |
 
 ```c
 fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
@@ -791,6 +841,7 @@ while (startFlag) {
 ```
 
 **追问**
+
 1. 为什么要用 mmap 而不是 read?(mmap 零拷贝、效率高,适合高帧率视频采集)
 2. DQBUF 和 QBUF 为什么成对出现?(出队的缓冲处理完必须重新入队,采集才能继续)
 
@@ -841,17 +892,18 @@ void MainWindow::videoUpdate() {
 
 两条路线对比:
 
-| 维度 | Nginx + FFmpeg(RTMP) | V4L2 + JPEG 广播 |
-| ---- | ---------------------- | ---------------- |
-| 是否需要服务器进程 | 需要(Nginx) | 不需要,服务端直接广播 |
-| 传输层 | TCP(RTMP) | UDP 广播 |
-| 适用网络 | 局域网/公网 | 同网段 |
-| 实现复杂度 | 高(移植 Nginx、配 RTMP) | 低(Qt 一个线程) |
-| 带宽 | 较低(可 H.264 编码) | 高(逐帧 JPEG + base64) |
-| 延迟 | 本板实测 5~6 秒 | 取决于帧率与网络 |
-| 局限 | 软编解码耗时 | 本地显示与广播互相制约 |
+| 维度               | Nginx + FFmpeg(RTMP)    | V4L2 + JPEG 广播         |
+| ------------------ | ------------------------- | ------------------------ |
+| 是否需要服务器进程 | 需要(Nginx)             | 不需要,服务端直接广播   |
+| 传输层             | TCP(RTMP)               | UDP 广播                 |
+| 适用网络           | 局域网/公网               | 同网段                   |
+| 实现复杂度         | 高(移植 Nginx、配 RTMP) | 低(Qt 一个线程)        |
+| 带宽               | 较低(可 H.264 编码)     | 高(逐帧 JPEG + base64) |
+| 延迟               | 本板实测 5~6 秒           | 取决于帧率与网络         |
+| 局限               | 软编解码耗时              | 本地显示与广播互相制约   |
 
 **追问**
+
 1. 为什么广播会导致本地显示卡顿?(同一采集线程串行处理显示与广播,二者互相制约)
 2. 想用公网或更低带宽该怎么做?(走 RTMP/RTSP 并做 H.264 编码;但 I.MX6U 无硬件编码,需评估性能)
 

+ 69 - 62
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/01-C++基础与面向对象.md

@@ -1,6 +1,22 @@
 ---
 title: C++基础与面向对象
-tags: [嵌入式Linux, Qt, C++, 面向对象, 类与对象, 构造析构, this指针, 继承, 重载, 多态, 虚函数, 封装, 抽象类, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    Qt,
+    C++,
+    面向对象,
+    类与对象,
+    构造析构,
+    this指针,
+    继承,
+    重载,
+    多态,
+    虚函数,
+    封装,
+    抽象类,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《【正点原子】I.MX6U嵌入式Qt开发指南V1.2》第一章 在Ubuntu下编写C++、第二章 C++基础;例程 Embedded-Qt-Tutorial/C++/01_hello_world~11_abstract_class"
@@ -36,17 +52,17 @@ C++ 由 Bjarne Stroustrup 于 1979 年在贝尔实验室开始设计,最初命
 
 原书第二章开篇即说明:指针、数据类型、变量类型、判断、循环等知识在 C 与 C++ 中基本一样,**不再重复讲**;重点讲 C++ 的新特性和面向对象。差异可归纳如下:
 
-| 对比项 | C | C++ |
-| ------ | - | --- |
-| 布尔类型 | 旧标准无 `bool`,用 `int` 代替 | 原生 `bool`(`true` / `false`) |
-| 变量定义位置 | 传统上必须集中在程序(块)开头 | **随用随定义** |
-| 初始化方式 | `int x = 100;` | 还支持 `int x(100);` 直接初始化 |
-| 输入输出 | `scanf` / `printf` + 格式符 | `cin` / `cout` + `<<` `>>`,无需格式符 |
-| 命名空间 | 无 | 有 `namespace`,避免命名冲突 |
-| 字符串 | `char[]` + `<string.h>` | `std::string` 类型,方便好用 |
-| 编程范式 | 过程化 | 过程化 + 面向对象 + 泛型 |
-| 数据结构升级 | `struct` 只有数据 | `class` = 数据 + 成员函数 + 访问控制 |
-| 编译方式 | `gcc test.c -o test` | `g++ test.cpp -o test` |
+| 对比项       | C                              | C++                                    |
+| ------------ | ------------------------------ | -------------------------------------- |
+| 布尔类型     | 旧标准无 `bool`,用 `int` 代替 | 原生 `bool`(`true` / `false`)        |
+| 变量定义位置 | 传统上必须集中在程序(块)开头 | **随用随定义**                         |
+| 初始化方式   | `int x = 100;`                 | 还支持 `int x(100);` 直接初始化        |
+| 输入输出     | `scanf` / `printf` + 格式符    | `cin` / `cout` + `<<` `>>`,无需格式符 |
+| 命名空间     | 无                             | 有 `namespace`,避免命名冲突           |
+| 字符串       | `char[]` + `<string.h>`        | `std::string` 类型,方便好用           |
+| 编程范式     | 过程化                         | 过程化 + 面向对象 + 泛型               |
+| 数据结构升级 | `struct` 只有数据              | `class` = 数据 + 成员函数 + 访问控制   |
+| 编译方式     | `gcc test.c -o test`           | `g++ test.cpp -o test`                 |
 
 一句话:**C 是 C++ 的子集**,C++ 在保留 C 能力的同时,把"函数 + 数据"组织成了"类"。
 
@@ -196,7 +212,6 @@ int main()
 - 第 16 行:打印 `A::x`。
 - 第 17 行:显式调用 `A::fun()`。
 
-
 初学 Qt 时较少看到命名空间,但 Qt 里同样可以自定义命名空间并正常使用。
 
 ---
@@ -214,10 +229,10 @@ int main()
 
 从类实例化对象有两种方式:
 
-| 方式 | 语法 | 内存位置 | 释放方式 |
-| ---- | ---- | -------- | -------- |
-| 栈中实例化 | `Dog dog1;` | 栈 | 离开作用域自动释放 |
-| 堆中实例化 | `Dog *dog2 = new Dog();` | 堆 | 必须 `delete dog2;` |
+| 方式       | 语法                     | 内存位置 | 释放方式            |
+| ---------- | ------------------------ | -------- | ------------------- |
+| 栈中实例化 | `Dog dog1;`              | 栈       | 离开作用域自动释放  |
+| 堆中实例化 | `Dog *dog2 = new Dog();` | 堆       | 必须 `delete dog2;` |
 
 完整源码(`03_class_dog_example.cpp`):
 
@@ -272,7 +287,6 @@ int main()
 - 第 29~31 行:给 `dog2` 成员赋值并调用 `run()`。
 - 第 34、35 行:释放内存,并将 `dog2` 重新指向 `NULL`。
 
-
 类与结构体的关系:**类可以说是结构体的升级版本**——结构体只有数据,类额外提供成员函数和访问控制。
 
 ### 2.2 构造函数与析构函数(2.2.1.1)
@@ -332,7 +346,6 @@ Dog::~Dog()
 - 第 19~22 行:**类的函数可以在类里实现,也可以在类外实现**,在类外实现时需要用 `::`。这里把构造函数定义在类外,打印"构造函数执行!"。
 - 第 24~27 行:同样在类外定义析构函数,打印"析构函数执行!"。
 
-
 执行顺序可以预测:对象实例化时先调用构造函数(`Dog()` 先执行)→ 执行 `main()` 中的 `cout` → 对象生命周期结束时调用析构函数。
 
 ### 2.3 this 指针(2.2.1.2)
@@ -380,7 +393,6 @@ void Dog::func()
 
 - 第 21、22 行:在成员函数里使用 `this` 指针指向类成员 `name`,先赋值为"旺财",再打印。程序输出"小狗的名字叫:旺财"。
 
-
 后面学 Qt 时会经常遇到 `this`,例如 `ui->setupUi(this)`、`new QPushButton(this)`——这里的 `this` 就是"当前窗口对象自身",作为父对象传给子控件。
 
 ### 2.4 继承(2.2.2)
@@ -400,11 +412,11 @@ class derived-class : access-specifier base-class
 
 `access-specifier` 是 `public`、`protected`、`private` 之一;**不使用则默认为 `private`**。继承方式:
 
-| 继承方式 | 基类 public 成员 | 基类 protected 成员 | 基类 private 成员 |
-| -------- | ---------------- | ------------------- | ----------------- |
-| public 公有继承 | 成为派生类 public | 成为派生类 protected | 不可直接访问(可通过基类 public/protected 成员间接访问) |
-| protected 保护继承 | 成为派生类 protected | 成为派生类 protected | 不可直接访问 |
-| private 私有继承 | 成为派生类 private | 成为派生类 private | 不可直接访问 |
+| 继承方式           | 基类 public 成员     | 基类 protected 成员  | 基类 private 成员                                        |
+| ------------------ | -------------------- | -------------------- | -------------------------------------------------------- |
+| public 公有继承    | 成为派生类 public    | 成为派生类 protected | 不可直接访问(可通过基类 public/protected 成员间接访问) |
+| protected 保护继承 | 成为派生类 protected | 成为派生类 protected | 不可直接访问                                             |
+| private 私有继承   | 成为派生类 private   | 成为派生类 private   | 不可直接访问                                             |
 
 完整源码(`06_inherit_example.cpp`,公有继承):
 
@@ -504,7 +516,6 @@ int main()
 - 第 13 行:同名函数 `getWeight(double weight)`,参数类型不同,构成函数重载。
 - 第 22、23 行:分别传入 `int` 和 `double`,程序匹配不同的重载函数。
 
-
 注意:如果用户传入的参数类型不在任何重载函数里(例如 `string`),编译器匹配不到相应重载函数,**编译时就会报错**。可以多写几种类型(`string`、`char`、`float` 等)覆盖更多情况。
 
 #### 2.5.2 运算符重载(2.2.3.2)
@@ -520,28 +531,28 @@ int main()
 
 可重载的运算符:
 
-| 类别 | 运算符 |
-| ---- | ------ |
-| 双目算术 | `+` `-` `*` `/` `%` |
-| 关系 | `==` `!=` `<` `>` `<=` `>=` |
-| 逻辑 | `||` `&&` `!` |
-| 单目 | `+` `-` `*`(指针) `&`(取地址) |
-| 自增自减 | `++` `--` |
-| 位 | `\|` `&` `~` `^` `<<` `>>` |
-| 赋值 | `=` `+=` `-=` `*=` `/=` `%=` `&=` `\|=` `^=` `<<=` `>>=` |
-| 空间申请与释放 | `new` `delete` `new[]` `delete[]` |
-| 其他 | `()`(函数调用) `->`(成员访问) `,` `[]` |
+| 类别           | 运算符                                                   |
+| -------------- | -------------------------------------------------------- |
+| 双目算术       | `+` `-` `*` `/` `%`                                      |
+| 关系           | `==` `!=` `<` `>` `<=` `>=`                              |
+| 逻辑           | `                                                        |     | ` `&&` `!` |
+| 单目           | `+` `-` `*`(指针) `&`(取地址)                        |
+| 自增自减       | `++` `--`                                                |
+| 位             | `\|` `&` `~` `^` `<<` `>>`                               |
+| 赋值           | `=` `+=` `-=` `*=` `/=` `%=` `&=` `\|=` `^=` `<<=` `>>=` |
+| 空间申请与释放 | `new` `delete` `new[]` `delete[]`                        |
+| 其他           | `()`(函数调用) `->`(成员访问) `,` `[]`               |
 
 **不可重载**的运算符:
 
-| 运算符 | 含义 |
-| ------ | ---- |
-| `.` | 成员访问运算符 |
+| 运算符     | 含义               |
+| ---------- | ------------------ |
+| `.`        | 成员访问运算符     |
 | `.*` `->*` | 成员指针访问运算符 |
-| `::` | 域运算符 |
-| `sizeof` | 长度运算符 |
-| `?:` | 条件运算符 |
-| `#` | 预处理符号 |
+| `::`       | 域运算符           |
+| `sizeof`   | 长度运算符         |
+| `?:`       | 条件运算符         |
+| `#`        | 预处理符号         |
 
 以重载 `+` 为例(`08_operator_example.cpp`),把两个 `Dog` 对象的体重相加,返回新的 `Dog`:
 
@@ -581,7 +592,6 @@ int main()
 - 第 9~13 行:重载 `+` 运算符。成员函数形式下,左操作数是 `this`,右操作数是参数 `d`,使用 `this->weight` 访问左对象成员,返回一个新的 `dog` 对象。
 - 第 25 行:`dog1 + dog2` 触发重载后的 `+`,普通算术运算符 `+` 本来不能把两个对象相加,这正是重载的意义。
 
-
 实际开发中运算符重载用得较少,**理解这种思想即可**;但 Qt 里 `<<`(`cout` 插入)本身就是重载的典型例子。
 
 ### 2.6 多态(2.2.4)
@@ -663,7 +673,6 @@ int main()
 - 基类 `Animal` 声明指针 `animal`,通过基类指针访问 `Dog`、`Cat` 对象的 `run()`。**前提是基类的 `run()` 必须声明为虚函数**;若不声明,基类指针将访问到基类自己的 `run()`。
 - 第 44、49 行:把派生类对象地址赋给基类指针。当基类 `run()` 是虚函数时,编译器不静态链接到该函数,而是链接到派生类的 `run()`,实现运行时多态。
 
-
 可以尝试去掉 `virtual` 再编译:输出会变成两次 `Animal 的run()方法`,多态失效。
 
 虚函数表的机制(扩展):
@@ -741,7 +750,6 @@ int main()
 - 第 33、34 行:添加食物份数。
 - 第 36 行:打印总份数。
 
-
 ### 2.8 数据抽象(2.2.6)
 
 数据抽象是指**只向外界提供关键信息,隐藏后台实现细节**,即只表现必要的信息而不呈现细节。它是一种依赖**接口和实现分离**的编程(设计)技术,好处:
@@ -817,17 +825,16 @@ int main()
 }
 ```
 
-
 结果与 2.6 的多态例程相同,但体现了**两种不同的思想**:多态例程是"基类给默认实现,派生类可选择重写",抽象类例程是"基类只给接口,派生类必须实现"。
 
 多态与抽象类对比:
 
-| 对比项 | 多态(虚函数有实现) | 接口(纯虚函数) |
-| ------ | -------------------- | ---------------- |
-| 基类函数 | `virtual void run() {...}` | `virtual void run() = 0;` |
-| 基类可实例化 | 可以 | **不可以**(抽象类) |
-| 派生类是否必须重写 | 否,不重写就用基类实现 | 是,否则派生类也无法实例化 |
-| 典型用途 | 提供可覆盖的默认行为 | 定义统一接口/规范 |
+| 对比项             | 多态(虚函数有实现)       | 接口(纯虚函数)           |
+| ------------------ | -------------------------- | -------------------------- |
+| 基类函数           | `virtual void run() {...}` | `virtual void run() = 0;`  |
+| 基类可实例化       | 可以                       | **不可以**(抽象类)       |
+| 派生类是否必须重写 | 否,不重写就用基类实现     | 是,否则派生类也无法实例化 |
+| 典型用途           | 提供可覆盖的默认行为       | 定义统一接口/规范          |
 
 Qt 中大量使用这两者:继承 `QMainWindow` 时可以重写 `paintEvent()` 等虚函数;而 `QAbstractItemModel` 这类抽象基类必须实现其纯虚接口才能使用。
 
@@ -859,14 +866,14 @@ g++ 03_class_dog_example.cpp -o 03_class_dog_example
 
 ### 3.3 常见编译问题
 
-| 现象 | 可能原因 | 排查手段 |
-| ---- | -------- | -------- |
-| `undefined reference to ...` | 函数只声明未定义 | 检查是否写了函数体;构造函数/析构函数是否在类外漏写 `Dog::` |
-| `error: 'cout' was not declared` | 缺少头文件或命名空间 | 加 `#include <iostream>` 与 `using namespace std;` |
-| `error: 'string' was not declared` | 漏包含 `<string>` | 加 `#include <string>` |
-| `cannot be used as a function` | 变量名与函数名冲突或少了分号 | 检查类定义末尾的 `};` |
-| 多态未生效 | 基类函数漏写 `virtual` | 给基类函数加 `virtual` |
-| 内存/段错误 | 堆对象未 `delete` 或野指针 | `new` 配 `delete`,释放后置 `NULL` |
+| 现象                               | 可能原因                     | 排查手段                                                    |
+| ---------------------------------- | ---------------------------- | ----------------------------------------------------------- |
+| `undefined reference to ...`       | 函数只声明未定义             | 检查是否写了函数体;构造函数/析构函数是否在类外漏写 `Dog::` |
+| `error: 'cout' was not declared`   | 缺少头文件或命名空间         | 加 `#include <iostream>` 与 `using namespace std;`          |
+| `error: 'string' was not declared` | 漏包含 `<string>`            | 加 `#include <string>`                                      |
+| `cannot be used as a function`     | 变量名与函数名冲突或少了分号 | 检查类定义末尾的 `};`                                       |
+| 多态未生效                         | 基类函数漏写 `virtual`       | 给基类函数加 `virtual`                                      |
+| 内存/段错误                        | 堆对象未 `delete` 或野指针   | `new` 配 `delete`,释放后置 `NULL`                          |
 
 ---
 

+ 90 - 78
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/02-Qt入门与项目结构.md

@@ -1,6 +1,18 @@
 ---
 title: Qt入门与项目结构
-tags: [嵌入式Linux, Qt, QtCreator, qmake, pro文件, ui文件, QtDesigner, moc, qrc, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    Qt,
+    QtCreator,
+    qmake,
+    pro文件,
+    ui文件,
+    QtDesigner,
+    moc,
+    qrc,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《【正点原子】I.MX6U嵌入式Qt开发指南V1.2》第三章 初识Qt、第四章 使用Qt Designer开发;例程 Embedded-Qt-Tutorial/Qt/01/01_hello_world、02_designer_example"
@@ -22,15 +34,15 @@ pdf_ref: "《【正点原子】I.MX6U嵌入式Qt开发指南V1.2》第三章 初
 
 Qt 是一个**跨平台的 C++ 开发库**,主要用来开发图形用户界面(Graphical User Interface,GUI)程序。但 GUI 并不是 Qt 的全部,它还内置了大量非界面能力:
 
-| 能力类别 | 说明 |
-| -------- | ---- |
-| 图形界面 | 控件、布局、交互、绘图 |
-| 多线程 | `QThread` 等 |
-| 数据库 | Qt SQL |
+| 能力类别 | 说明                            |
+| -------- | ------------------------------- |
+| 图形界面 | 控件、布局、交互、绘图          |
+| 多线程   | `QThread` 等                    |
+| 数据库   | Qt SQL                          |
 | 图像处理 | `QImage`、`QPixmap`、`QPainter` |
-| 音频视频 | Qt Multimedia |
-| 网络通信 | `QTcpSocket`、`QUdpSocket` |
-| 文件操作 | `QFile`、`QTextStream` |
+| 音频视频 | Qt Multimedia                   |
+| 网络通信 | `QTcpSocket`、`QUdpSocket`      |
+| 文件操作 | `QFile`、`QTextStream`          |
 
 Qt 还存在 Python、Ruby、Perl 等脚本语言的绑定;支持的操作系统非常多,例如通用操作系统 Windows、Linux、Unix,智能手机系统 Android、iOS、WinPhone,嵌入式系统 QNX、VxWorks 等。
 
@@ -42,9 +54,9 @@ Qt 最让人喜欢的特性是**跨平台**:一份代码无需修改或只需
 
 原书 3.1.1 小节仅列出标题未展开正文。这两个名字初学者最容易混淆:
 
-| 名称 | 是什么 | 类比 |
-| ---- | ------ | ---- |
-| **Qt** | 跨平台 C++ **开发库**(一堆 `.so`/`.dll` + 头文件 + 工具链) | 相当于"语言的标准库" |
+| 名称           | 是什么                                                               | 类比                             |
+| -------------- | -------------------------------------------------------------------- | -------------------------------- |
+| **Qt**         | 跨平台 C++ **开发库**(一堆 `.so`/`.dll` + 头文件 + 工具链)         | 相当于"语言的标准库"             |
 | **Qt Creator** | 官方出品的 **IDE(集成开发环境)**,用来写代码、编译、调试、设计界面 | 相当于"编辑器 + 构建器 + 调试器" |
 
 关系可以这样理解:**Qt Creator 自带的 Qt Designer(UI 设计器)也只是它的一个功能模块**,属于 Qt Creator,不要和 Qt 库混为一谈。没有 Qt Creator,用 `vim` + `qmake` + `make` 一样能开发 Qt 程序,只是没有可视化界面而已。
@@ -85,12 +97,12 @@ flowchart TB
 
 选择版本的总原则:**不要盲目追最新,选长期支持(LTS)版本**。
 
-| 版本 | 说明 |
-| ---- | ---- |
-| Qt4 / Qt5 | 信号槽写法有区别(Qt4 的旧写法本教程不讲解) |
-| Qt5.6 / Qt5.9 | Qt 长期支持版本(LTS,Long Term Support) |
-| Qt5.12 | LTS 版本,**本教程以 Qt5.12.9 开发及实验** |
-| Qt6 | 理论上与 Qt5 兼容,但仍需时间普及;许多公司仍在用 Qt5 甚至 Qt4 |
+| 版本          | 说明                                                           |
+| ------------- | -------------------------------------------------------------- |
+| Qt4 / Qt5     | 信号槽写法有区别(Qt4 的旧写法本教程不讲解)                   |
+| Qt5.6 / Qt5.9 | Qt 长期支持版本(LTS,Long Term Support)                      |
+| Qt5.12        | LTS 版本,**本教程以 Qt5.12.9 开发及实验**                     |
+| Qt6           | 理论上与 Qt5 兼容,但仍需时间普及;许多公司仍在用 Qt5 甚至 Qt4 |
 
 几点说明:
 
@@ -113,13 +125,13 @@ flowchart TB
 
 Windows 组件选择要点:
 
-| 组件 | 说明 |
-| ---- | ---- |
-| MSVC | 微软 VC 编译器,需要额外安装 VC 相关库 |
-| **MinGW** | Minimalist GNU on Windows,可在 GNU/Linux 与 Windows 上生成 Windows 程序,**教程只勾选 MinGW** |
-| Android / ARM 等 | 做嵌入式交叉编译时才需要 |
-| Sources | Qt 源码,可按需选择 |
-| Source 以下 | Qt 的各个模块,可按需勾选 |
+| 组件             | 说明                                                                                           |
+| ---------------- | ---------------------------------------------------------------------------------------------- |
+| MSVC             | 微软 VC 编译器,需要额外安装 VC 相关库                                                         |
+| **MinGW**        | Minimalist GNU on Windows,可在 GNU/Linux 与 Windows 上生成 Windows 程序,**教程只勾选 MinGW** |
+| Android / ARM 等 | 做嵌入式交叉编译时才需要                                                                       |
+| Sources          | Qt 源码,可按需选择                                                                            |
+| Source 以下      | Qt 的各个模块,可按需勾选                                                                      |
 
 安装完成后打开 Qt Creator 即可。
 
@@ -170,13 +182,13 @@ Ubuntu 默认是英文环境,且需要中文输入法才能在 Qt Creator 里
 
 启动 Qt Creator 后默认打开**欢迎页面**,左侧栏主要按钮:
 
-| 区域 | 作用 |
-| ---- | ---- |
-| 欢迎 | 打开示例、最近项目、入门教程 |
-| 编辑 | 写代码、编辑 `.ui`(切到设计器) |
-| 设计 | UI 设计界面 |
-| 调试 | 调试 |
-| 项目 | 构建设置 |
+| 区域     | 作用                                                              |
+| -------- | ----------------------------------------------------------------- |
+| 欢迎     | 打开示例、最近项目、入门教程                                      |
+| 编辑     | 写代码、编辑 `.ui`(切到设计器)                                  |
+| 设计     | UI 设计界面                                                       |
+| 调试     | 调试                                                              |
+| 项目     | 构建设置                                                          |
 | **帮助** | Qt 官方文档,可搜索类、方法、信号与槽,**是学习 Qt 最重要的资料** |
 
 Qt Creator 自带很多示例。由于在 Ubuntu 里 Qt 安装在 `/opt` 下没有写权限,打开示例时要先选"**复制项目并打开**"。
@@ -187,12 +199,12 @@ Qt Creator 自带很多示例。由于在 Ubuntu 里 Qt 安装在 `/opt` 下没
 
 顶部菜单"工具 → 选项"。常用设置项:
 
-| 设置项 | 作用 |
-| ------ | ---- |
-| **Kits** | 显示编译工具(如 `Desktop Qt5.12.9 GCC 64bit`);也可在此配置 ARM 平台编译器,Qt 跨平台正靠不同平台的编译工具实现 |
-| 环境 | 设置主题、语言等(默认即可) |
-| 文本编辑器 | 设置字体大小、颜色,以及关键字/字符串/注释的配色 |
-| 构建和运行 | 常用的是设置项目的构建目录,其余默认即可 |
+| 设置项     | 作用                                                                                                              |
+| ---------- | ----------------------------------------------------------------------------------------------------------------- |
+| **Kits**   | 显示编译工具(如 `Desktop Qt5.12.9 GCC 64bit`);也可在此配置 ARM 平台编译器,Qt 跨平台正靠不同平台的编译工具实现 |
+| 环境       | 设置主题、语言等(默认即可)                                                                                      |
+| 文本编辑器 | 设置字体大小、颜色,以及关键字/字符串/注释的配色                                                                  |
+| 构建和运行 | 常用的是设置项目的构建目录,其余默认即可                                                                          |
 
 ---
 
@@ -210,22 +222,22 @@ Qt Creator 自带很多示例。由于在 Ubuntu 里 Qt 安装在 `/opt` 下没
 
 **基类怎么选**:
 
-| 基类 | 说明 | 适用场景 |
-| ---- | ---- | -------- |
-| `QMainWindow` | 主窗口类,带主菜单栏、工具栏、状态栏 | 桌面式主窗口程序 |
-| `QWidget` | 可视界面类的基类;`QMainWindow` 也是由 `QWidget` 继承封装而来,功能更少 | **嵌入式常用**(嵌入式一般不需要标题栏、状态栏) |
-| `QDialog` | 对话框类 | 较少作为主基类 |
+| 基类          | 说明                                                                    | 适用场景                                         |
+| ------------- | ----------------------------------------------------------------------- | ------------------------------------------------ |
+| `QMainWindow` | 主窗口类,带主菜单栏、工具栏、状态栏                                    | 桌面式主窗口程序                                 |
+| `QWidget`     | 可视界面类的基类;`QMainWindow` 也是由 `QWidget` 继承封装而来,功能更少 | **嵌入式常用**(嵌入式一般不需要标题栏、状态栏) |
+| `QDialog`     | 对话框类                                                                | 较少作为主基类                                   |
 
 ### 5.2 项目文件介绍(3.6.2)
 
 新建项目后,左侧项目文件结构分组如下:
 
-| 文件/分组 | 内容 |
-| --------- | ---- |
+| 文件/分组            | 内容                                                                                |
+| -------------------- | ----------------------------------------------------------------------------------- |
 | `01_hello_world.pro` | **项目管理文件**,加入/删除文件时 Qt Creator 会自动修改它;有时需手动打开添加设置项 |
-| Header 分组 | 项目内所有头文件 `*.h` |
-| Source 分组 | 项目内所有 C++ 源码 `*.cpp` |
-| Forms 分组 | 项目内所有界面文件 `*.ui`(XML 描述,编译时生成相应 cpp 文件供交叉编译器编译) |
+| Header 分组          | 项目内所有头文件 `*.h`                                                              |
+| Source 分组          | 项目内所有 C++ 源码 `*.cpp`                                                         |
+| Forms 分组           | 项目内所有界面文件 `*.ui`(XML 描述,编译时生成相应 cpp 文件供交叉编译器编译)      |
 
 项目名以粗体显示表示它是**活动项目**。
 
@@ -332,12 +344,12 @@ else: unix:!android: target.path = /opt/$${TARGET}/bin
 
 双击 `.ui` 文件进入设计界面,主要区域:
 
-| 区域 | 说明 |
-| ---- | ---- |
-| ① 控件栏 | 各种控件;上方 Filter 输入首字母可快速定位 |
-| ② 设计窗体 | 显示窗口程序,默认 MainWindow 带菜单栏和状态栏 |
-| ③ 对象栏 | ②中用到的对象都在此显示(树状结构) |
-| ④ 属性栏 | 选中③中对象后,在此编辑其属性(位置、大小、文字、颜色、字体等) |
+| 区域       | 说明                                                            |
+| ---------- | --------------------------------------------------------------- |
+| ① 控件栏   | 各种控件;上方 Filter 输入首字母可快速定位                      |
+| ② 设计窗体 | 显示窗口程序,默认 MainWindow 带菜单栏和状态栏                  |
+| ③ 对象栏   | ②中用到的对象都在此显示(树状结构)                             |
+| ④ 属性栏   | 选中③中对象后,在此编辑其属性(位置、大小、文字、颜色、字体等) |
 
 #### 5.2.3 头文件 `*.h`(3.6.2.3)
 
@@ -457,12 +469,12 @@ sudo apt-get install libglu1-mesa-dev
 
 `.pro` 文件本身不是 Makefile,qmake 先把它翻译成 Makefile,再由 make 驱动编译器。过程中还涉及三个工具:
 
-| 工具 | 输入 | 输出 | 作用 |
-| ---- | ---- | ---- | ---- |
-| **qmake** | `*.pro` | `Makefile` | 根据工程配置生成构建规则 |
-| **moc** | 含 `Q_OBJECT` 的 `*.h` | `moc_*.cpp` | 元对象编译器,实现信号槽等元对象能力 |
-| **uic** | `*.ui` | `ui_*.h` | 把界面 XML 转成 C++ 头文件 |
-| **rcc** | `*.qrc` | `qrc_*.cpp` | 把资源文件编译进程序 |
+| 工具      | 输入                   | 输出        | 作用                                 |
+| --------- | ---------------------- | ----------- | ------------------------------------ |
+| **qmake** | `*.pro`                | `Makefile`  | 根据工程配置生成构建规则             |
+| **moc**   | 含 `Q_OBJECT` 的 `*.h` | `moc_*.cpp` | 元对象编译器,实现信号槽等元对象能力 |
+| **uic**   | `*.ui`                 | `ui_*.h`    | 把界面 XML 转成 C++ 头文件           |
+| **rcc**   | `*.qrc`                | `qrc_*.cpp` | 把资源文件编译进程序                 |
 
 ```mermaid
 flowchart LR
@@ -494,15 +506,15 @@ Qt 安装路径随版本和安装方式变化,可用 `which qmake` 或 `qmake
 
 ### 6.3 项目文件一览
 
-| 文件 | 由谁生成 | 是否手改 | 作用 |
-| ---- | -------- | -------- | ---- |
-| `*.pro` | Qt Creator | 可小改 | 工程配置:模块、源文件、编译选项 |
-| `*.ui` | Qt Designer | 不手改 | 界面 XML,uic 转成 `ui_*.h` |
-| `*.h` | 开发者 | 改 | 类声明、信号槽声明 |
-| `*.cpp` | 开发者 | 改 | 类实现 |
-| `ui_*.h` | uic | 不手改 | 构建时生成,位于 build 目录 |
-| `moc_*.cpp` | moc | 不手改 | 构建时生成 |
-| `*.qrc` | 开发者 | 可改 | 资源索引,rcc 编译进程序 |
+| 文件        | 由谁生成    | 是否手改 | 作用                             |
+| ----------- | ----------- | -------- | -------------------------------- |
+| `*.pro`     | Qt Creator  | 可小改   | 工程配置:模块、源文件、编译选项 |
+| `*.ui`      | Qt Designer | 不手改   | 界面 XML,uic 转成 `ui_*.h`      |
+| `*.h`       | 开发者      | 改       | 类声明、信号槽声明               |
+| `*.cpp`     | 开发者      | 改       | 类实现                           |
+| `ui_*.h`    | uic         | 不手改   | 构建时生成,位于 build 目录      |
+| `moc_*.cpp` | moc         | 不手改   | 构建时生成                       |
+| `*.qrc`     | 开发者      | 可改     | 资源索引,rcc 编译进程序         |
 
 ### 6.4 资源文件 `.qrc`
 
@@ -751,15 +763,15 @@ int main(int argc, char *argv[])
 
 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
 
-| 维度 | Ubuntu(本教程) | Windows | I.MX6ULL(嵌入式) |
-| ---- | --------------- | ------- | ------------------ |
-| 安装方式 | `.run` 安装到 `/opt` | 官方 installer,选 MinGW | 交叉编译工具链 + 目标板 Qt 库 |
-| 编译器 | `gcc`/`g++` | MinGW 的 `g++` 或 MSVC | `arm-linux-gnueabihf-g++` |
-| Qt 版本 | Qt5.12.9 LTS | Qt5.12.9 | 与目标板根文件系统 Qt 版本一致 |
-| 基类选择 | `QMainWindow` 或 `QWidget` | 同 | 常用 `QWidget`(无标题栏/状态栏) |
-| `.pro` 写法 | 三端基本一致 | 三端基本一致 | 只需切换 qmake/Kit |
-| 运行方式 | `./app` | 双击 exe | 拷贝到开发板 `./app` |
-| 显示后端 | X11/Wayland | Windows 原生 | LinuxFB / EGLFS(无桌面) |
+| 维度        | Ubuntu(本教程)           | Windows                  | I.MX6ULL(嵌入式)                |
+| ----------- | -------------------------- | ------------------------ | --------------------------------- |
+| 安装方式    | `.run` 安装到 `/opt`       | 官方 installer,选 MinGW | 交叉编译工具链 + 目标板 Qt 库     |
+| 编译器      | `gcc`/`g++`                | MinGW 的 `g++` 或 MSVC   | `arm-linux-gnueabihf-g++`         |
+| Qt 版本     | Qt5.12.9 LTS               | Qt5.12.9                 | 与目标板根文件系统 Qt 版本一致    |
+| 基类选择    | `QMainWindow` 或 `QWidget` | 同                       | 常用 `QWidget`(无标题栏/状态栏) |
+| `.pro` 写法 | 三端基本一致               | 三端基本一致             | 只需切换 qmake/Kit                |
+| 运行方式    | `./app`                    | 双击 exe                 | 拷贝到开发板 `./app`              |
+| 显示后端    | X11/Wayland                | Windows 原生             | LinuxFB / EGLFS(无桌面)         |
 
 结论:**`.pro`/`.ui`/`.h`/`.cpp` 工程结构在三端完全通用**,跨平台的关键只是切换 Kit(编译器 + qmake + Qt 库)。嵌入式的特殊点在于常无窗口系统、常用 `QWidget` 基类。
 

+ 111 - 97
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/03-信号与槽.md

@@ -1,6 +1,20 @@
 ---
 title: 信号与槽
-tags: [嵌入式Linux, Qt, 信号与槽, signal, slot, connect, Q_OBJECT, moc, 元对象系统, QtCreator, 快捷键, IMX6ULL]
+tags:
+  [
+    嵌入式Linux,
+    Qt,
+    信号与槽,
+    signal,
+    slot,
+    connect,
+    Q_OBJECT,
+    moc,
+    元对象系统,
+    QtCreator,
+    快捷键,
+    IMX6ULL,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《【正点原子】I.MX6U嵌入式Qt开发指南V1.2》第五章 Qt信号与槽、第六章 Qt Creator的使用技巧;例程 Embedded-Qt-Tutorial/Qt/01/03_signal_slot_example"
@@ -20,10 +34,10 @@ pdf_ref: "《【正点原子】I.MX6U嵌入式Qt开发指南V1.2》第五章 Qt
 
 ### 1.1 信号与槽是什么
 
-| 概念 | 定义 | 例子 |
-| ---- | ---- | ---- |
+| 概念               | 定义                         | 例子                                                                                       |
+| ------------------ | ---------------------------- | ------------------------------------------------------------------------------------------ |
 | **信号(Signal)** | 在特定情况下被**发射**的事件 | PushButton 被鼠标单击时发射 `clicked()`;ComboBox 列表项变化时发射 `CurrentIndexChanged()` |
-| **槽(Slot)** | 对信号响应的**函数** | 与信号关联后,信号发射时自动执行 |
+| **槽(Slot)**     | 对信号响应的**函数**         | 与信号关联后,信号发射时自动执行                                                           |
 
 槽与普通 C++ 函数的相同点:可以定义在类的任何部分(`public`、`private`、`protected`),可以有任意参数,也可以被直接调用。不同点:**槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行**。
 
@@ -43,12 +57,12 @@ QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
 connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
 ```
 
-| 参数 | 含义 |
-| ---- | ---- |
-| `sender` | 发射信号的**对象名称** |
+| 参数               | 含义                                                           |
+| ------------------ | -------------------------------------------------------------- |
+| `sender`           | 发射信号的**对象名称**                                         |
 | `SIGNAL(signal())` | 信号名称,信号可看作特殊函数,**需要带括号**,有参数时要写参数 |
-| `receiver` | 接收信号的**对象名称** |
-| `SLOT(slot())` | 槽函数名称,**需要带括号**,有参数时要写参数 |
+| `receiver`         | 接收信号的**对象名称**                                         |
+| `SLOT(slot())`     | 槽函数名称,**需要带括号**,有参数时要写参数                   |
 
 `SIGNAL` 和 `SLOT` 是 Qt 的**宏**,用于指明信号和槽,并把它们的参数转换为相应的字符串。
 
@@ -62,14 +76,14 @@ QObject::connect(pushButton, SIGNAL(clicked()), MainWindow, SLOT(close()));
 
 ### 1.3 信号与槽的规则
 
-| 规则 | 说明 | 示例 |
-| ---- | ---- | ---- |
-| 一个信号可连接**多个槽** | 槽按连接建立的顺序依次执行 | `connect(btn, SIGNAL(clicked()), this, SLOT(hide()));` 与 `connect(btn, SIGNAL(clicked()), this, SLOT(close()));` |
-| 多个信号可连接**同一个槽** | 任一信号发射都执行该槽 | 三个 pushButton 的 `clicked()` 都连到 `close()` |
-| 一个信号可连接**另外一个信号** | 信号发射时也发射另一个信号 | `connect(btn, SIGNAL(objectNameChanged(QString)), this, SIGNAL(windowTitleChanged(QString)));` |
-| 参数匹配 | 严格情况下信号与槽的参数**个数和类型需一致**,至少**信号的参数不能少于槽的参数**;不匹配会编译错误或运行错误 | 有参数的信号连接无参槽通常可以,反之不行 |
-| 必须加 `Q_OBJECT` | 使用信号与槽的类,类定义中**必须**加入宏 `Q_OBJECT`(特别重要) | 见下方 `mainwindow.h` |
-| 执行时机 | 信号发射时,关联槽通常被**立即执行**,像正常调用函数;所有槽执行完,才执行发射信号处后面的代码 | 顺序执行 |
+| 规则                           | 说明                                                                                                         | 示例                                                                                                              |
+| ------------------------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
+| 一个信号可连接**多个槽**       | 槽按连接建立的顺序依次执行                                                                                   | `connect(btn, SIGNAL(clicked()), this, SLOT(hide()));` 与 `connect(btn, SIGNAL(clicked()), this, SLOT(close()));` |
+| 多个信号可连接**同一个槽**     | 任一信号发射都执行该槽                                                                                       | 三个 pushButton 的 `clicked()` 都连到 `close()`                                                                   |
+| 一个信号可连接**另外一个信号** | 信号发射时也发射另一个信号                                                                                   | `connect(btn, SIGNAL(objectNameChanged(QString)), this, SIGNAL(windowTitleChanged(QString)));`                    |
+| 参数匹配                       | 严格情况下信号与槽的参数**个数和类型需一致**,至少**信号的参数不能少于槽的参数**;不匹配会编译错误或运行错误 | 有参数的信号连接无参槽通常可以,反之不行                                                                          |
+| 必须加 `Q_OBJECT`              | 使用信号与槽的类,类定义中**必须**加入宏 `Q_OBJECT`(特别重要)                                              | 见下方 `mainwindow.h`                                                                                             |
+| 执行时机                       | 信号发射时,关联槽通常被**立即执行**,像正常调用函数;所有槽执行完,才执行发射信号处后面的代码               | 顺序执行                                                                                                          |
 
 "一个信号连多个槽"的补充:如果两个槽一个是 `hide()`、一个是 `close()`,由于 `close()` 写在后面且执行后程序退出,`hide()` 其实已无意义——但它确实先执行了,因为连接在前。
 
@@ -99,11 +113,11 @@ bool QObject::disconnect(const QObject *sender, const char *signal,
                          const QObject *receiver, const char *method)
 ```
 
-| 需求 | 静态函数写法 | 非静态重载写法 |
-| ---- | ------------ | -------------- |
-| 断开与 `myObject` 连接的一切信号或槽 | `disconnect(myObject, 0, 0, 0);` | `myObject->disconnect();` |
-| 断开所有连接到特定信号的东西 | `disconnect(myObject, SIGNAL(mySignal()), 0, 0);` | `myObject->disconnect(SIGNAL(mySignal()));` |
-| 与指定的接收者断开连接 | `disconnect(myObject, 0, myReceiver, 0);` | `myObject->disconnect(myReceiver);` |
+| 需求                                 | 静态函数写法                                      | 非静态重载写法                              |
+| ------------------------------------ | ------------------------------------------------- | ------------------------------------------- |
+| 断开与 `myObject` 连接的一切信号或槽 | `disconnect(myObject, 0, 0, 0);`                  | `myObject->disconnect();`                   |
+| 断开所有连接到特定信号的东西         | `disconnect(myObject, SIGNAL(mySignal()), 0, 0);` | `myObject->disconnect(SIGNAL(mySignal()));` |
+| 与指定的接收者断开连接               | `disconnect(myObject, 0, myReceiver, 0);`         | `myObject->disconnect(myReceiver);`         |
 
 其中 `0` 表示"任意匹配"(C 风格的 NULL)。
 
@@ -126,13 +140,13 @@ disconnect(c);   // 只断开这一条连接
 
 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
 
-| 对比项 | 信号与槽 | 回调函数(函数指针) | Qt 事件(event) |
-| ------ | -------- | -------------------- | ---------------- |
-| 耦合度 | 发送者只负责 `emit`,不关心谁接收,**低耦合** | 调用者必须持有并注册回调指针,耦合较高 | 由 `QApplication` 分发到目标对象 |
-| 连接数量 | 一个信号可连多个槽;多个信号可连同一槽 | 通常一个回调只能挂一个函数 | 一个事件到达一个对象 |
-| 类型安全 | 函数指针写法编译期检查;字符串写法运行期检查 | 编译期检查 | 需重写 `event()` 或具体 `xxxEvent()` |
-| 跨线程 | 有 `QueuedConnection` 自动支持 | 需自行加锁/投递 | 需自行 `postEvent` |
-| 典型场景 | 控件交互、模块解耦 | 标准库/底层库回调 | 重绘、鼠标键盘、自定义事件 |
+| 对比项   | 信号与槽                                      | 回调函数(函数指针)                   | Qt 事件(event)                     |
+| -------- | --------------------------------------------- | -------------------------------------- | ------------------------------------ |
+| 耦合度   | 发送者只负责 `emit`,不关心谁接收,**低耦合** | 调用者必须持有并注册回调指针,耦合较高 | 由 `QApplication` 分发到目标对象     |
+| 连接数量 | 一个信号可连多个槽;多个信号可连同一槽        | 通常一个回调只能挂一个函数             | 一个事件到达一个对象                 |
+| 类型安全 | 函数指针写法编译期检查;字符串写法运行期检查  | 编译期检查                             | 需重写 `event()` 或具体 `xxxEvent()` |
+| 跨线程   | 有 `QueuedConnection` 自动支持                | 需自行加锁/投递                        | 需自行 `postEvent`                   |
+| 典型场景 | 控件交互、模块解耦                            | 标准库/底层库回调                      | 重绘、鼠标键盘、自定义事件           |
 
 信号槽本质上是 Qt 在"回调"之上加了一层元对象系统,换来**松耦合 + 多对多 + 跨线程**三个好处,代价是 `moc` 生成代码和轻微的性能开销。
 
@@ -163,12 +177,12 @@ flowchart LR
 
 ### 2.2 moc 生成的核心内容
 
-| 生成内容 | 作用 |
-| -------- | ---- |
-| `staticMetaObject` | 类的元对象,保存类名、信号槽签名、父类信息等 |
-| 信号的实现函数 | 调用 `QMetaObject::activate()` 发射信号 |
-| `qt_metacall` 等 | 按索引分发信号与槽调用 |
-| `metaObject()` 覆写 | 让运行时能查询类型信息 |
+| 生成内容            | 作用                                         |
+| ------------------- | -------------------------------------------- |
+| `staticMetaObject`  | 类的元对象,保存类名、信号槽签名、父类信息等 |
+| 信号的实现函数      | 调用 `QMetaObject::activate()` 发射信号      |
+| `qt_metacall` 等    | 按索引分发信号与槽调用                       |
+| `metaObject()` 覆写 | 让运行时能查询类型信息                       |
 
 因此 Qt 能在运行时通过**字符串签名**(`SIGNAL(...)`/`SLOT(...)` 宏最终转为字符串)查找并调用对应函数,这就是信号槽跨对象、跨线程也能工作的基础。
 
@@ -537,13 +551,13 @@ connect(pushButton, &QPushButton::clicked, this, &MainWindow::pushButtonClicked)
 - 重载信号需要 `qOverload` 或 `static_cast` 消歧,例如 `&QComboBox::currentIndexChanged` 有重载版本。
 - 不能用于"信号连信号"中目标也是信号的情况?——可以,函数指针同样支持信号连信号。
 
-| 对比项 | `SIGNAL/SLOT` 字符串写法 | 函数指针写法 |
-| ------ | ------------------------ | ------------ |
-| 错误检查时机 | 运行期 | 编译期 |
-| 是否需要 `SIGNAL`/`SLOT` | 需要 | 不需要 |
-| 重载信号 | 字符串自动区分 | 需 `qOverload`/`static_cast` |
-| 可否给槽传 lambda | 否 | 可以 |
-| Qt5/Qt6 支持 | 支持(保留) | 推荐 |
+| 对比项                   | `SIGNAL/SLOT` 字符串写法 | 函数指针写法                 |
+| ------------------------ | ------------------------ | ---------------------------- |
+| 错误检查时机             | 运行期                   | 编译期                       |
+| 是否需要 `SIGNAL`/`SLOT` | 需要                     | 不需要                       |
+| 重载信号                 | 字符串自动区分           | 需 `qOverload`/`static_cast` |
+| 可否给槽传 lambda        | 否                       | 可以                         |
+| Qt5/Qt6 支持             | 支持(保留)             | 推荐                         |
 
 ### 7.2 槽可以是 lambda 表达式
 
@@ -567,13 +581,13 @@ connect(pushButton, &QPushButton::clicked, this, [=]() {
 connect(sender, SIGNAL(sig()), receiver, SLOT(slot()), Qt::ConnectionType type);
 ```
 
-| 枚举值 | 行为 | 线程关系要求 |
-| ------ | ---- | ------------ |
-| `Qt::AutoConnection`(默认) | 发送者与接收者同线程→Direct;跨线程→Queued | 通用 |
-| `Qt::DirectConnection` | 立即在**发送者所在线程**直接调用槽 | 任意 |
-| `Qt::QueuedConnection` | 把调用封装为事件投递到**接收者线程**的事件队列,异步执行 | 接收者线程须有事件循环 |
-| `Qt::BlockingQueuedConnection` | 同 Queued,但发送者阻塞直到槽执行完 | **同线程使用会死锁** |
-| `Qt::UniqueConnection` | 与上述之一按位或使用,防止**重复连接**同一对信号槽 | 通用 |
+| 枚举值                         | 行为                                                     | 线程关系要求           |
+| ------------------------------ | -------------------------------------------------------- | ---------------------- |
+| `Qt::AutoConnection`(默认)   | 发送者与接收者同线程→Direct;跨线程→Queued               | 通用                   |
+| `Qt::DirectConnection`         | 立即在**发送者所在线程**直接调用槽                       | 任意                   |
+| `Qt::QueuedConnection`         | 把调用封装为事件投递到**接收者线程**的事件队列,异步执行 | 接收者线程须有事件循环 |
+| `Qt::BlockingQueuedConnection` | 同 Queued,但发送者阻塞直到槽执行完                      | **同线程使用会死锁**   |
+| `Qt::UniqueConnection`         | 与上述之一按位或使用,防止**重复连接**同一对信号槽       | 通用                   |
 
 ```mermaid
 flowchart TD
@@ -595,41 +609,41 @@ flowchart TD
 
 **一般快捷键**
 
-| 功能 | 快捷键 |
-| ---- | ------ |
-| 新建文件或项目(N) | `Ctrl + N` |
-| 关闭当前窗口/文件 | `Ctrl + W` |
-| 关闭所有文件 | `Ctrl + Shift + W` |
-| 关闭当前文件(Windows) | `Ctrl + F4` |
-| **运行** | `Ctrl + R` |
-| 返回上一级(常用于跳转代码) | `Alt + ←` |
-| 进入下一级(常用于跳转代码) | `Alt + →` |
-| Qt 自动排版对齐代码 | `Ctrl + I` |
-| 减小字体 | `Ctrl + -`(或 `Ctrl + 滚轮向下`) |
-| 增大字体 | `Ctrl + +`(或 `Ctrl + 滚轮向上`) |
-| 重置字体大小 | `Ctrl + 0` |
-| 折叠 | `Ctrl + <` |
+| 功能                         | 快捷键                             |
+| ---------------------------- | ---------------------------------- |
+| 新建文件或项目(N)            | `Ctrl + N`                         |
+| 关闭当前窗口/文件            | `Ctrl + W`                         |
+| 关闭所有文件                 | `Ctrl + Shift + W`                 |
+| 关闭当前文件(Windows)      | `Ctrl + F4`                        |
+| **运行**                     | `Ctrl + R`                         |
+| 返回上一级(常用于跳转代码) | `Alt + ←`                          |
+| 进入下一级(常用于跳转代码) | `Alt + →`                          |
+| Qt 自动排版对齐代码          | `Ctrl + I`                         |
+| 减小字体                     | `Ctrl + -`(或 `Ctrl + 滚轮向下`) |
+| 增大字体                     | `Ctrl + +`(或 `Ctrl + 滚轮向上`) |
+| 重置字体大小                 | `Ctrl + 0`                         |
+| 折叠                         | `Ctrl + <`                         |
 
 **常用编辑快捷键**
 
-| 功能 | 快捷键 |
-| ---- | ------ |
-| 展开 | `Ctrl + >` |
-| 复制行 | `Ctrl + Ins` |
-| 复制到行下 | `Ctrl + Alt + Down` |
-| 复制到行上 | `Ctrl + Alt + Up` |
-| 在当前行上方插入新行 | `Ctrl + Shift + Enter` |
-| 在当前行下方插入新行 | `Ctrl + Enter` |
-| 查看剪切板历史 | `Ctrl + Shift + V` |
-| 剪切行 | `Shift + Del` |
-| 追加行 | `Ctrl + J` |
-| 向下移动当前行 | `Ctrl + Shift + Down` |
-| 向上移动当前行 | `Ctrl + Shift + Up` |
-| 切换函数声明/定义 | `Ctrl + 鼠标左键` / `Shift + F2` |
-| **编辑信号和槽** | `F4` |
-| 跳转至以 `}` 结尾的块 | `Ctrl + }` |
-| 跳转至以 `{` 开始的块 | `Ctrl + {` |
-| 打开类型层次窗口 | `Ctrl + Shift + T` |
+| 功能                  | 快捷键                           |
+| --------------------- | -------------------------------- |
+| 展开                  | `Ctrl + >`                       |
+| 复制行                | `Ctrl + Ins`                     |
+| 复制到行下            | `Ctrl + Alt + Down`              |
+| 复制到行上            | `Ctrl + Alt + Up`                |
+| 在当前行上方插入新行  | `Ctrl + Shift + Enter`           |
+| 在当前行下方插入新行  | `Ctrl + Enter`                   |
+| 查看剪切板历史        | `Ctrl + Shift + V`               |
+| 剪切行                | `Shift + Del`                    |
+| 追加行                | `Ctrl + J`                       |
+| 向下移动当前行        | `Ctrl + Shift + Down`            |
+| 向上移动当前行        | `Ctrl + Shift + Up`              |
+| 切换函数声明/定义     | `Ctrl + 鼠标左键` / `Shift + F2` |
+| **编辑信号和槽**      | `F4`                             |
+| 跳转至以 `}` 结尾的块 | `Ctrl + }`                       |
+| 跳转至以 `{` 开始的块 | `Ctrl + {`                       |
+| 打开类型层次窗口      | `Ctrl + Shift + T`               |
 
 快捷键可能与 Ubuntu/Windows 系统冲突。可在"工具 → 选项 → 环境 → 键盘"里修改:选中命令,按"Record"重新记录新组合键,再 Apply / OK。**注意不要与系统快捷键重复**(例如 `Ctrl + M` 显示红色即表示与系统冲突)。没有定义快捷键的命令,也可以按同样步骤自定义。
 
@@ -646,13 +660,13 @@ Qt 帮助文档在安装时已随 Qt 一起安装,位于"安装目录 / Qt5.12
 
 **如何阅读一个类的文档页**:
 
-| 标题 | 内容 |
-| ---- | ---- |
-| Properties | 类有哪些属性 |
-| Public Functions | 类有哪些公开函数(方法) |
-| **Public Slots** | 类有哪些可用槽 |
-| Signals | 类有哪些信号 |
-| Detailed Description | 详细说明与示例 |
+| 标题                 | 内容                     |
+| -------------------- | ------------------------ |
+| Properties           | 类有哪些属性             |
+| Public Functions     | 类有哪些公开函数(方法) |
+| **Public Slots**     | 类有哪些可用槽           |
+| Signals              | 类有哪些信号             |
+| Detailed Description | 详细说明与示例           |
 
 查看某个方法的用法,直接单击进入即可。记得用 `Alt + ←` / `Alt + →` 在上下级文档间跳转。
 
@@ -673,15 +687,15 @@ flowchart LR
 
 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
 
-| 现象 | 可能原因 | 排查手段 |
-| ---- | -------- | -------- |
-| 点击按钮没反应 | 漏写 `connect`,或连接写反了发送者/接收者 | 检查 `connect(sender, SIGNAL(...), receiver, SLOT(...))` 顺序;确认在 `ui->setupUi(this)` 之后连接 |
-| 编译报 `undefined reference to vtable for ...` | 类里写了 `Q_OBJECT` 却没重新运行 qmake/moc | Qt Creator 里执行"构建 → 执行 qmake",再重新构建;确认 `.pro` 里 `HEADERS` 包含该头文件 |
-| 运行时报 `QObject::connect: No such signal/slot` | `SIGNAL`/`SLOT` 字符串拼写错误、参数不一致、忘写括号 | 用 `Ctrl` 点击跳转核对签名;优先改用函数指针写法让编译期报错 |
-| 槽执行了两次 | 同一对信号槽被 connect 了多次 | 用 `Qt::UniqueConnection`,或检查是否重复初始化 |
-| 跨线程槽里操作界面崩溃 | 在子线程直接调用了 UI 对象 | 用信号槽(默认 `AutoConnection`)把结果投递回主线程再更新界面 |
-| 自定义信号编译报 `error: no matching function` | 信号只声明未定义?其实正常 | 信号本就不该写函数体;检查是否误把信号写成普通函数、或忘加 `signals:` 区 |
-| 中文字符显示乱码 | 源文件编码或未配置中文输入 | 见 [[05-Qt应用开发/02-Qt入门与项目结构]] 的中文环境/输入法配置 |
+| 现象                                             | 可能原因                                             | 排查手段                                                                                           |
+| ------------------------------------------------ | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
+| 点击按钮没反应                                   | 漏写 `connect`,或连接写反了发送者/接收者            | 检查 `connect(sender, SIGNAL(...), receiver, SLOT(...))` 顺序;确认在 `ui->setupUi(this)` 之后连接 |
+| 编译报 `undefined reference to vtable for ...`   | 类里写了 `Q_OBJECT` 却没重新运行 qmake/moc           | Qt Creator 里执行"构建 → 执行 qmake",再重新构建;确认 `.pro` 里 `HEADERS` 包含该头文件            |
+| 运行时报 `QObject::connect: No such signal/slot` | `SIGNAL`/`SLOT` 字符串拼写错误、参数不一致、忘写括号 | 用 `Ctrl` 点击跳转核对签名;优先改用函数指针写法让编译期报错                                       |
+| 槽执行了两次                                     | 同一对信号槽被 connect 了多次                        | 用 `Qt::UniqueConnection`,或检查是否重复初始化                                                    |
+| 跨线程槽里操作界面崩溃                           | 在子线程直接调用了 UI 对象                           | 用信号槽(默认 `AutoConnection`)把结果投递回主线程再更新界面                                      |
+| 自定义信号编译报 `error: no matching function`   | 信号只声明未定义?其实正常                           | 信号本就不该写函数体;检查是否误把信号写成普通函数、或忘加 `signals:` 区                           |
+| 中文字符显示乱码                                 | 源文件编码或未配置中文输入                           | 见 [[05-Qt应用开发/02-Qt入门与项目结构]] 的中文环境/输入法配置                                     |
 
 ---
 

+ 269 - 235
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/04-Qt控件详解.md

@@ -1,6 +1,40 @@
 ---
 title: Qt控件详解
-tags: [嵌入式Linux, Qt, Qt Widgets, 控件, QPushButton, QToolButton, QRadioButton, QCheckBox, QCommandLinkButton, QDialogButtonBox, QComboBox, QFontComboBox, QLineEdit, QTextEdit, QPlainTextEdit, QSpinBox, QDoubleSpinBox, QTimeEdit, QDateEdit, QDateTimeEdit, QDial, QScrollBar, QSlider, QKeySequenceEdit, QLabel, QCalendarWidget, QLCDNumber, QProgressBar, QFrame, QTextBrowser, QGraphicsView, I.MX6U]
+tags:
+  [
+    嵌入式Linux,
+    Qt,
+    Qt Widgets,
+    控件,
+    QPushButton,
+    QToolButton,
+    QRadioButton,
+    QCheckBox,
+    QCommandLinkButton,
+    QDialogButtonBox,
+    QComboBox,
+    QFontComboBox,
+    QLineEdit,
+    QTextEdit,
+    QPlainTextEdit,
+    QSpinBox,
+    QDoubleSpinBox,
+    QTimeEdit,
+    QDateEdit,
+    QDateTimeEdit,
+    QDial,
+    QScrollBar,
+    QSlider,
+    QKeySequenceEdit,
+    QLabel,
+    QCalendarWidget,
+    QLCDNumber,
+    QProgressBar,
+    QFrame,
+    QTextBrowser,
+    QGraphicsView,
+    I.MX6U,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《【正点原子】I.MX6U嵌入式Qt开发指南V1.2》第七章 Qt控件 7.1 按钮、7.2 输入窗口部件、7.3 显示窗口部件、7.4 显示窗口部件之浏览器;例程 Embedded-Qt-Tutorial/Qt/01/04_qpushbutton~28_qgraphicsview"
@@ -82,33 +116,33 @@ classDiagram
 
 ### 0.2 本章控件索引
 
-| 分组 | 控件 | 一句话作用 | 例程 |
-| ---- | ---- | ---------- | ---- |
-| 按钮 | `QPushButton` | 下压按钮,执行命令或触发事件 | 04 |
-| 按钮 | `QToolButton` | 工具按钮,通常显示图标,用于工具栏 | 05 |
-| 按钮 | `QRadioButton` | 单选按钮,一组内多选一 | 06 |
-| 按钮 | `QCheckBox` | 复选按钮,可多选多,支持三态 | 07 |
-| 按钮 | `QCommandLinkButton` | 命令链接按钮,带描述文本与箭头 | 08 |
-| 按钮 | `QDialogButtonBox` | 对话框标准按钮盒 | 09 |
-| 输入 | `QComboBox` | 下拉组合框 | 10 |
-| 输入 | `QFontComboBox` | 字体组合框 | 11 |
-| 输入 | `QLineEdit` | 单行文本编辑框 | 12 |
-| 输入 | `QTextEdit` | 富文本编辑框 | 13 |
-| 输入 | `QPlainTextEdit` | 纯文本编辑框 | 14 |
-| 输入 | `QSpinBox` | 整数微调框 | 15 |
-| 输入 | `QDoubleSpinBox` | 浮点微调框 | 16 |
-| 输入 | `QTimeEdit` / `QDateEdit` / `QDateTimeEdit` | 时间/日期/日期时间编辑 | 17 |
-| 输入 | `QDial` | 圆形范围控制(拨盘) | 18 |
-| 输入 | `QScrollBar` | 滚动条 | 19 |
-| 输入 | `QSlider` | 滑动条 | 20 |
-| 输入 | `QKeySequenceEdit` | 快捷键序列录入 | 21 |
-| 显示 | `QLabel` | 显示文本或图像 | 22 |
-| 显示 | `QCalendarWidget` | 月历,选择日期 | 23 |
-| 显示 | `QLCDNumber` | LCD 风格数字显示 | 24 |
-| 显示 | `QProgressBar` | 进度条 | 25 |
-| 显示 | `QFrame` | 框架基类,可画水平/垂直线 | 26 |
-| 浏览器 | `QTextBrowser` | 带超文本导航的文本浏览器 | 27 |
-| 浏览器 | `QGraphicsView` | 图形视图框架,可视化场景 | 28 |
+| 分组   | 控件                                        | 一句话作用                         | 例程 |
+| ------ | ------------------------------------------- | ---------------------------------- | ---- |
+| 按钮   | `QPushButton`                               | 下压按钮,执行命令或触发事件       | 04   |
+| 按钮   | `QToolButton`                               | 工具按钮,通常显示图标,用于工具栏 | 05   |
+| 按钮   | `QRadioButton`                              | 单选按钮,一组内多选一             | 06   |
+| 按钮   | `QCheckBox`                                 | 复选按钮,可多选多,支持三态       | 07   |
+| 按钮   | `QCommandLinkButton`                        | 命令链接按钮,带描述文本与箭头     | 08   |
+| 按钮   | `QDialogButtonBox`                          | 对话框标准按钮盒                   | 09   |
+| 输入   | `QComboBox`                                 | 下拉组合框                         | 10   |
+| 输入   | `QFontComboBox`                             | 字体组合框                         | 11   |
+| 输入   | `QLineEdit`                                 | 单行文本编辑框                     | 12   |
+| 输入   | `QTextEdit`                                 | 富文本编辑框                       | 13   |
+| 输入   | `QPlainTextEdit`                            | 纯文本编辑框                       | 14   |
+| 输入   | `QSpinBox`                                  | 整数微调框                         | 15   |
+| 输入   | `QDoubleSpinBox`                            | 浮点微调框                         | 16   |
+| 输入   | `QTimeEdit` / `QDateEdit` / `QDateTimeEdit` | 时间/日期/日期时间编辑             | 17   |
+| 输入   | `QDial`                                     | 圆形范围控制(拨盘)               | 18   |
+| 输入   | `QScrollBar`                                | 滚动条                             | 19   |
+| 输入   | `QSlider`                                   | 滑动条                             | 20   |
+| 输入   | `QKeySequenceEdit`                          | 快捷键序列录入                     | 21   |
+| 显示   | `QLabel`                                    | 显示文本或图像                     | 22   |
+| 显示   | `QCalendarWidget`                           | 月历,选择日期                     | 23   |
+| 显示   | `QLCDNumber`                                | LCD 风格数字显示                   | 24   |
+| 显示   | `QProgressBar`                              | 进度条                             | 25   |
+| 显示   | `QFrame`                                    | 框架基类,可画水平/垂直线          | 26   |
+| 浏览器 | `QTextBrowser`                              | 带超文本导航的文本浏览器           | 27   |
+| 浏览器 | `QGraphicsView`                             | 图形视图框架,可视化场景           | 28   |
 
 ---
 
@@ -127,17 +161,17 @@ Qt 内置六种按钮部件,都直接或间接继承 `QAbstractButton`:
 >
 > `QAbstractButton` 是所有按钮的公共基类,它提供按钮共有的一组属性与信号(下方各控件小节中会用到):
 >
-> | 成员 | 类型 | 说明 |
-> | ---- | ---- | ---- |
-> | `setText(QString)` / `text()` | 属性 | 按钮显示文本 |
-> | `setIcon(QIcon)` / `icon()` | 属性 | 按钮图标 |
-> | `setCheckable(bool)` | 属性 | 是否可切换选中状态 |
-> | `setChecked(bool)` / `isChecked()` | 属性 | 选中状态 |
-> | `setEnabled(bool)` | 属性 | 是否可用 |
-> | `clicked()` | 信号 | 被点击(最常用) |
-> | `pressed()` / `released()` | 信号 | 按下 / 释放 |
-> | `toggled(bool)` | 信号 | 可选中按钮的状态变化 |
-> | `setShortcut(QKeySequence)` | 方法 | 设置快捷键 |
+> | 成员                               | 类型 | 说明                 |
+> | ---------------------------------- | ---- | -------------------- |
+> | `setText(QString)` / `text()`      | 属性 | 按钮显示文本         |
+> | `setIcon(QIcon)` / `icon()`        | 属性 | 按钮图标             |
+> | `setCheckable(bool)`               | 属性 | 是否可切换选中状态   |
+> | `setChecked(bool)` / `isChecked()` | 属性 | 选中状态             |
+> | `setEnabled(bool)`                 | 属性 | 是否可用             |
+> | `clicked()`                        | 信号 | 被点击(最常用)     |
+> | `pressed()` / `released()`         | 信号 | 按下 / 释放          |
+> | `toggled(bool)`                    | 信号 | 可选中按钮的状态变化 |
+> | `setShortcut(QKeySequence)`        | 方法 | 设置快捷键           |
 
 ### 1.1 QPushButton
 
@@ -147,13 +181,13 @@ Qt 内置六种按钮部件,都直接或间接继承 `QAbstractButton`:
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
+| 成员                                                | 说明                           |
+| --------------------------------------------------- | ------------------------------ |
 | `QPushButton(const QString &text, QWidget *parent)` | 构造时直接传入显示文本与父对象 |
-| `setGeometry(x, y, w, h)` | 设置位置与大小 |
-| `setText(QString)` | 设置文本 |
-| `setStyleSheet(QString)` | 设置样式表,例如背景色 |
-| `clicked()` | 点击信号 |
+| `setGeometry(x, y, w, h)`                           | 设置位置与大小                 |
+| `setText(QString)`                                  | 设置文本                       |
+| `setStyleSheet(QString)`                            | 设置样式表,例如背景色         |
+| `clicked()`                                         | 点击信号                       |
 
 #### 用法示例
 
@@ -251,12 +285,12 @@ void MainWindow::pushButton2_Clicked()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setIcon(QIcon)` | 设置图标 |
-| `setText(QString)` | 设置文本 |
+| 成员                                      | 说明                 |
+| ----------------------------------------- | -------------------- |
+| `setIcon(QIcon)`                          | 设置图标             |
+| `setText(QString)`                        | 设置文本             |
 | `setToolButtonStyle(Qt::ToolButtonStyle)` | 图标与文本的排布方式 |
-| `clicked()` | 点击信号 |
+| `clicked()`                               | 点击信号             |
 
 常用的 `Qt::ToolButtonStyle`:`Qt::ToolButtonIconOnly`(只显示图标)、`Qt::ToolButtonTextOnly`(只显示文本)、`Qt::ToolButtonTextBesideIcon`(文本在图标旁)、`Qt::ToolButtonTextUnderIcon`(文本在图标下)、`Qt::ToolButtonFollowStyle`。
 
@@ -350,12 +384,12 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setText(QString)` | 设置文本 |
+| 成员                               | 说明              |
+| ---------------------------------- | ----------------- |
+| `setText(QString)`                 | 设置文本          |
 | `setChecked(bool)` / `isChecked()` | 设置/读取选中状态 |
-| `toggled(bool)` | 选中状态变化信号 |
-| `clicked()` | 点击信号 |
+| `toggled(bool)`                    | 选中状态变化信号  |
+| `clicked()`                        | 点击信号          |
 
 #### 用法示例
 
@@ -456,19 +490,19 @@ int main(int argc, char *argv[])
 `style.qss`:
 
 ```css
-QRadioButton{
-    spacing: 2px;
-    color: white;
+QRadioButton {
+  spacing: 2px;
+  color: white;
 }
 QRadioButton::indicator {
-    width: 45px;
-    height: 30px;
+  width: 45px;
+  height: 30px;
 }
 QRadioButton::indicator:unchecked {
-    image: url(:/images/switch_off.png);
+  image: url(:/images/switch_off.png);
 }
 QRadioButton::indicator:checked {
-    image: url(:/images/switch_on.png);
+  image: url(:/images/switch_on.png);
 }
 ```
 
@@ -488,13 +522,13 @@ QRadioButton::indicator:checked {
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setText(QString)` | 设置文本 |
-| `setTristate(bool)` | 开启三态(不开启只有选中/未选中) |
-| `setCheckState(Qt::CheckState)` | 设置状态 |
-| `checkState()` | 读取状态 |
-| `stateChanged(int)` | 状态变化信号(返回 `Qt::Checked` / `Qt::Unchecked` / `Qt::PartiallyChecked`) |
+| 成员                            | 说明                                                                          |
+| ------------------------------- | ----------------------------------------------------------------------------- |
+| `setText(QString)`              | 设置文本                                                                      |
+| `setTristate(bool)`             | 开启三态(不开启只有选中/未选中)                                             |
+| `setCheckState(Qt::CheckState)` | 设置状态                                                                      |
+| `checkState()`                  | 读取状态                                                                      |
+| `stateChanged(int)`             | 状态变化信号(返回 `Qt::Checked` / `Qt::Unchecked` / `Qt::PartiallyChecked`) |
 
 #### 用法示例
 
@@ -590,22 +624,22 @@ void MainWindow::checkBoxStateChanged(int state)
 `style.qss`:
 
 ```css
-QCheckBox{
-    spacing: 5px;
-    color: white;
+QCheckBox {
+  spacing: 5px;
+  color: white;
 }
 QCheckBox::indicator {
-    width: 50px;
-    height: 50px;
+  width: 50px;
+  height: 50px;
 }
 QCheckBox::indicator:enabled:unchecked {
-    image: url(:/images/unchecked.png);
+  image: url(:/images/unchecked.png);
 }
 QCheckBox::indicator:enabled:checked {
-    image: url(:/images/checked.png);
+  image: url(:/images/checked.png);
 }
 QCheckBox::indicator:enabled:indeterminate {
-    image: url(:/images/indeterminate.png);
+  image: url(:/images/indeterminate.png);
 }
 ```
 
@@ -625,12 +659,12 @@ QCheckBox::indicator:enabled:indeterminate {
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
+| 成员                                                                                   | 说明                       |
+| -------------------------------------------------------------------------------------- | -------------------------- |
 | `QCommandLinkButton(const QString &text, const QString &description, QWidget *parent)` | 构造:文本 + 描述 + 父对象 |
-| `setDescription(QString)` | 设置描述文本 |
-| `setIcon(QIcon)` | 设置图标 |
-| `clicked()` | 点击信号 |
+| `setDescription(QString)`                                                              | 设置描述文本               |
+| `setIcon(QIcon)`                                                                       | 设置图标                   |
+| `clicked()`                                                                            | 点击信号                   |
 
 #### 用法示例
 
@@ -720,12 +754,12 @@ void MainWindow::commandLinkButtonClicked()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `addButton(StandardButton)` | 添加标准按钮 |
-| `addButton(QAbstractButton*, ButtonRole)` | 添加自定义按钮并指定角色 |
-| `button(StandardButton)` | 取得某个标准按钮的指针 |
-| `clicked(QAbstractButton*)` | 按钮点击信号,可判断点了哪个 |
+| 成员                                      | 说明                         |
+| ----------------------------------------- | ---------------------------- |
+| `addButton(StandardButton)`               | 添加标准按钮                 |
+| `addButton(QAbstractButton*, ButtonRole)` | 添加自定义按钮并指定角色     |
+| `button(StandardButton)`                  | 取得某个标准按钮的指针       |
+| `clicked(QAbstractButton*)`               | 按钮点击信号,可判断点了哪个 |
 
 #### 用法示例
 
@@ -838,13 +872,13 @@ Qt Designer 的输入部件面板提供 16 种输入部件,涵盖组合框、
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `addItem(QString)` | 添加一项 |
-| `itemText(int index)` | 取第 index 项的文本 |
-| `currentIndex()` | 当前索引 |
-| `count()` | 项数 |
-| `currentIndexChanged(int)` | 当前项改变信号 |
+| 成员                       | 说明                |
+| -------------------------- | ------------------- |
+| `addItem(QString)`         | 添加一项            |
+| `itemText(int index)`      | 取第 index 项的文本 |
+| `currentIndex()`           | 当前索引            |
+| `count()`                  | 项数                |
+| `currentIndexChanged(int)` | 当前项改变信号      |
 
 #### 用法示例
 
@@ -934,11 +968,11 @@ void MainWindow::comboBoxIndexChanged(int index)
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `currentFontChanged(QFont)` | 当前字体变化信号 |
-| `itemText(int index)` | 取第 index 项名称 |
-| `currentIndex()` | 当前索引 |
+| 成员                        | 说明              |
+| --------------------------- | ----------------- |
+| `currentFontChanged(QFont)` | 当前字体变化信号  |
+| `itemText(int index)`       | 取第 index 项名称 |
+| `currentIndex()`            | 当前索引          |
 
 #### 用法示例
 
@@ -1035,13 +1069,13 @@ void MainWindow::fontComboBoxFontChanged(QFont font)
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `text()` | 读取文本 |
-| `setText(QString)` | 设置文本 |
-| `clear()` | 清空 |
-| `setEchoMode(QLineEdit::EchoMode)` | 回显模式(如 `Password`) |
-| `textChanged(QString)` / `returnPressed()` | 文本变化 / 回车信号 |
+| 成员                                       | 说明                      |
+| ------------------------------------------ | ------------------------- |
+| `text()`                                   | 读取文本                  |
+| `setText(QString)`                         | 设置文本                  |
+| `clear()`                                  | 清空                      |
+| `setEchoMode(QLineEdit::EchoMode)`         | 回显模式(如 `Password`) |
+| `textChanged(QString)` / `returnPressed()` | 文本变化 / 回车信号       |
 
 #### 用法示例
 
@@ -1141,14 +1175,14 @@ void MainWindow::pushButtonClicked()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
+| 成员                                 | 说明            |
+| ------------------------------------ | --------------- |
 | `setText(QString)` / `toPlainText()` | 设置/读取纯文本 |
-| `setHtml(QString)` / `toHtml()` | 设置/读取富文本 |
-| `selectAll()` | 全选 |
-| `clear()` | 清空 |
-| `setFocus()` | 设置焦点 |
-| `textChanged()` | 文本变化信号 |
+| `setHtml(QString)` / `toHtml()`      | 设置/读取富文本 |
+| `selectAll()`                        | 全选            |
+| `clear()`                            | 清空            |
+| `setFocus()`                         | 设置焦点        |
+| `textChanged()`                      | 文本变化信号    |
 
 #### 用法示例
 
@@ -1258,12 +1292,12 @@ void MainWindow::pushButtonClearAllClicked()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `insertPlainText(QString)` | 插入纯文本 |
+| 成员                                      | 说明            |
+| ----------------------------------------- | --------------- |
+| `insertPlainText(QString)`                | 插入纯文本      |
 | `setPlainText(QString)` / `toPlainText()` | 设置/读取纯文本 |
-| `setReadOnly(bool)` | 只读模式 |
-| `textChanged()` | 文本变化信号 |
+| `setReadOnly(bool)`                       | 只读模式        |
+| `textChanged()`                           | 文本变化信号    |
 
 #### 用法示例
 
@@ -1374,13 +1408,13 @@ void MainWindow::radioButtonClicked()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setRange(int min, int max)` | 范围 |
-| `setSingleStep(int)` | 步长 |
-| `setValue(int)` / `value()` | 当前值 |
-| `setPrefix(QString)` / `setSuffix(QString)` | 前缀/后缀 |
-| `valueChanged(int)` | 值变化信号 |
+| 成员                                        | 说明       |
+| ------------------------------------------- | ---------- |
+| `setRange(int min, int max)`                | 范围       |
+| `setSingleStep(int)`                        | 步长       |
+| `setValue(int)` / `value()`                 | 当前值     |
+| `setPrefix(QString)` / `setSuffix(QString)` | 前缀/后缀  |
+| `valueChanged(int)`                         | 值变化信号 |
 
 #### 用法示例
 
@@ -1475,13 +1509,13 @@ void MainWindow::spinBoxValueChanged(int opacity)
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setRange(double min, double max)` | 范围 |
-| `setSingleStep(double)` | 步长 |
-| `setValue(double)` / `value()` | 当前值 |
-| `setPrefix(QString)` / `setSuffix(QString)` | 前缀/后缀 |
-| `valueChanged(double)` | 值变化信号 |
+| 成员                                        | 说明       |
+| ------------------------------------------- | ---------- |
+| `setRange(double min, double max)`          | 范围       |
+| `setSingleStep(double)`                     | 步长       |
+| `setValue(double)` / `value()`              | 当前值     |
+| `setPrefix(QString)` / `setSuffix(QString)` | 前缀/后缀  |
+| `valueChanged(double)`                      | 值变化信号 |
 
 #### 用法示例
 
@@ -1591,14 +1625,14 @@ void MainWindow::doubleSpinBoxValueChanged(double value)
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `QDateTimeEdit(const QDateTime&, QWidget*)` | 以日期时间构造 |
-| `QTimeEdit(const QTime&, QWidget*)` | 以时间构造 |
-| `QDateEdit(const QDate&, QWidget*)` | 以日期构造 |
-| `setCalendarPopup(bool)` | 是否弹出日历选择 |
-| `dateTime()` / `time()` / `date()` | 读取值 |
-| `dateTimeChanged(QDateTime)` / `timeChanged(QTime)` / `dateChanged(QDate)` | 值变化信号 |
+| 成员                                                                       | 说明             |
+| -------------------------------------------------------------------------- | ---------------- |
+| `QDateTimeEdit(const QDateTime&, QWidget*)`                                | 以日期时间构造   |
+| `QTimeEdit(const QTime&, QWidget*)`                                        | 以时间构造       |
+| `QDateEdit(const QDate&, QWidget*)`                                        | 以日期构造       |
+| `setCalendarPopup(bool)`                                                   | 是否弹出日历选择 |
+| `dateTime()` / `time()` / `date()`                                         | 读取值           |
+| `dateTimeChanged(QDateTime)` / `timeChanged(QTime)` / `dateChanged(QDate)` | 值变化信号       |
 
 #### 用法示例
 
@@ -1678,14 +1712,14 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setRange(int min, int max)` | 范围 |
-| `setPageStep(int)` | 页长(两个最大刻度间距) |
-| `setNotchesVisible(bool)` | 刻度是否可见 |
-| `setNotchTarget(double)` | 两个凹槽间的目标像素数 |
-| `setWrapping(bool)` | 是否可指向整圈任意角度 |
-| `valueChanged(int)` | 值变化信号 |
+| 成员                         | 说明                     |
+| ---------------------------- | ------------------------ |
+| `setRange(int min, int max)` | 范围                     |
+| `setPageStep(int)`           | 页长(两个最大刻度间距) |
+| `setNotchesVisible(bool)`    | 刻度是否可见             |
+| `setNotchTarget(double)`     | 两个凹槽间的目标像素数   |
+| `setWrapping(bool)`          | 是否可指向整圈任意角度   |
+| `valueChanged(int)`          | 值变化信号               |
 
 #### 用法示例
 
@@ -1789,13 +1823,13 @@ void MainWindow::dialValueChanged(int val)
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
+| 成员                                    | 说明                |
+| --------------------------------------- | ------------------- |
 | `QScrollBar(Qt::Orientation, QWidget*)` | 构造:方向 + 父对象 |
-| `setRange(int min, int max)` | 范围 |
-| `setValue(int)` / `value()` | 当前值 |
-| `setPageStep(int)` | 页步长 |
-| `valueChanged(int)` | 值变化信号 |
+| `setRange(int min, int max)`            | 范围                |
+| `setValue(int)` / `value()`             | 当前值              |
+| `setPageStep(int)`                      | 页步长              |
+| `valueChanged(int)`                     | 值变化信号          |
 
 #### 用法示例
 
@@ -1874,14 +1908,14 @@ MainWindow::MainWindow(QWidget *parent)
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
+| 成员                                 | 说明                |
+| ------------------------------------ | ------------------- |
 | `QSlider(Qt::Orientation, QWidget*)` | 构造:方向 + 父对象 |
-| `setRange(int min, int max)` | 范围 |
-| `setValue(int)` / `value()` | 当前值 |
-| `setSliderPosition(int)` | 设置滑块位置 |
-| `valueChanged(int)` | 值变化信号 |
-| `sliderMoved(int)` | 拖动时信号 |
+| `setRange(int min, int max)`         | 范围                |
+| `setValue(int)` / `value()`          | 当前值              |
+| `setSliderPosition(int)`             | 设置滑块位置        |
+| `valueChanged(int)`                  | 值变化信号          |
+| `sliderMoved(int)`                   | 拖动时信号          |
 
 #### 用法示例
 
@@ -1997,11 +2031,11 @@ void MainWindow::verticalSliderValueChanged(int val)
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
+| 成员                                             | 说明                |
+| ------------------------------------------------ | ------------------- |
 | `keySequence()` / `setKeySequence(QKeySequence)` | 读取/设置快捷键序列 |
-| `keySequenceChanged(const QKeySequence &)` | 序列变化信号 |
-| `clear()` | 清空 |
+| `keySequenceChanged(const QKeySequence &)`       | 序列变化信号        |
+| `clear()`                                        | 清空                |
 
 #### 用法示例
 
@@ -2099,13 +2133,13 @@ Qt 显示部件面板提供 Label、Text Browser、Graphics View、Calendar Widg
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setText(QString)` | 设置文本 |
-| `setPixmap(QPixmap)` | 设置图像 |
-| `setScaledContents(bool)` | 允许缩放填充 |
-| `setAlignment(Qt::Alignment)` | 对齐方式 |
-| `setFont(QFont)` | 字体 |
+| 成员                          | 说明         |
+| ----------------------------- | ------------ |
+| `setText(QString)`            | 设置文本     |
+| `setPixmap(QPixmap)`          | 设置图像     |
+| `setScaledContents(bool)`     | 允许缩放填充 |
+| `setAlignment(Qt::Alignment)` | 对齐方式     |
+| `setFont(QFont)`              | 字体         |
 
 #### 用法示例
 
@@ -2181,13 +2215,13 @@ MainWindow::MainWindow(QWidget *parent)
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `selectedDate()` | 当前选中日期 |
+| 成员                     | 说明         |
+| ------------------------ | ------------ |
+| `selectedDate()`         | 当前选中日期 |
 | `setSelectedDate(QDate)` | 设置选中日期 |
-| `selectionChanged()` | 选择变化信号 |
-| `setFont(QFont)` | 设置字体 |
-| `setGridVisible(bool)` | 是否显示网格 |
+| `selectionChanged()`     | 选择变化信号 |
+| `setFont(QFont)`         | 设置字体     |
+| `setGridVisible(bool)`   | 是否显示网格 |
 
 #### 用法示例
 
@@ -2298,13 +2332,13 @@ void MainWindow::pushButtonClicked()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setDigitCount(int)` | 显示位数 |
+| 成员                                        | 说明                                  |
+| ------------------------------------------- | ------------------------------------- |
+| `setDigitCount(int)`                        | 显示位数                              |
 | `setSegmentStyle(QLCDNumber::SegmentStyle)` | 段样式(`Flat`、`Outline`、`Filled`) |
-| `display(...)` | 显示数字或字符串(重载槽) |
-| `setMode(QLCDNumber::Mode)` | 进制(`Dec`/`Hex`/`Oct`/`Bin`) |
-| `overflow()` | 溢出信号 |
+| `display(...)`                              | 显示数字或字符串(重载槽)            |
+| `setMode(QLCDNumber::Mode)`                 | 进制(`Dec`/`Hex`/`Oct`/`Bin`)       |
+| `overflow()`                                | 溢出信号                              |
 
 #### 用法示例
 
@@ -2407,14 +2441,14 @@ void MainWindow::timerTimeOut()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setRange(int min, int max)` | 范围 |
-| `setValue(int)` / `value()` | 当前值 |
-| `setFormat(QString)` | 文本格式,`%p%` 表示百分比 |
-| `setOrientation(Qt::Orientation)` | 方向 |
-| `setTextVisible(bool)` | 是否显示文本 |
-| `valueChanged(int)` | 值变化信号 |
+| 成员                              | 说明                       |
+| --------------------------------- | -------------------------- |
+| `setRange(int min, int max)`      | 范围                       |
+| `setValue(int)` / `value()`       | 当前值                     |
+| `setFormat(QString)`              | 文本格式,`%p%` 表示百分比 |
+| `setOrientation(Qt::Orientation)` | 方向                       |
+| `setTextVisible(bool)`            | 是否显示文本               |
+| `valueChanged(int)`               | 值变化信号                 |
 
 #### 用法示例
 
@@ -2531,18 +2565,18 @@ void MainWindow::timerTimeOut()
 
 `QFrame::Shape` 定义框架外形:
 
-| 取值 | 效果 |
-| ---- | ---- |
-| `NoFrame` | 不画任何东西 |
-| `Box` | 在内容周围画一个框 |
-| `Panel` | 画一个平板,内容看起来凸起或凹陷 |
-| `WinPanel` | 类似 Panel,但三维效果与 Windows 95 一致 |
-| `ToolBarPanel` | 调用 `QStyle::drawToolBarPanel()` |
-| `MenuBarPanel` | 调用 `QStyle::drawMenuBarPanel()` |
-| `HLine` | 画水平线,用于分隔 |
-| `VLine` | 画竖直线,用于分隔 |
-| `StyledPanel` | 调用 `QStyle::drawPanel()` |
-| `PopupPanel` | 调用 `QStyle::drawPopupPanel()` |
+| 取值           | 效果                                     |
+| -------------- | ---------------------------------------- |
+| `NoFrame`      | 不画任何东西                             |
+| `Box`          | 在内容周围画一个框                       |
+| `Panel`        | 画一个平板,内容看起来凸起或凹陷         |
+| `WinPanel`     | 类似 Panel,但三维效果与 Windows 95 一致 |
+| `ToolBarPanel` | 调用 `QStyle::drawToolBarPanel()`        |
+| `MenuBarPanel` | 调用 `QStyle::drawMenuBarPanel()`        |
+| `HLine`        | 画水平线,用于分隔                       |
+| `VLine`        | 画竖直线,用于分隔                       |
+| `StyledPanel`  | 调用 `QStyle::drawPanel()`               |
+| `PopupPanel`   | 调用 `QStyle::drawPopupPanel()`          |
 
 阴影风格(`QFrame::Shadow`):`Plain`(无三维效果)、`Raised`(凸起)、`Sunken`(凹陷)。
 
@@ -2550,12 +2584,12 @@ void MainWindow::timerTimeOut()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setFrameShape(QFrame::Shape)` | 框架外形 |
-| `setFrameShadow(QFrame::Shadow)` | 阴影风格 |
-| `frameRect()` | 框架矩形 |
-| `setFrameStyle(int)` | 同时设置外形与阴影 |
+| 成员                             | 说明               |
+| -------------------------------- | ------------------ |
+| `setFrameShape(QFrame::Shape)`   | 框架外形           |
+| `setFrameShadow(QFrame::Shadow)` | 阴影风格           |
+| `frameRect()`                    | 框架矩形           |
+| `setFrameStyle(int)`             | 同时设置外形与阴影 |
 
 #### 用法示例
 
@@ -2644,14 +2678,14 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setPlainText(QString)` | 按纯文本设置内容 |
-| `setHtml(QString)` | 按 HTML 设置内容 |
-| `setSource(QUrl)` | 加载指定 URL |
-| `backward()` / `forward()` / `home()` | 后退/前进/主页 |
-| `anchorClicked(QUrl)` | 链接被点击信号 |
-| `setOpenExternalLinks(bool)` | 是否用外部浏览器打开链接 |
+| 成员                                  | 说明                     |
+| ------------------------------------- | ------------------------ |
+| `setPlainText(QString)`               | 按纯文本设置内容         |
+| `setHtml(QString)`                    | 按 HTML 设置内容         |
+| `setSource(QUrl)`                     | 加载指定 URL             |
+| `backward()` / `forward()` / `home()` | 后退/前进/主页           |
+| `anchorClicked(QUrl)`                 | 链接被点击信号           |
+| `setOpenExternalLinks(bool)`          | 是否用外部浏览器打开链接 |
 
 #### 用法示例
 
@@ -2780,14 +2814,14 @@ void MainWindow::openActionTriggered()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setScene(QGraphicsScene*)` | 设置场景 |
-| `scene()` | 取得场景 |
-| `QGraphicsScene::addPixmap(QPixmap)` | 向场景添加图元 |
-| `QGraphicsScene::clear()` | 清空场景 |
-| `QPixmap::scaled(w, h, aspectMode, transformMode)` | 缩放图像 |
-| `setRenderHint(...)` | 渲染质量提示 |
+| 成员                                               | 说明           |
+| -------------------------------------------------- | -------------- |
+| `setScene(QGraphicsScene*)`                        | 设置场景       |
+| `scene()`                                          | 取得场景       |
+| `QGraphicsScene::addPixmap(QPixmap)`               | 向场景添加图元 |
+| `QGraphicsScene::clear()`                          | 清空场景       |
+| `QPixmap::scaled(w, h, aspectMode, transformMode)` | 缩放图像       |
+| `setRenderHint(...)`                               | 渲染质量提示   |
 
 #### 用法示例
 
@@ -2945,14 +2979,14 @@ mindmap
 
 ### 5.2 关键规律
 
-| 规律 | 说明 |
-| ---- | ---- |
-| 位置与大小 | 所有 `QWidget` 子类都可用 `setGeometry(x, y, w, h)` |
-| 信号槽写法 | 例程统一使用 `connect(obj, SIGNAL(...), this, SLOT(...))` |
-| 容器 | `QWidget` 可作容器;`setCentralWidget()` 把控件设为主窗口中央部件 |
-| 布局 | 例程大量使用 `setGeometry` 绝对定位,下一篇将学习布局管理器 |
-| 样式表 | `setStyleSheet()` 可实现换肤、充电条、开关等美化效果 |
-| 资源路径 | 引用 qrc 资源格式为 `:/前缀/文件路径`,如 `:/images/battery.png` |
+| 规律       | 说明                                                              |
+| ---------- | ----------------------------------------------------------------- |
+| 位置与大小 | 所有 `QWidget` 子类都可用 `setGeometry(x, y, w, h)`               |
+| 信号槽写法 | 例程统一使用 `connect(obj, SIGNAL(...), this, SLOT(...))`         |
+| 容器       | `QWidget` 可作容器;`setCentralWidget()` 把控件设为主窗口中央部件 |
+| 布局       | 例程大量使用 `setGeometry` 绝对定位,下一篇将学习布局管理器       |
+| 样式表     | `setStyleSheet()` 可实现换肤、充电条、开关等美化效果              |
+| 资源路径   | 引用 qrc 资源格式为 `:/前缀/文件路径`,如 `:/images/battery.png`  |
 
 ---
 

+ 257 - 229
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/05-布局容器与模型视图.md

@@ -1,6 +1,34 @@
 ---
 title: 布局容器与模型视图
-tags: [嵌入式Linux, Qt, Qt Widgets, 布局管理, QBoxLayout, QGridLayout, QFormLayout, QSpacerItem, QGroupBox, QScrollArea, QToolBox, QTabWidget, QStackedWidget, QWidget, QMdiArea, QDockWidget, QListView, QTreeView, QTableView, QColumnView, QUndoView, QListWidget, QTreeWidget, QTableWidget, 模型视图, I.MX6U]
+tags:
+  [
+    嵌入式Linux,
+    Qt,
+    Qt Widgets,
+    布局管理,
+    QBoxLayout,
+    QGridLayout,
+    QFormLayout,
+    QSpacerItem,
+    QGroupBox,
+    QScrollArea,
+    QToolBox,
+    QTabWidget,
+    QStackedWidget,
+    QWidget,
+    QMdiArea,
+    QDockWidget,
+    QListView,
+    QTreeView,
+    QTableView,
+    QColumnView,
+    QUndoView,
+    QListWidget,
+    QTreeWidget,
+    QTableWidget,
+    模型视图,
+    I.MX6U,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《【正点原子】I.MX6U嵌入式Qt开发指南V1.2》第七章 Qt控件 7.5 布局管理、7.6 空间间隔、7.7 容器、7.8 项目视图组(基于模型)、7.9 项目控件组(基于项);例程 Embedded-Qt-Tutorial/Qt/01/29_qboxlayout~47_qtablewidget"
@@ -50,37 +78,37 @@ classDiagram
 
 各布局作用:
 
-| 布局 | 作用 |
-| ---- | ---- |
-| `QBoxLayout` | 水平或垂直排列子部件 |
-| `QGridLayout` | 网格(二维)方式管理界面组件 |
-| `QFormLayout` | 以两列形式布局输入部件与其标签 |
-| `QStackedLayout` | 一组布局后的部件,可分布显示 |
+| 布局             | 作用                           |
+| ---------------- | ------------------------------ |
+| `QBoxLayout`     | 水平或垂直排列子部件           |
+| `QGridLayout`    | 网格(二维)方式管理界面组件   |
+| `QFormLayout`    | 以两列形式布局输入部件与其标签 |
+| `QStackedLayout` | 一组布局后的部件,可分布显示   |
 
 ### 0.2 容器与模型/视图索引
 
-| 分组 | 控件 | 一句话作用 | 例程 |
-| ---- | ---- | ---------- | ---- |
-| 布局 | `QBoxLayout`(`QHBoxLayout`/`QVBoxLayout`) | 水平/垂直布局 | 29 |
-| 布局 | `QGridLayout` | 网格布局 | 30 |
-| 布局 | `QFormLayout` | 表单布局 | 31 |
-| 间隔 | `QSpacerItem` | 布局中的空白间隔 | 32 |
-| 容器 | `QGroupBox` | 带标题的组框 | 33 |
-| 容器 | `QScrollArea` | 滚动区域 | 34 |
-| 容器 | `QToolBox` | 列状层叠工具箱(抽屉) | 35 |
-| 容器 | `QTabWidget` | 选项卡多页面 | 36 |
-| 容器 | `QStackedWidget` | 堆叠小部件,一次显示一个 | 37 |
-| 容器 | `QWidget` | 所有 UI 对象的基类,可作容器 | 无独立例程 |
-| 容器 | `QMdiArea` | 多文档界面(MDI)区域 | 38 |
-| 容器 | `QDockWidget` | 可停靠窗体部件 | 39 |
-| 模型视图 | `QListView` | 基于模型的列表/图标视图 | 40 |
-| 模型视图 | `QTreeView` | 基于模型的树视图 | 41 |
-| 模型视图 | `QTableView` | 基于模型的表视图 | 42 |
-| 模型视图 | `QColumnView` | 级联列表视图 | 43 |
-| 模型视图 | `QUndoView` | 撤销堆栈视图 | 44 |
-| 项目控件 | `QListWidget` | 基于项的列表 | 45 |
-| 项目控件 | `QTreeWidget` | 基于项的树 | 46 |
-| 项目控件 | `QTableWidget` | 基于项的表 | 47 |
+| 分组     | 控件                                        | 一句话作用                   | 例程       |
+| -------- | ------------------------------------------- | ---------------------------- | ---------- |
+| 布局     | `QBoxLayout`(`QHBoxLayout`/`QVBoxLayout`) | 水平/垂直布局                | 29         |
+| 布局     | `QGridLayout`                               | 网格布局                     | 30         |
+| 布局     | `QFormLayout`                               | 表单布局                     | 31         |
+| 间隔     | `QSpacerItem`                               | 布局中的空白间隔             | 32         |
+| 容器     | `QGroupBox`                                 | 带标题的组框                 | 33         |
+| 容器     | `QScrollArea`                               | 滚动区域                     | 34         |
+| 容器     | `QToolBox`                                  | 列状层叠工具箱(抽屉)       | 35         |
+| 容器     | `QTabWidget`                                | 选项卡多页面                 | 36         |
+| 容器     | `QStackedWidget`                            | 堆叠小部件,一次显示一个     | 37         |
+| 容器     | `QWidget`                                   | 所有 UI 对象的基类,可作容器 | 无独立例程 |
+| 容器     | `QMdiArea`                                  | 多文档界面(MDI)区域        | 38         |
+| 容器     | `QDockWidget`                               | 可停靠窗体部件               | 39         |
+| 模型视图 | `QListView`                                 | 基于模型的列表/图标视图      | 40         |
+| 模型视图 | `QTreeView`                                 | 基于模型的树视图             | 41         |
+| 模型视图 | `QTableView`                                | 基于模型的表视图             | 42         |
+| 模型视图 | `QColumnView`                               | 级联列表视图                 | 43         |
+| 模型视图 | `QUndoView`                                 | 撤销堆栈视图                 | 44         |
+| 项目控件 | `QListWidget`                               | 基于项的列表                 | 45         |
+| 项目控件 | `QTreeWidget`                               | 基于项的树                   | 46         |
+| 项目控件 | `QTableWidget`                              | 基于项的表                   | 47         |
 
 ### 0.3 模型/视图与基于项的区别
 
@@ -89,12 +117,12 @@ classDiagram
 
 Qt 提供的常用现成模型:
 
-| 模型 | 用途 |
-| ---- | ---- |
-| `QStringListModel` | 存储简单的 `QString` 列表 |
-| `QStandardItemModel` | 管理复杂的树型结构数据项,每项可包含任意数据 |
-| `QDirModel` | 提供本地文件系统的文件与目录信息 |
-| `QSqlQueryModel` / `QSqlTableModel` / `QSqlRelationTableModel` | 访问数据库 |
+| 模型                                                           | 用途                                         |
+| -------------------------------------------------------------- | -------------------------------------------- |
+| `QStringListModel`                                             | 存储简单的 `QString` 列表                    |
+| `QStandardItemModel`                                           | 管理复杂的树型结构数据项,每项可包含任意数据 |
+| `QDirModel`                                                    | 提供本地文件系统的文件与目录信息             |
+| `QSqlQueryModel` / `QSqlTableModel` / `QSqlRelationTableModel` | 访问数据库                                   |
 
 ---
 
@@ -110,17 +138,17 @@ Qt 提供的常用现成模型:
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `QHBoxLayout()` / `QVBoxLayout()` | 构造水平/垂直布局 |
-| `addWidget(QWidget*)` | 添加部件 |
-| `addLayout(QLayout*)` | 嵌套添加子布局 |
-| `addStretch(int)` | 添加伸缩量 |
-| `addSpacerItem(QSpacerItem*)` | 添加间隔 |
-| `setSpacing(int)` | 设置部件间距 |
-| `setMargin(int)` / `setContentsMargins(...)` | 设置外框留白 |
-| `setDirection(Direction)` | 设置排列方向 |
-| `setStretch(int, int)` | 设置某部件的伸缩因子 |
+| 成员                                         | 说明                 |
+| -------------------------------------------- | -------------------- |
+| `QHBoxLayout()` / `QVBoxLayout()`            | 构造水平/垂直布局    |
+| `addWidget(QWidget*)`                        | 添加部件             |
+| `addLayout(QLayout*)`                        | 嵌套添加子布局       |
+| `addStretch(int)`                            | 添加伸缩量           |
+| `addSpacerItem(QSpacerItem*)`                | 添加间隔             |
+| `setSpacing(int)`                            | 设置部件间距         |
+| `setMargin(int)` / `setContentsMargins(...)` | 设置外框留白         |
+| `setDirection(Direction)`                    | 设置排列方向         |
+| `setStretch(int, int)`                       | 设置某部件的伸缩因子 |
 
 #### 用法示例
 
@@ -236,14 +264,14 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `addWidget(QWidget*, int row, int col)` | 把部件放入指定单元格 |
-| `addWidget(QWidget*, row, col, rowSpan, colSpan)` | 跨行/跨列 |
-| `setRowStretch(int row, int stretch)` | 行比例系数 |
-| `setColumnStretch(int col, int stretch)` | 列比例系数 |
-| `setSpacing(int)` | 间距 |
-| `setMargin(int)` | 外框留白 |
+| 成员                                              | 说明                 |
+| ------------------------------------------------- | -------------------- |
+| `addWidget(QWidget*, int row, int col)`           | 把部件放入指定单元格 |
+| `addWidget(QWidget*, row, col, rowSpan, colSpan)` | 跨行/跨列            |
+| `setRowStretch(int row, int stretch)`             | 行比例系数           |
+| `setColumnStretch(int col, int stretch)`          | 列比例系数           |
+| `setSpacing(int)`                                 | 间距                 |
+| `setMargin(int)`                                  | 外框留白             |
 
 #### 用法示例
 
@@ -360,15 +388,15 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
+| 成员                                               | 说明                          |
+| -------------------------------------------------- | ----------------------------- |
 | `addRow(const QString &labelText, QWidget *field)` | 添加一行:标签文本 + 字段部件 |
-| `addRow(QWidget *label, QWidget *field)` | 用部件作标签 |
-| `setSpacing(int)` | 水平/垂直间距 |
-| `setMargin(int)` | 布局外框宽度 |
-| `setRowWrapPolicy(RowWrapPolicy)` | 换行策略 |
-| `setLabelAlignment(Qt::Alignment)` | 标签对齐 |
-| `setFormAlignment(Qt::Alignment)` | 表单对齐 |
+| `addRow(QWidget *label, QWidget *field)`           | 用部件作标签                  |
+| `setSpacing(int)`                                  | 水平/垂直间距                 |
+| `setMargin(int)`                                   | 布局外框宽度                  |
+| `setRowWrapPolicy(RowWrapPolicy)`                  | 换行策略                      |
+| `setLabelAlignment(Qt::Alignment)`                 | 标签对齐                      |
+| `setFormAlignment(Qt::Alignment)`                  | 表单对齐                      |
 
 #### 用法示例
 
@@ -462,12 +490,12 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `QSpacerItem(int w, int h, QSizePolicy::Policy hPolicy, QSizePolicy::Policy vPolicy)` | 构造间隔 |
-| `changeSize(int w, int h, ...)` | 改变尺寸策略 |
-| `QLayout::addSpacerItem(QSpacerItem*)` | 把间隔加入布局 |
-| `QLayout::addStretch(int)` | 直接加伸缩量(更简便) |
+| 成员                                                                                  | 说明                   |
+| ------------------------------------------------------------------------------------- | ---------------------- |
+| `QSpacerItem(int w, int h, QSizePolicy::Policy hPolicy, QSizePolicy::Policy vPolicy)` | 构造间隔               |
+| `changeSize(int w, int h, ...)`                                                       | 改变尺寸策略           |
+| `QLayout::addSpacerItem(QSpacerItem*)`                                                | 把间隔加入布局         |
+| `QLayout::addStretch(int)`                                                            | 直接加伸缩量(更简便) |
 
 #### 用法示例
 
@@ -601,14 +629,14 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
+| 成员                                               | 说明                |
+| -------------------------------------------------- | ------------------- |
 | `QGroupBox(const QString &title, QWidget *parent)` | 构造:标题 + 父对象 |
-| `setTitle(QString)` / `title()` | 标题 |
-| `setCheckable(bool)` | 是否带复选框 |
-| `setChecked(bool)` | 复选状态 |
-| `setLayout(QLayout*)` | 设置内部布局 |
-| `toggled(bool)` | 复选状态变化信号 |
+| `setTitle(QString)` / `title()`                    | 标题                |
+| `setCheckable(bool)`                               | 是否带复选框        |
+| `setChecked(bool)`                                 | 复选状态            |
+| `setLayout(QLayout*)`                              | 设置内部布局        |
+| `toggled(bool)`                                    | 复选状态变化信号    |
 
 #### 用法示例
 
@@ -693,13 +721,13 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setWidget(QWidget*)` | 设置要显示的子部件 |
-| `widget()` | 取得子部件 |
-| `setWidgetResizable(bool)` | 是否随区域自动调整大小 |
-| `setHorizontalScrollBarPolicy(Qt::ScrollBarPolicy)` | 水平滚动条策略 |
-| `setVerticalScrollBarPolicy(Qt::ScrollBarPolicy)` | 垂直滚动条策略 |
+| 成员                                                | 说明                   |
+| --------------------------------------------------- | ---------------------- |
+| `setWidget(QWidget*)`                               | 设置要显示的子部件     |
+| `widget()`                                          | 取得子部件             |
+| `setWidgetResizable(bool)`                          | 是否随区域自动调整大小 |
+| `setHorizontalScrollBarPolicy(Qt::ScrollBarPolicy)` | 水平滚动条策略         |
+| `setVerticalScrollBarPolicy(Qt::ScrollBarPolicy)`   | 垂直滚动条策略         |
 
 #### 用法示例
 
@@ -773,14 +801,14 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `addItem(QWidget *widget, const QString &text)` | 添加页 |
-| `insertItem(int index, QWidget*, QString)` | 插入页 |
-| `removeItem(int index)` | 移除页 |
-| `count()` | 页数 |
-| `setCurrentIndex(int)` / `currentIndex()` | 当前页 |
-| `currentChanged(int)` | 当前页变化信号 |
+| 成员                                            | 说明           |
+| ----------------------------------------------- | -------------- |
+| `addItem(QWidget *widget, const QString &text)` | 添加页         |
+| `insertItem(int index, QWidget*, QString)`      | 插入页         |
+| `removeItem(int index)`                         | 移除页         |
+| `count()`                                       | 页数           |
+| `setCurrentIndex(int)` / `currentIndex()`       | 当前页         |
+| `currentChanged(int)`                           | 当前页变化信号 |
 
 #### 用法示例
 
@@ -897,17 +925,17 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `addTab(QWidget *page, const QString &label)` | 添加页面 |
-| `addTab(QWidget *page, QIcon, QString)` | 带图标添加 |
-| `insertTab(int, QWidget*, QIcon, QString)` | 插入页面 |
-| `removeTab(int index)` | 移除页面 |
-| `setCurrentIndex(int)` / `currentIndex()` | 当前页 |
-| `setTabsClosable(bool)` | 是否显示关闭按钮 |
-| `setTabPosition(QTabWidget::TabPosition)` | 页签位置 |
-| `currentChanged(int)` | 当前页变化信号 |
-| `tabCloseRequested(int)` | 页签关闭请求信号 |
+| 成员                                          | 说明             |
+| --------------------------------------------- | ---------------- |
+| `addTab(QWidget *page, const QString &label)` | 添加页面         |
+| `addTab(QWidget *page, QIcon, QString)`       | 带图标添加       |
+| `insertTab(int, QWidget*, QIcon, QString)`    | 插入页面         |
+| `removeTab(int index)`                        | 移除页面         |
+| `setCurrentIndex(int)` / `currentIndex()`     | 当前页           |
+| `setTabsClosable(bool)`                       | 是否显示关闭按钮 |
+| `setTabPosition(QTabWidget::TabPosition)`     | 页签位置         |
+| `currentChanged(int)`                         | 当前页变化信号   |
+| `tabCloseRequested(int)`                      | 页签关闭请求信号 |
 
 #### 用法示例
 
@@ -1013,14 +1041,14 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `addWidget(QWidget*)` | 添加页面 |
-| `insertWidget(int, QWidget*)` | 插入页面 |
-| `removeWidget(QWidget*)` | 移除页面 |
+| 成员                                      | 说明                                    |
+| ----------------------------------------- | --------------------------------------- |
+| `addWidget(QWidget*)`                     | 添加页面                                |
+| `insertWidget(int, QWidget*)`             | 插入页面                                |
+| `removeWidget(QWidget*)`                  | 移除页面                                |
 | `setCurrentIndex(int)` / `currentIndex()` | 当前页(对应槽 `setCurrentIndex(int)`) |
-| `count()` | 页面数 |
-| `currentChanged(int)` | 当前页变化信号 |
+| `count()`                                 | 页面数                                  |
+| `currentChanged(int)`                     | 当前页变化信号                          |
 
 #### 用法示例
 
@@ -1150,16 +1178,16 @@ Z-order 指沿 z 轴(垂直于屏幕)的堆叠顺序。GUI 窗口可能重
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setLayout(QLayout*)` | 设置布局 |
-| `setGeometry(x, y, w, h)` | 位置与大小 |
-| `show()` / `hide()` / `close()` | 显示/隐藏/关闭 |
-| `setStyleSheet(QString)` | 样式表 |
-| `setWindowTitle(QString)` | 窗口标题 |
-| `setAttribute(Qt::WidgetAttribute)` | 设置属性(如 `WA_DeleteOnClose`) |
-| `setSizePolicy(QSizePolicy::Policy h, v)` | 尺寸策略 |
-| `update()` | 请求重绘 |
+| 成员                                      | 说明                              |
+| ----------------------------------------- | --------------------------------- |
+| `setLayout(QLayout*)`                     | 设置布局                          |
+| `setGeometry(x, y, w, h)`                 | 位置与大小                        |
+| `show()` / `hide()` / `close()`           | 显示/隐藏/关闭                    |
+| `setStyleSheet(QString)`                  | 样式表                            |
+| `setWindowTitle(QString)`                 | 窗口标题                          |
+| `setAttribute(Qt::WidgetAttribute)`       | 设置属性(如 `WA_DeleteOnClose`) |
+| `setSizePolicy(QSizePolicy::Policy h, v)` | 尺寸策略                          |
+| `update()`                                | 请求重绘                          |
 
 #### 用法示例
 
@@ -1193,16 +1221,16 @@ this->setCentralWidget(container);
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `addSubWindow(QWidget *widget)` | 添加子窗口 |
-| `removeSubWindow(QWidget*)` | 移除子窗口 |
-| `setActiveSubWindow(QMdiSubWindow*)` | 激活子窗口 |
-| `activeSubWindow()` | 当前活动子窗口 |
-| `subWindowList()` | 子窗口列表 |
-| `cascadeSubWindows()` | 级联排列 |
-| `tileSubWindows()` | 平铺排列 |
-| `closeAllSubWindows()` | 关闭全部子窗口 |
+| 成员                                 | 说明           |
+| ------------------------------------ | -------------- |
+| `addSubWindow(QWidget *widget)`      | 添加子窗口     |
+| `removeSubWindow(QWidget*)`          | 移除子窗口     |
+| `setActiveSubWindow(QMdiSubWindow*)` | 激活子窗口     |
+| `activeSubWindow()`                  | 当前活动子窗口 |
+| `subWindowList()`                    | 子窗口列表     |
+| `cascadeSubWindows()`                | 级联排列       |
+| `tileSubWindows()`                   | 平铺排列       |
+| `closeAllSubWindows()`               | 关闭全部子窗口 |
 
 #### 用法示例
 
@@ -1305,15 +1333,15 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `QDockWidget(const QString &title, QWidget *parent)` | 构造:标题 + 父对象 |
-| `setWidget(QWidget*)` | 设置内部部件 |
-| `QMainWindow::addDockWidget(area, dock)` | 添加到主窗口指定停靠区 |
-| `setAllowedAreas(Qt::DockWidgetAreas)` | 允许停靠的区域 |
-| `setFloating(bool)` | 是否浮动 |
-| `setFeatures(Features)` | 可移动/可关闭/可浮动等特性 |
-| `visibilityChanged(bool)` | 可见性变化信号 |
+| 成员                                                 | 说明                       |
+| ---------------------------------------------------- | -------------------------- |
+| `QDockWidget(const QString &title, QWidget *parent)` | 构造:标题 + 父对象        |
+| `setWidget(QWidget*)`                                | 设置内部部件               |
+| `QMainWindow::addDockWidget(area, dock)`             | 添加到主窗口指定停靠区     |
+| `setAllowedAreas(Qt::DockWidgetAreas)`               | 允许停靠的区域             |
+| `setFloating(bool)`                                  | 是否浮动                   |
+| `setFeatures(Features)`                              | 可移动/可关闭/可浮动等特性 |
+| `visibilityChanged(bool)`                            | 可见性变化信号             |
 
 #### 用法示例
 
@@ -1396,14 +1424,14 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setModel(QAbstractItemModel*)` | 设置模型 |
-| `setViewMode(QListView::ViewMode)` | `ListMode` / `IconMode` |
-| `setDragEnabled(bool)` | 是否可拖动 |
-| `setSelectionMode(QAbstractItemView::SelectionMode)` | 选择模式 |
-| `setModelColumn(int)` | 显示模型哪一列 |
-| `clicked(QModelIndex)` / `doubleClicked(QModelIndex)` | 点击信号 |
+| 成员                                                  | 说明                    |
+| ----------------------------------------------------- | ----------------------- |
+| `setModel(QAbstractItemModel*)`                       | 设置模型                |
+| `setViewMode(QListView::ViewMode)`                    | `ListMode` / `IconMode` |
+| `setDragEnabled(bool)`                                | 是否可拖动              |
+| `setSelectionMode(QAbstractItemView::SelectionMode)`  | 选择模式                |
+| `setModelColumn(int)`                                 | 显示模型哪一列          |
+| `clicked(QModelIndex)` / `doubleClicked(QModelIndex)` | 点击信号                |
 
 #### 用法示例
 
@@ -1486,15 +1514,15 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setModel(QAbstractItemModel*)` | 设置模型 |
-| `setHeaderHidden(bool)` | 是否隐藏表头 |
-| `expandAll()` / `collapseAll()` | 展开/折叠全部 |
-| `setColumnWidth(int, int)` | 列宽 |
-| `QStandardItemModel::appendRow(...)` | 追加行 |
-| `QStandardItem::appendRow(...)` | 给某项追加子行 |
-| `clicked(QModelIndex)` / `expanded(QModelIndex)` | 信号 |
+| 成员                                             | 说明           |
+| ------------------------------------------------ | -------------- |
+| `setModel(QAbstractItemModel*)`                  | 设置模型       |
+| `setHeaderHidden(bool)`                          | 是否隐藏表头   |
+| `expandAll()` / `collapseAll()`                  | 展开/折叠全部  |
+| `setColumnWidth(int, int)`                       | 列宽           |
+| `QStandardItemModel::appendRow(...)`             | 追加行         |
+| `QStandardItem::appendRow(...)`                  | 给某项追加子行 |
+| `clicked(QModelIndex)` / `expanded(QModelIndex)` | 信号           |
 
 #### 用法示例
 
@@ -1599,15 +1627,15 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setModel(QAbstractItemModel*)` | 设置模型 |
-| `setShowGrid(bool)` | 显示网格线 |
-| `horizontalHeader()` / `verticalHeader()` | 水平/垂直表头 |
-| `QHeaderView::setSectionResizeMode(mode)` | 表头缩放模式(`Stretch` 平均分) |
-| `setSelectionBehavior(...)` | 选择行为 |
-| `setEditTriggers(...)` | 编辑触发方式 |
-| `QStandardItemModel::setItem(row, col, item)` | 设置单元格 |
+| 成员                                          | 说明                             |
+| --------------------------------------------- | -------------------------------- |
+| `setModel(QAbstractItemModel*)`               | 设置模型                         |
+| `setShowGrid(bool)`                           | 显示网格线                       |
+| `horizontalHeader()` / `verticalHeader()`     | 水平/垂直表头                    |
+| `QHeaderView::setSectionResizeMode(mode)`     | 表头缩放模式(`Stretch` 平均分) |
+| `setSelectionBehavior(...)`                   | 选择行为                         |
+| `setEditTriggers(...)`                        | 编辑触发方式                     |
+| `QStandardItemModel::setItem(row, col, item)` | 设置单元格                       |
 
 #### 用法示例
 
@@ -1699,12 +1727,12 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setModel(QAbstractItemModel*)` | 设置模型 |
-| `setColumnWidths(QList<int>)` | 各列宽度 |
-| `updatePreviewWidget(QModelIndex)` | 更新预览部件 |
-| `setResizeGripsVisible(bool)` | 是否显示拖动柄 |
+| 成员                               | 说明           |
+| ---------------------------------- | -------------- |
+| `setModel(QAbstractItemModel*)`    | 设置模型       |
+| `setColumnWidths(QList<int>)`      | 各列宽度       |
+| `updatePreviewWidget(QModelIndex)` | 更新预览部件   |
+| `setResizeGripsVisible(bool)`      | 是否显示拖动柄 |
 
 #### 用法示例
 
@@ -1795,16 +1823,16 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `QUndoView(QUndoStack *stack, QWidget *parent)` | 用撤销栈构造 |
-| `setStack(QUndoStack*)` / `setGroup(QUndoGroup*)` | 设置栈/组 |
-| `setEmptyLabel(QString)` | 空栈时显示文本 |
-| `QUndoStack::push(QUndoCommand*)` | 入栈(会自动调用命令的 `redo()`) |
-| `QUndoStack::undo()` / `redo()` | 撤销/重做 |
-| `QUndoStack::createUndoAction()` / `createRedoAction()` | 生成撤销/重做动作 |
-| `QUndoStack::indexChanged(int)` | 索引变化信号 |
-| `QUndoCommand::redo()` / `undo()` / `setText(QString)` | 重写/设置命令描述 |
+| 成员                                                    | 说明                              |
+| ------------------------------------------------------- | --------------------------------- |
+| `QUndoView(QUndoStack *stack, QWidget *parent)`         | 用撤销栈构造                      |
+| `setStack(QUndoStack*)` / `setGroup(QUndoGroup*)`       | 设置栈/组                         |
+| `setEmptyLabel(QString)`                                | 空栈时显示文本                    |
+| `QUndoStack::push(QUndoCommand*)`                       | 入栈(会自动调用命令的 `redo()`) |
+| `QUndoStack::undo()` / `redo()`                         | 撤销/重做                         |
+| `QUndoStack::createUndoAction()` / `createRedoAction()` | 生成撤销/重做动作                 |
+| `QUndoStack::indexChanged(int)`                         | 索引变化信号                      |
+| `QUndoCommand::redo()` / `undo()` / `setText(QString)`  | 重写/设置命令描述                 |
 
 #### 用法示例
 
@@ -2064,16 +2092,16 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `addItem(const QString&)` / `addItem(QListWidgetItem*)` | 添加项 |
-| `insertItem(int row, const QString&)` | 插入项 |
-| `takeItem(int row)` | 移除并返回项 |
-| `item(int row)` | 取项 |
-| `count()` | 项数 |
-| `clear()` | 清空 |
-| `currentRowChanged(int)` | 当前行变化信号 |
-| `itemClicked(QListWidgetItem*)` / `itemDoubleClicked(...)` | 点击/双击信号 |
+| 成员                                                       | 说明           |
+| ---------------------------------------------------------- | -------------- |
+| `addItem(const QString&)` / `addItem(QListWidgetItem*)`    | 添加项         |
+| `insertItem(int row, const QString&)`                      | 插入项         |
+| `takeItem(int row)`                                        | 移除并返回项   |
+| `item(int row)`                                            | 取项           |
+| `count()`                                                  | 项数           |
+| `clear()`                                                  | 清空           |
+| `currentRowChanged(int)`                                   | 当前行变化信号 |
+| `itemClicked(QListWidgetItem*)` / `itemDoubleClicked(...)` | 点击/双击信号  |
 
 #### 用法示例
 
@@ -2174,17 +2202,17 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `addTopLevelItem(QTreeWidgetItem*)` | 添加顶层项 |
-| `QTreeWidgetItem(QTreeWidget*)` / `QTreeWidgetItem(QTreeWidgetItem*)` | 构造顶层项/子项 |
-| `setText(int col, QString)` / `text(int)` | 设置/读取文本 |
-| `setCheckState(int col, Qt::CheckState)` / `checkState(int)` | 设置/读取勾选状态 |
-| `setFlags(Qt::ItemFlags)` | 项属性(可勾选、可用、可选) |
-| `childCount()` / `child(int)` | 子项数量/子项 |
-| `parent()` | 父项 |
-| `itemChanged(QTreeWidgetItem*, int)` | 项变化信号 |
-| `clear()` | 清空 |
+| 成员                                                                  | 说明                         |
+| --------------------------------------------------------------------- | ---------------------------- |
+| `addTopLevelItem(QTreeWidgetItem*)`                                   | 添加顶层项                   |
+| `QTreeWidgetItem(QTreeWidget*)` / `QTreeWidgetItem(QTreeWidgetItem*)` | 构造顶层项/子项              |
+| `setText(int col, QString)` / `text(int)`                             | 设置/读取文本                |
+| `setCheckState(int col, Qt::CheckState)` / `checkState(int)`          | 设置/读取勾选状态            |
+| `setFlags(Qt::ItemFlags)`                                             | 项属性(可勾选、可用、可选) |
+| `childCount()` / `child(int)`                                         | 子项数量/子项                |
+| `parent()`                                                            | 父项                         |
+| `itemChanged(QTreeWidgetItem*, int)`                                  | 项变化信号                   |
+| `clear()`                                                             | 清空                         |
 
 #### 用法示例
 
@@ -2366,16 +2394,16 @@ MainWindow::~MainWindow()
 
 #### 常用属性与信号
 
-| 成员 | 说明 |
-| ---- | ---- |
-| `setColumnCount(int)` / `setRowCount(int)` | 设置列数/行数 |
-| `setHorizontalHeaderLabels(QStringList)` | 设置水平表头 |
-| `setVerticalHeaderLabels(QStringList)` | 设置垂直表头 |
-| `setItem(int row, int col, QTableWidgetItem*)` | 设置单元格 |
-| `item(int row, int col)` | 取单元格 |
-| `QTableWidgetItem(const QString&)` | 构造项 |
-| `setTextAlignment(Qt::Alignment)` | 文本对齐 |
-| `cellClicked(int, int)` / `cellChanged(int, int)` | 单元格信号 |
+| 成员                                              | 说明          |
+| ------------------------------------------------- | ------------- |
+| `setColumnCount(int)` / `setRowCount(int)`        | 设置列数/行数 |
+| `setHorizontalHeaderLabels(QStringList)`          | 设置水平表头  |
+| `setVerticalHeaderLabels(QStringList)`            | 设置垂直表头  |
+| `setItem(int row, int col, QTableWidgetItem*)`    | 设置单元格    |
+| `item(int row, int col)`                          | 取单元格      |
+| `QTableWidgetItem(const QString&)`                | 构造项        |
+| `setTextAlignment(Qt::Alignment)`                 | 文本对齐      |
+| `cellClicked(int, int)` / `cellChanged(int, int)` | 单元格信号    |
 
 #### 用法示例
 
@@ -2466,18 +2494,18 @@ MainWindow::~MainWindow()
 
 ### 6.1 布局选择速查
 
-| 需求 | 首选 |
-| ---- | ---- |
-| 一行/一列排列 | `QHBoxLayout` / `QVBoxLayout` |
-| 多行多列、需要行列比例 | `QGridLayout` |
-| 少量“标签 + 输入框”表单 | `QFormLayout` |
-| 布局中留空白/顶开 | `QSpacerItem` 或 `addStretch()` |
-| 页面切换(页签) | `QTabWidget` |
-| 页面切换(列表联动) | `QListWidget` + `QStackedWidget` |
-| 折叠面板 | `QToolBox` |
-| 多窗口工作区 | `QMdiArea` |
-| 可拖动工具面板 | `QDockWidget` |
-| 大内容滚动查看 | `QScrollArea` |
+| 需求                    | 首选                             |
+| ----------------------- | -------------------------------- |
+| 一行/一列排列           | `QHBoxLayout` / `QVBoxLayout`    |
+| 多行多列、需要行列比例  | `QGridLayout`                    |
+| 少量“标签 + 输入框”表单 | `QFormLayout`                    |
+| 布局中留空白/顶开       | `QSpacerItem` 或 `addStretch()`  |
+| 页面切换(页签)        | `QTabWidget`                     |
+| 页面切换(列表联动)    | `QListWidget` + `QStackedWidget` |
+| 折叠面板                | `QToolBox`                       |
+| 多窗口工作区            | `QMdiArea`                       |
+| 可拖动工具面板          | `QDockWidget`                    |
+| 大内容滚动查看          | `QScrollArea`                    |
 
 ### 6.2 基于模型 vs 基于项
 
@@ -2497,13 +2525,13 @@ flowchart LR
     I -->|"数据量小、开发快"| V
 ```
 
-| 对比项 | 基于模型(View) | 基于项(Widget) |
-| ------ | ---------------- | ---------------- |
-| 代表类 | `QListView`/`QTreeView`/`QTableView` | `QListWidget`/`QTreeWidget`/`QTableWidget` |
-| 数据 | 需自建 Model | 自带内部模型 |
-| 数据冗余 | 少 | 相对多 |
-| 效率 | 高(适合大数据) | 适合中小数据 |
-| 操作难度 | 需了解建模 | `addItem`/`setItem` 即可 |
+| 对比项   | 基于模型(View)                     | 基于项(Widget)                           |
+| -------- | ------------------------------------ | ------------------------------------------ |
+| 代表类   | `QListView`/`QTreeView`/`QTableView` | `QListWidget`/`QTreeWidget`/`QTableWidget` |
+| 数据     | 需自建 Model                         | 自带内部模型                               |
+| 数据冗余 | 少                                   | 相对多                                     |
+| 效率     | 高(适合大数据)                     | 适合中小数据                               |
+| 操作难度 | 需了解建模                           | `addItem`/`setItem` 即可                   |
 
 ---
 

+ 189 - 172
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/06-文本绘图与图表.md

@@ -1,6 +1,23 @@
 ---
 title: 文本读写、绘图与图表
-tags: [Qt, QFile, QTextStream, QPainter, QChart, QtCharts, 文本读写, 绘图, 图表, QPen, QBrush, QColor, QIODevice, IMX6ULL, 嵌入式Qt]
+tags:
+  [
+    Qt,
+    QFile,
+    QTextStream,
+    QPainter,
+    QChart,
+    QtCharts,
+    文本读写,
+    绘图,
+    图表,
+    QPen,
+    QBrush,
+    QColor,
+    QIODevice,
+    IMX6ULL,
+    嵌入式Qt,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Qt开发指南V1.2》第八章 文本读写、第九章 绘图与图表"
@@ -31,10 +48,10 @@ pdf_ref: "《I.MX6U嵌入式Qt开发指南V1.2》第八章 文本读写、第九
 
 Qt 提供了两条主要路线:
 
-| 路线 | 核心类 | 特点 | 适用场景 |
-| ---- | ------ | ---- | -------- |
-| 直接 I/O | `QFile`(继承 `QIODevice`) | 一次读全部 / 按行读 / 直接写字节 | 小文件、整体读写、字符设备 |
-| 流式 I/O | `QTextStream` + `QFile` | 支持 `<<` `>>` 运算符、格式化、编码转换 | 需要按字段/格式化读写文本 |
+| 路线     | 核心类                      | 特点                                    | 适用场景                   |
+| -------- | --------------------------- | --------------------------------------- | -------------------------- |
+| 直接 I/O | `QFile`(继承 `QIODevice`) | 一次读全部 / 按行读 / 直接写字节        | 小文件、整体读写、字符设备 |
+| 流式 I/O | `QTextStream` + `QFile`     | 支持 `<<` `>>` 运算符、格式化、编码转换 | 需要按字段/格式化读写文本  |
 
 两者的关系是:`QFile` 负责「打开/关闭/定位/读写字节流」,`QTextStream` 负责「把字节流解释成文本并按格式处理」。`QTextStream` 不能独立存在,它必须绑定到一个 `QIODevice`(如 `QFile`)、`QByteArray` 或 `QString` 上。
 
@@ -76,14 +93,14 @@ flowchart LR
 
 `QFile::open()` 需要一个 `QIODevice::OpenModeFlag` 枚举参数,决定以什么方式打开文件:
 
-| 枚举值 | 含义 | 说明 |
-| ------ | ---- | ---- |
-| `QIODevice::ReadOnly` | 以只读方式打开 | 用于载入文件 |
-| `QIODevice::WriteOnly` | 以只写方式打开 | 用于保存文件 |
-| `QIODevice::ReadWrite` | 以读写方式打开 | 同时可读可写 |
-| `QIODevice::Append` | 以添加模式打开 | 新写入的数据追加到文件尾部 |
-| `QIODevice::Truncate` | 以截取方式打开 | 文件原有内容全部被删除 |
-| `QIODevice::Text` | 以文本方式打开 | 读取时 `\n` 自动翻译为换行符;写入时字符串结束符自动翻译为平台编码,如 Windows 下是 `\r\n` |
+| 枚举值                 | 含义           | 说明                                                                                       |
+| ---------------------- | -------------- | ------------------------------------------------------------------------------------------ |
+| `QIODevice::ReadOnly`  | 以只读方式打开 | 用于载入文件                                                                               |
+| `QIODevice::WriteOnly` | 以只写方式打开 | 用于保存文件                                                                               |
+| `QIODevice::ReadWrite` | 以读写方式打开 | 同时可读可写                                                                               |
+| `QIODevice::Append`    | 以添加模式打开 | 新写入的数据追加到文件尾部                                                                 |
+| `QIODevice::Truncate`  | 以截取方式打开 | 文件原有内容全部被删除                                                                     |
+| `QIODevice::Text`      | 以文本方式打开 | 读取时 `\n` 自动翻译为换行符;写入时字符串结束符自动翻译为平台编码,如 Windows 下是 `\r\n` |
 
 这些取值可以**按位或组合**,例如 `QIODevice::ReadOnly | QIODevice::Text` 表示以「只读 + 文本」方式打开。
 
@@ -91,25 +108,25 @@ flowchart LR
 
 ### 2.3 QFile 常用 API
 
-| API | 返回类型 | 说明 |
-| --- | -------- | ---- |
-| `QFile(const QString &name)` | — | 构造并指定文件名 |
-| `QFile(QObject *parent)` | — | 构造,父对象为 parent |
-| `setFileName(const QString &name)` | `void` | 设置文件名 |
-| `fileName() const` | `QString` | 获取文件名 |
-| `exists() const` | `bool` | 文件是否存在 |
-| `remove()` | `bool` | 删除文件 |
-| `open(OpenMode flags)` | `bool` | 按模式打开文件 |
-| `close()` | `void` | 关闭文件 |
-| `flush()` | `bool` | 刷新缓冲区 |
-| `readAll()` | `QByteArray` | 读取全部剩余内容 |
-| `read(qint64 maxSize)` | `QByteArray` | 读取最多 maxSize 字节 |
-| `readLine(qint64 maxSize = 0)` | `QByteArray` | 读取一行 |
-| `write(const QByteArray &byteArray)` | `qint64` | 写入字节数组,返回实际写入字节数 |
-| `size() const` | `qint64` | 文件大小 |
-| `pos() const` | `qint64` | 当前读写位置 |
-| `seek(qint64 pos)` | `bool` | 移动到指定位置 |
-| `atEnd() const` | `bool` | 是否到达末尾 |
+| API                                  | 返回类型     | 说明                             |
+| ------------------------------------ | ------------ | -------------------------------- |
+| `QFile(const QString &name)`         | —            | 构造并指定文件名                 |
+| `QFile(QObject *parent)`             | —            | 构造,父对象为 parent            |
+| `setFileName(const QString &name)`   | `void`       | 设置文件名                       |
+| `fileName() const`                   | `QString`    | 获取文件名                       |
+| `exists() const`                     | `bool`       | 文件是否存在                     |
+| `remove()`                           | `bool`       | 删除文件                         |
+| `open(OpenMode flags)`               | `bool`       | 按模式打开文件                   |
+| `close()`                            | `void`       | 关闭文件                         |
+| `flush()`                            | `bool`       | 刷新缓冲区                       |
+| `readAll()`                          | `QByteArray` | 读取全部剩余内容                 |
+| `read(qint64 maxSize)`               | `QByteArray` | 读取最多 maxSize 字节            |
+| `readLine(qint64 maxSize = 0)`       | `QByteArray` | 读取一行                         |
+| `write(const QByteArray &byteArray)` | `qint64`     | 写入字节数组,返回实际写入字节数 |
+| `size() const`                       | `qint64`     | 文件大小                         |
+| `pos() const`                        | `qint64`     | 当前读写位置                     |
+| `seek(qint64 pos)`                   | `bool`       | 移动到指定位置                   |
+| `atEnd() const`                      | `bool`       | 是否到达末尾                     |
 
 ### 2.4 QFile 操作流程
 
@@ -388,12 +405,12 @@ flowchart LR
 
 `QTextStream` 默认按本地编码(`QTextCodec::codecForLocale()`)读写。跨平台或处理中文时,建议显式设置编码:
 
-| API | 说明 |
-| --- | ---- |
-| `setCodec(QTextCodec *codec)` | 设置编解码器(Qt5 常见写法) |
-| `setCodec(const char *codecName)` | 用编码名设置,如 `"UTF-8"` |
-| `setAutoDetectUnicode(bool)` | 自动探测 Unicode |
-| `setGenerateByteOrderMark(bool)` | 写入时是否输出 BOM |
+| API                               | 说明                         |
+| --------------------------------- | ---------------------------- |
+| `setCodec(QTextCodec *codec)`     | 设置编解码器(Qt5 常见写法) |
+| `setCodec(const char *codecName)` | 用编码名设置,如 `"UTF-8"`   |
+| `setAutoDetectUnicode(bool)`      | 自动探测 Unicode             |
+| `setGenerateByteOrderMark(bool)`  | 写入时是否输出 BOM           |
 
 示例:
 
@@ -412,15 +429,15 @@ file.close();
 
 `QTextStream` 支持 C++ iostream 风格的格式化:
 
-| API | 说明 |
-| --- | ---- |
-| `setFieldWidth(int)` | 字段总宽度 |
-| `setFieldAlignment(FieldAlignment)` | 对齐方式:`AlignLeft` / `AlignRight` / `AlignCenter` / `AlignAccountingStyle` |
-| `setPadChar(QChar)` | 填充字符,默认空格 |
-| `setRealNumberPrecision(int)` | 浮点数有效位数 |
-| `setRealNumberNotation(RealNumberNotation)` | 浮点表示法:`SmartNotation` / `FixedNotation` / `ScientificNotation` |
-| `setIntegerBase(int)` | 整数进制:2/8/10/16 |
-| `setNumberFlags(NumberFlags)` | 如 `ShowBase`、`UppercaseBase`、`ForceSign` 等 |
+| API                                         | 说明                                                                          |
+| ------------------------------------------- | ----------------------------------------------------------------------------- |
+| `setFieldWidth(int)`                        | 字段总宽度                                                                    |
+| `setFieldAlignment(FieldAlignment)`         | 对齐方式:`AlignLeft` / `AlignRight` / `AlignCenter` / `AlignAccountingStyle` |
+| `setPadChar(QChar)`                         | 填充字符,默认空格                                                            |
+| `setRealNumberPrecision(int)`               | 浮点数有效位数                                                                |
+| `setRealNumberNotation(RealNumberNotation)` | 浮点表示法:`SmartNotation` / `FixedNotation` / `ScientificNotation`          |
+| `setIntegerBase(int)`                       | 整数进制:2/8/10/16                                                           |
+| `setNumberFlags(NumberFlags)`               | 如 `ShowBase`、`UppercaseBase`、`ForceSign` 等                                |
 
 带对齐的表格化输出示例:
 
@@ -435,17 +452,17 @@ out << "张三" << 95 << endl;
 
 `QTextStream` 常用读写 API:
 
-| API | 返回类型 | 说明 |
-| --- | -------- | ---- |
-| `readAll()` | `QString` | 读取全部剩余文本 |
-| `readLine(qint64 maxlen = 0)` | `QString` | 读取一行 |
-| `read(qint64 maxlen)` | `QString` | 读取指定长度 |
-| `atEnd() const` | `bool` | 是否到末尾 |
-| `operator<<(T value)` | `QTextStream &` | 流插入,写入 |
-| `operator>>(T &value)` | `QTextStream &` | 流提取,读取 |
-| `flush()` | `void` | 刷新 |
-| `seek(qint64 pos)` | `bool` | 定位 |
-| `pos() const` | `qint64` | 当前位置 |
+| API                           | 返回类型        | 说明             |
+| ----------------------------- | --------------- | ---------------- |
+| `readAll()`                   | `QString`       | 读取全部剩余文本 |
+| `readLine(qint64 maxlen = 0)` | `QString`       | 读取一行         |
+| `read(qint64 maxlen)`         | `QString`       | 读取指定长度     |
+| `atEnd() const`               | `bool`          | 是否到末尾       |
+| `operator<<(T value)`         | `QTextStream &` | 流插入,写入     |
+| `operator>>(T &value)`        | `QTextStream &` | 流提取,读取     |
+| `flush()`                     | `void`          | 刷新             |
+| `seek(qint64 pos)`            | `bool`          | 定位             |
+| `pos() const`                 | `qint64`        | 当前位置         |
 
 ### 3.4 完整实例 02_qtextstream:文本流读写(难度:简单)
 
@@ -554,11 +571,11 @@ file.close();
 
 Qt 里的所有绘图——包括一个按钮、一个 Label 的显示——都由**绘图系统**执行。绘图系统基于三个类:
 
-| 类 | 角色 | 说明 |
-| -- | ---- | ---- |
-| `QPainter` | 画家 / 绘图操作类 | 提供 `drawLine()`、`drawRect()`、`drawText()` 等绘图接口 |
-| `QPaintDevice` | 画布 / 绘图设备基类 | 提供绘制对象的基类,代表「能画在哪」 |
-| `QPaintEngine` | 绘图引擎 | 提供一个平台上设备的抽象定义,起「抽象平台」的作用 |
+| 类             | 角色                | 说明                                                     |
+| -------------- | ------------------- | -------------------------------------------------------- |
+| `QPainter`     | 画家 / 绘图操作类   | 提供 `drawLine()`、`drawRect()`、`drawText()` 等绘图接口 |
+| `QPaintDevice` | 画布 / 绘图设备基类 | 提供绘制对象的基类,代表「能画在哪」                     |
+| `QPaintEngine` | 绘图引擎            | 提供一个平台上设备的抽象定义,起「抽象平台」的作用       |
 
 它们的关系是:`QPainter` 是直接用来操作绘图的类;`QPaintDevice` 和 `QPaintEngine` 都比 `QPainter` 更底层,一般只需要了解即可。
 
@@ -599,21 +616,21 @@ void Widget::paintEvent(QPaintEvent *)
 
 `QPainter` 需要两个「工具」才能画出好看的东西:
 
-| 工具 | 类 | 作用 |
-| ---- | -- | ---- |
-| 画笔 | `QPen` | 决定线条(轮廓)的颜色、宽度、线型、端点样式、连接样式 |
-| 画刷 | `QBrush` | 决定封闭图形**内部填充**的颜色和样式 |
-| 颜色 | `QColor` | 被 `QPen`、`QBrush` 使用,表示颜色 |
+| 工具 | 类       | 作用                                                   |
+| ---- | -------- | ------------------------------------------------------ |
+| 画笔 | `QPen`   | 决定线条(轮廓)的颜色、宽度、线型、端点样式、连接样式 |
+| 画刷 | `QBrush` | 决定封闭图形**内部填充**的颜色和样式                   |
+| 颜色 | `QColor` | 被 `QPen`、`QBrush` 使用,表示颜色                     |
 
 #### 4.3.1 QColor
 
-| 构造 / 方法 | 说明 |
-| ----------- | ---- |
-| `QColor(Qt::GlobalColor)` | 用预定义颜色构造,如 `Qt::red`、`Qt::blue`、`Qt::gray` |
-| `QColor(int r, int g, int b, int a = 255)` | 用 RGB(A) 构造,分量 0~255 |
-| `QColor(const QString &name)` | 用颜色名构造,如 `"#RRGGBB"`、`"red"` |
-| `red()/green()/blue()/alpha()` | 取各分量 |
-| `setRgb(...)` | 设置 RGB(A) |
+| 构造 / 方法                                | 说明                                                   |
+| ------------------------------------------ | ------------------------------------------------------ |
+| `QColor(Qt::GlobalColor)`                  | 用预定义颜色构造,如 `Qt::red`、`Qt::blue`、`Qt::gray` |
+| `QColor(int r, int g, int b, int a = 255)` | 用 RGB(A) 构造,分量 0~255                             |
+| `QColor(const QString &name)`              | 用颜色名构造,如 `"#RRGGBB"`、`"red"`                  |
+| `red()/green()/blue()/alpha()`             | 取各分量                                               |
+| `setRgb(...)`                              | 设置 RGB(A)                                            |
 
 ```cpp
 QColor c1(Qt::red);          /* 预定义 */
@@ -624,15 +641,15 @@ QColor c4 = QColor::fromHsv(200, 255, 200);  /* HSV */
 
 #### 4.3.2 QPen
 
-| API | 说明 |
-| --- | ---- |
-| `QPen(const QColor &color)` | 用颜色构造 |
-| `QPen(const QBrush &brush, qreal width)` | 用画刷和线宽构造 |
-| `setColor(QColor)` | 设置颜色 |
-| `setWidth(int)` | 设置线宽(像素) |
-| `setStyle(Qt::PenStyle)` | 线型:`SolidLine`、`DashLine`、`DotLine`、`DashDotLine`、`DashDotDotLine`、`NoPen` |
-| `setCapStyle(Qt::PenCapStyle)` | 端点样式:`FlatCap`、`SquareCap`、`RoundCap` |
-| `setJoinStyle(Qt::PenJoinStyle)` | 连接样式:`MiterJoin`、`BevelJoin`、`RoundJoin` |
+| API                                      | 说明                                                                               |
+| ---------------------------------------- | ---------------------------------------------------------------------------------- |
+| `QPen(const QColor &color)`              | 用颜色构造                                                                         |
+| `QPen(const QBrush &brush, qreal width)` | 用画刷和线宽构造                                                                   |
+| `setColor(QColor)`                       | 设置颜色                                                                           |
+| `setWidth(int)`                          | 设置线宽(像素)                                                                   |
+| `setStyle(Qt::PenStyle)`                 | 线型:`SolidLine`、`DashLine`、`DotLine`、`DashDotLine`、`DashDotDotLine`、`NoPen` |
+| `setCapStyle(Qt::PenCapStyle)`           | 端点样式:`FlatCap`、`SquareCap`、`RoundCap`                                       |
+| `setJoinStyle(Qt::PenJoinStyle)`         | 连接样式:`MiterJoin`、`BevelJoin`、`RoundJoin`                                    |
 
 ```cpp
 QPen pen;
@@ -645,14 +662,14 @@ painter.setPen(pen);
 
 #### 4.3.3 QBrush
 
-| API | 说明 |
-| --- | ---- |
-| `QBrush(Qt::GlobalColor)` | 用预定义颜色构造 |
-| `QBrush(const QColor &color, Qt::BrushStyle)` | 用颜色和样式构造 |
-| `setColor(QColor)` | 设置颜色 |
-| `setStyle(Qt::BrushStyle)` | 填充样式:`SolidPattern`、`NoBrush`、`CrossPattern`、`DiagCrossPattern`、`HorPattern`、`VerPattern`、`Dense1Pattern`~`Dense7Pattern` 等 |
-| `setTexture(QPixmap)` | 用图片纹理填充 |
-| `setTextureImage(QImage)` | 用图片填充 |
+| API                                           | 说明                                                                                                                                    |
+| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
+| `QBrush(Qt::GlobalColor)`                     | 用预定义颜色构造                                                                                                                        |
+| `QBrush(const QColor &color, Qt::BrushStyle)` | 用颜色和样式构造                                                                                                                        |
+| `setColor(QColor)`                            | 设置颜色                                                                                                                                |
+| `setStyle(Qt::BrushStyle)`                    | 填充样式:`SolidPattern`、`NoBrush`、`CrossPattern`、`DiagCrossPattern`、`HorPattern`、`VerPattern`、`Dense1Pattern`~`Dense7Pattern` 等 |
+| `setTexture(QPixmap)`                         | 用图片纹理填充                                                                                                                          |
+| `setTextureImage(QImage)`                     | 用图片填充                                                                                                                              |
 
 ```cpp
 QBrush brush;
@@ -665,33 +682,33 @@ painter.setBrush(brush);
 
 ### 4.4 QPainter 常用绘制 API
 
-| API | 说明 |
-| --- | ---- |
-| `drawPoint(const QPointF &position)` | 画点 |
-| `drawLine(const QLineF &line)` | 画直线 |
-| `drawRect(const QRectF &rectangle)` | 画矩形(轮廓 + 填充) |
-| `drawRoundedRect(rect, xRadius, yRadius)` | 画圆角矩形 |
-| `drawEllipse(const QRectF &rectangle)` | 在矩形内画椭圆(内切) |
-| `drawArc(rect, startAngle, spanAngle)` | 画圆弧,角度单位为 1/16 度 |
-| `drawPie(rect, startAngle, spanAngle)` | 画扇形 |
-| `drawPolygon(const QPolygonF &points)` | 画多边形 |
-| `drawPolyline(const QPolygonF &points)` | 画折线 |
-| `drawConvexPolygon(...)` | 画凸多边形 |
-| `drawText(const QPointF &position, const QString &text)` | 在指定位置画文字 |
-| `drawText(const QRectF &rectangle, int flags, const QString &text)` | 在矩形内按对齐标志画文字,如 `Qt::AlignCenter` |
-| `drawImage(const QRectF &target, const QImage &image, const QRectF &source)` | 画图片(指定源与目标矩形) |
-| `drawPixmap(const QRectF &target, const QPixmap &pixmap, const QRectF &source)` | 画 Pixmap |
-| `fillRect(const QRectF &rectangle, const QColor &color)` | 用颜色填充矩形 |
-| `eraseRect(const QRectF &rectangle)` | 擦除矩形区域 |
-| `setPen(const QPen &pen)` / `pen()` | 设置 / 获取画笔 |
-| `setBrush(const QBrush &brush)` / `brush()` | 设置 / 获取画刷 |
-| `setFont(const QFont &font)` | 设置文字字体 |
-| `setRenderHint(RenderHint, bool on = true)` | 设置渲染提示,如抗锯齿 |
-| `translate(qreal dx, qreal dy)` | 平移坐标原点 |
-| `rotate(qreal angle)` | 绕原点旋转(单位:度,顺时针为正) |
-| `scale(qreal sx, qreal sy)` | 缩放坐标系统 |
-| `shear(qreal sh, qreal sv)` | 错切 |
-| `save()` / `restore()` | 保存 / 恢复画笔状态与坐标变换状态 |
+| API                                                                             | 说明                                           |
+| ------------------------------------------------------------------------------- | ---------------------------------------------- |
+| `drawPoint(const QPointF &position)`                                            | 画点                                           |
+| `drawLine(const QLineF &line)`                                                  | 画直线                                         |
+| `drawRect(const QRectF &rectangle)`                                             | 画矩形(轮廓 + 填充)                          |
+| `drawRoundedRect(rect, xRadius, yRadius)`                                       | 画圆角矩形                                     |
+| `drawEllipse(const QRectF &rectangle)`                                          | 在矩形内画椭圆(内切)                         |
+| `drawArc(rect, startAngle, spanAngle)`                                          | 画圆弧,角度单位为 1/16 度                     |
+| `drawPie(rect, startAngle, spanAngle)`                                          | 画扇形                                         |
+| `drawPolygon(const QPolygonF &points)`                                          | 画多边形                                       |
+| `drawPolyline(const QPolygonF &points)`                                         | 画折线                                         |
+| `drawConvexPolygon(...)`                                                        | 画凸多边形                                     |
+| `drawText(const QPointF &position, const QString &text)`                        | 在指定位置画文字                               |
+| `drawText(const QRectF &rectangle, int flags, const QString &text)`             | 在矩形内按对齐标志画文字,如 `Qt::AlignCenter` |
+| `drawImage(const QRectF &target, const QImage &image, const QRectF &source)`    | 画图片(指定源与目标矩形)                     |
+| `drawPixmap(const QRectF &target, const QPixmap &pixmap, const QRectF &source)` | 画 Pixmap                                      |
+| `fillRect(const QRectF &rectangle, const QColor &color)`                        | 用颜色填充矩形                                 |
+| `eraseRect(const QRectF &rectangle)`                                            | 擦除矩形区域                                   |
+| `setPen(const QPen &pen)` / `pen()`                                             | 设置 / 获取画笔                                |
+| `setBrush(const QBrush &brush)` / `brush()`                                     | 设置 / 获取画刷                                |
+| `setFont(const QFont &font)`                                                    | 设置文字字体                                   |
+| `setRenderHint(RenderHint, bool on = true)`                                     | 设置渲染提示,如抗锯齿                         |
+| `translate(qreal dx, qreal dy)`                                                 | 平移坐标原点                                   |
+| `rotate(qreal angle)`                                                           | 绕原点旋转(单位:度,顺时针为正)             |
+| `scale(qreal sx, qreal sy)`                                                     | 缩放坐标系统                                   |
+| `shear(qreal sh, qreal sv)`                                                     | 错切                                           |
+| `save()` / `restore()`                                                          | 保存 / 恢复画笔状态与坐标变换状态              |
 
 ### 4.5 坐标变换与状态保存
 
@@ -719,11 +736,11 @@ painter.restore();                  /* 恢复坐标系统与画笔 */
 
 默认绘制的线条和图形边缘有明显锯齿。开启抗锯齿让曲线、斜线更平滑:
 
-| 渲染提示 | 作用 |
-| -------- | ---- |
-| `QPainter::Antialiasing` | 图形边缘抗锯齿 |
-| `QPainter::TextAntialiasing` | 文字抗锯齿 |
-| `QPainter::SmoothPixmapTransform` | 图片平滑缩放 |
+| 渲染提示                          | 作用           |
+| --------------------------------- | -------------- |
+| `QPainter::Antialiasing`          | 图形边缘抗锯齿 |
+| `QPainter::TextAntialiasing`      | 文字抗锯齿     |
+| `QPainter::SmoothPixmapTransform` | 图片平滑缩放   |
 
 ```cpp
 painter.setRenderHints(QPainter::Antialiasing
@@ -921,50 +938,50 @@ flowchart BT
 
 ### 5.3 Qt Charts 主要类
 
-| 类 | 作用 |
-| -- | ---- |
-| `QChart` | 图表对象,管理系列(Series)、坐标轴(Axis)、图例(Legend)、标题 |
-| `QChartView` | 图表视图,承载并显示 `QChart` |
-| `QAbstractSeries` | 所有系列的基类 |
-| `QLineSeries` | 折线图系列 |
-| `QSplineSeries` | 平滑曲线系列 |
-| `QScatterSeries` | 散点图系列 |
-| `QBarSeries` / `QHorizontalBarSeries` | 柱状图系列 |
-| `QStackedBarSeries` | 堆叠柱状图系列 |
-| `QBarSet` | 柱状图的一组数据 |
-| `QPieSeries` / `QPieSlice` | 饼图系列 / 饼图扇区 |
-| `QAreaSeries` | 面积图系列 |
-| `QValueAxis` | 数值坐标轴 |
-| `QBarCategoryAxis` | 分类坐标轴(柱状图横轴) |
-| `QDateTimeAxis` | 时间坐标轴 |
-| `QLogValueAxis` | 对数坐标轴 |
-| `QLegend` | 图例,用颜色区分不同系列 |
+| 类                                    | 作用                                                               |
+| ------------------------------------- | ------------------------------------------------------------------ |
+| `QChart`                              | 图表对象,管理系列(Series)、坐标轴(Axis)、图例(Legend)、标题 |
+| `QChartView`                          | 图表视图,承载并显示 `QChart`                                      |
+| `QAbstractSeries`                     | 所有系列的基类                                                     |
+| `QLineSeries`                         | 折线图系列                                                         |
+| `QSplineSeries`                       | 平滑曲线系列                                                       |
+| `QScatterSeries`                      | 散点图系列                                                         |
+| `QBarSeries` / `QHorizontalBarSeries` | 柱状图系列                                                         |
+| `QStackedBarSeries`                   | 堆叠柱状图系列                                                     |
+| `QBarSet`                             | 柱状图的一组数据                                                   |
+| `QPieSeries` / `QPieSlice`            | 饼图系列 / 饼图扇区                                                |
+| `QAreaSeries`                         | 面积图系列                                                         |
+| `QValueAxis`                          | 数值坐标轴                                                         |
+| `QBarCategoryAxis`                    | 分类坐标轴(柱状图横轴)                                           |
+| `QDateTimeAxis`                       | 时间坐标轴                                                         |
+| `QLogValueAxis`                       | 对数坐标轴                                                         |
+| `QLegend`                             | 图例,用颜色区分不同系列                                           |
 
 ### 5.4 QChart 常用 API
 
-| 类 | API | 说明 |
-| -- | --- | ---- |
-| `QChart` | `addSeries(QAbstractSeries *series)` | 添加系列 |
-| `QChart` | `removeSeries(...)` / `removeAllSeries()` | 移除系列 |
-| `QChart` | `addAxis(QAbstractAxis *axis, Qt::Alignment)` | 添加坐标轴并指定方位 |
-| `QChart` | `setTitle(const QString &)` | 设置标题 |
-| `QChart` | `legend()` | 返回 `QLegend *`,`hide()` / `show()` / `setVisible()` |
-| `QChart` | `setTheme(QChart::ChartTheme)` | 设置主题 |
-| `QChart` | `createDefaultAxes()` | 根据系列自动创建默认坐标轴 |
-| `QChartView` | `setChart(QChart *chart)` | 设置要显示的图表 |
-| `QChartView` | `setRenderHint(QPainter::RenderHint)` | 设置渲染提示,如抗锯齿 |
-| `QAbstractSeries` | `attachAxis(QAbstractAxis *axis)` | 把系列绑定到某条坐标轴 |
-| `QValueAxis` | `setRange(qreal min, qreal max)` | 设置轴范围 |
-| `QValueAxis` | `setTitleText(const QString &)` | 设置轴标题 |
-| `QValueAxis` | `setLabelFormat(const QString &)` | 设置标签格式,如 `"%i"`、`"%g"` |
-| `QValueAxis` | `setTickCount(int count)` | 设置刻度数量 |
-| `QLineSeries` | `append(qreal x, qreal y)` | 追加数据点 |
-| `QLineSeries` | `clear()` | 清空数据点 |
-| `QBarSeries` | `append(QBarSet *set)` | 追加一组数据 |
-| `QBarSet` | `operator<<(qreal value)` | 追加一个数据 |
-| `QBarCategoryAxis` | `append(const QStringList &categories)` | 设置分类标签 |
-| `QPieSeries` | `append(const QString &label, qreal value)` | 追加一个扇区 |
-| `QPieSlice` | `setExploded(bool)` / `setLabelVisible(bool)` | 扇区分离 / 显示标签 |
+| 类                 | API                                           | 说明                                                   |
+| ------------------ | --------------------------------------------- | ------------------------------------------------------ |
+| `QChart`           | `addSeries(QAbstractSeries *series)`          | 添加系列                                               |
+| `QChart`           | `removeSeries(...)` / `removeAllSeries()`     | 移除系列                                               |
+| `QChart`           | `addAxis(QAbstractAxis *axis, Qt::Alignment)` | 添加坐标轴并指定方位                                   |
+| `QChart`           | `setTitle(const QString &)`                   | 设置标题                                               |
+| `QChart`           | `legend()`                                    | 返回 `QLegend *`,`hide()` / `show()` / `setVisible()` |
+| `QChart`           | `setTheme(QChart::ChartTheme)`                | 设置主题                                               |
+| `QChart`           | `createDefaultAxes()`                         | 根据系列自动创建默认坐标轴                             |
+| `QChartView`       | `setChart(QChart *chart)`                     | 设置要显示的图表                                       |
+| `QChartView`       | `setRenderHint(QPainter::RenderHint)`         | 设置渲染提示,如抗锯齿                                 |
+| `QAbstractSeries`  | `attachAxis(QAbstractAxis *axis)`             | 把系列绑定到某条坐标轴                                 |
+| `QValueAxis`       | `setRange(qreal min, qreal max)`              | 设置轴范围                                             |
+| `QValueAxis`       | `setTitleText(const QString &)`               | 设置轴标题                                             |
+| `QValueAxis`       | `setLabelFormat(const QString &)`             | 设置标签格式,如 `"%i"`、`"%g"`                        |
+| `QValueAxis`       | `setTickCount(int count)`                     | 设置刻度数量                                           |
+| `QLineSeries`      | `append(qreal x, qreal y)`                    | 追加数据点                                             |
+| `QLineSeries`      | `clear()`                                     | 清空数据点                                             |
+| `QBarSeries`       | `append(QBarSet *set)`                        | 追加一组数据                                           |
+| `QBarSet`          | `operator<<(qreal value)`                     | 追加一个数据                                           |
+| `QBarCategoryAxis` | `append(const QStringList &categories)`       | 设置分类标签                                           |
+| `QPieSeries`       | `append(const QString &label, qreal value)`   | 追加一个扇区                                           |
+| `QPieSlice`        | `setExploded(bool)` / `setLabelVisible(bool)` | 扇区分离 / 显示标签                                    |
 
 ### 5.5 完整实例 04_qtchart:实时动态曲线(难度:一般)
 
@@ -1192,7 +1209,7 @@ void MainWindow::receivedData(int value)
   2. `while (data.size() > maxSize) data.removeFirst()` 在数据超过上限后删除最老的一个数据;
   3. `splineSeries->clear()` 清空旧点;
   4. 按 `xSpace * i` 重新铺 x 坐标,`data.at(i)` 作为 y 值,把窗口内的数据重新画一遍。
-  由于每次 x 位置固定、y 值滚动,曲线看起来就在「向左移动」,实现了数据移动的过程。
+     由于每次 x 位置固定、y 值滚动,曲线看起来就在「向左移动」,实现了数据移动的过程。
 
 > **改造为真实数据源**:把 `qrand() % maxY` 换成串口/CAN/传感器的读数,把 `timerTimeOut()` 换成数据到达信号触发的槽函数,即可用于实际项目的实时曲线。
 
@@ -1272,12 +1289,12 @@ view->setRenderHint(QPainter::Antialiasing);
 
 ### 6.1 所需 Qt 模块
 
-| 例程 | 额外模块 | `.pro` 修改 |
-| ---- | -------- | ----------- |
-| 01_qfile | 无(core gui widgets) | `QT += core gui` |
-| 02_qtextstream | 无 | `QT += core gui` |
-| 03_qpainter | 无(用资源文件) | `QT += core gui`,并加 `RESOURCES += res.qrc` |
-| 04_qtchart | **charts** | `QT += core gui charts` |
+| 例程           | 额外模块               | `.pro` 修改                                   |
+| -------------- | ---------------------- | --------------------------------------------- |
+| 01_qfile       | 无(core gui widgets) | `QT += core gui`                              |
+| 02_qtextstream | 无                     | `QT += core gui`                              |
+| 03_qpainter    | 无(用资源文件)       | `QT += core gui`,并加 `RESOURCES += res.qrc` |
+| 04_qtchart     | **charts**             | `QT += core gui charts`                       |
 
 通用做法:打开 `.pro` 文件,把第一行改成对应内容;`03_qpainter`、`04_qtchart` 的图片/资源需要随工程一起编译。
 

+ 68 - 48
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/07-多线程网络多媒体与数据库.md

@@ -1,6 +1,26 @@
 ---
 title: 多线程、网络、多媒体与数据库
-tags: [Qt, QThread, moveToThread, QMutex, QTcpServer, QTcpSocket, QUdpSocket, 多播, QHostInfo, QNetworkAccessManager, QMediaPlayer, QSound, QAudioRecorder, QtSQL, SQLite, QSqlTableModel, IMX6ULL, 嵌入式Qt]
+tags:
+  [
+    Qt,
+    QThread,
+    moveToThread,
+    QMutex,
+    QTcpServer,
+    QTcpSocket,
+    QUdpSocket,
+    多播,
+    QHostInfo,
+    QNetworkAccessManager,
+    QMediaPlayer,
+    QSound,
+    QAudioRecorder,
+    QtSQL,
+    SQLite,
+    QSqlTableModel,
+    IMX6ULL,
+    嵌入式Qt,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Qt开发指南V1.2》第十章 多线程、第十一章 网络编程、第十二章 多媒体、第十三章 数据库"
@@ -38,9 +58,9 @@ pdf_ref: "《I.MX6U嵌入式Qt开发指南V1.2》第十章 多线程、第十一
 
 `QThread` 是 Qt 实现多线程的核心类。Qt 有两种多线程写法:
 
-| 方法 | 核心思想 | 说明 |
-| ---- | -------- | ---- |
-| 继承 `QThread`,重写 `run()` | 子类中只有 `run()` 在新线程执行 | Qt 4.8 之前的主流写法 |
+| 方法                             | 核心思想                          | 说明                        |
+| -------------------------------- | --------------------------------- | --------------------------- |
+| 继承 `QThread`,重写 `run()`     | 子类中只有 `run()` 在新线程执行   | Qt 4.8 之前的主流写法       |
 | 继承 `QObject`,`moveToThread()` | 整个 `QObject` 对象移到线程里执行 | Qt 4.8 之后官方推荐,更灵活 |
 
 > Qt 官方从 4.8 之后建议使用第二种方法。两种方法区别不大,但继承 `QObject` 的方法更灵活,所以 Qt 帮助文档里先给继承 `QObject` 的写法,再给继承 `QThread` 的写法。
@@ -446,13 +466,13 @@ void MainWindow::handleResults(const QString &results)
 
 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。原书只提到 Qt 提供 `QMutex`、`QMutexLocker`、`QReadLocker`、`QWriteLocker` 等类用于线程同步,此处的对照表为 Qt 官方文档标准内容,补充以便实际开发查阅。
 
-| 类 | 作用 | 典型用法 |
-| -- | ---- | -------- |
-| `QMutex` | 互斥锁,保护临界区 | `lock.lock(); ...; lock.unlock();` |
-| `QMutexLocker` | RAII 方式安全使用 `QMutex` | `QMutexLocker locker(&lock);` 作用域结束自动解锁 |
-| `QReadLocker` / `QWriteLocker` | 配合 `QReadWriteLock` 的读/写锁 | 读多写少场景提升并发 |
-| `QSemaphore` | 信号量,控制同时访问资源的线程数 | 生产者/消费者缓冲 |
-| `QWaitCondition` | 条件变量,等待某个条件成立 | 线程间通知 |
+| 类                             | 作用                             | 典型用法                                         |
+| ------------------------------ | -------------------------------- | ------------------------------------------------ |
+| `QMutex`                       | 互斥锁,保护临界区               | `lock.lock(); ...; lock.unlock();`               |
+| `QMutexLocker`                 | RAII 方式安全使用 `QMutex`       | `QMutexLocker locker(&lock);` 作用域结束自动解锁 |
+| `QReadLocker` / `QWriteLocker` | 配合 `QReadWriteLock` 的读/写锁  | 读多写少场景提升并发                             |
+| `QSemaphore`                   | 信号量,控制同时访问资源的线程数 | 生产者/消费者缓冲                                |
+| `QWaitCondition`               | 条件变量,等待某个条件成立       | 线程间通知                                       |
 
 > ⚠️ **易错点**:跨线程访问共享变量(如本例的 `isCanRun`)必须加锁,否则可能出现读到旧值、编译器重排等问题。锁的粒度要尽量小,不要在处理耗时操作时持锁。
 
@@ -831,11 +851,11 @@ connect(tcpSocket,
 
 UDP 是**无连接**的传输层协议,不需要事先建立连接,直接把数据报(datagram)发出去,速度快但不保证可靠。UDP 支持三种发送方式:
 
-| 方式 | 目标地址 | 说明 |
-| ---- | -------- | ---- |
-| 单播 | 某个具体 IP | 一对一,点对点发送 |
-| 广播 | `QHostAddress::Broadcast`(255.255.255.255) | 同一网段内监听目标端口的程序都会收到 |
-| 组播(多播) | D 类地址,如 `239.255.255.1` | 加入同一组播组的主机都能收到 |
+| 方式         | 目标地址                                     | 说明                                 |
+| ------------ | -------------------------------------------- | ------------------------------------ |
+| 单播         | 某个具体 IP                                  | 一对一,点对点发送                   |
+| 广播         | `QHostAddress::Broadcast`(255.255.255.255) | 同一网段内监听目标端口的程序都会收到 |
+| 组播(多播) | D 类地址,如 `239.255.255.1`                 | 加入同一组播组的主机都能收到         |
 
 ```mermaid
 flowchart TB
@@ -1208,16 +1228,16 @@ Qt 从 4.4 版本开始提供多媒体框架。Qt 4.6 中实现多媒体播放
 
 Qt 多媒体模块的主要类:
 
-| 类 | 用途 |
-| -- | ---- |
-| `QSound` | 播放本地 WAV 音效(异步方式) |
-| `QSoundEffect` | 播放音效,支持本地与网络文件(用 `QUrl`) |
-| `QMediaPlayer` | 播放音乐/视频 |
-| `QMediaPlaylist` | 播放列表,管理多个媒体文件 |
-| `QVideoWidget` | 视频显示窗口 |
-| `QAudioRecorder` | 录音 |
-| `QAudioProbe` | 探测音频缓冲区(用于显示音量 level) |
-| `QAudioOutput` / `QAudioInput` | 音频输出/输入 |
+| 类                             | 用途                                      |
+| ------------------------------ | ----------------------------------------- |
+| `QSound`                       | 播放本地 WAV 音效(异步方式)             |
+| `QSoundEffect`                 | 播放音效,支持本地与网络文件(用 `QUrl`) |
+| `QMediaPlayer`                 | 播放音乐/视频                             |
+| `QMediaPlaylist`               | 播放列表,管理多个媒体文件                |
+| `QVideoWidget`                 | 视频显示窗口                              |
+| `QAudioRecorder`               | 录音                                      |
+| `QAudioProbe`                  | 探测音频缓冲区(用于显示音量 level)      |
+| `QAudioOutput` / `QAudioInput` | 音频输出/输入                             |
 
 在 `.pro` 里添加:
 
@@ -1940,22 +1960,22 @@ flowchart LR
 
 ### 4.3 Qt SQL 核心类与 API
 
-| 类 | 作用 | 常用 API |
-| -- | ---- | -------- |
-| `QSqlDatabase` | 数据库连接 | `addDatabase(driver)`、`setDatabaseName(name)`、`open()`、`close()`、`lastError()`、`drivers()` |
-| `QSqlQuery` | 执行 SQL 语句 | `exec(sql)`、`prepare(sql)`、`bindValue(placeholder, value)`、`first()`、`next()`、`value(field)` |
-| `QSqlTableModel` | 单表模型,可编辑 | `setTable()`、`setEditStrategy()`、`select()`、`insertRow()`、`setData()`、`data()`、`submit()`、`submitAll()`、`removeRow()`、`isDirty()`、`rowCount()` |
-| `QSqlQueryModel` | 查询结果模型,只读 | `setQuery()`、`record()`、`setHeaderData()`、`lastError()` |
-| `QDataWidgetMapper` | 模型字段映射到控件 | `setModel()`、`setSubmitPolicy()`、`addMapping()`、`setCurrentModelIndex()` |
-| `QItemSelectionModel` | 选择模型 | `currentRowChanged` 信号、`currentIndex()` |
-| `QTableView` | 表格视图 | `setModel()`、`setSelectionModel()`、`hideColumn()`、`horizontalHeader()` |
+| 类                    | 作用               | 常用 API                                                                                                                                                 |
+| --------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `QSqlDatabase`        | 数据库连接         | `addDatabase(driver)`、`setDatabaseName(name)`、`open()`、`close()`、`lastError()`、`drivers()`                                                          |
+| `QSqlQuery`           | 执行 SQL 语句      | `exec(sql)`、`prepare(sql)`、`bindValue(placeholder, value)`、`first()`、`next()`、`value(field)`                                                        |
+| `QSqlTableModel`      | 单表模型,可编辑   | `setTable()`、`setEditStrategy()`、`select()`、`insertRow()`、`setData()`、`data()`、`submit()`、`submitAll()`、`removeRow()`、`isDirty()`、`rowCount()` |
+| `QSqlQueryModel`      | 查询结果模型,只读 | `setQuery()`、`record()`、`setHeaderData()`、`lastError()`                                                                                               |
+| `QDataWidgetMapper`   | 模型字段映射到控件 | `setModel()`、`setSubmitPolicy()`、`addMapping()`、`setCurrentModelIndex()`                                                                              |
+| `QItemSelectionModel` | 选择模型           | `currentRowChanged` 信号、`currentIndex()`                                                                                                               |
+| `QTableView`          | 表格视图           | `setModel()`、`setSelectionModel()`、`hideColumn()`、`horizontalHeader()`                                                                                |
 
 `QSqlTableModel::EditStrategy` 三种编辑策略:
 
-| 策略 | 说明 |
-| ---- | ---- |
-| `OnFieldChange` | 字段改变即同步到数据库 |
-| `OnRowChange` | 行改变(切换到别的行)时同步 |
+| 策略             | 说明                                       |
+| ---------------- | ------------------------------------------ |
+| `OnFieldChange`  | 字段改变即同步到数据库                     |
+| `OnRowChange`    | 行改变(切换到别的行)时同步               |
 | `OnManualSubmit` | 手动调用 `submit()` / `submitAll()` 才提交 |
 
 > **注意**:`setEditStrategy(OnFieldChange)` 这类规则需要与 `QTableView` 这样的控件配合才生效,因为 `QTableView` 可以直接编辑表里的内容。
@@ -2358,15 +2378,15 @@ setCentralWidget(view);
 
 ### 5.1 各例程所需模块
 
-| 例程 | `.pro` 额外模块 |
-| ---- | --------------- |
-| 05、06 多线程 | 无(`core gui` 即可) |
-| 07、08、09、10、11、12 网络 | `QT += network` |
-| 13 按钮音效 | `QT += multimedia` |
-| 14 音乐播放器 | `QT += multimedia` |
-| 15 视频播放器 | `QT += multimedia multimediawidgets` |
-| 16 录音 | `QT += multimedia` |
-| 17、18 数据库 | `QT += core gui sql` |
+| 例程                        | `.pro` 额外模块                      |
+| --------------------------- | ------------------------------------ |
+| 05、06 多线程               | 无(`core gui` 即可)                |
+| 07、08、09、10、11、12 网络 | `QT += network`                      |
+| 13 按钮音效                 | `QT += multimedia`                   |
+| 14 音乐播放器               | `QT += multimedia`                   |
+| 15 视频播放器               | `QT += multimedia multimediawidgets` |
+| 16 录音                     | `QT += multimedia`                   |
+| 17、18 数据库               | `QT += core gui sql`                 |
 
 ### 5.2 Ubuntu 上编译运行
 

+ 73 - 18
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/08-嵌入式Qt交叉编译与项目实战.md

@@ -1,6 +1,61 @@
 ---
 title: 嵌入式Qt交叉编译与项目实战
-tags: [嵌入式Linux, Qt, I.MX6U, IMX6ULL, ALPHA, 交叉编译, Qt Creator, qmake, make, linuxfb, eglfs, Qt控制LED, Qt控制BEEP, QFile, QSerialPort, 串口, CAN, QCanBus, SocketCAN, QCanBusFrame, QCamera, OpenCV, 摄像头, 蓝牙, QBluetoothServer, QBluetoothSocket, BlueZ, Rfcomm, USER-KEY, QKeyEvent, Key_VolumeDown, AP3216C, I2C, 杂项设备, ICM20608, SPI, 智能家居, ESP8266, 原子云, WebSocket, QWebSocket, 语音识别, 百度AI, ASR, 车牌识别, OCR, APP主界面, QScroller, QPropertyAnimation, 视频监控, V4L2, UDP广播, 项目实战]
+tags:
+  [
+    嵌入式Linux,
+    Qt,
+    I.MX6U,
+    IMX6ULL,
+    ALPHA,
+    交叉编译,
+    Qt Creator,
+    qmake,
+    make,
+    linuxfb,
+    eglfs,
+    Qt控制LED,
+    Qt控制BEEP,
+    QFile,
+    QSerialPort,
+    串口,
+    CAN,
+    QCanBus,
+    SocketCAN,
+    QCanBusFrame,
+    QCamera,
+    OpenCV,
+    摄像头,
+    蓝牙,
+    QBluetoothServer,
+    QBluetoothSocket,
+    BlueZ,
+    Rfcomm,
+    USER-KEY,
+    QKeyEvent,
+    Key_VolumeDown,
+    AP3216C,
+    I2C,
+    杂项设备,
+    ICM20608,
+    SPI,
+    智能家居,
+    ESP8266,
+    原子云,
+    WebSocket,
+    QWebSocket,
+    语音识别,
+    百度AI,
+    ASR,
+    车牌识别,
+    OCR,
+    APP主界面,
+    QScroller,
+    QPropertyAnimation,
+    视频监控,
+    V4L2,
+    UDP广播,
+    项目实战,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Qt开发指南V1.2》第十四章 I.MX6U Qt开发 ~ 第二十八章 视频监控项目;配套例程 Embedded-Qt-Tutorial/Qt/03、Qt/04"
@@ -53,9 +108,9 @@ flowchart TD
 
 《I.MX6U嵌入式Qt开发指南》第十四章明确指出,交叉编译 Qt 应用程序有两种方法:
 
-| 方式 | 说明 | 优点 | 缺点 |
-| ---- | ---- | ---- | ---- |
-| **命令行编译** | 直接用交叉工具链的 `qmake` + `make` 编译 | 简单、可控、适合脚本化 | 需要手动拷贝可执行文件到板子运行 |
+| 方式                    | 说明                                                    | 优点                         | 缺点                               |
+| ----------------------- | ------------------------------------------------------- | ---------------------------- | ---------------------------------- |
+| **命令行编译**          | 直接用交叉工具链的 `qmake` + `make` 编译                | 简单、可控、适合脚本化       | 需要手动拷贝可执行文件到板子运行   |
 | **Qt Creator 交叉套件** | 在 Qt Creator 中配置 ARM 的 Kit(Qt Versions + 编译器) | 可远程连接开发板一键运行调试 | 环境变量、路径等配置不对时较难排查 |
 
 教程建议:**先在 Ubuntu 上运行仿真**(x86 套件),确认界面与逻辑正确后,再**交叉编译到开发板**查看实际效果。若 Qt Creator 远程运行结果不正确(例如缺少环境变量或路径),**直接拷贝可执行文件到开发板上运行**更稳妥。
@@ -137,11 +192,11 @@ Qt 程序在不同平台上显示,依赖不同的**平台插件(platform plu
 ./01_led -platform eglfs       # 使用 EGL + OpenGL ES 全屏输出
 ```
 
-| 平台插件 | 全称 | 依赖 | 适用场景 |
-| -------- | ---- | ---- | -------- |
-| `linuxfb` | Linux Framebuffer | `/dev/fb0`,无需 GPU | 没有 GPU、或只想在帧缓冲上直接显示 |
-| `eglfs` | EGL Full Screen | EGL/OpenGL ES 驱动、GPU | 有 GPU 加速(I.MX6ULL 出厂系统常用),性能好 |
-| `xcb` | X11 | X Server | 桌面 Linux(Ubuntu 仿真时) |
+| 平台插件  | 全称              | 依赖                    | 适用场景                                     |
+| --------- | ----------------- | ----------------------- | -------------------------------------------- |
+| `linuxfb` | Linux Framebuffer | `/dev/fb0`,无需 GPU    | 没有 GPU、或只想在帧缓冲上直接显示           |
+| `eglfs`   | EGL Full Screen   | EGL/OpenGL ES 驱动、GPU | 有 GPU 加速(I.MX6ULL 出厂系统常用),性能好 |
+| `xcb`     | X11               | X Server                | 桌面 Linux(Ubuntu 仿真时)                  |
 
 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。第十四章正文并未展开平台插件,此处补充 `-platform` 的工作原理与常见取值,方便调试“程序能跑但屏幕没反应”的问题。在 Ubuntu 上仿真时默认使用 `xcb`,在开发板上运行时 Qt 会依据 `QT_QPA_PLATFORM` 环境变量或 `-platform` 参数选择插件。
 
@@ -430,10 +485,10 @@ pushButton->setText(buf == "1" ? "BEEP 开" : "BEEP 关");
 
 I.MX6U 出厂系统默认配置了两路可用串口:
 
-| 串口 | 设备节点 | 说明 |
-| ---- | -------- | ---- |
+| 串口  | 设备节点       | 说明                             |
+| ----- | -------------- | -------------------------------- |
 | UART1 | `/dev/ttymxc0` | 已作为**调试串口**使用,不可占用 |
-| UART3 | `/dev/ttymxc2` | 留给用户编程使用 |
+| UART3 | `/dev/ttymxc2` | 留给用户编程使用                 |
 
 因此 Qt 程序只能对 `ttymxc2` 编程。默认参数:波特率 115200、数据位 8、无校验、停止位 1、无流控。
 
@@ -1367,12 +1422,12 @@ HEADERS += chatclient.h chatserver.h mainwindow.h remoteselector.h
 
 项目分为四大部分:
 
-| 文件 | 职责 |
-| ---- | ---- |
-| `chatserver.*` | 服务端:创建 RFCOMM 服务器、注册服务、收发消息 |
-| `chatclient.*` | 客户端:连接远端服务、收发消息 |
-| `remoteselector.*` | 远程选择器:扫描发现周围蓝牙服务 |
-| `mainwindow.*` | 主界面:Tab 页面、按钮逻辑,串联上述三类 |
+| 文件               | 职责                                           |
+| ------------------ | ---------------------------------------------- |
+| `chatserver.*`     | 服务端:创建 RFCOMM 服务器、注册服务、收发消息 |
+| `chatclient.*`     | 客户端:连接远端服务、收发消息                 |
+| `remoteselector.*` | 远程选择器:扫描发现周围蓝牙服务               |
+| `mainwindow.*`     | 主界面:Tab 页面、按钮逻辑,串联上述三类       |
 
 服务 UUID 定义为:`e8e10f95-1a70-4b27-9ccf-02010264e9c8`。
 

+ 149 - 62
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/面试-Qt应用开发.md

@@ -1,6 +1,53 @@
 ---
 title: 面试-Qt应用开发
-tags: [嵌入式Linux, 面试, Qt, C++, 面向对象, 类与对象, 继承, 重载, 多态, 虚函数, 命名空间, Qt Creator, qmake, ui文件, 信号与槽, connect, Q_OBJECT, 控件, QPushButton, QComboBox, QLineEdit, 布局, QBoxLayout, QGridLayout, 模型视图, QStandardItemModel, QListView, QTableView, QFile, QTextStream, QPainter, QChart, QThread, QMutexLocker, QTcpSocket, QTcpServer, QUdpSocket, QMediaPlayer, QSqlDatabase, QSqlTableModel, SQLite, 交叉编译, linuxfb, eglfs, I.MX6U]
+tags:
+  [
+    嵌入式Linux,
+    面试,
+    Qt,
+    C++,
+    面向对象,
+    类与对象,
+    继承,
+    重载,
+    多态,
+    虚函数,
+    命名空间,
+    Qt Creator,
+    qmake,
+    ui文件,
+    信号与槽,
+    connect,
+    Q_OBJECT,
+    控件,
+    QPushButton,
+    QComboBox,
+    QLineEdit,
+    布局,
+    QBoxLayout,
+    QGridLayout,
+    模型视图,
+    QStandardItemModel,
+    QListView,
+    QTableView,
+    QFile,
+    QTextStream,
+    QPainter,
+    QChart,
+    QThread,
+    QMutexLocker,
+    QTcpSocket,
+    QTcpServer,
+    QUdpSocket,
+    QMediaPlayer,
+    QSqlDatabase,
+    QSqlTableModel,
+    SQLite,
+    交叉编译,
+    linuxfb,
+    eglfs,
+    I.MX6U,
+  ]
 created: 2026-09-18
 updated: 2026-09-18
 pdf_ref: "《I.MX6U嵌入式Qt开发指南V1.2》第一章~第二十八章:在Ubuntu下编写C++、C++基础、初识Qt、使用Qt Designer开发、Qt信号与槽、Qt Creator使用技巧、Qt控件、文本读写、绘图与图表、多线程、网络编程、多媒体、数据库、I.MX6U Qt开发 ~ 视频监控项目"
@@ -43,6 +90,7 @@ int main() {
 - 注意 `#include <iostream>` 是标准流(无 `.h`,需命名空间);`#include <iostream.h>` 是非标准 C 库。
 
 **追问**
+
 1. `std::cin` 中的 `::` 是什么运算符?(作用域运算符,表示“std 命名空间下的”)
 2. 什么时候必须写全名而不用 `using namespace`?(库或公司自定义命名空间存在同名冲突时)
 
@@ -73,6 +121,7 @@ delete dog2; dog2 = NULL;
 构造函数特点:与类名同名、可重载、**没有返回类型(void 也不行)**、实例化时自动调用且仅一次。析构函数特点:`~类名()`、无参数、无返回值、生命周期结束时释放资源。类内实现或类外 `::` 实现均可。
 
 **追问**
+
 1. 不写构造函数和析构函数会怎样?(编译器会生成空的默认版本)
 2. `this` 指针是什么?(指向当前实例对象的指针,只能在成员函数使用,成员函数默认首参为 `T * const this`)
 
@@ -82,11 +131,11 @@ delete dog2; dog2 = NULL;
 
 **详细解答**:
 
-| 继承方式 | 基类 public | 基类 protected | 基类 private |
-| -------- | ----------- | -------------- | ------------ |
-| public | public | protected | 不可直接访问 |
-| protected | protected | protected | 不可直接访问 |
-| private | private | private | 不可直接访问 |
+| 继承方式  | 基类 public | 基类 protected | 基类 private |
+| --------- | ----------- | -------------- | ------------ |
+| public    | public      | protected      | 不可直接访问 |
+| protected | protected   | protected      | 不可直接访问 |
+| private   | private     | private        | 不可直接访问 |
 
 ```cpp
 class Animal { public: string color; int weight; };
@@ -97,6 +146,7 @@ class Dog : public Animal { public: string name; int age; void run(); };
 继承达到代码重用、提高效率的目的。Qt 中大量通过继承来重写/扩展已有类。
 
 **追问**
+
 1. 基类私有成员在派生类中能直接访问吗?(不能,但可通过基类公有/保护成员间接访问)
 2. 一个类能继承多个基类吗?(可以,多继承)
 
@@ -133,6 +183,7 @@ public:
 不可重载:成员访问 `.`、成员指针访问 `.*` 与 `->*`、域运算符 `::`、`sizeof`、条件运算符 `?:`、预处理符号 `#`。
 
 **追问**
+
 1. 为什么要重载运算符?(让自定义类型也能像基本类型一样使用 `+`、`==` 等,形式统一)
 2. 重载时对象属性如何访问?(通过 `this`,因为对象是作为参数传入的)
 
@@ -159,6 +210,7 @@ animal = &cat; animal->run();   /* 输出 Cat */
 - **纯虚函数**:`virtual void funtion1() = 0;`,没有实现,用来规范派生类接口;含纯虚函数的类是**抽象类**,不能实例化,只能作接口。
 
 **追问**
+
 1. 去掉 `virtual` 会怎样?(静态链接到基类函数,调用基类自己的 `run()`,多态失效)
 2. `QWidget` 的 `paintEvent()` 为什么能被子类重写?(它在基类中定义为虚函数,这正是多态的典型应用)
 
@@ -175,6 +227,7 @@ animal = &cat; animal->run();   /* 输出 Cat */
 Qt C++ 效率更高、底层功能突出;QML 是描述性语言,配合 Qt Quick 做移动端风格的界面,但底层(文件读写、图像处理、多线程通信)仍靠 C++。低版本(Qt5.6 以下)QML 不够成熟,Qt5.7 以上更完善。本教程主打 **Qt C++**。
 
 **追问**
+
 1. “跨平台”对 Qt 意味着什么?(同一份源码在不同平台用不同套件编译即可运行)
 2. 嵌入式里为什么常用 QWidget 作为基类?(它不带标题栏/状态栏,适合全屏界面)
 
@@ -184,12 +237,12 @@ Qt C++ 效率更高、底层功能突出;QML 是描述性语言,配合 Qt Qu
 
 **详细解答**:
 
-| 文件 | 作用 |
-| ---- | ---- |
+| 文件    | 作用                                                         |
+| ------- | ------------------------------------------------------------ |
 | `*.pro` | 项目管理文件,增删文件时 Qt Creator 自动维护,也可手动加配置 |
-| `*.h` | 类的声明;用到信号的类必须有 `Q_OBJECT` |
-| `*.cpp` | 类的实现 |
-| `*.ui` | XML 描述的界面文件,编译时生成 `ui_*.h` |
+| `*.h`   | 类的声明;用到信号的类必须有 `Q_OBJECT`                      |
+| `*.cpp` | 类的实现                                                     |
+| `*.ui`  | XML 描述的界面文件,编译时生成 `ui_*.h`                      |
 
 基类选择:
 
@@ -209,6 +262,7 @@ int main(int argc, char *argv[]) {
 ```
 
 **追问**
+
 1. 为什么 Qt 程序的 `main()` 最后是 `a.exec()`?(把控制权交给 Qt,进入事件循环处理用户/系统事件)
 2. `QMainWindow` 与 `QWidget` 的继承关系?(`QMainWindow` 继承自 `QWidget`)
 
@@ -238,6 +292,7 @@ QT       += network                               # 网络模块
 第 1 行 core 与 gui 是默认设置;部署规则 `qnx: ... / else: unix:!android: ...` 用于指定安装路径。
 
 **追问**
+
 1. 要用 Qt 串口需要在 `.pro` 加什么?(`QT += core gui serialport`)
 2. `.pri` 文件是什么?(语法同 `.pro`,可被 `include()` 引入,便于复用与拆分大型工程)
 
@@ -275,6 +330,7 @@ MainWindow::~MainWindow() {
 `ui_mainwindow.h` 是**构建后才生成**的(位于 build 目录),所以使用 UI 设计的信号槽连接,需要编译后才能在 `setupUi()` 里看到。
 
 **追问**
+
 1. 为什么使用 `ui->` 里控件必须写在 `setupUi(this)` 之后?(该调用才完成控件初始化)
 2. `Q_OBJECT` 宏的作用?(Qt 元对象系统的基础,凡用到信号槽的类都要加)
 
@@ -291,6 +347,7 @@ MainWindow::~MainWindow() {
 Qt Creator 技巧:左侧“帮助”可查类/方法/信号槽用法;`Ctrl + N` 新建项目;`Ctrl + R` 编译并运行;`Ctrl + Shift + T` 查看类的继承关系;`F1` 查看光标处符号的帮助;按住 `Ctrl` 点击符号跳转到定义。
 
 **追问**
+
 1. 无 UI 文件时如何设置窗口大小?(`this->resize(800, 480);` 或 `setGeometry(0,0,800,480)`)
 2. 嵌入式常用的分辨率是多少?(本教程统一 800×480)
 
@@ -312,6 +369,7 @@ connect(pushButton, SIGNAL(clicked()), this, SLOT(pushButtonClicked()));
 `connect()` 是 `QObject` 的静态函数,`QObject` 是所有 Qt 类的基类,故常省略限定符。`SIGNAL`/`SLOT` 把信号和槽的参数转换为相应字符串。带参时只需写参数类型,可省略参数名。
 
 **追问**
+
 1. `connect` 中发送者与接收者的顺序能颠倒吗?(不能,sender 与信号在一起,receiver 与槽在一起)
 2. 信号与槽的参数必须完全一致吗?(至少信号参数不能少于槽参数,否则编译或运行错误)
 
@@ -338,6 +396,7 @@ connect(pushButton, SIGNAL(objectNameChanged(QString)),
 要点:严格情况下参数个数与类型需一致;类中必须加 `Q_OBJECT` 宏;信号发射时槽立即执行,像普通函数调用一样,全部槽完成才执行发射点之后的代码。
 
 **追问**
+
 1. 不写 `Q_OBJECT` 会怎样?(信号槽机制无法工作,编译或链接报错)
 2. 一个信号连多个槽时,执行顺序如何?(按建立连接的先后顺序)
 
@@ -362,6 +421,7 @@ signals:
 信号由 moc 自动生成实现,因此不能也不必自己写函数体;信号没有 `public/protected/private` 概念,名称建议贴合含义(如“按钮文本改变后”)。
 
 **追问**
+
 1. 发射信号用什么关键字?(`emit`,如 `emit pushButtonTextChanged();`)
 2. 信号能有返回值吗?(不能)
 
@@ -404,6 +464,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
 槽特点:① 可以是任何成员函数、普通全局函数、静态函数;② 与信号的参数和返回值要一致。本例流程:点击按钮 → `pushButtonClicked()` → 发射 `pushButtonTextChanged()` → `changeButtonText()` 响应,按钮文本变为“被点击了!”。
 
 **追问**
+
 1. 槽函数只声明不实现行吗?(不行,编译报错)
 2. `QPushButton` 还有哪些构造重载?(`QPushButton(const QString &text, QWidget *parent)`、`QPushButton(const QIcon &icon, const QString &text, QWidget *parent)`)
 
@@ -427,6 +488,7 @@ disconnect(myObject, 0, myReceiver, 0);          /* 等价 myObject->disconnect(
 初学者一般很少主动断开连接;但动态创建/销毁对象时要注意避免悬空连接。
 
 **追问**
+
 1. Qt4 与 Qt5 的 connect 写法有何区别?(Qt5 支持函数指针语法,编译期检查,更安全;Qt4 依赖 `SIGNAL/SLOT` 字符串,运行期解析)
 2. 对象被销毁后其连接会自动清理吗?(QObject 析构时会自动断开相关连接)
 
@@ -440,18 +502,19 @@ disconnect(myObject, 0, myReceiver, 0);          /* 等价 myObject->disconnect(
 
 **详细解答**:
 
-| 控件 | 特点 | 常用信号 |
-| ---- | ---- | -------- |
-| QPushButton | 普通按钮,可显示文本/图标 | `clicked()` |
-| QToolButton | 工具按钮,常用于工具栏,可带下拉菜单 | `clicked()` |
-| QRadioButton | 单选按钮,同组互斥 | `toggled(bool)` |
-| QCheckBox | 复选按钮,支持三态 | `stateChanged(int)` |
-| QCommandLinkButton | 命令链接按钮,Vista 风格 | `clicked()` |
-| QDialogButtonBox | 对话框标准按钮集合 | `clicked(QAbstractButton*)` |
+| 控件               | 特点                                 | 常用信号                    |
+| ------------------ | ------------------------------------ | --------------------------- |
+| QPushButton        | 普通按钮,可显示文本/图标            | `clicked()`                 |
+| QToolButton        | 工具按钮,常用于工具栏,可带下拉菜单 | `clicked()`                 |
+| QRadioButton       | 单选按钮,同组互斥                   | `toggled(bool)`             |
+| QCheckBox          | 复选按钮,支持三态                   | `stateChanged(int)`         |
+| QCommandLinkButton | 命令链接按钮,Vista 风格             | `clicked()`                 |
+| QDialogButtonBox   | 对话框标准按钮集合                   | `clicked(QAbstractButton*)` |
 
 常用方法:`setText()`、`setIcon()`、`setCheckable()`、`setChecked()`、`setEnabled()`、`setMinimumSize()`。
 
 **追问**
+
 1. `setCheckable(true)` 有什么效果?(按钮变为可选中/取消的切换状态,如“开始/关闭”)
 2. 单选按钮如何分组保证互斥?(放在同一父容器或用 `QButtonGroup`)
 
@@ -461,18 +524,19 @@ disconnect(myObject, 0, myReceiver, 0);          /* 等价 myObject->disconnect(
 
 **详细解答**:
 
-| 控件 | 用途 | 常用方法 |
-| ---- | ---- | -------- |
-| QComboBox | 下拉选择 | `addItem()`、`addItems()`、`setCurrentIndex()`、`currentText()` |
-| QLineEdit | 单行输入 | `setText()`、`text()`、`setEchoMode()`、`setPlaceholderText()` |
-| QTextEdit | 多行富文本 | `setPlainText()`、`toPlainText()`、`insertPlainText()` |
-| QPlainTextEdit | 多行纯文本 | `setPlainText()`、`appendPlainText()` |
-| QSpinBox | 整数微调 | `setRange()`、`setValue()`、`value()`、`valueChanged(int)` |
-| QDoubleSpinBox | 浮点微调 | 同上 |
-| QDateTimeEdit | 日期时间 | `setDateTime()`、`dateTime()` |
-| QDial / QSlider | 旋钮 / 滑块 | `setValue()`、`valueChanged(int)` |
+| 控件            | 用途        | 常用方法                                                        |
+| --------------- | ----------- | --------------------------------------------------------------- |
+| QComboBox       | 下拉选择    | `addItem()`、`addItems()`、`setCurrentIndex()`、`currentText()` |
+| QLineEdit       | 单行输入    | `setText()`、`text()`、`setEchoMode()`、`setPlaceholderText()`  |
+| QTextEdit       | 多行富文本  | `setPlainText()`、`toPlainText()`、`insertPlainText()`          |
+| QPlainTextEdit  | 多行纯文本  | `setPlainText()`、`appendPlainText()`                           |
+| QSpinBox        | 整数微调    | `setRange()`、`setValue()`、`value()`、`valueChanged(int)`      |
+| QDoubleSpinBox  | 浮点微调    | 同上                                                            |
+| QDateTimeEdit   | 日期时间    | `setDateTime()`、`dateTime()`                                   |
+| QDial / QSlider | 旋钮 / 滑块 | `setValue()`、`valueChanged(int)`                               |
 
 **追问**
+
 1. `QLineEdit` 密码框怎么实现?(`setEchoMode(QLineEdit::Password)`)
 2. `QComboBox` 如何给选项附带数据?(`addItem(text, userData)` + `currentData()`)
 
@@ -495,6 +559,7 @@ bar->setValue(50);                    /* 更新到 50% */
 `QLCDNumber` 显示数字风格的数值,`QCalendarWidget` 提供日历,`QTextBrowser` 支持富文本浏览(带链接),`QGraphicsView` 用于图形场景显示。
 
 **追问**
+
 1. `QLabel` 的自动换行怎么设置?(`setWordWrap(true)`)
 2. 图像资源为什么要放进 `.qrc`?(编译进可执行文件,运行时用 `:/` 前缀访问,避免路径问题)
 
@@ -520,6 +585,7 @@ tabWidget->setCurrentIndex(0);
 `QGroupBox` 用于给一组控件加标题边框;`QScrollArea` 提供滚动区域(内容大于可视区时使用);`QToolBox` 是抽屉式分组;`QTabWidget` 是标签页;`QStackedWidget` 是“同一时间只显示一页”的堆叠容器。
 
 **追问**
+
 1. `QStackedWidget` 与 `QTabWidget` 的本质关系?(`QTabWidget` 内部基于堆叠布局,多了标签栏)
 2. 内容超出窗口时用什么容器?(`QScrollArea`)
 
@@ -529,16 +595,17 @@ tabWidget->setCurrentIndex(0);
 
 **详细解答**:
 
-| 维度 | 基于模型(View) | 基于项(Widget) |
-| ---- | ---------------- | ---------------- |
-| 代表 | QListView、QTreeView、QTableView | QListWidget、QTreeWidget、QTableWidget |
-| 数据来源 | 外部 Model(如 QStandardItemModel) | 内部自带模型 |
-| 操作方式 | `setModel()` | `addItem()`、`setItem()` |
-| 适用 | 大数据、复杂结构、多视图共享 | 中小数据、快速开发 |
+| 维度     | 基于模型(View)                    | 基于项(Widget)                       |
+| -------- | ----------------------------------- | -------------------------------------- |
+| 代表     | QListView、QTreeView、QTableView    | QListWidget、QTreeWidget、QTableWidget |
+| 数据来源 | 外部 Model(如 QStandardItemModel) | 内部自带模型                           |
+| 操作方式 | `setModel()`                        | `addItem()`、`setItem()`               |
+| 适用     | 大数据、复杂结构、多视图共享        | 中小数据、快速开发                     |
 
 `QListWidget` 继承 `QListView`,`QTreeWidget` 继承 `QTreeView`,`QTableWidget` 继承 `QTableView`——即“控件版”是“视图版”的便捷封装。
 
 **追问**
+
 1. 一般处理大数据用哪种?(基于模型的 View,避免为每项创建对象)
 2. 列表项如何放置自定义控件?(`QListWidget::setItemWidget(item, widget)`)
 
@@ -550,14 +617,15 @@ tabWidget->setCurrentIndex(0);
 
 **详细解答**:绝对定位(`setGeometry`/`move`)写死后,窗口缩放或换屏幕分辨率时界面就错乱;嵌入式设备分辨率各异,布局管理是必须。常见布局:
 
-| 布局 | 排列方式 | 典型场景 |
-| ---- | -------- | -------- |
-| QHBoxLayout | 水平 | 一排按钮、标签+输入框 |
-| QVBoxLayout | 垂直 | 上下分区 |
-| QGridLayout | 网格 | 串口/CAN 参数表 |
-| QFormLayout | 两列表单 | 登录、设置表单 |
+| 布局        | 排列方式 | 典型场景              |
+| ----------- | -------- | --------------------- |
+| QHBoxLayout | 水平     | 一排按钮、标签+输入框 |
+| QVBoxLayout | 垂直     | 上下分区              |
+| QGridLayout | 网格     | 串口/CAN 参数表       |
+| QFormLayout | 两列表单 | 登录、设置表单        |
 
 **追问**
+
 1. 嵌入式里界面的基准分辨率教材用多少?(800×480)
 2. 布局能嵌套吗?(能,布局内可再 `addLayout`,实现复杂界面)
 
@@ -588,6 +656,7 @@ hBoxLayout->addSpacerItem(hSpacer);
 `QSpacerItem` 继承 `QLayoutItem`,含水平/垂直间隔,用来把控件挤向一侧或在控件之间留白。
 
 **追问**
+
 1. `addStretch()` 与 `QSpacerItem` 的区别?(前者是快捷方式,本质也是插入可伸缩间隔)
 2. `setContentsMargins` 与 `setSpacing` 分别控制什么?(布局四周外边距 / 相邻控件间距)
 
@@ -614,6 +683,7 @@ tableView->setModel(model);                       /* 视图绑定模型 */
 好处是数据与显示分离:同一份 Model 可被 List/Tree/Table 多种 View 共享,也可加 `QSortFilterProxyModel` 做排序过滤。
 
 **追问**
+
 1. Delegate 的作用?(决定每一项如何绘制、如何进入编辑状态,如自定义进度条单元格)
 2. 视图只读怎么设置?(`setEditTriggers(QAbstractItemView::NoEditTriggers)`)
 
@@ -649,6 +719,7 @@ treeView->setModel(treeModel);
 ```
 
 **追问**
+
 1. `QTreeView` 如何展开全部节点?(`expandAll()`)
 2. `QTableView` 如何隐藏网格线?(`setShowGrid(false)`)
 
@@ -679,6 +750,7 @@ listWidget->setItemWidget(item, customWidget);
 基于项控件本质是“封装好模型的视图”,操作直观;数据量大时应改用 View + Model。
 
 **追问**
+
 1. `QTableWidget` 如何让表格自动适应内容?(`resizeColumnsToContents()`)
 2. `QListWidget` 单击切换页面用什么信号?(`currentRowChanged(int)` 连 `QStackedWidget::setCurrentIndex`)
 
@@ -701,18 +773,19 @@ file.write("1");
 file.close();
 ```
 
-| 模式 | 含义 |
-| ---- | ---- |
-| ReadOnly | 只读 |
-| WriteOnly | 只写 |
-| ReadWrite | 读写 |
-| Append | 追加到尾部 |
-| Truncate | 清空原内容 |
-| Text | 文本模式,`\n` 自动翻译 |
+| 模式      | 含义                    |
+| --------- | ----------------------- |
+| ReadOnly  | 只读                    |
+| WriteOnly | 只写                    |
+| ReadWrite | 读写                    |
+| Append    | 追加到尾部              |
+| Truncate  | 清空原内容              |
+| Text      | 文本模式,`\n` 自动翻译 |
 
 **关键点**:无论 Windows、Linux 还是 Mac,`QFile` 的分隔符都是 `/`,不要用 `\`。
 
 **追问**
+
 1. 嵌入式里为什么常用 QFile 直接操作 `/sys` 节点?(Linux 一切皆文件,无需驱动即可控制 GPIO/LED)
 2. 如何读取全部内容?(`file.readAll()`,文本模式配合 `QTextStream::readAll()`)
 
@@ -739,6 +812,7 @@ file.close();
 `QFile` 更底层(`read/write/readLine` 字节级),`QTextStream` 面向文本,提供 `<<`/`>>`、`readLine()`、编码与格式化选项。需要逐行处理或格式化输出时优先用它。
 
 **追问**
+
 1. `QTextStream` 能操作 `QString`/`QByteArray` 吗?(能,它可在 QIODevice、QByteArray、QString 上操作)
 2. 读一行用什么?(`stream.readLine()`)
 
@@ -762,6 +836,7 @@ void Widget::paintEvent(QPaintEvent *)
 - `paintEvent()` 在父类中定义为空,专供子类画图;构造对象时会执行一次,之后要靠 `update()` 触发重绘。
 
 **追问**
+
 1. 为什么动画要用定时器 + `update()`?(`paintEvent` 不会自动反复执行)
 2. 抗锯齿怎么开?(`setRenderHints(QPainter::Antialiasing, true)`)
 
@@ -791,6 +866,7 @@ painter.drawRect(rect.toRect());
 配合 `QTimer` 每 100ms `update()` 一次,`angle++` 到 360 归零,CD 就转起来了。
 
 **追问**
+
 1. `QRectF` 与 `QRect` 的区别?(前者浮点坐标,绘图更精确)
 2. 如何只旋转某次绘制而不影响后续?(`painter.save()` / `painter.restore()`)
 
@@ -816,6 +892,7 @@ void MainWindow::receivedData(int value)
 ```
 
 **追问**
+
 1. 折线图用什么类?(`QLineSeries`)
 2. 坐标轴如何绑定曲线?(`splineSeries->attachAxis(axisX/axisY)`)
 
@@ -829,12 +906,13 @@ void MainWindow::receivedData(int value)
 
 **详细解答**:嵌入式 CPU 主频低,摄像头拍照、图像处理、文件读写都可能卡界面。两种方式区别:
 
-| 方式 | 新线程中执行的代码 | 灵活度 |
-| ---- | ------------------ | ------ |
-| 继承 QThread | 只有 `run()` 在新线程,其它方法仍在主线程 | 一般 |
-| 继承 QObject + moveToThread | 整个对象的方法(槽函数)都在新线程 | 更灵活 |
+| 方式                        | 新线程中执行的代码                        | 灵活度 |
+| --------------------------- | ----------------------------------------- | ------ |
+| 继承 QThread                | 只有 `run()` 在新线程,其它方法仍在主线程 | 一般   |
+| 继承 QObject + moveToThread | 整个对象的方法(槽函数)都在新线程        | 更灵活 |
 
 **追问**
+
 1. 继承 QThread 时哪部分在新线程?(只有重写的 `run()`)
 2. 如何避免忘记解锁?(用 `QMutexLocker`,作用域结束自动解锁)
 
@@ -872,6 +950,7 @@ MainWindow::~MainWindow() {
 ```
 
 **追问**
+
 1. `start()` 与直接调用 `run()` 有何区别?(`start()` 创建新线程并调用 `run()`;直接调用等于在主线程执行)
 2. `wait()` 的作用?(阻塞等待线程结束,可带超时)
 
@@ -919,6 +998,7 @@ connect(this, SIGNAL(startWork(QString)), worker, SLOT(doWork1(QString)));
 `quit()/exit()` 不会中途终止线程,`terminate()` 不安全。加锁虽然损耗性能,但能避免访问冲突。
 
 **追问**
+
 1. 为什么 `stopWork()` 不能做成槽?(否则它会被排到新线程的事件队列里,无法及时打断)
 2. 线程结束后如何释放对象?(`deleteLater()` 在事件循环空闲时安全销毁)
 
@@ -946,6 +1026,7 @@ foreach (QNetworkInterface interface, list) {
 使用网络需在 `.pro` 加 `QT += network`(嵌入式视频监控、云端 API 都用到)。
 
 **追问**
+
 1. `QHostInfo::fromName()` 与 `localHostName()` 区别?(前者按名字解析地址,后者取本机名)
 2. 访问一个 URL 用哪个类?(`QNetworkAccessManager::get/post`)
 
@@ -982,6 +1063,7 @@ connect(&udpSocket, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
 ```
 
 **追问**
+
 1. UDP 单播与广播的区别?(单播发给指定 IP,广播发给同一网段所有主机)
 2. 组播要额外做什么?(接收端 `joinMulticastGroup()` 加入组播组)
 
@@ -995,14 +1077,15 @@ connect(&udpSocket, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
 
 **详细解答**:Qt5 起弃用 phonon,改用 Qt Multimedia 模块,`pro` 加 `QT += multimedia`。Ubuntu 需安装 GStreamer 解码插件,Windows 需安装 LAVFilters。
 
-| 类 | 播放源 | 特点 |
-| -- | ------ | ---- |
-| QSound | 本地 WAV | 异步、低延时、体积大,适合短提示音 |
-| QSoundEffect | 本地或网络 | 支持 `QUrl` 网络文件 |
+| 类           | 播放源     | 特点                               |
+| ------------ | ---------- | ---------------------------------- |
+| QSound       | 本地 WAV   | 异步、低延时、体积大,适合短提示音 |
+| QSoundEffect | 本地或网络 | 支持 `QUrl` 网络文件               |
 
 播放音效示例:`QSound::play(":/audio/bell.wav");`
 
 **追问**
+
 1. 嵌入式为什么常用 WAV 做按键音?(低延时、无解码负担)
 2. 播放器为何要装解码插件?(Qt 只提供接口,实际解码由平台插件完成)
 
@@ -1033,6 +1116,7 @@ recorder->stop();                        /* 停止录音 */
 播放器还可通过 `QMediaPlaylist` 管理多首歌曲,或用 `setPlaybackMode` 设置循环。
 
 **追问**
+
 1. 视频画面为什么不能直接贴到普通 `QLabel` 上?(需要 `QVideoWidget` 作为渲染目标)
 2. 录音保存 wav 要设置哪个属性?(容器格式设为 `audio/x-wav`)
 
@@ -1065,6 +1149,7 @@ view->setModel(model);
 ```
 
 **追问**
+
 1. `QSqlQuery` 与 `QSqlTableModel` 各自适合什么场景?(前者适合精确 SQL 操作,后者适合表格化显示与编辑)
 2. 想只读展示查询结果用什么?(`QSqlQueryModel`)
 
@@ -1086,15 +1171,16 @@ scp 01_led root@192.168.1.100:/home/root/
 ./01_led -platform linuxfb    # Linux Framebuffer(无 GPU)
 ```
 
-| 插件 | 依赖 | 场景 |
-| ---- | ---- | ---- |
-| linuxfb | `/dev/fb0` | 无 GPU、直接帧缓冲 |
-| eglfs | EGL/OpenGL ES | 有 GPU,I.MX6ULL 出厂系统常用 |
-| xcb | X Server | Ubuntu 桌面上仿真 |
+| 插件    | 依赖          | 场景                          |
+| ------- | ------------- | ----------------------------- |
+| linuxfb | `/dev/fb0`    | 无 GPU、直接帧缓冲            |
+| eglfs   | EGL/OpenGL ES | 有 GPU,I.MX6ULL 出厂系统常用 |
+| xcb     | X Server      | Ubuntu 桌面上仿真             |
 
 > ⚠️ **来源说明**:平台插件部分不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
 
 **追问**
+
 1. 交叉编译后为什么常要拷贝可执行文件到板子运行?(远程运行时环境变量/路径可能不一致)
 2. 先在哪里验证界面逻辑再交叉编译?(Ubuntu 桌面套件仿真)
 
@@ -1112,6 +1198,7 @@ scp 01_led root@192.168.1.100:/home/root/
 综合实例(智能家居物联网):ESP8266 通过串口用 AT 指令接入 WIFI 与原子云 → 定时发心跳;控制端用 `QNetworkAccessManager` 走 HTTPS 拿设备列表与 `token`,用 `QWebSocket` 鉴权并订阅消息;点开关时下发 `0x03` 命令,云端转发到设备,设备端收到“开/关”后写 LED 节点。整个链路把**串口 + 网络 + 硬件控制 + 信号槽 + 多模块协作**都串了起来。
 
 **追问**
+
 1. 摄像头/视频监控为什么必须开子线程?(采集与编码耗时会卡主线程)
 2. 为什么 Qt 蓝牙只能在 Linux/BlueZ 上跑?(依赖 BlueZ 协议栈,Windows 需自行移植)
 

+ 52 - 52
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/PLAN.md

@@ -7,71 +7,71 @@
 
 ### 01-Linux应用编程基础(6)
 
-| 文件 | PDF | 例程 |
-| ---- | --- | ---- |
-| 01-应用编程概念与开发环境 | 第一章 应用编程概念 | — |
-| 02-文件IO基础与深入探究 | 第二章 文件I_O基础、第三章 深入探究文件I_O | — |
-| 03-标准IO库 | 第四章 标准I_O库 | — |
-| 04-文件属性与目录 | 第五章 文件属性与目录 | — |
-| 05-字符串处理与系统信息 | 第六章 字符串处理、第七章 系统信息与系统资源 | — |
-| 面试-Linux应用编程基础 | — | — |
+| 文件                      | PDF                                          | 例程 |
+| ------------------------- | -------------------------------------------- | ---- |
+| 01-应用编程概念与开发环境 | 第一章 应用编程概念                          | —    |
+| 02-文件IO基础与深入探究   | 第二章 文件I_O基础、第三章 深入探究文件I_O   | —    |
+| 03-标准IO库               | 第四章 标准I_O库                             | —    |
+| 04-文件属性与目录         | 第五章 文件属性与目录                        | —    |
+| 05-字符串处理与系统信息   | 第六章 字符串处理、第七章 系统信息与系统资源 | —    |
+| 面试-Linux应用编程基础    | —                                            | —    |
 
 ### 02-进程线程与IPC(5)
 
-| 文件 | PDF |
-| ---- | --- |
-| 01-信号机制 | 第八章 信号:基础 |
-| 02-进程管理 | 第九章 进程 |
-| 03-进程间通信 | 第十章 进程间通信简介 |
-| 04-线程与线程同步 | 第十一章 线程、第十二章 线程同步 |
-| 面试-进程线程与IPC | — |
+| 文件               | PDF                              |
+| ------------------ | -------------------------------- |
+| 01-信号机制        | 第八章 信号:基础                |
+| 02-进程管理        | 第九章 进程                      |
+| 03-进程间通信      | 第十章 进程间通信简介            |
+| 04-线程与线程同步  | 第十一章 线程、第十二章 线程同步 |
+| 面试-进程线程与IPC | —                                |
 
 ### 03-外设与高级IO编程(7)
 
-| 文件 | PDF | 例程 |
-| ---- | --- | ---- |
-| 01-高级IO | 第十三章 高级I_O | — |
-| 02-GPIO与LED应用编程 | 第十五章 点亮LED、第十六章 GPIO应用编程 | 15_led、16_gpio |
-| 03-输入设备与tslib | 第十七章 输入设备应用编程、第十八章 使用tslib库 | 17_input、18_tslib |
-| 04-FrameBuffer与LCD应用编程 | 第十九~二十三章 | 19_lcd、20_libjpeg、21_libpng、22_lcd_vertical_display、23_freetype |
-| 05-PWM与看门狗 | 第二十四章 PWM、第二十七章 看门狗 | 24_pwm、27_watchdog |
-| 06-摄像头串口与音频 | 第二十五章 V4L2、第二十六章 串口、第二十八章 音频 | 25_v4l2_camera、26_uart、28_alsa-lib |
-| 面试-外设与高级IO编程 | — | — |
+| 文件                        | PDF                                               | 例程                                                                |
+| --------------------------- | ------------------------------------------------- | ------------------------------------------------------------------- |
+| 01-高级IO                   | 第十三章 高级I_O                                  | —                                                                   |
+| 02-GPIO与LED应用编程        | 第十五章 点亮LED、第十六章 GPIO应用编程           | 15_led、16_gpio                                                     |
+| 03-输入设备与tslib          | 第十七章 输入设备应用编程、第十八章 使用tslib库   | 17_input、18_tslib                                                  |
+| 04-FrameBuffer与LCD应用编程 | 第十九~二十三章                                   | 19_lcd、20_libjpeg、21_libpng、22_lcd_vertical_display、23_freetype |
+| 05-PWM与看门狗              | 第二十四章 PWM、第二十七章 看门狗                 | 24_pwm、27_watchdog                                                 |
+| 06-摄像头串口与音频         | 第二十五章 V4L2、第二十六章 串口、第二十八章 音频 | 25_v4l2_camera、26_uart、28_alsa-lib                                |
+| 面试-外设与高级IO编程       | —                                                 | —                                                                   |
 
 ### 04-网络编程与项目实战(5)
 
-| 文件 | PDF | 例程 |
-| ---- | --- | ---- |
-| 01-网络基础与socket编程 | 第二十九章 网络基础知识、第三十章 socket编程基础 | 30_socket |
-| 02-CAN应用编程 | 第三十一章 CAN应用编程基础 | 31_can |
-| 03-CMake入门与进阶 | 第三十二章 CMake入门与进阶 | — |
-| 04-实战项目MQTT与视频监控 | 第三十三章 MQTT物联网、第三十四章 视频监控 | 33_mqtt |
-| 面试-网络编程与项目实战 | — | — |
+| 文件                      | PDF                                              | 例程      |
+| ------------------------- | ------------------------------------------------ | --------- |
+| 01-网络基础与socket编程   | 第二十九章 网络基础知识、第三十章 socket编程基础 | 30_socket |
+| 02-CAN应用编程            | 第三十一章 CAN应用编程基础                       | 31_can    |
+| 03-CMake入门与进阶        | 第三十二章 CMake入门与进阶                       | —         |
+| 04-实战项目MQTT与视频监控 | 第三十三章 MQTT物联网、第三十四章 视频监控       | 33_mqtt   |
+| 面试-网络编程与项目实战   | —                                                | —         |
 
 ### 05-Qt应用开发(9)
 
-| 文件 | Qt指南章节 | 例程 |
-| ---- | ---------- | ---- |
-| 01-C++基础与面向对象 | 第一、二章 | Embedded-Qt-Tutorial/C++/01~11 |
-| 02-Qt入门与项目结构 | 第三、四章 | Qt/01/01_hello_world、02_designer_example |
-| 03-信号与槽 | 第五、六章 | Qt/01/03_signal_slot_example |
-| 04-Qt控件详解 | 第七章(按钮/输入/显示窗口部件) | Qt/01/04~28 |
-| 05-布局容器与模型视图 | 第七章(布局/容器/项目视图组/项目控件组) | Qt/01/29~47 |
-| 06-文本绘图与图表 | 第八、九章 | Qt/02/01~04 |
-| 07-多线程网络多媒体与数据库 | 第十~十三章 | Qt/02/05~18 |
-| 08-嵌入式Qt交叉编译与项目实战 | 第十四~二十八章 | Qt/03、Qt/04、09综合例程 |
-| 面试-Qt应用开发 | — | — |
+| 文件                          | Qt指南章节                                | 例程                                      |
+| ----------------------------- | ----------------------------------------- | ----------------------------------------- |
+| 01-C++基础与面向对象          | 第一、二章                                | Embedded-Qt-Tutorial/C++/01~11            |
+| 02-Qt入门与项目结构           | 第三、四章                                | Qt/01/01_hello_world、02_designer_example |
+| 03-信号与槽                   | 第五、六章                                | Qt/01/03_signal_slot_example              |
+| 04-Qt控件详解                 | 第七章(按钮/输入/显示窗口部件)          | Qt/01/04~28                               |
+| 05-布局容器与模型视图         | 第七章(布局/容器/项目视图组/项目控件组) | Qt/01/29~47                               |
+| 06-文本绘图与图表             | 第八、九章                                | Qt/02/01~04                               |
+| 07-多线程网络多媒体与数据库   | 第十~十三章                               | Qt/02/05~18                               |
+| 08-嵌入式Qt交叉编译与项目实战 | 第十四~二十八章                           | Qt/03、Qt/04、09综合例程                  |
+| 面试-Qt应用开发               | —                                         | —                                         |
 
 ## 跨库交叉引用(旧笔记仅作「延伸阅读」,不修改不删除)
 
-| 新 KB 篇 | 延伸阅读 |
-| -------- | -------- |
-| 01/02-文件IO基础与深入探究 | 系统编程/2. 系统调用IO、3. 文件描述符操作 |
-| 01/03-标准IO库 | 系统编程/1. C库文件IO |
-| 01/04-文件属性与目录 | 系统编程/4. 目录操作与文件属性 |
-| 02/01-信号机制 | 系统编程/6. 信号机制 |
-| 02/02-进程管理 | 系统编程/5. 进程创建与管理、7. 进程控制、8. 守护进程 |
-| 02/03-进程间通信 | 系统编程/19. 管道、20. 共享内存与mmap、21. 消息队列、22. 信号量 |
-| 02/04-线程与线程同步 | 系统编程/9~12;深入 → 13. 线程池模式 |
+| 新 KB 篇                   | 延伸阅读                                                                                         |
+| -------------------------- | ------------------------------------------------------------------------------------------------ |
+| 01/02-文件IO基础与深入探究 | 系统编程/2. 系统调用IO、3. 文件描述符操作                                                        |
+| 01/03-标准IO库             | 系统编程/1. C库文件IO                                                                            |
+| 01/04-文件属性与目录       | 系统编程/4. 目录操作与文件属性                                                                   |
+| 02/01-信号机制             | 系统编程/6. 信号机制                                                                             |
+| 02/02-进程管理             | 系统编程/5. 进程创建与管理、7. 进程控制、8. 守护进程                                             |
+| 02/03-进程间通信           | 系统编程/19. 管道、20. 共享内存与mmap、21. 消息队列、22. 信号量                                  |
+| 02/04-线程与线程同步       | 系统编程/9~12;深入 → 13. 线程池模式                                                             |
 | 04/01-网络基础与socket编程 | 系统编程/14. socket网络编程基础、15. TCP编程、16. UDP编程;深入 → 17. IO多路复用、18. 高并发模型 |
-| 05/01~08 | 编程学习项目/使用Qt和C++构造UI界面/* |
+| 05/01~08                   | 编程学习项目/使用Qt和C++构造UI界面/*                                                             |

+ 13 - 13
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/README.md

@@ -12,11 +12,11 @@ updated: 2026-09-18
 
 ## 与「驱动开发实战」及既有笔记的关系
 
-| 知识库 | 定位 | 路径 |
-| ------ | ---- | ---- |
-| 嵌入式Linux驱动开发实战 | 起点:ARM 裸机 → 驱动 → 系统移植 | `raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/` |
-| **嵌入式Linux应用与Qt开发实战(本库)** | 用户态应用编程 → 外设应用 → 网络 → Qt | `raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/` |
-| Linux+C+C++技术体系梳理 | 语言与系统编程「深入专题」(延伸阅读) | `raw/Joplin/Linux+C+C++技术体系梳理/` |
+| 知识库                                  | 定位                                   | 路径                                                   |
+| --------------------------------------- | -------------------------------------- | ------------------------------------------------------ |
+| 嵌入式Linux驱动开发实战                 | 起点:ARM 裸机 → 驱动 → 系统移植       | `raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/`     |
+| **嵌入式Linux应用与Qt开发实战(本库)** | 用户态应用编程 → 外设应用 → 网络 → Qt  | `raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/` |
+| Linux+C+C++技术体系梳理                 | 语言与系统编程「深入专题」(延伸阅读) | `raw/Joplin/Linux+C+C++技术体系梳理/`                  |
 
 > 本库每篇内容自包含,**不依赖**延伸笔记即可学习;延伸笔记仅在各篇「关联知识 / 延伸阅读」中提供进一步深入的入口。
 
@@ -79,14 +79,14 @@ C应用编程基础 → 进程线程与IPC → 外设与高级IO → 网络与
 
 ## 跨平台资源
 
-| 知识领域 | 本项目 | 已有资源 |
-| -------- | ------ | -------- |
-| 系统编程深入 | 01/02 模块 | [[Linux+C+C++技术体系梳理/2. Linux系统编程/2. 系统调用IO]] |
-| 并发与锁 | 02 模块 | [[Linux+C+C++技术体系梳理/2. Linux系统编程/12. 读写锁与自旋锁]]、[[嵌入式Linux驱动开发实战/03-Linux驱动开发核心/04-并发同步与原子操作]] |
-| 网络编程 | 04 模块 | [[Linux+C+C++技术体系梳理/2. Linux系统编程/15. TCP编程]] |
-| Qt 界面开发 | 05 模块 | [[编程学习项目/使用Qt和C++构造UI界面/4. 信号和槽机制]] |
-| 驱动与硬件 | — | [[嵌入式Linux驱动开发实战/README]] |
-| STM32 嵌入式 | — | [[STM32学习笔记/01-STM32概述与开发环境搭建]] |
+| 知识领域     | 本项目     | 已有资源                                                                                                                                |
+| ------------ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------- |
+| 系统编程深入 | 01/02 模块 | [[Linux+C+C++技术体系梳理/2. Linux系统编程/2. 系统调用IO]]                                                                              |
+| 并发与锁     | 02 模块    | [[Linux+C+C++技术体系梳理/2. Linux系统编程/12. 读写锁与自旋锁]]、[[嵌入式Linux驱动开发实战/03-Linux驱动开发核心/04-并发同步与原子操作]] |
+| 网络编程     | 04 模块    | [[Linux+C+C++技术体系梳理/2. Linux系统编程/15. TCP编程]]                                                                                |
+| Qt 界面开发  | 05 模块    | [[编程学习项目/使用Qt和C++构造UI界面/4. 信号和槽机制]]                                                                                  |
+| 驱动与硬件   | —          | [[嵌入式Linux驱动开发实战/README]]                                                                                                      |
+| STM32 嵌入式 | —          | [[STM32学习笔记/01-STM32概述与开发环境搭建]]                                                                                            |
 
 ## 使用说明
 

+ 47 - 47
X-Knowledge-Base/wiki/index.md

@@ -221,60 +221,60 @@ Wiki 页面:
 
 ### 01-Linux应用编程基础(6 篇)
 
-| 编号 | 主题 | 核心内容 |
-| ---- | ---- | -------- |
-| 01 | 应用编程概念与开发环境 | 系统调用 vs 库函数、main、errno、gcc 编译流程 |
-| 02 | 文件IO基础与深入探究 | fd、open/read/write/lseek、文件共享、原子操作、fcntl/ioctl |
-| 03 | 标准IO库 | FILE、fopen/fread/fwrite、格式化 IO、缓冲机制 |
-| 04 | 文件属性与目录 | stat、权限、硬软链接、目录遍历 |
-| 05 | 字符串处理与系统信息 | str*/mem* 函数族、正则、时间日期、proc 文件系统 |
-| 面试 | 面试-Linux应用编程基础 | 30 道题:概念/文件IO/标准IO/属性/字符串/系统信息 |
+| 编号 | 主题                   | 核心内容                                                   |
+| ---- | ---------------------- | ---------------------------------------------------------- |
+| 01   | 应用编程概念与开发环境 | 系统调用 vs 库函数、main、errno、gcc 编译流程              |
+| 02   | 文件IO基础与深入探究   | fd、open/read/write/lseek、文件共享、原子操作、fcntl/ioctl |
+| 03   | 标准IO库               | FILE、fopen/fread/fwrite、格式化 IO、缓冲机制              |
+| 04   | 文件属性与目录         | stat、权限、硬软链接、目录遍历                             |
+| 05   | 字符串处理与系统信息   | str*/mem* 函数族、正则、时间日期、proc 文件系统            |
+| 面试 | 面试-Linux应用编程基础 | 30 道题:概念/文件IO/标准IO/属性/字符串/系统信息           |
 
 ### 02-进程线程与IPC(5 篇)
 
-| 编号 | 主题 | 核心内容 |
-| ---- | ---- | -------- |
-| 01 | 信号机制 | 信号分类、signal/sigaction、信号集、sigsuspend |
-| 02 | 进程管理 | fork/vfork/exec、wait、僵尸孤儿、守护进程、单例 |
-| 03 | 进程间通信 | 管道/FIFO、消息队列、信号量、共享内存 |
-| 04 | 线程与线程同步 | pthread、互斥锁、条件变量、读写锁、自旋锁、生产者消费者 |
-| 面试 | 面试-进程线程与IPC | 30 道题:信号/进程/IPC/线程同步 |
+| 编号 | 主题               | 核心内容                                                |
+| ---- | ------------------ | ------------------------------------------------------- |
+| 01   | 信号机制           | 信号分类、signal/sigaction、信号集、sigsuspend          |
+| 02   | 进程管理           | fork/vfork/exec、wait、僵尸孤儿、守护进程、单例         |
+| 03   | 进程间通信         | 管道/FIFO、消息队列、信号量、共享内存                   |
+| 04   | 线程与线程同步     | pthread、互斥锁、条件变量、读写锁、自旋锁、生产者消费者 |
+| 面试 | 面试-进程线程与IPC | 30 道题:信号/进程/IPC/线程同步                         |
 
 ### 03-外设与高级IO编程(7 篇)
 
-| 编号 | 主题 | 核心内容 |
-| ---- | ---- | -------- |
-| 01 | 高级IO | 非阻塞 IO、select/poll/epoll、异步 IO、mmap、文件锁 |
-| 02 | GPIO与LED应用编程 | sysfs GPIO、LED 子系统、ioctl 控制 LED |
-| 03 | 输入设备与tslib | input 子系统、input_event、tslib 校准与读取 |
-| 04 | FrameBuffer与LCD应用编程 | fb 结构体、mmap 显存、画笔、libjpeg/libpng、FreeType |
-| 05 | PWM与看门狗 | PWM sysfs、占空比、/dev/watchdog、WDIOC_* |
-| 06 | 摄像头串口与音频 | V4L2 采集、termios 串口、ALSA 放音录音 |
-| 面试 | 面试-外设与高级IO编程 | 30 道题:高级IO/GPIO/输入/LCD/PWM/摄像头串口音频 |
+| 编号 | 主题                     | 核心内容                                             |
+| ---- | ------------------------ | ---------------------------------------------------- |
+| 01   | 高级IO                   | 非阻塞 IO、select/poll/epoll、异步 IO、mmap、文件锁  |
+| 02   | GPIO与LED应用编程        | sysfs GPIO、LED 子系统、ioctl 控制 LED               |
+| 03   | 输入设备与tslib          | input 子系统、input_event、tslib 校准与读取          |
+| 04   | FrameBuffer与LCD应用编程 | fb 结构体、mmap 显存、画笔、libjpeg/libpng、FreeType |
+| 05   | PWM与看门狗              | PWM sysfs、占空比、/dev/watchdog、WDIOC_*            |
+| 06   | 摄像头串口与音频         | V4L2 采集、termios 串口、ALSA 放音录音               |
+| 面试 | 面试-外设与高级IO编程    | 30 道题:高级IO/GPIO/输入/LCD/PWM/摄像头串口音频     |
 
 ### 04-网络编程与项目实战(5 篇)
 
-| 编号 | 主题 | 核心内容 |
-| ---- | ---- | -------- |
-| 01 | 网络基础与socket编程 | TCP/IP、socket API、TCP/UDP、并发服务器 |
-| 02 | CAN应用编程 | CAN 帧格式、SocketCAN、can-utils |
-| 03 | CMake入门与进阶 | CMake 语法、多目录工程、交叉编译工具链 |
-| 04 | 实战项目MQTT与视频监控 | MQTT/原子云、cJSON、V4L2+编码+传输 |
-| 面试 | 面试-网络编程与项目实战 | 25 道题:网络/CAN/CMake/MQTT/视频监控 |
+| 编号 | 主题                    | 核心内容                                |
+| ---- | ----------------------- | --------------------------------------- |
+| 01   | 网络基础与socket编程    | TCP/IP、socket API、TCP/UDP、并发服务器 |
+| 02   | CAN应用编程             | CAN 帧格式、SocketCAN、can-utils        |
+| 03   | CMake入门与进阶         | CMake 语法、多目录工程、交叉编译工具链  |
+| 04   | 实战项目MQTT与视频监控  | MQTT/原子云、cJSON、V4L2+编码+传输      |
+| 面试 | 面试-网络编程与项目实战 | 25 道题:网络/CAN/CMake/MQTT/视频监控   |
 
 ### 05-Qt应用开发(9 篇)
 
-| 编号 | 主题 | 核心内容 |
-| ---- | ---- | -------- |
-| 01 | C++基础与面向对象 | 命名空间、类对象、构造析构、继承重载多态、抽象类 |
-| 02 | Qt入门与项目结构 | Qt Creator、.pro/.ui/.h/.cpp、qmake、Qt Designer |
-| 03 | 信号与槽 | 信号槽机制、moc、自定义信号槽、连接类型 |
-| 04 | Qt控件详解 | 按钮/输入/显示/浏览器类控件(27 个) |
-| 05 | 布局容器与模型视图 | 布局、容器、Model/View、QListWidget 等(20 个) |
-| 06 | 文本绘图与图表 | QFile/QTextStream、QPainter、QChart |
-| 07 | 多线程网络多媒体与数据库 | QThread、TCP/UDP、QMediaPlayer、SQLite |
-| 08 | 嵌入式Qt交叉编译与项目实战 | 交叉编译、Qt 控制 LED/串口/CAN/摄像头、5 个项目 |
-| 面试 | 面试-Qt应用开发 | 40 道题:C++/Qt基础/信号槽/控件/布局/绘图/网络/Qt嵌入式 |
+| 编号 | 主题                       | 核心内容                                                |
+| ---- | -------------------------- | ------------------------------------------------------- |
+| 01   | C++基础与面向对象          | 命名空间、类对象、构造析构、继承重载多态、抽象类        |
+| 02   | Qt入门与项目结构           | Qt Creator、.pro/.ui/.h/.cpp、qmake、Qt Designer        |
+| 03   | 信号与槽                   | 信号槽机制、moc、自定义信号槽、连接类型                 |
+| 04   | Qt控件详解                 | 按钮/输入/显示/浏览器类控件(27 个)                    |
+| 05   | 布局容器与模型视图         | 布局、容器、Model/View、QListWidget 等(20 个)         |
+| 06   | 文本绘图与图表             | QFile/QTextStream、QPainter、QChart                     |
+| 07   | 多线程网络多媒体与数据库   | QThread、TCP/UDP、QMediaPlayer、SQLite                  |
+| 08   | 嵌入式Qt交叉编译与项目实战 | 交叉编译、Qt 控制 LED/串口/CAN/摄像头、5 个项目         |
+| 面试 | 面试-Qt应用开发            | 40 道题:C++/Qt基础/信号槽/控件/布局/绘图/网络/Qt嵌入式 |
 
 ---
 
@@ -282,11 +282,11 @@ Wiki 页面:
 
 围绕 I.MX6ULL 的三套知识库,按学习阶段衔接:
 
-| 知识库 | 定位 | 入口 |
-| ------ | ---- | ---- |
-| 嵌入式Linux驱动开发实战 | 起点:ARM 裸机 → 内核 → 驱动 → 系统移植 | `raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/README.md` |
-| **嵌入式Linux应用与Qt开发实战** | 用户态应用编程 → 外设应用 → 网络 → Qt | `raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/README.md` |
-| Linux+C+C++技术体系梳理 | 语言与系统编程「深入专题」(延伸阅读) | `raw/Joplin/Linux+C+C++技术体系梳理/` |
+| 知识库                          | 定位                                    | 入口                                                            |
+| ------------------------------- | --------------------------------------- | --------------------------------------------------------------- |
+| 嵌入式Linux驱动开发实战         | 起点:ARM 裸机 → 内核 → 驱动 → 系统移植 | `raw/Joplin/嵌入式+Linux/嵌入式Linux驱动开发实战/README.md`     |
+| **嵌入式Linux应用与Qt开发实战** | 用户态应用编程 → 外设应用 → 网络 → Qt   | `raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/README.md` |
+| Linux+C+C++技术体系梳理         | 语言与系统编程「深入专题」(延伸阅读)  | `raw/Joplin/Linux+C+C++技术体系梳理/`                           |
 
 **衔接关系**
 

+ 7 - 8
X-Knowledge-Base/wiki/log.md

@@ -466,7 +466,6 @@ type: log
 
   更新 `wiki/index.md` 嵌入式章节为完整 46 篇索引
 
-
 ## 2026-09-18
 
 - `ingest`: 新建「嵌入式Linux应用与Qt开发实战」知识库(5 模块 32 篇 + README,共 34 文件 / 41,733 行)
@@ -475,13 +474,13 @@ type: log
 
   路径:`raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/`
 
-  | 模块 | 篇数 | 内容 |
-  | ---- | ---- | ---- |
-  | 01-Linux应用编程基础 | 6 | 应用编程概念、文件IO、标准IO、文件属性与目录、字符串与系统信息、面试 |
-  | 02-进程线程与IPC | 5 | 信号、进程管理、进程间通信、线程与同步、面试 |
-  | 03-外设与高级IO编程 | 7 | 高级IO、GPIO/LED、输入设备与tslib、FrameBuffer/LCD、PWM/看门狗、摄像头串口音频、面试 |
-  | 04-网络编程与项目实战 | 5 | socket、CAN、CMake、MQTT与视频监控、面试 |
-  | 05-Qt应用开发 | 9 | C++基础、Qt入门、信号槽、控件、布局与模型视图、绘图图表、多线程网络多媒体数据库、嵌入式Qt与项目、面试 |
+  | 模块                  | 篇数 | 内容                                                                                                  |
+  | --------------------- | ---- | ----------------------------------------------------------------------------------------------------- |
+  | 01-Linux应用编程基础  | 6    | 应用编程概念、文件IO、标准IO、文件属性与目录、字符串与系统信息、面试                                  |
+  | 02-进程线程与IPC      | 5    | 信号、进程管理、进程间通信、线程与同步、面试                                                          |
+  | 03-外设与高级IO编程   | 7    | 高级IO、GPIO/LED、输入设备与tslib、FrameBuffer/LCD、PWM/看门狗、摄像头串口音频、面试                  |
+  | 04-网络编程与项目实战 | 5    | socket、CAN、CMake、MQTT与视频监控、面试                                                              |
+  | 05-Qt应用开发         | 9    | C++基础、Qt入门、信号槽、控件、布局与模型视图、绘图图表、多线程网络多媒体数据库、嵌入式Qt与项目、面试 |
 
   写作规范:每篇自包含完整(原理 + Mermaid + API/命令表 + 完整可复制源码 + 实验调试 + 跨平台对比 + 面试题),教材外内容统一标注「来源说明」,不含错误指摘类注解。