Browse Source

ingest: 01-Linux应用编程基础(6篇)

OpenCode 13 hours ago
parent
commit
7d428a244c

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

@@ -0,0 +1,698 @@
+---
+title: 应用编程概念与开发环境
+tags: [嵌入式Linux, Linux应用编程, 系统调用, 库函数, glibc, gcc, 交叉编译, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第一章 应用编程概念"
+---
+
+# 应用编程概念与开发环境
+
+> 💡 **关联知识**:[[01-Linux应用编程基础/02-文件IO基础与深入探究]]、[[01-Linux应用编程基础/03-标准IO库]]、[[01-Linux应用编程基础/04-文件属性与目录]];延伸阅读:[[嵌入式Linux驱动开发实战/03-Linux驱动开发核心/01-字符设备驱动基础]]
+
+本篇是应用编程的"地图"。它不教你写具体功能,而是先回答三个绕不开的问题:**应用编程到底在和谁打交道**(内核)、**我们调用的是什么东西**(系统调用 / 库函数)、**程序从哪里开始、又在哪里结束**(main / 返回值 / errno)。最后落地到开发环境:在 Ubuntu 上用 gcc 编译,在 I.MX6ULL 上用交叉编译器编译。
+
+---
+
+## 1. 三种编程形态:裸机、驱动、应用
+
+在嵌入式 Linux 平台上做开发,代码大致落在三层上:
+
+| 形态 | 运行位置 | 运行环境 | 典型载体 | 典型产物 |
+| ---- | -------- | -------- | -------- | -------- |
+| 裸机编程 | 直接跑在硬件上 | 无操作系统 | 51、STM32 单片机 | `.hex` / `.bin` / `.elf` |
+| 驱动编程 | 内核空间(内核态) | Linux 内核 | 字符设备、平台设备驱动 | `.ko` 内核模块 |
+| 应用编程 | 用户空间(用户态) | Linux 应用层 | open/read/write 等系统调用 | 可执行文件(ELF) |
+
+三者的根本区别在于**硬件操作与用户逻辑是否隔离**、**有没有操作系统做中介**。
+
+### 1.1 用"点亮 LED"看三者差异
+
+**裸机程序**:硬件操作和用户逻辑写在同一份代码里,没有操作系统,编译后直接"裸跑"。
+
+```c
+/* LED 裸机程序:硬件操作与用户逻辑混在一起 */
+static void led_on(void)
+{
+    /* 点亮 LED 硬件操作代码:直接写寄存器 */
+}
+
+static void led_off(void)
+{
+    /* 熄灭 LED 硬件操作代码:直接写寄存器 */
+}
+
+int main(void)
+{
+    for ( ; ; ) {
+        led_on();       /* 点亮 LED */
+        delay();        /* 延时 */
+        led_off();      /* 熄灭 LED */
+        delay();        /* 延时 */
+    }
+}
+```
+
+**驱动程序**:只实现硬件操作,注册到内核;应用层通过系统调用"遥控"它。下面是一个简化的字符设备 LED 驱动骨架。
+
+```c
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/of_gpio.h>
+#include <linux/cdev.h>
+#include <linux/uaccess.h>
+
+static void led_on(void)
+{
+    /* 点亮 LED 硬件操作代码 */
+}
+
+static void led_off(void)
+{
+    /* 熄灭 LED 硬件操作代码 */
+}
+
+static int led_open(struct inode *inode, struct file *filp)
+{
+    /* 打开设备时需要做的事情 */
+    return 0;
+}
+
+static ssize_t led_write(struct file *filp, const char __user *buf,
+                         size_t size, loff_t *offt)
+{
+    int flag;
+
+    /* 获取应用层 write 的数据,存放在 flag 变量 */
+    if (copy_from_user(&flag, buf, size))
+        return -EFAULT;
+
+    /* 0 熄灭 LED,非 0 点亮 LED */
+    if (flag)
+        led_on();
+    else
+        led_off();
+
+    return 0;
+}
+
+static int led_release(struct inode *inode, struct file *filp)
+{
+    /* 关闭设备时需要做的事情 */
+    return 0;
+}
+
+static struct file_operations led_fops = {
+    .owner   = THIS_MODULE,
+    .open    = led_open,
+    .write   = led_write,
+    .release = led_release,
+};
+
+static int led_probe(struct platform_device *pdev)
+{
+    /* 驱动加载时需要做的事情 */
+    return 0;
+}
+
+static int led_remove(struct platform_device *pdev)
+{
+    /* 驱动卸载时需要做的事情 */
+    return 0;
+}
+
+static const struct of_device_id led_of_match[] = {
+    { .compatible = "alientek,led", },
+    { /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(of, led_of_match);
+
+static struct platform_driver led_driver = {
+    .driver = {
+        .owner          = THIS_MODULE,
+        .name           = "led",
+        .of_match_table = led_of_match,
+    },
+    .probe  = led_probe,
+    .remove = led_remove,
+};
+module_platform_driver(led_driver);
+
+MODULE_DESCRIPTION("LED Driver");
+MODULE_LICENSE("GPL");
+```
+
+驱动里的 `led_fops` 把 `open / write / release` 三个方法注册给内核:应用层调用 `open` 会执行 `led_open`,调用 `close` 会执行 `led_release`,调用 `write` 会执行 `led_write`。约定是写入非 0 点亮、写入 0 熄灭。
+
+**应用程序**:只写业务逻辑,通过系统调用控制设备,完全不碰寄存器。
+
+```c
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+int main(int argc, char **argv)
+{
+    int fd;
+    int data;
+
+    /* 打开 LED 设备(假定设备文件为 /dev/led) */
+    fd = open("/dev/led", O_WRONLY);
+    if (0 > fd)
+        return -1;
+
+    for ( ; ; ) {
+        data = 1;
+        write(fd, &data, sizeof(data));   /* 写 1 点亮 LED */
+        sleep(1);                         /* 延时 1 秒 */
+
+        data = 0;
+        write(fd, &data, sizeof(data));   /* 写 0 熄灭 LED */
+        sleep(1);                         /* 延时 1 秒 */
+    }
+
+    close(fd);
+    return 0;
+}
+```
+
+三者关系可以用一张图概括:
+
+```mermaid
+flowchart TB
+    subgraph 用户态["用户态(User Space)"]
+        A["应用程序<br/>open / read / write / close"]
+        L["C 标准库 glibc<br/>fopen / fread / printf"]
+    end
+    subgraph 内核态["内核态(Kernel Space)"]
+        S["系统调用接口<br/>sys_open / sys_read ..."]
+        D["设备驱动<br/>file_operations"]
+        K["内核核心<br/>进程 / 内存 / VFS"]
+    end
+    HW["硬件<br/>LED / GPIO / 磁盘 / 网卡"]
+
+    A -->|"系统调用陷入"| S
+    L -->|"内部再调用系统调用"| S
+    S --> D
+    S --> K
+    D --> HW
+    K --> HW
+```
+
+> 应用与驱动是**分离**的:单独编译、单独部署,一个跑在用户态、一个跑在内核态。这就是应用编程与裸机编程"质的区别"。
+
+---
+
+## 2. 系统调用(System Call)
+
+系统调用是 **Linux 内核提供给应用层的应用编程接口(API)**,是应用层进入内核的入口。不只是 Linux,所有操作系统都会向应用层提供系统调用。
+
+应用程序通过系统调用请求内核"以自己的名义"执行某些事情:打开磁盘文件、读写文件、关闭文件、控制各种硬件外设。应用层与内核的交互路径如下:
+
+```mermaid
+flowchart LR
+    APP["应用程序"] -->|"调用系统调用 API"| SC["系统调用"]
+    SC -->|"请求内核服务"| KER["Linux 内核"]
+    KER -->|"操作"| RES["资源:文件 / 设备 / 内存 / 网络"]
+```
+
+几个关键点:
+
+- 系统调用是**内核的一部分**,运行在内核态;
+- 应用程序运行在用户态,调用系统调用时会从用户态**陷入**内核态;
+- 常见的系统调用:`open`、`read`、`write`、`close`、`lseek`、`fork`、`ioctl` 等。
+
+---
+
+## 3. 库函数(C 语言库函数)
+
+库函数是 **C 语言库**提供的应用层函数库。在 Linux 下通常以动态库(`.so`)形式提供,存放在根文件系统的 `/lib` 目录下。
+
+库函数与系统调用的关系要分两类看:
+
+- **有些库函数不调用任何系统调用**:如字符串处理函数 `strlen()`、`strcat()`、`memcpy()`、`memset()`、`strchr()` 等,纯用户空间计算;
+- **有些库函数构建于系统调用之上**:如 `fopen()` 内部调用 `open()`、`fread()` 内部调用 `read()`、`fwrite()` 内部调用 `write()`。
+
+既然可以直接用系统调用,为什么还要设计库函数?因为**有些系统调用用起来并不方便**,C 语言库函数提供了更方便、更好用、且更具可移植性的调用接口。
+
+### 3.1 系统调用与库函数对比
+
+| 对比项 | 系统调用 | 库函数(C 库) |
+| ------ | -------- | -------------- |
+| 归属 | 内核提供给应用层的接口,属于内核一部分 | 属于应用层 |
+| 运行空间 | 调用时从用户态陷入内核态 | 运行在用户空间 |
+| 缓存 | 无缓存 | 通常有缓存(stdio 缓冲),性能、效率通常更优 |
+| 可移植性 | 不同操作系统差异大(定义/功能/参数/返回值往往不同) | 不同操作系统接口定义几乎一样,可移植性更好 |
+| 例子 | `open`、`read`、`write`、`close` | `fopen`、`fread`、`fwrite`、`fclose`、`printf` |
+
+```mermaid
+flowchart TB
+    subgraph 用户空间
+        U1["strlen / memcpy(不涉及系统调用)"]
+        U2["fopen / fread / fwrite(有 stdio 缓冲)"]
+    end
+    subgraph 内核空间
+        S1["open / read / write ..."]
+    end
+    U2 -->|"内部封装"| S1
+    U1 -.->|"纯用户态计算,无陷入"| U1
+```
+
+> 从**实现者**角度看,系统调用与库函数有根本区别;但从**使用者**角度看,区别并不重要,它们都是 C 语言函数。实际编程中两者都会用到,只需知道"我调用的这个函数是系统调用还是库函数"即可。
+
+**一句话总结应用编程**:开发 Linux 应用程序,通过调用内核提供的系统调用,或使用 C 库函数,来实现具有相应功能的应用程序。
+
+---
+
+## 4. 标准 C 语言函数库(glibc)
+
+Linux 下的标准 C 语言函数库是 **GNU C 语言函数库(glibc)**,官方地址:`http://www.gnu.org/software/libc/`。
+
+### 4.1 确定系统的 glibc 版本
+
+C 语言库以动态库文件形式提供,通常存放在 `/lib` 目录,命名方式通常是 `libc.so.6`。注意它是一个**软链接文件**,会链接到真正的库文件。
+
+Ubuntu 16.04 的库文件实际位于 `/lib/x86_64-linux-gnu`:
+
+```bash
+ls -l /lib/x86_64-linux-gnu/libc.so.6
+# lrwxrwxrwx ... libc.so.6 -> libc-2.23.so
+```
+
+其中 `2.23` 就是 glibc 的版本号。也可以直接运行共享库获取信息:
+
+```bash
+/lib/x86_64-linux-gnu/libc.so.6
+# GNU C Library ... stable release version 2.23 ...
+```
+
+### 4.2 获取 glibc 源码
+
+glibc 源码可直接从 git 仓库下载,也可以通过 ftp 下载。想研究某个库函数的具体实现时,获取源码分析即可。
+
+---
+
+## 5. main 函数
+
+在 Linux 应用程序中,`main` 函数是**应用程序的入口函数**。它有两种常见写法。
+
+### 5.1 无需传参
+
+```c
+int main(void)
+{
+    /* 代码 */
+    return 0;
+}
+```
+
+### 5.2 需要传参
+
+```c
+int main(int argc, char **argv)
+{
+    /* 代码 */
+    return 0;
+}
+```
+
+- `argc`:表示传入参数的个数,**包括应用程序自身路径和程序名**;
+- `argv`:字符串指针数组,保存每一个参数。
+
+例如运行当前目录下的 `hello` 并传入参数:
+
+```bash
+./hello 112233
+```
+
+此时参数个数为 2,且都以字符串形式传递:
+
+| 下标 | 值 | 含义 |
+| ---- | -- | ---- |
+| `argv[0]` | `"./hello"` | 程序自身路径与名字 |
+| `argv[1]` | `"112233"` | 用户传入的第 1 个参数 |
+| `argv[2]` | `NULL` | 数组结尾哨兵 |
+
+完整示例:
+
+```c
+#include <stdio.h>
+
+int main(int argc, char **argv)
+{
+    int i;
+
+    printf("argc = %d\n", argc);
+    for (i = 0; i < argc; i++)
+        printf("argv[%d] = %s\n", i, argv[i]);
+
+    return 0;
+}
+```
+
+```bash
+gcc -o hello hello.c
+./hello 112233 445566
+# argc = 3
+# argv[0] = ./hello
+# argv[1] = 112233
+# argv[2] = 445566
+```
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+>
+> `main` 还有第三种标准写法,用于接收**环境变量**:
+>
+> ```c
+> int main(int argc, char **argv, char **envp);
+> ```
+>
+> `envp` 是一个字符串数组,每个元素形如 `"NAME=value"`,以 `NULL` 结尾;它不是 `main` 的形式参数,而是紧跟在 `argv` 数组之后由内核放置的。遍历方法:
+>
+> ```c
+> #include <stdio.h>
+>
+> int main(int argc, char **argv, char **envp)
+> {
+>     int i = 0;
+>     while (envp[i] != NULL) {
+>         printf("%s\n", envp[i]);
+>         i++;
+>     }
+>     return 0;
+> }
+> ```
+>
+> 也可以直接使用全局变量 `environ` 或 `getenv("PATH")` 获取环境变量。在应用层最常用的是 `getenv()`,`envp` 主要用于需要整体遍历环境的场景。
+
+---
+
+## 6. 返回值与 errno(初步)
+
+Linux 下几乎所有 API 函数都有返回值,用来告诉调用者"执行成功还是失败"。约定规律:
+
+- 绝大多数函数**成功返回 0,失败返回负数(通常是 -1)**;
+- 但并非绝对,例如 `open` 成功返回文件描述符(非负整数)、`read`/`write` 成功返回实际读写字节数、`malloc` 失败返回 `NULL`。
+
+因此常见的判断方式是:
+
+```c
+if (func()) {
+    /* 执行失败 */
+} else {
+    /* 执行成功 */
+}
+```
+
+当我们只知道"失败返回 -1",却不知道失败原因时,就需要 `errno`:
+
+- `errno` 本质是一个 `int` 类型的**全局变量**,每个进程维护自己的 `errno`;
+- 错误发生时,操作系统把错误编号赋给 `errno`;**下一次错误会覆盖上一次**,所以要及时读取;
+- 并非所有函数出错都会设置 `errno`,这要查 man 手册的返回值描述;
+- 使用需包含头文件 `<errno.h>`。
+
+把编号翻译成人话,用 `strerror()` 或 `perror()`:
+
+```c
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+
+int main(void)
+{
+    printf("errno = %d\n", errno);              /* 直接读编号 */
+    printf("msg   = %s\n", strerror(errno));    /* 转成字符串 */
+    return 0;
+}
+```
+
+`errno`、`strerror`、`perror` 的完整用法(含错误码表)见 [[01-Linux应用编程基础/02-文件IO基础与深入探究]]。
+
+---
+
+## 7. 开发环境:Ubuntu + gcc
+
+本书的示例代码围绕 Ubuntu 编写,推荐 16.04 或 14.04(较稳定),也可用 CentOS、Redhat 等发行版。IDE 可选 Eclipse、VSCode;本书使用 **VSCode + gcc**,与正点原子驱动开发文档保持一致。用什么 IDE 都不重要,哪怕直接用 `vi`,重点是学习应用编程本身。
+
+### 7.1 两个阶段,两套编译器
+
+| 阶段 | 运行目标 | 编译器 | 产物运行位置 |
+| ---- | -------- | ------ | ------------ |
+| 入门篇(本模块) | PC 端 Ubuntu | 本地 gcc(x86) | Ubuntu 上直接运行 |
+| 提高篇(外设) | ALPHA/Mini I.MX6U(ARM 架构) | 交叉编译器 ARM gcc | 开发板上运行 |
+
+入门篇的示例都在 Ubuntu 下测试验证,所以用 `vscode + gcc` 即可;当代码要跑到 I.MX6U 这类 ARM 平台上时,必须用 **交叉编译工具(ARM 架构下的 gcc)** 编译,才能得到能在开发板上运行的可执行文件。
+
+### 7.2 gcc 本地编译
+
+```bash
+gcc -o testApp_1 testApp_1.c
+```
+
+- `gcc`:Ubuntu 下使用的 C 语言编译器;
+- `-o`:指定生成的可执行文件名;
+- `testApp_1.c`:待编译的 C 源文件。
+
+### 7.3 交叉编译
+
+```bash
+# 以 arm-linux-gnueabihf 工具链为例(I.MX6U 属于 Cortex-A7,带硬件浮点)
+arm-linux-gnueabihf-gcc -o testApp_1 testApp_1.c
+
+# 把可执行文件拷贝到开发板运行
+scp testApp_1 root@192.168.1.10:/home/root/
+```
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+>
+> **gcc 的编译流程**可分为四步,每一步都可以单独执行、单独观察:
+>
+> ```bash
+> # 1) 预处理:展开宏、头文件,去掉注释  -> .i
+> gcc -E hello.c -o hello.i
+> # 2) 编译:把 C 转成汇编              -> .s
+> gcc -S hello.i -o hello.s
+> # 3) 汇编:把汇编转成目标文件          -> .o
+> gcc -c hello.s -o hello.o
+> # 4) 链接:把目标文件、库链接成可执行文件
+> gcc hello.o -o hello
+>
+> # 也可一步到位
+> gcc hello.c -o hello
+> ```
+>
+> ```mermaid
+> flowchart LR
+>     C["hello.c<br/>源文件"] -->|"gcc -E 预处理"| I["hello.i<br/>预处理结果"]
+>     I -->|"gcc -S 编译"| S["hello.s<br/>汇编代码"]
+>     S -->|"gcc -c 汇编"| O["hello.o<br/>目标文件"]
+>     O -->|"gcc 链接(+ libc)"| E["hello<br/>可执行文件"]
+> ```
+>
+> 常见选项:`-Wall`(开警告)、`-O2`(优化)、`-g`(带调试信息)、`-D宏`(命令行定义宏)、`-I路径`(指定头文件目录)、`-L路径 -l库名`(指定库路径与库)。交叉编译只是把 `gcc` 换成对应的 `arm-...-gcc`,流程完全一致。
+
+### 7.4 程序运行
+
+编译产物是可在目标平台运行的 ELF 文件。在 Ubuntu 下:
+
+```bash
+./testApp_1
+```
+
+在开发板上:把可执行文件放到文件系统(如通过 `scp`、NFS、SD 卡),修改可执行权限后运行:
+
+```bash
+chmod +x testApp_1
+./testApp_1
+```
+
+---
+
+## 8. 完整示例源码与逐段解释
+
+下面这份程序把本篇的几个要点串起来:`main(argc, argv)` 接收参数、`open/write/close` 系统调用、返回值与 `errno` 判断。它模拟"向 LED 设备写入数据"的应用层逻辑,并打印参数。
+
+```c
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+
+int main(int argc, char **argv)
+{
+    int fd;
+    int data;
+    int i;
+
+    /* 1. 打印命令行参数,验证 main 的 argc / argv */
+    printf("argc = %d\n", argc);
+    for (i = 0; i < argc; i++)
+        printf("argv[%d] = %s\n", i, argv[i]);
+
+    /* 2. 以只写方式打开设备文件(这里用普通文件代替 /dev/led) */
+    fd = open("./led_dev", O_WRONLY | O_CREAT, 0664);
+    if (-1 == fd) {
+        /* 3. 出错时用 strerror 把 errno 翻译成可读信息 */
+        printf("open error: %s\n", strerror(errno));
+        return -1;
+    }
+
+    /* 4. 写入非 0 表示点亮 */
+    data = 1;
+    if (sizeof(data) != write(fd, &data, sizeof(data))) {
+        printf("write error: %s\n", strerror(errno));
+        close(fd);
+        return -1;
+    }
+
+    printf("LED on command sent\n");
+
+    /* 5. 显式关闭文件,释放文件描述符 */
+    close(fd);
+    return 0;
+}
+```
+
+逐段解释:
+
+| 行 | 代码 | 说明 |
+| -- | ---- | ---- |
+| 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)` | 不再使用时显式关闭,归还文件描述符 |
+
+编译与运行:
+
+```bash
+gcc -Wall -o testApp testApp.c
+./testApp hello 123
+```
+
+预期输出:
+
+```text
+argc = 3
+argv[0] = ./testApp
+argv[1] = hello
+argv[2] = 123
+LED on command sent
+```
+
+---
+
+## 9. 实验步骤与调试方法
+
+### 9.1 实验步骤
+
+1. 在 Ubuntu 下建工作目录并创建源文件:
+
+   ```bash
+   mkdir -p ~/vscode_ws/1_chapter
+   cd ~/vscode_ws/1_chapter
+   ```
+
+2. 用 VSCode 打开该目录,创建 `testApp.c`,粘贴上面的源码。
+3. 在 VSCode 菜单"终端 → 新终端"打开终端,编译:
+
+   ```bash
+   gcc -Wall -o testApp testApp.c
+   ```
+
+4. 运行并传入参数:
+
+   ```bash
+   ./testApp hello 123
+   ```
+
+5. 观察 `argc/argv` 输出,以及 `led_dev` 文件是否被创建(`ls -l led_dev`)。
+
+### 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)` |
+
+调试常用命令:
+
+```bash
+gcc -Wall -Wextra -o testApp testApp.c   # 打开更多警告
+man 2 open                               # 查看系统调用手册(2 系统调用、3 库函数)
+echo $?                                  # 查看上一条命令/程序的退出状态
+```
+
+`man` 手册章节编号:
+
+| 编号 | 内容 |
+| ---- | ---- |
+| 1 | Linux 命令 |
+| 2 | 系统调用 |
+| 3 | 标准 C 库函数 |
+
+---
+
+## 10. 跨平台对比:IMX6ULL vs STM32 vs RK3568
+
+> ⚠️ **来源说明**:本节不属于《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 与 RK3568 同属"Linux 应用编程"范畴,本篇所学的系统调用、glibc、gcc/交叉编译流程**可直接复用**;STM32 裸机则是另一套世界观,没有系统调用、没有文件描述符、没有 `errno`,切忌混用概念。
+
+---
+
+## 11. 面试精选
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+### Q1:系统调用和库函数有什么区别?
+
+**答**:
+- **归属不同**:系统调用是内核提供给应用层的接口,属于内核的一部分;库函数属于应用层。
+- **运行空间不同**:库函数运行在用户空间;调用系统调用会由用户态陷入内核态。
+- **缓存不同**:库函数通常有缓存(如 stdio 缓冲),系统调用无缓存,因此在性能和效率上库函数通常更优。
+- **可移植性不同**:不同操作系统的系统调用在定义、功能、参数、返回值上往往不同;而 C 库函数在各系统间接口几乎一致,可移植性更好。
+- **联系**:部分库函数构建于系统调用之上,如 `fopen` 内部调用 `open`、`fread` 调用 `read`、`fwrite` 调用 `write`;也有库函数(`strlen`、`memcpy`)完全不涉及系统调用。
+- 从使用者角度看,二者都是 C 函数,实际编程中都会用到,知道自己在调用哪一类即可。
+
+### Q2:什么是用户态和内核态?应用程序调用 `open` 时发生了什么?
+
+**答**:操作系统下有两种状态:内核态与用户态,应用程序运行在用户态、内核运行在内核态。应用程序调用 `open` 时,会通过系统调用从用户态陷入内核态,内核根据路径找到文件、分配文件描述符并完成打开操作,然后把结果(文件描述符或 -1)返回给用户态。涉及硬件和资源的操作必须由内核在内核态完成,用户态程序不能直接访问。
+
+### Q3:`main` 函数的 `argc`、`argv` 各是什么?执行 `./app a b` 后它们的值是多少?
+
+**答**:`argc` 是传入参数个数,**包含程序自身路径与程序名**;`argv` 是字符串指针数组。执行 `./app a b` 后 `argc == 3`;`argv[0] == "./app"`、`argv[1] == "a"`、`argv[2] == "b"`,且 `argv[argc] == NULL`。因此遍历时循环条件应写 `i < argc`。
+
+### Q4:函数返回 -1 就代表失败,但如何知道失败原因?
+
+**答**:Linux 为常见错误做了编号,函数出错时操作系统会把错误编号写入进程私有的全局变量 `errno`(`int` 类型,下次出错会被覆盖,要及时读取)。包含 `<errno.h>` 即可使用 `errno`;用 `strerror(errno)` 可把它转成可读字符串,用 `perror("...")` 可直接打印并自动附加冒号和错误描述。需要注意并非所有函数出错都会设置 `errno`,需查阅 man 手册的返回值说明。
+
+### Q5:入门篇用 gcc,提高篇为什么要用交叉编译?两者产物有何不同?
+
+**答**:入门篇示例在 PC 端 Ubuntu 上运行验证,CPU 是 x86 架构,用本机 gcc 编译出的 ELF 可直接在 Ubuntu 运行。提高篇的代码要在 ALPHA/Mini I.MX6U 开发板上运行,板子是 ARM 架构,指令集与 x86 不同,必须使用面向 ARM 的交叉编译器(ARM 架构下的 gcc)编译,才能生成在开发板上运行的可执行文件。二者编译流程一致,区别只在于编译器目标架构和最终产物运行的平台。
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第一章 应用编程概念

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

@@ -0,0 +1,1527 @@
+---
+title: 文件 IO 基础与深入探究
+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"
+---
+
+# 文件 IO 基础与深入探究
+
+> 💡 **关联知识**:[[01-Linux应用编程基础/01-应用编程概念与开发环境]]、[[01-Linux应用编程基础/03-标准IO库]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/2. 系统调用IO]]、[[Linux+C+C++技术体系梳理/2. Linux系统编程/3. 文件描述符操作]]
+
+Linux 下"一切皆文件"。文件 I/O 既是应用编程最基础的部分,也是最容易"会用但不懂"的部分。本篇分两层:
+
+- **基础层**(第二章):`open / read / write / close / lseek` 五个系统调用,够你完成绝大多数简单文件读写;
+- **深入层**(第三章):文件描述符背后发生了什么、`errno` 怎么用、`O_APPEND/O_TRUNC` 的细节、多次打开同一文件、`dup`、文件共享、原子操作与竞争冒险、`fcntl/ioctl`、文件截断。
+
+---
+
+## 1. 一个简单的文件 IO 示例
+
+通用 IO 模型通常包括:打开文件、读写文件、关闭文件,涉及 4 个函数 `open()`、`read()`、`write()`、`close()`。
+
+```c
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+int main(void)
+{
+    char buff[1024];
+    int fd1, fd2;
+    int ret;
+
+    /* 打开源文件 src_file(只读方式) */
+    fd1 = open("./src_file", O_RDONLY);
+    if (-1 == fd1)
+        return fd1;
+
+    /* 打开目标文件 dest_file(只写方式) */
+    fd2 = open("./dest_file", O_WRONLY);
+    if (-1 == fd2) {
+        ret = fd2;
+        goto out1;
+    }
+
+    /* 读取源文件 1KB 数据到 buff 中 */
+    ret = read(fd1, buff, sizeof(buff));
+    if (-1 == ret)
+        goto out2;
+
+    /* 将 buff 中的数据写入目标文件 */
+    ret = write(fd2, buff, sizeof(buff));
+    if (-1 == ret)
+        goto out2;
+
+    ret = 0;
+
+out2:
+    /* 关闭目标文件 */
+    close(fd2);
+
+out1:
+    /* 关闭源文件 */
+    close(fd1);
+    return ret;
+}
+```
+
+流程:先 `open` 源文件和目标文件,成功后 `read` 源文件 1KB 数据,再 `write` 到目标文件,最后 `close` 两个文件。
+
+```mermaid
+flowchart LR
+    A["open 源文件<br/>O_RDONLY"] --> B["open 目标文件<br/>O_WRONLY"]
+    B --> C["read 源文件<br/>1024 字节"]
+    C --> D["write 目标文件<br/>1024 字节"]
+    D --> E["close 目标文件"]
+    E --> F["close 源文件"]
+```
+
+---
+
+## 2. 文件描述符(File Descriptor)
+
+`open` 执行成功时返回一个**非负整数**,这个返回值就是**文件描述符(fd)**。Linux 内核用文件描述符索引所有已打开的文件;所有 IO 系统调用都通过文件描述符找到对应文件。
+
+- 一个进程可以打开多个文件,但**数量有限**;
+- 默认上限为 1024,即文件描述符取值范围 `0~1023`;
+- 分配原则:**从最小的没有被使用的文件描述符开始分配**;文件关闭后 fd 被释放,可再次分配给新打开的文件;
+- 每个被打开的文件在同一进程内有唯一 fd,不会重复;
+- 超过进程可打开的最大文件数限制,内核会发送警告信号并结束进程。
+
+查看上限:
+
+```bash
+ulimit -n
+# 1024
+```
+
+### 2.1 0、1、2 去哪了
+
+程序里 `open` 得到的 fd 通常从 **3** 开始,因为 `0、1、2` 已被系统默认占用:
+
+| 文件描述符 | 名称 | 宏 | 默认对应设备 |
+| ---------- | ---- | -- | ------------ |
+| 0 | 标准输入 | `STDIN_FILENO` | 键盘 |
+| 1 | 标准输出 | `STDOUT_FILENO` | LCD 显示器 |
+| 2 | 标准错误 | `STDERR_FILENO` | LCD 显示器 |
+
+这三个宏定义在 `<unistd.h>` 中:
+
+```c
+#define STDIN_FILENO  0  /* Standard input.  */
+#define STDOUT_FILENO 1  /* Standard output. */
+#define STDERR_FILENO 2  /* Standard error output. */
+```
+
+> Linux 下一切皆文件,也包括各种硬件设备。每个硬件设备都对应系统下的某个**设备文件**,应用通过对设备文件读写来操控硬件,如 LCD、串口、音频、键盘等。标准输入可理解为"打开键盘设备文件得到的 fd",标准输出/标准错误可理解为"打开显示设备文件得到的 fd"。
+
+---
+
+## 3. open 打开文件
+
+操作文件前要先打开,得到 fd,读写后再关闭。`open` 既可以打开已存在的文件,也可以创建新文件。
+
+```c
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+int open(const char *pathname, int flags);
+int open(const char *pathname, int flags, mode_t mode);
+```
+
+`open` 有两种原型,原因是 C 语言不支持重载,它是**可变参函数**:可传 2 个参数,也可传 3 个参数,第三个参数 `mode` 只在 `flags` 含 `O_CREAT` 或 `O_TMPFILE` 时有效。
+
+用 man 查看帮助:
+
+```bash
+man 2 open      # 2 表示系统调用;1 是命令,3 是标准 C 库函数
+```
+
+### 3.1 参数说明
+
+| 参数 | 类型 | 说明 |
+| ---- | ---- | ---- |
+| `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` 是符号链接直接返回错误;不加则解引用 |
+
+> 不同内核版本支持的 flags 存在差别,man 手册中会说明某标志从哪个版本开始支持。
+
+多个标志通过位或组合:
+
+```c
+open("./src_file", O_RDONLY);                    /* 单独使用一个标志 */
+open("./src_file", O_RDONLY | O_NOFOLLOW);       /* 多个标志组合 */
+```
+
+其它常用但本篇稍后展开的标志还有:`O_APPEND`、`O_TRUNC`、`O_NONBLOCK`、`O_SYNC`、`O_DSYNC`、`O_ASYNC`、`O_NOATIME`、`O_TMPFILE` 等。
+
+### 3.3 mode 文件权限
+
+`mode` 类型是 `mode_t`(`u32` 无符号整型)。权限位从低位往上看,每 3 个 bit 一组:
+
+```text
+S  U  U  U  G  G  G  O  O  O
+   rwx   rwx   rwx
+```
+
+| 字段 | 含义 |
+| ---- | ---- |
+| `O`(3 bit) | 其他用户的权限 |
+| `G`(3 bit) | 同组用户(与文件所有者有相同组 ID)的权限 |
+| `U`(3 bit) | 文件所属用户的权限 |
+| `S`(3 bit) | 文件特殊权限,一般少用 |
+
+每组 3 个 bit 按 `rwx` 顺序分配:最高位(权值 4)读、中间位(权值 2)写、最低位(权值 1)执行。
+
+举例:
+
+- `111111111`(二进制)= `777`(八进制)= `511`(十进制):所有用户都有读/写/执行权限;
+- `111000000`:仅文件所属者拥有读、写、执行权限;
+- `100100100`:所属者、同组用户、其他用户都只有读权限。
+
+实际编程中直接使用 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_ISGID` | set-group-ID(特殊权限) |
+| `S_ISVTX` | sticky(特殊权限) |
+
+这些宏可单独使用,也可位或组合:
+
+```c
+S_IRUSR | S_IWUSR | S_IROTH
+```
+
+> `O_RDONLY / O_WRONLY / O_RDWR` 决定"以什么方式打开",与文件权限是两回事:**只有用户对该文件具有相应权限时,才能用对应标志打开文件,否则打开失败**。若程序对该文件只有只读权限,用 `O_RDWR` 或 `O_WRONLY` 打开会失败。
+
+### 3.4 open 使用示例
+
+```c
+/* (1) 只读方式打开已存在文件 */
+int fd = open("./app.c", O_RDONLY);
+if (-1 == fd)
+    return fd;
+
+/* (2) 可读可写方式打开已存在文件 */
+int fd = open("./app.c", O_RDWR);
+if (-1 == fd)
+    return fd;
+
+/* (3) 可读可写打开;若是符号链接则不解引用,直接返回错误 */
+int fd = open("/home/dengtao/hello", O_RDWR | O_NOFOLLOW);
+if (-1 == fd)
+    return fd;
+
+/* (4) 不存在则创建:所属者 rwx,同组和其他用户只读 */
+int fd = open("/home/dengtao/hello", O_RDWR | O_CREAT,
+              S_IRWXU | S_IRGRP | S_IROTH);
+if (-1 == fd)
+    return fd;
+```
+
+---
+
+## 4. write 写文件
+
+```c
+#include <unistd.h>
+
+ssize_t write(int fd, const void *buf, size_t count);
+```
+
+| 参数 | 说明 |
+| ---- | ---- |
+| `fd` | 文件描述符 |
+| `buf` | 待写入数据所在的缓冲区 |
+| `count` | 要写入的字节数 |
+
+**返回值**:成功返回实际写入的字节数(0 表示未写入任何字节);若返回值小于 `count`,**这不是错误**(例如磁盘空间已满可能发生这种情况);写入出错返回 `-1`。
+
+---
+
+## 5. read 读文件
+
+```c
+#include <unistd.h>
+
+ssize_t read(int fd, void *buf, size_t count);
+```
+
+| 参数 | 说明 |
+| ---- | ---- |
+| `fd` | 文件描述符 |
+| `buf` | 存放读取数据的缓冲区 |
+| `count` | 希望读取的字节数 |
+
+**返回值**:成功返回实际读取的字节数;实际值可能小于 `count`,也可能为 0(如已到文件末尾)。
+
+例如文件在到达末尾前只剩 30 字节,而要求读 100 字节,则 `read` 返回 30;下一次再 `read` 返回 0。
+
+---
+
+## 6. close 关闭文件
+
+```c
+#include <unistd.h>
+
+int close(int fd);
+```
+
+**返回值**:成功返回 0,失败返回 -1。
+
+- 进程终止时,内核会**自动关闭**该进程打开的所有文件,所以很多程序不显式调用 `close`;
+- 但显式关闭是良好编程习惯,代码更可读、更可靠;
+- 文件描述符是有限资源,不再需要时应释放归还系统。
+
+---
+
+## 7. lseek 定位读写位置
+
+每个打开的文件,系统都会记录它的**读写位置偏移量**(读写偏移量),`read/write` 从当前偏移量开始操作。文件第一个字节的位置偏移量为 0;打开文件时偏移量指向文件开始处;每次 `read/write` 后会自动移动,指向已读/已写数据后的下一字节。
+
+```c
+#include <sys/types.h>
+#include <unistd.h>
+
+off_t lseek(int fd, off_t offset, int whence);
+```
+
+| 参数 | 说明 |
+| ---- | ---- |
+| `fd` | 文件描述符 |
+| `offset` | 偏移量,以字节为单位 |
+| `whence` | 偏移量参考基准,取以下宏之一 |
+
+| `whence` | 含义 |
+| -------- | ---- |
+| `SEEK_SET` | 指向 `offset` 字节处(从文件头算) |
+| `SEEK_CUR` | 指向「当前位置 + offset」处,`offset` 可正可负 |
+| `SEEK_END` | 指向「文件末尾 + offset」处,`offset` 可正可负 |
+
+**返回值**:成功返回从文件头部算起的当前位置偏移量(字节);出错返回 `-1`。
+
+```c
+/* 移动到文件开头 */
+off_t off = lseek(fd, 0, SEEK_SET);
+
+/* 移动到文件末尾 */
+off_t off = lseek(fd, 0, SEEK_END);
+
+/* 移动到偏移文件头 100 字节处 */
+off_t off = lseek(fd, 100, SEEK_SET);
+
+/* 获取当前读写位置偏移量 */
+off_t off = lseek(fd, 0, SEEK_CUR);
+```
+
+---
+
+## 8. 完整示例:文件拷贝 + 偏移读取
+
+下面这份源码对应第二章的编程实战例 1:打开 `src_file`(只读),新建 `dest_file`(只写、权限 `rwxr--r--`),从 `src_file` 偏移 500 字节处读取 1KB,写入 `dest_file` 开头,最后显式关闭所有文件。
+
+```c
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdio.h>
+
+int main(void)
+{
+    char buffer[1024];
+    int fd1, fd2;
+    int ret;
+
+    /* 打开 src_file 文件 */
+    fd1 = open("./src_file", O_RDONLY);
+    if (-1 == fd1) {
+        printf("Error: open src_file failed!\n");
+        return -1;
+    }
+
+    /* 新建 dest_file 文件并打开 */
+    fd2 = open("./dest_file", O_WRONLY | O_CREAT | O_EXCL,
+               S_IRWXU | S_IRGRP | S_IROTH);
+    if (-1 == fd2) {
+        printf("Error: open dest_file failed!\n");
+        ret = -1;
+        goto err1;
+    }
+
+    /* 将 src_file 读写位置移动到偏移文件头 500 个字节处 */
+    ret = lseek(fd1, 500, SEEK_SET);
+    if (-1 == ret)
+        goto err2;
+
+    /* 读取 src_file 数据,大小 1KByte */
+    ret = read(fd1, buffer, sizeof(buffer));
+    if (-1 == ret) {
+        printf("Error: read src_file failed!\n");
+        goto err2;
+    }
+
+    /* 将 dest_file 读写位置移动到文件头 */
+    ret = lseek(fd2, 0, SEEK_SET);
+    if (-1 == ret)
+        goto err2;
+
+    /* 将 buffer 中的数据写入 dest_file,大小 1KByte */
+    ret = write(fd2, buffer, sizeof(buffer));
+    if (-1 == ret) {
+        printf("Error: write dest_file failed!\n");
+        goto err2;
+    }
+
+    printf("OK: test successful\n");
+    ret = 0;
+
+err2:
+    close(fd2);
+
+err1:
+    close(fd1);
+    return ret;
+}
+```
+
+逐段解释:
+
+| 代码 | 说明 |
+| ---- | ---- |
+| `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 泄漏 |
+
+编译运行:
+
+```bash
+gcc -o testApp_1 testApp_1.c
+# 准备一个 >= 1KB 的 src_file
+ls -l src_file
+./testApp_1
+ls -l dest_file          # 大小应为 1024,权限为 rwxr--r--
+```
+
+---
+
+## 9. Linux 系统如何管理文件(深入)
+
+### 9.1 静态文件与 inode
+
+文件未被打开时存放在磁盘文件系统中,称为**静态文件**。磁盘的最小存储单位是**扇区**(Sector,512 字节);操作系统一次读取一个**块**(block),通常 4KB(连续 8 个扇区)。
+
+磁盘分区、格式化时会分为两个区域:
+
+- **数据区**:存储文件中的数据;
+- **inode 区**:存放 `inode table`,其中是一个个 `inode`。
+
+每个文件对应唯一一个 inode,inode 本质是一个结构体,记录:文件字节大小、文件所有者、读/写/执行权限、时间戳、文件类型、**文件数据存储的 block 位置**等信息。注意:**文件名不记录在 inode 中**。
+
+查看 inode 编号:
+
+```bash
+ls -i file
+# 或
+stat file
+```
+
+```mermaid
+flowchart LR
+    F["文件名"] -->|"1. 找到 inode 编号"| N["inode 编号"]
+    N -->|"2. 在 inode table 中定位"| IT["inode table<br/>inode 结构体"]
+    IT -->|"3. 根据 inode 记录的 block 位置"| B["数据区中的 block"]
+```
+
+打开一个文件,系统内部大致分三步:
+
+1. 找到文件名对应的 inode 编号;
+2. 通过 inode 编号从 inode table 找到 inode 结构体;
+3. 根据 inode 记录的信息确定数据所在的 block,读出数据。
+
+> U 盘"快速格式化"很快,因为它只清除了 inode table,真正存储文件数据的区域没动,所以数据仍可能被找回。
+
+### 9.2 打开文件时的状态
+
+调用 `open` 时,内核会申请一段内存(缓冲区),把静态文件数据从磁盘读取到内存中管理、缓存。内存中的这份叫**动态文件(内核缓冲区)**。之后对该文件的读写都是针对内存中的动态文件,而不是磁盘中的静态文件。
+
+当动态文件被改写后,与磁盘静态文件不同步,内核会在之后把动态文件更新到磁盘。这解释了:
+
+- 打开大文件比较慢(要先读入内存);
+- 文档写了一半没保存就断电,重启后内容丢失(尚未同步到磁盘)。
+
+这样设计的原因:磁盘/硬盘/U 盘多为 Flash 块设备,块设备以"块"为单位读写,改一个字节也要把整块读出、修改、写回,非常不灵活;而内存可按字节随机操作,且读写速率远高于磁盘。因此**先把静态文件缓存为动态文件**,读写针对内存,效率更高。
+
+### 9.3 进程、文件描述符表、文件表、inode
+
+内核为每个进程设置专门的数据结构管理进程状态、运行特征等,称为**进程控制块(PCB)**。
+
+PCB 中有指针指向**文件描述符表(File descriptors)**;表中的每个元素索引到对应的**文件表(File table)**;文件表记录:文件状态标志、引用计数、当前文件读写偏移量、i-node 指针(指向该文件对应的 inode)等。
+
+```mermaid
+flowchart LR
+    subgraph 进程["进程 PCB"]
+        FDT["文件描述符表"]
+        FD0["fd 0"]
+        FD1["fd 1"]
+        FD2["fd 2"]
+        FD3["fd 3"]
+        FDT --- FD0
+        FDT --- FD1
+        FDT --- FD2
+        FDT --- FD3
+    end
+    subgraph 打开文件表["打开文件表(File table)"]
+        FT1["文件表项<br/>状态标志 / 引用计数<br/>偏移量 / i-node 指针"]
+    end
+    INODE["inode<br/>数据 block 位置 / 权限 / 大小"]
+    FD3 --> FT1
+    FT1 --> INODE
+    INODE --> BLK["磁盘数据块"]
+```
+
+---
+
+## 10. 返回错误处理与 errno
+
+前面示例出错后只能返回 -1,却不知道原因。Linux 为常见错误做了编号,函数出错时操作系统把错误编号赋给 **`errno`**:
+
+- `errno` 是 `int` 类型变量,每个进程维护自己的 `errno`;
+- 存储**就近发生**的错误编号,下一次出错会覆盖上一次;
+- **并非所有**系统调用或库函数出错都会设置 `errno`,可查 man 手册的返回值描述;
+- 包含 `<errno.h>` 即可使用。
+
+```c
+#include <stdio.h>
+#include <errno.h>
+
+int main(void)
+{
+    printf("%d\n", errno);
+    return 0;
+}
+```
+
+### 10.1 strerror()
+
+把错误编号转换成可读字符串(C 库函数,`man 3 strerror`):
+
+```c
+#include <string.h>
+
+char *strerror(int errnum);
+```
+
+| 参数 | 说明 |
+| ---- | ---- |
+| `errnum` | 错误编号 `errno` |
+
+**返回值**:对应错误编号的字符串描述。
+
+```c
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+
+int main(void)
+{
+    int fd;
+
+    fd = open("./test_file", O_RDONLY);
+    if (-1 == fd) {
+        printf("Error: %s\n", strerror(errno));
+        return -1;
+    }
+
+    close(fd);
+    return 0;
+}
+```
+
+文件不存在时打印:
+
+```text
+Error: No such file or directory
+```
+
+### 10.2 perror()
+
+更常用:内部自动读取 `errno`,直接把错误信息打印出来,还可在前面加自己的提示信息(`man 3 perror`):
+
+```c
+#include <stdio.h>
+
+void perror(const char *s);
+```
+
+| 参数 | 说明 |
+| ---- | ---- |
+| `s` | 错误提示字符串之前附加的信息,不加则传空字符串 |
+
+**返回值**:无(`void`)。
+
+```c
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdio.h>
+
+int main(void)
+{
+    int fd;
+
+    fd = open("./test_file", O_RDONLY);
+    if (-1 == fd) {
+        perror("open error");
+        return -1;
+    }
+
+    close(fd);
+    return 0;
+}
+```
+
+输出:
+
+```text
+open error: No such file or directory
+```
+
+`perror` 会在附加信息后自动加入冒号和空格分隔。推荐优先使用 `perror`。
+
+---
+
+## 11. exit、_exit、_Exit
+
+进程退出分**正常退出**和**异常退出**,这里只讨论正常退出。正常退出有三种方式:
+
+### 11.1 `_exit()` 与 `_Exit()`(系统调用)
+
+`_exit()` 会清除进程使用的内存空间,销毁内核中的各种数据结构,关闭进程所有文件描述符,结束进程并把控制权交给操作系统。它会**直接进入内核终止进程**,不执行 stdio 缓冲刷新等清理。
+
+```c
+#include <unistd.h>
+
+void _exit(int status);
+```
+
+```c
+#include <stdlib.h>
+
+void _Exit(int status);
+```
+
+`_exit()` 和 `_Exit()` 两者等价,用法作用一样,都是系统调用。`status` 为状态标志:0 表示正常结束,其它值表示执行过程中检测到有错误发生。
+
+```c
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdio.h>
+
+int main(void)
+{
+    int fd;
+
+    fd = open("./test_file", O_RDONLY);
+    if (-1 == fd) {
+        perror("open error");
+        _exit(-1);
+    }
+
+    close(fd);
+    _exit(0);
+}
+```
+
+### 11.2 `exit()`(标准 C 库函数)
+
+`exit()` 是标准 C 库函数,会执行一些清理工作,**最后调用 `_exit()`**。用法与 `_exit()`/`_Exit()` 一样。
+
+```c
+#include <stdlib.h>
+
+void exit(int status);
+```
+
+### 11.3 三种终止方法对比
+
+| 方式 | 类型 | 是否刷新 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库]])。
+
+---
+
+## 12. 空洞文件(Hole File)
+
+`lseek` 不仅能改变位置偏移量,还**允许偏移量超出文件长度**。例如一个 4K 的文件,把偏移量移到 6000 字节处再 `write`,那么 4096~6000 字节之间没有写入数据,形成**空洞**,该文件称为**空洞文件**。
+
+- 空洞部分**不占用物理空间**,直到对该区域写入数据时才分配;
+- 但空洞文件形成时,**逻辑上文件大小包含空洞部分**。
+
+用途:对多线程共同操作文件非常有用。要构建一个大文件,可把文件分段,多个线程各写一段。实际场景:
+
+- 迅雷下载未完成时文件已占据全部大小,从不同地址同时写入;
+- 创建虚拟机分配 100G 磁盘,实际只用了 3、4G。
+
+```c
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+
+int main(void)
+{
+    int fd;
+    int ret;
+    char buffer[1024];
+    int i;
+
+    /* 打开文件:不存在则创建,权限 rw-r--r-- */
+    fd = open("./hole_file", O_WRONLY | O_CREAT | O_EXCL,
+              S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
+    if (-1 == fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 将文件读写位置移动到偏移文件头 4096 字节(4K)处 */
+    ret = lseek(fd, 4096, SEEK_SET);
+    if (-1 == ret) {
+        perror("lseek error");
+        goto err;
+    }
+
+    /* 初始化 buffer 为 0xFF */
+    memset(buffer, 0xFF, sizeof(buffer));
+
+    /* 循环写入 4 次,每次 1K */
+    for (i = 0; i < 4; i++) {
+        ret = write(fd, buffer, sizeof(buffer));
+        if (-1 == ret) {
+            perror("write error");
+            goto err;
+        }
+    }
+
+    ret = 0;
+err:
+    close(fd);
+    exit(ret);
+}
+```
+
+测试:
+
+```bash
+gcc -o hole hole.c && ./hole
+ls -l hole_file     # 逻辑大小 8K(4K 空洞 + 4K 数据)
+du -h hole_file     # 实际占用 4K
+```
+
+`ls` 看到的是**逻辑大小**(含空洞),`du` 看到的是**实际占用存储块大小**。
+
+> 思考题:用 `read` 读取文件空洞部分,读到的是什么?(可自己写代码验证。)
+
+---
+
+## 13. O_APPEND 与 O_TRUNC
+
+### 13.1 O_TRUNC
+
+使用 `O_TRUNC` 打开文件时,会将文件原本的内容**全部丢弃**,文件大小变为 0。
+
+```c
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+int main(void)
+{
+    int fd;
+
+    fd = open("./test_file", O_WRONLY | O_TRUNC);
+    if (-1 == fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    close(fd);
+    exit(0);
+}
+```
+
+即使不读写,仅用 `O_TRUNC` 打开再关闭,文件也会被清空(比如 8760 字节 → 0)。
+
+### 13.2 O_APPEND
+
+使用 `O_APPEND` 打开文件后,**每次 `write` 都会自动把写位置偏移量移动到文件末尾**,从文件末尾开始写入。
+
+```c
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+int main(void)
+{
+    char buffer[16];
+    int fd;
+    int ret;
+
+    /* 打开文件,携带 O_APPEND */
+    fd = open("./test_file", O_RDWR | O_APPEND);
+    if (-1 == fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 初始化 buffer 中的数据 */
+    memset(buffer, 0x55, sizeof(buffer));
+
+    /* 写入 4 个字节数据 */
+    ret = write(fd, buffer, 4);
+    if (-1 == ret) {
+        perror("write error");
+        goto err;
+    }
+
+    /* 将 buffer 清 0 */
+    memset(buffer, 0x00, sizeof(buffer));
+
+    /* 将位置偏移量移动到距离文件末尾 4 个字节处 */
+    ret = lseek(fd, -4, SEEK_END);
+    if (-1 == ret) {
+        perror("lseek error");
+        goto err;
+    }
+
+    /* 读取最后 4 个字节 */
+    ret = read(fd, buffer, 4);
+    if (-1 == ret) {
+        perror("read error");
+        goto err;
+    }
+
+    printf("0x%x 0x%x 0x%x 0x%x\n", buffer[0], buffer[1],
+           buffer[2], buffer[3]);
+    ret = 0;
+err:
+    close(fd);
+    exit(ret);
+}
+```
+
+打印 `0x55 0x55 0x55 0x55`,说明 `O_APPEND` 生效:写入确实发生在文件末尾。
+
+两个重要细节:
+
+1. **`O_APPEND` 不影响读**:读文件时位置偏移量默认仍在文件头;
+2. **`O_APPEND` 下用 `lseek` 改不了写位置**:写入依然从文件末尾开始。
+
+> 思考题:`O_APPEND` 与 `O_TRUNC` 同时指定时会怎样?
+
+---
+
+## 14. 多次打开同一个文件
+
+同一个文件可以被多次打开:在一个进程中多次打开,或在多个进程中分别打开。
+
+### 14.1 得到的 fd 不同
+
+一个进程内多次 `open` 同一文件,会得到**多个不同的 fd**,关闭时需依次 `close`。
+
+```c
+fd1 = open("./test_file", O_RDWR);
+fd2 = open("./test_file", O_RDWR);
+fd3 = open("./test_file", O_RDWR);
+printf("%d  %d  %d\n", fd1, fd2, fd3);   /* 例如 6 7 8 */
+```
+
+> 在 VSCode 提供的终端里运行,得到的可能是 6、7、8 而不是 3、4、5,因为 3、4、5 已被 VSCode 进程占用,而子进程会继承父进程的文件描述符。直接在 Ubuntu Terminal 里运行则得到 3、4、5。
+
+### 14.2 内存中只有一份动态文件
+
+多次打开同一文件,**内存中不会存在多份动态文件**。验证:先用 `fd1` 写入 4 字节,再用 `fd2`(未 `lseek`)从文件头读取,若能读到刚写的数据,说明共享同一份动态文件。
+
+```c
+/* 通过 fd1 写入 4 个字节 0x11 0x22 0x33 0x44 */
+write(fd1, buffer, 4);
+
+/* fd2 从文件头读取 */
+lseek(fd2, 0, SEEK_SET);
+read(fd2, buffer, 4);
+/* 读出的仍是 0x11 0x22 0x33 0x44,证明只有一份动态文件 */
+```
+
+### 14.3 不同 fd 的读写偏移量相互独立
+
+多次打开会得到多个 fd,对应多个**文件表**,读写偏移量记录在文件表中,因此不同 fd 的偏移量**相互独立**;但文件表中的 i-node 指针指向同一个 inode。
+
+```mermaid
+flowchart LR
+    subgraph P["进程"]
+        A["fd1"]
+        B["fd2"]
+    end
+    A --> T1["文件表 1<br/>偏移量 off1<br/>i-node 指针"]
+    B --> T2["文件表 2<br/>偏移量 off2<br/>i-node 指针"]
+    T1 --> I["同一个 inode"]
+    T2 --> I
+```
+
+> 多个不同进程 `open` 同一文件,内存中同样只维护一份动态文件,多个进程共享,但各有独立的读写偏移量。
+
+**动态文件何时关闭?** 当文件的**引用计数为 0** 时,系统自动关闭它。文件表中记录引用计数,表示当前被多少个 fd 关联。
+
+### 14.4 分别写还是接续写
+
+两个 fd 各自独立偏移量,所以默认是**分别写**(各从各的位置写,会互相覆盖)。测试:
+
+```c
+/* 循环 4 次,交替写入 */
+write(fd1, buffer1, 4);   /* 0x11 0x22 0x33 0x44 */
+write(fd2, buffer2, 4);   /* 0xAA 0xBB 0xCC 0xDD */
+```
+
+因为两者偏移量独立、都从 0 开始,读取结果是 `aabbccdd...`(fd1 写的数据被 fd2 覆盖)。
+
+想变成**接续写**,可用 `O_APPEND`:`O_APPEND` 使每次 `write` 都自动到文件末尾,于是变成 `11223344aabbccdd...`。
+
+---
+
+## 15. 复制文件描述符:dup 与 dup2
+
+复制成功得到新的 fd,新旧 fd 都可操作文件,权限相同。**复制得到的 fd 与旧 fd 指向同一个文件表**,所以对文件的读写权限、状态标志、文件偏移量都相同——"复制"实则是复制文件表。使用完也要 `close`。
+
+### 15.1 dup
+
+```c
+#include <unistd.h>
+
+int dup(int oldfd);
+```
+
+| 参数 | 说明 |
+| ---- | ---- |
+| `oldfd` | 需要被复制的文件描述符 |
+
+**返回值**:成功返回新的文件描述符(由系统按 fd 分配原则分配);失败返回 `-1` 并设置 `errno`。
+
+```c
+fd2 = dup(fd1);   /* fd1=6 -> 得到 fd2=7 */
+```
+
+由于新旧 fd 指向同一个文件表,偏移量共享,因此**不用 `O_APPEND` 也能实现接续写**。
+
+### 15.2 dup2
+
+`dup` 不能自己指定 fd,`dup2` 修复了这个缺陷:
+
+```c
+#include <unistd.h>
+
+int dup2(int oldfd, int newfd);
+```
+
+| 参数 | 说明 |
+| ---- | ---- |
+| `oldfd` | 需要被复制的文件描述符 |
+| `newfd` | 指定一个文件描述符(需是当前进程没有使用的) |
+
+**返回值**:成功返回手动指定的 `newfd`;失败返回 `-1` 并设置 `errno`。
+
+```c
+fd2 = dup2(fd1, 100);   /* 得到 fd2 == 100 */
+```
+
+> 同一个 fd 可以多次调用 `dup`/`dup2` 复制,得到多个不同的 fd。
+
+**完整 dup 测试源码**(实现接续写):
+
+```c
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+int main(void)
+{
+    unsigned char buffer1[4], buffer2[4];
+    int fd1, fd2;
+    int ret;
+    int i;
+
+    /* 创建新文件 test_file 并打开 */
+    fd1 = open("./test_file", O_RDWR | O_CREAT | O_EXCL,
+               S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
+    if (-1 == fd1) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 复制文件描述符 */
+    fd2 = dup(fd1);
+    if (-1 == fd2) {
+        perror("dup error");
+        ret = -1;
+        goto err1;
+    }
+
+    printf("fd1: %d\nfd2: %d\n", fd1, fd2);
+
+    buffer1[0] = 0x11; buffer1[1] = 0x22;
+    buffer1[2] = 0x33; buffer1[3] = 0x44;
+    buffer2[0] = 0xAA; buffer2[1] = 0xBB;
+    buffer2[2] = 0xCC; buffer2[3] = 0xDD;
+
+    /* 循环写入数据,共享偏移量,因此是接续写 */
+    for (i = 0; i < 4; i++) {
+        write(fd1, buffer1, sizeof(buffer1));
+        write(fd2, buffer2, sizeof(buffer2));
+    }
+
+    /* 回到文件头读取全部数据 */
+    lseek(fd1, 0, SEEK_SET);
+    for (i = 0; i < 8; i++) {
+        read(fd1, buffer1, sizeof(buffer1));
+        printf("%x%x%x%x", buffer1[0], buffer1[1],
+               buffer1[2], buffer1[3]);
+    }
+    printf("\n");
+
+    ret = 0;
+err2:
+    close(fd2);
+err1:
+    close(fd1);
+    exit(ret);
+}
+```
+
+输出应为 `11223344aabbccdd11223344aabbccdd`,证明是接续写。
+
+---
+
+## 16. 文件共享
+
+**文件共享**指同一个文件(对应同一个 inode)被多个独立读写体同时进行 IO 操作。独立读写体可理解为同一个文件的多个不同 fd(多次 `open` 得到,或 `dup/dup2` 得到)。核心问题:**如何制造多个不同 fd 指向同一个文件**。
+
+三种常见实现方式:
+
+```mermaid
+flowchart TB
+    subgraph M1["方式一:同进程多次 open"]
+        A1["fd1"] --> TA1["文件表 1"]
+        A2["fd2"] --> TA2["文件表 2"]
+        TA1 --> IA["同一 inode"]
+        TA2 --> IA
+    end
+    subgraph M2["方式二:不同进程各自 open"]
+        B1["进程1 fd1"] --> TB1["文件表 1"]
+        B2["进程2 fd2"] --> TB2["文件表 2"]
+        TB1 --> IB["同一 inode"]
+        TB2 --> IB
+    end
+    subgraph M3["方式三:dup / dup2 复制"]
+        C1["fd1"] --> TC["同一个文件表<br/>共享偏移量"]
+        C2["fd2"] --> TC
+        TC --> IC["同一 inode"]
+    end
+```
+
+| 方式 | fd 是否各自独立 | 是否共享偏移量 | 典型用途 |
+| ---- | --------------- | -------------- | -------- |
+| 同进程多次 `open` | 是(多个文件表) | 否 | 需要独立读写位置 |
+| 不同进程各自 `open` | 是(多个文件表) | 否 | 进程间共享文件 |
+| `dup/dup2` 复制 | 是(同一文件表) | **是** | 接续写、重定向 |
+
+文件共享存在**竞争冒险**,且要关心不同读写体之间是分别写还是接续写。
+
+---
+
+## 17. 原子操作与竞争冒险
+
+### 17.1 竞争冒险
+
+假设进程 A 和 B 都对同一文件追加写(文件末尾写入),都调用了 `open` 但**未使用 `O_APPEND`**。每个进程有自己的 PCB、自己的文件表(独立偏移量),但共享同一个 inode。
+
+1. 进程 A 运行,`lseek` 把 A 的偏移量设为 1500(文件末尾);
+2. A 时间片耗尽,切换到 B;
+3. 进程 B `lseek`,也把 B 的偏移量设为 1500;
+4. B `write` 100 字节,B 的偏移量变为 1600;
+5. B 时间片耗尽,切回 A;
+6. A `write`,从 A 的偏移量 1500 开始写——但 1500 处已不是文件末尾,**覆盖了 B 刚写入的数据**。
+
+```mermaid
+sequenceDiagram
+    participant A as 进程 A
+    participant B as 进程 B
+    participant F as 文件
+    A->>F: lseek 到 1500(末尾)
+    Note over A: 时间片耗尽
+    B->>F: lseek 到 1500(末尾)
+    B->>F: write 100 字节(到 1600)
+    Note over B: 时间片耗尽
+    A->>F: write(仍从 1500 开始)→ 覆盖 B 的数据
+```
+
+这种两个(或多个)操作共享资源、结果不可预期的情形,就是**竞争状态(竞争冒险)**。因为进程获得 CPU 使用权的先后由操作系统调配,不可预期。
+
+### 17.2 原子操作
+
+问题出在"先定位到文件末尾、再写"分成了两个函数调用。解决办法是把这两步合并成一个**原子操作**:原子操作由多步组成,要么一步也不执行,一旦执行就必须执行完所有步骤,不可能只执行子集。
+
+**(1) `O_APPEND` 实现原子操作**
+
+`O_APPEND` 使每次 `write` 时把「移动写位置到文件末尾、写入数据」组成一个原子操作。无论怎么调换顺序,写入都从文件末尾开始,不会出现 A 覆盖 B 的情况。
+
+**(2) `pread()` 与 `pwrite()`**
+
+与 `read`/`write` 作用一样,区别是可用于实现原子操作,并传入位置偏移量 `offset`。调用 `pread` 相当于 `lseek` + `read`,且把两步组成原子操作。
+
+```c
+#include <unistd.h>
+
+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` | 当前读或写的位置偏移量 |
+
+**返回值**:与 `read`/`write` 意义相同。
+
+与「`lseek` + `read/write`」的区别:
+
+1. 调用 `pread` 时,无法中断其定位和读操作(即**原子操作**);
+2. **不更新文件表中的当前位置偏移量**。
+
+验证第 2 点:`pread(fd, buffer, 100, 1024)` 后,`lseek(fd, 0, SEEK_CUR)` 返回 0 而不是 1124。
+
+```c
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+int main(void)
+{
+    unsigned char buffer[100];
+    int fd;
+    int ret;
+
+    fd = open("./test_file", O_RDWR);
+    if (-1 == fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 从偏移文件头 1024 字节处读取 100 字节 */
+    ret = pread(fd, buffer, sizeof(buffer), 1024);
+    if (-1 == ret) {
+        perror("pread error");
+        goto err;
+    }
+
+    /* 获取当前位置偏移量,预期仍为 0 */
+    ret = lseek(fd, 0, SEEK_CUR);
+    if (-1 == ret) {
+        perror("lseek error");
+        goto err;
+    }
+
+    printf("Current Offset: %d\n", ret);   /* 输出 0 */
+    ret = 0;
+err:
+    close(fd);
+    exit(ret);
+}
+```
+
+**(3) 创建一个文件(`O_EXCL`)**
+
+进程 A、B 都要打开一个不存在的文件:A 先 `open("./file", O_RDWR)` 失败,时间片耗尽;B 同样失败,接着 B 用 `O_CREAT` 创建成功;切回 A,A 也用 `O_CREAT` 创建"成功"——同一个文件被创建两次,这是不允许的。
+
+用 `O_EXCL` + `O_CREAT`:若文件已存在则返回错误,若不存在则创建,把「判断是否存在、创建文件」合并成一个原子操作,保证进程是文件的创建者。
+
+---
+
+## 18. fcntl 与 ioctl
+
+### 18.1 fcntl
+
+`fcntl()` 可对一个已打开的 fd 执行一系列控制操作,类似"多功能文件描述符管理工具箱"。
+
+```c
+#include <unistd.h>
+#include <fcntl.h>
+
+int fcntl(int fd, int cmd, ... /* arg */ );
+```
+
+| 参数 | 说明 |
+| ---- | ---- |
+| `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` |
+
+**返回值**:失败返回 `-1` 并设置 `errno`;成功时返回值与 `cmd` 有关(如 `F_DUPFD` 返回新 fd、`F_GETFD` 返回描述符标志、`F_GETFL` 返回文件状态标志)。
+
+**(1) 复制文件描述符**
+
+`cmd = F_DUPFD`:根据 `fd` 复制出新 fd,第三个参数指出新 fd 是**大于或等于该参数的可用 fd**;若该参数等于已存在的 fd,则取大于它的可用 fd。
+
+```c
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+int main(void)
+{
+    int fd1, fd2;
+    int ret;
+
+    fd1 = open("./test_file", O_RDONLY);
+    if (-1 == fd1) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 复制 fd1,新 fd 需大于等于 0 */
+    fd2 = fcntl(fd1, F_DUPFD, 0);
+    if (-1 == fd2) {
+        perror("fcntl error");
+        ret = -1;
+        goto err;
+    }
+
+    printf("fd1: %d\nfd2: %d\n", fd1, fd2);
+
+    ret = 0;
+    close(fd2);
+err:
+    close(fd1);
+    exit(ret);
+}
+```
+
+**(2) 获取/设置文件状态标志**
+
+`F_GETFL` 获取、`F_SETFL` 设置。设置的就是调用 `open` 时传入的 flags,但有限制:
+
+- **文件权限标志**(`O_RDONLY`、`O_WRONLY`、`O_RDWR`)不能被设置;
+- **文件创建标志**(`O_CREAT`、`O_EXCL`、`O_NOCTTY`、`O_TRUNC`)不能被设置,会被忽略;
+- Linux 下只有 `O_APPEND`、`O_ASYNC`、`O_DIRECT`、`O_NOATIME`、`O_NONBLOCK` 这些标志可以被修改。
+
+```c
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+int main(void)
+{
+    int fd;
+    int ret;
+    int flag;
+
+    fd = open("./test_file", O_RDWR);
+    if (-1 == fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 获取文件状态标志 */
+    flag = fcntl(fd, F_GETFL);
+    if (-1 == flag) {
+        perror("fcntl F_GETFL error");
+        ret = -1;
+        goto err;
+    }
+    printf("flags: 0x%x\n", flag);
+
+    /* 在原标志基础上添加 O_APPEND */
+    ret = fcntl(fd, F_SETFL, flag | O_APPEND);
+    if (-1 == ret) {
+        perror("fcntl F_SETFL error");
+        goto err;
+    }
+
+    ret = 0;
+err:
+    close(fd);
+    exit(ret);
+}
+```
+
+### 18.2 ioctl
+
+`ioctl()` 可认为是一个文件 IO 操作的"杂物箱",处理的事情非常杂、不统一,一般用于操作特殊文件或硬件外设(例如用它获取 LCD 相关信息)。
+
+```c
+#include <sys/ioctl.h>
+
+int ioctl(int fd, unsigned long request, ...);
+```
+
+| 参数 | 说明 |
+| ---- | ---- |
+| `fd` | 文件描述符 |
+| `request` | 与具体要操作的对象有关,没有统一值,表示向 fd 请求相应操作 |
+| `...` | 可变参,第三个参数根据 `request` 决定 |
+
+**返回值**:成功返回 0,失败返回 -1。
+
+---
+
+## 19. 截断文件:truncate 与 ftruncate
+
+使用 `truncate()` 或 `ftruncate()` 可将普通文件截断为指定字节长度。
+
+```c
+#include <unistd.h>
+#include <sys/types.h>
+
+int truncate(const char *path, off_t length);
+int ftruncate(int fd, off_t length);
+```
+
+两者功能一样,区别在于:`ftruncate()` 用文件描述符 `fd` 指定目标文件,`truncate()` 直接用文件路径 `path`。
+
+**行为**:
+
+- 若当前文件大小 **大于** `length`,多余数据丢失(被"砍"掉);
+- 若当前文件大小 **小于** `length`,文件被扩展,扩展部分读取得到空字节 `"\0"`。
+
+**使用 `ftruncate()` 前**必须先用 `open()` 打开文件得到 fd,且必须具有可写权限(`O_WRONLY` 或 `O_RDWR`)。
+
+**注意**:调用这两个函数**不会改变文件读写位置偏移量**,截断后一般需要重新设置读写位置,以免之前指向的位置已不存在而出错。
+
+**返回值**:成功返回 0,失败返回 `-1` 并设置 `errno`。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+int main(void)
+{
+    int fd;
+
+    /* 打开 file1 文件 */
+    if (0 > (fd = open("./file1", O_RDWR))) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 使用 ftruncate 将 file1 截断为 0 字节 */
+    if (0 > ftruncate(fd, 0)) {
+        perror("ftruncate error");
+        exit(-1);
+    }
+
+    /* 使用 truncate 将 file2 截断为 1024 字节 */
+    if (0 > truncate("./file2", 1024)) {
+        perror("truncate error");
+        exit(-1);
+    }
+
+    close(fd);
+    exit(0);
+}
+```
+
+运行前 `file1`、`file2` 均为 592 字节;运行后 `file1` 变 0,`file2` 变 1024。
+
+---
+
+## 20. 实验步骤与调试方法
+
+### 20.1 通用实验流程
+
+```bash
+# 1. 建目录
+mkdir -p ~/vscode_ws/2_chapter && cd ~/vscode_ws/2_chapter
+
+# 2. 创建源文件(用 VSCode 或 vi)
+#    粘贴本篇示例,如 testApp_1.c、hole.c、dup_test.c 等
+
+# 3. 编译
+gcc -Wall -o testApp_1 testApp_1.c
+
+# 4. 准备测试文件
+dd if=/dev/urandom of=src_file bs=1K count=2   # 生成 2KB 随机文件
+
+# 5. 运行、验证
+./testApp_1
+ls -l dest_file
+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 运行验证 |
+
+常用工具:
+
+```bash
+man 2 open            # 系统调用手册
+man 3 strerror        # 库函数手册
+strace ./testApp      # 跟踪程序发起的系统调用与参数、返回值
+lsof -p $(pgrep testApp)   # 查看进程打开的文件描述符
+ls -i file            # 查看 inode 编号
+stat file             # 查看文件元数据
+du -h file            # 查看实际占用
+```
+
+`strace` 尤其适合验证本篇知识点:
+
+```bash
+strace -e trace=open,openat,read,write,lseek,close ./testApp_1
+```
+
+---
+
+## 21. 跨平台对比:IMX6ULL vs STM32 vs RK3568
+
+> ⚠️ **来源说明**:本节不属于《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 与 RK3568 的**应用层文件 IO 接口完全通用**,代码可直接迁移,差异主要在架构位宽、glibc 版本、存储介质(NAND/eMMC)与内核版本;
+- STM32 是"另一套世界观":没有系统调用、没有 fd、没有 inode、没有权限,不要把这些概念套到裸机上;
+- 在 I.MX6ULL 上操作 `/dev` 下的字符设备时,`open/read/write/ioctl` 的语义由驱动定义,理解本篇的 fd 与文件表模型是读懂驱动行为的基础。
+
+---
+
+## 22. 面试精选
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+### Q1:什么是文件描述符?0、1、2 分别是什么?为什么 `open` 通常从 3 开始分配?
+
+**答**:文件描述符是内核返回给进程、用于索引已打开文件的**非负整数**。0、1、2 在进程启动时已被默认占用,分别对应标准输入、标准输出、标准错误(宏 `STDIN_FILENO`/`STDOUT_FILENO`/`STDERR_FILENO`)。fd 从最小可用值开始分配,所以 `open` 通常得到 3。默认上限 1024(`ulimit -n`),范围 0~1023;fd 关闭后会被释放并可重新分配。若超过进程最大文件数,内核会发信号并结束进程。
+
+### Q2:`open` 的 `O_APPEND` 和 `O_TRUNC` 分别有什么作用?为什么说 `O_APPEND` 实现了原子操作?
+
+**答**:`O_TRUNC` 在打开文件时将原有内容全部丢弃、文件大小变 0。`O_APPEND` 使每次 `write` 都自动把写位置偏移量移动到文件末尾,从文件末尾开始写。之所以说它是原子操作:追加写的本质是"先定位到文件末尾,再写",若分成 `lseek` + `write` 两步,在多进程环境下会出现 A 定位后被切走、B 写完、A 回来仍从旧位置写而覆盖 B 数据的竞争冒险。`O_APPEND` 把这两步合并成一个不可分割的原子操作,从而避免覆盖。补充:`O_APPEND` 不影响读;且使用 `O_APPEND` 后,`lseek` 无法改变写位置。
+
+### Q3:同一个文件被多次打开,内存中会有多份文件数据吗?不同 fd 的读写偏移量互相影响吗?
+
+**答**:不会有多份动态文件。多次打开同一文件,内核在内存中只维护一份动态文件(数据缓存),多个 fd 共享;但每次 `open` 会得到不同的 fd 和不同的**文件表**,而读写偏移量记录在文件表中,所以各 fd 的偏移量**相互独立**,默认是"分别写"。而通过 `dup`/`dup2` 复制得到的 fd 指向**同一个文件表**,因此共享偏移量,可实现"接续写"。动态文件在引用计数为 0 时被系统关闭。
+
+### Q4:`dup` 和 `dup2` 有什么区别?如何用它们在不用 `O_APPEND` 的情况下实现接续写?
+
+**答**:`dup(oldfd)` 复制文件描述符,新 fd 由系统按 fd 分配原则分配,不能指定;`dup2(oldfd, newfd)` 可手动指定新 fd(须是当前进程未使用的),失败均返回 -1 并设置 `errno`。二者复制得到的 fd 与原 fd 指向同一个文件表,因此共享文件偏移量、状态标志和访问权限。所以只要 `dup`/`dup2` 出第二个 fd,两个 fd 交替 `write` 时就会接着对方的位置写,实现接续写,而无需 `O_APPEND`。
+
+### Q5:什么是竞争冒险?`open`/`pread` 中分别用什么机制避免?`pread` 和 `lseek+read` 有何区别?
+
+**答**:竞争冒险指多个进程/线程并发操作同一共享资源时,因获得 CPU 的先后顺序不可预期,导致结果不可预期。典型场景是两个进程分别"先 `lseek` 到文件末尾、再 `write`"做追加写,结果互相覆盖。解决思路是把多步操作合并为**原子操作**:`open` 层面用 `O_APPEND`(把"定位末尾+写"合并),创建文件用 `O_EXCL|O_CREAT`(把"判断存在+创建"合并);读写层面用 `pread`/`pwrite`(把"定位+读写"合并)。`pread`/`pwrite` 与 `lseek`+`read`/`write` 的区别有两点:一是定位和读写不可中断、是原子操作;二是**不更新文件表中记录的当前位置偏移量**(`pread` 后 `lseek(SEEK_CUR)` 仍返回 0)。
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第二章 文件I/O基础、第三章 深入探究文件I/O

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

@@ -0,0 +1,1312 @@
+---
+title: 标准 IO 库
+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库"
+---
+
+# 标准 IO 库
+
+> 💡 **关联知识**:[[01-Linux应用编程基础/02-文件IO基础与深入探究]]、[[01-Linux应用编程基础/01-应用编程概念与开发环境]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/1. C库文件IO]]
+
+文件 I/O 围绕**文件描述符**转,标准 I/O 围绕 **FILE 指针**转。标准 I/O 不只是对文件 I/O 的封装,它还处理了很多细节——分配 stdio 缓冲区、以优化的块长度执行 I/O——让使用者不必操心块长度选择。本篇从 FILE 指针讲到 fopen/fread/fwrite,再到格式化 I/O 与两级缓冲,最后打通文件 I/O 与标准 I/O 的混用。
+
+---
+
+## 1. 标准 I/O 库简介
+
+标准 I/O 库是标准 C 库中用于文件 I/O 操作的一系列库函数的集合,函数定义都在头文件 `<stdio.h>` 中。
+
+标准 I/O 构建于文件 I/O 之上:
+
+- `fopen()` 内部利用系统调用 `open()` 打开文件;
+- `fread()` 内部利用 `read()` 读文件;
+- `fwrite()` 内部利用 `write()` 写文件。
+
+那为什么还要设计标准 I/O?因为它提供了比底层系统调用更方便、更易用的接口。
+
+### 1.1 标准 I/O 与文件 I/O 的区别
+
+| 对比项 | 标准 I/O | 文件 I/O |
+| ------ | -------- | -------- |
+| 性质 | 标准 C 库函数 | Linux 系统调用 |
+| 实现 | 由文件 I/O 封装而来,内部调用文件 I/O | 内核直接提供 |
+| 操作对象 | FILE 指针 | 文件描述符 fd |
+| 可移植性 | 更高,不同操作系统接口几乎一致 | 不同操作系统差异大 |
+| 缓冲 | 用户空间有 stdio 缓冲区,带缓存,性能效率更优 | 用户空间不带缓存 |
+
+```mermaid
+flowchart LR
+    A["应用程序"] -->|"调用"| S["标准 I/O 库函数<br/>fopen/fread/fwrite"]
+    S -->|"有 stdio 缓冲,封装"| F["文件 I/O 系统调用<br/>open/read/write"]
+    F -->|"内核缓冲"| K["内核缓冲区"]
+    K --> D["磁盘设备"]
+```
+
+---
+
+## 2. FILE 指针
+
+文件 I/O 的所有函数(`open/read/write/lseek`)围绕**文件描述符**进行;标准 I/O 的所有函数围绕 **FILE 指针**进行。
+
+使用标准 I/O 库打开或创建文件时,返回一个指向 **FILE 类型对象**的指针(`FILE *`),该指针与被打开/创建的文件关联,后续标准 I/O 操作都围绕它进行。
+
+- FILE 指针的作用**相当于文件描述符**,只不过 FILE 指针用于标准 I/O 库函数,文件描述符用于文件 I/O 系统调用;
+- FILE 是一个**结构体数据类型**,包含标准 I/O 库为管理文件所需的全部信息:用于实际 I/O 的文件描述符、指向文件缓冲区的指针、缓冲区长度、当前缓冲区中的字节数、出错标志等;
+- FILE 数据结构定义在 `<stdio.h>` 中。
+
+---
+
+## 3. 标准输入、标准输出与标准错误
+
+| 设备 | 含义 | 通常对应 |
+| ---- | ---- | -------- |
+| 标准输入 | 系统标准输入设备 | 键盘 |
+| 标准输出 | 输出正常信息的设备 | 显示器 |
+| 标准错误 | 显示错误信息的设备 | 显示器 |
+
+用户通过标准输入与系统交互:进程从标准输入(stdin)得到输入数据,把正常输出(如 `printf` 打印的字符串)输出到标准输出(stdout),把错误信息输出到标准错误(stderr)。标准输出和标准错误都对应终端屏幕,标准输入对应键盘。
+
+每个进程启动后默认打开这三个文件,得到三个文件描述符:
+
+```c
+/* Standard file descriptors. */
+#define STDIN_FILENO  0   /* Standard input.  */
+#define STDOUT_FILENO 1   /* Standard output. */
+#define STDERR_FILENO 2   /* Standard error output. */
+```
+
+0、1、2 是文件描述符,只能用于文件 I/O。标准 I/O 则使用 FILE 指针,在 `<stdio.h>` 中定义:
+
+```c
+/* Standard streams. */
+extern struct _IO_FILE *stdin;    /* Standard input stream.  */
+extern struct _IO_FILE *stdout;   /* Standard output stream. */
+extern struct _IO_FILE *stderr;   /* Standard error stream.  */
+/* C89/C99 say they're macros.  Make them happy. */
+#define stdin  stdin
+#define stdout stdout
+#define stderr stderr
+```
+
+> `struct _IO_FILE` 就是 FILE 结构体,使用 `typedef` 进行了重命名。所以标准 I/O 中用 `stdin`、`stdout`、`stderr` 表示标准输入、输出、错误。
+
+---
+
+## 4. 打开文件:fopen()
+
+```c
+#include <stdio.h>
+
+FILE *fopen(const char *path, const char *mode);
+```
+
+| 参数 | 说明 |
+| ---- | ---- |
+| `path` | 文件路径,可为绝对路径或相对路径 |
+| `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` |
+
+### 4.2 新建文件的权限
+
+`fopen()` 只有 `path` 和 `mode` 两个参数,没有参数指定新建文件权限。当 `mode` 为 `"w"`、`"w+"`、`"a"`、`"a+"` 之一且文件不存在时,新文件的权限有一个默认值:
+
+```text
+S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH   (0666)
+```
+
+### 4.3 使用示例
+
+```c
+fopen(path, "r");    /* 只读打开 */
+fopen(path, "r+");   /* 可读可写打开 */
+fopen(path, "w");    /* 只写打开,长度截断为 0,不存在则创建 */
+```
+
+### 4.4 fclose() 关闭文件
+
+```c
+#include <stdio.h>
+
+int fclose(FILE *stream);
+```
+
+| 参数 | 说明 |
+| ---- | ---- |
+| `stream` | FILE 类型指针 |
+
+**返回值**:成功返回 0;失败返回 `EOF`(也就是 -1),并设置 `errno`。
+
+> `fclose()` 会在关闭前**自动刷新该文件的 stdio 缓冲区**。
+
+---
+
+## 5. 读文件和写文件:fread() / fwrite()
+
+```c
+#include <stdio.h>
+
+size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
+size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
+```
+
+### 5.1 fread()
+
+| 参数 | 说明 |
+| ---- | ---- |
+| `ptr` | 存放读取数据的缓冲区 |
+| `size` | 每个数据项的字节大小 |
+| `nmemb` | 读取数据项的个数(总共读取 `nmemb * size` 字节) |
+| `stream` | FILE 指针 |
+
+**返回值**:成功返回读取到的数据项数目(不一定等于字节数,除非 `size == 1`);若发生错误或到达文件末尾,返回值小于 `nmemb`。`fread()` 本身**无法区分**是错误还是文件末尾,需用 `ferror()` 或 `feof()` 判断。
+
+### 5.2 fwrite()
+
+| 参数 | 说明 |
+| ---- | ---- |
+| `ptr` | 待写入数据的缓冲区 |
+| `size` | 每个数据项的字节大小 |
+| `nmemb` | 写入数据项的个数 |
+| `stream` | FILE 指针 |
+
+**返回值**:成功返回写入的数据项数目(不一定等于字节数,除非 `size == 1`);出错时返回值小于 `nmemb`(或等于 0)。
+
+> 指定数据大小的方式与系统调用不同:`fread/fwrite` 用 `nmemb(数据项个数)* size(每项大小)`,而 `read/write` 直接用一个 `size` 参数。
+>
+> 写一个结构体可以有两种等价写法:
+>
+> ```c
+> fwrite(buf, sizeof(struct mystr), 1, file);
+> fwrite(buf, 1, sizeof(struct mystr), file);
+> ```
+
+### 5.3 fwrite 示例
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+
+int main(void)
+{
+    char buf[] = "Hello World!\n";
+    FILE *fp = NULL;
+
+    /* 打开文件:w 表示只写,长度截断为 0,不存在则创建 */
+    if (NULL == (fp = fopen("./test_file", "w"))) {
+        perror("fopen error");
+        exit(-1);
+    }
+
+    printf("文件打开成功!\n");
+
+    /* 写入数据 */
+    if (sizeof(buf) > fwrite(buf, 1, sizeof(buf), fp)) {
+        printf("fwrite error\n");
+        fclose(fp);
+        exit(-1);
+    }
+
+    printf("数据写入成功!\n");
+
+    fclose(fp);
+    exit(0);
+}
+```
+
+### 5.4 fread 示例
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+
+int main(void)
+{
+    char buf[50] = {0};
+    FILE *fp = NULL;
+    int size;
+
+    if (NULL == (fp = fopen("./test_file", "r"))) {
+        perror("fopen error");
+        exit(-1);
+    }
+
+    printf("文件打开成功!\n");
+
+    /* 读取 12 * 1 = 12 字节 */
+    if (12 > (size = fread(buf, 1, 12, fp))) {
+        if (ferror(fp)) {            /* 用 ferror 判断是否发生错误 */
+            printf("fread error\n");
+            fclose(fp);
+            exit(-1);
+        }
+        /* 未发生错误则意味着到达了文件末尾 */
+    }
+
+    printf("成功读取%d 个字节数据: %s\n", size, buf);
+
+    fclose(fp);
+    exit(0);
+}
+```
+
+---
+
+## 6. 字符与行 I/O
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+>
+> 教材只讲解了 `fread`/`fwrite`,但在实际工程中,按字符和按行读写同样常用。
+
+`fgetc` 从流中读一个字符,`fputc` 写一个字符;`fgets` 读一行,`fputs` 写一行。
+
+```c
+#include <stdio.h>
+
+int fgetc(FILE *stream);                       /* 成功返回字符(提升为 int),EOF 表示结束/出错 */
+int fputc(int c, FILE *stream);                /* 成功返回写入的字符,EOF 表示出错 */
+char *fgets(char *s, int size, FILE *stream);  /* 成功返回 s,失败或到末尾返回 NULL */
+int fputs(const char *s, FILE *stream);        /* 成功返回非负值,失败返回 EOF */
+
+int getchar(void);                             /* 等价于 fgetc(stdin) */
+int putchar(int c);                            /* 等价于 fputc(c, stdout) */
+```
+
+| 函数 | 行为要点 |
+| ---- | -------- |
+| `fgetc` | 每次读 1 字节;文件末尾或出错返回 `EOF`,用 `feof`/`ferror` 区分 |
+| `fputc` | 写 1 字节;失败返回 `EOF` |
+| `fgets` | 最多读 `size - 1` 个字符,遇到换行符或 EOF 停止;**会把换行符保留在缓冲区中**,并自动追加 `'\0'` |
+| `fputs` | 输出字符串直到 `'\0'`,**不会自动加换行符**,这一点不像 `puts` |
+
+注意:`fgets` 与 `scanf("%s")` 不同,`fgets` 读取整行(含空格),而 `%s` 遇到空白就停。缓冲区必须足够大,`fgets` 最多写 `size` 字节(含 `'\0'`)。
+
+```c
+#include <stdio.h>
+
+int main(void)
+{
+    FILE *fp = fopen("./test_file", "r");
+    char line[256];
+
+    if (NULL == fp) {
+        perror("fopen error");
+        return -1;
+    }
+
+    /* 逐行读取并回显 */
+    while (fgets(line, sizeof(line), fp) != NULL)
+        fputs(line, stdout);      /* fgets 保留了 '\n',因此无需再补 */
+
+    fclose(fp);
+    return 0;
+}
+```
+
+> 对应地,标准 I/O 的“行缓冲”行为与此类行操作密切相关:终端默认行缓冲,遇到 `'\n'` 才真正输出。
+
+---
+
+## 7. 定位:fseek() / ftell() / rewind()
+
+`fseek()` 作用类似 `lseek()`,用于设置文件读写位置偏移量;`lseek()` 用于文件 I/O,`fseek()` 用于标准 I/O。
+
+```c
+#include <stdio.h>
+
+int fseek(FILE *stream, long offset, int whence);
+long ftell(FILE *stream);
+```
+
+### 7.1 fseek()
+
+| 参数 | 说明 |
+| ---- | ---- |
+| `stream` | FILE 指针 |
+| `offset` | 与 `lseek()` 的 `offset` 意义相同 |
+| `whence` | 与 `lseek()` 的 `whence` 意义相同(`SEEK_SET`/`SEEK_CUR`/`SEEK_END`) |
+
+**返回值**:成功返回 **0**;出错返回 **-1** 并设置 `errno`。**注意与 `lseek()` 不同**:`lseek()` 成功返回当前位置偏移量。
+
+读写文件时偏移量会自动递增,用 `fseek()` 可手动设置:
+
+```c
+fseek(file, 0, SEEK_SET);     /* 移动到文件开头 */
+fseek(file, 0, SEEK_END);     /* 移动到文件末尾 */
+fseek(file, 100, SEEK_SET);   /* 移动到偏移 100 字节处 */
+```
+
+### 7.2 ftell()
+
+```c
+long ftell(FILE *stream);
+```
+
+**返回值**:成功返回当前读写位置偏移量;失败返回 -1 并设置 `errno`。
+
+### 7.3 用 fseek + ftell 获取文件大小
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+
+int main(void)
+{
+    FILE *fp = NULL;
+    int ret;
+
+    if (NULL == (fp = fopen("./testApp.c", "r"))) {
+        perror("fopen error");
+        exit(-1);
+    }
+
+    printf("文件打开成功!\n");
+
+    /* 将读写位置移动到文件末尾 */
+    if (0 > fseek(fp, 0, SEEK_END)) {
+        perror("fseek error");
+        fclose(fp);
+        exit(-1);
+    }
+
+    /* 获取当前位置偏移量,即文件大小 */
+    if (0 > (ret = ftell(fp))) {
+        perror("ftell error");
+        fclose(fp);
+        exit(-1);
+    }
+
+    printf("文件大小: %d 个字节\n", ret);
+
+    fclose(fp);
+    exit(0);
+}
+```
+
+### 7.4 rewind()
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+`rewind()` 把读写位置复位到文件开头,并**清除错误标志**,等价于 `fseek(stream, 0, SEEK_SET)` 后再 `clearerr(stream)`:
+
+```c
+#include <stdio.h>
+
+void rewind(FILE *stream);   /* 无返回值 */
+```
+
+---
+
+## 8. 检查或复位状态:feof() / ferror() / clearerr()
+
+`fread()` 返回值小于 `nmemb` 时,无法区分是出错还是到达文件末尾,此时用下面两个函数判断。
+
+### 8.1 feof()
+
+```c
+#include <stdio.h>
+
+int feof(FILE *stream);
+```
+
+测试 `end-of-file` 标志:标志被设置返回非零值,否则返回 0。当文件读写位置移动到文件末尾时,该标志会被设置。
+
+```c
+if (feof(file)) {
+    /* 到达文件末尾 */
+} else {
+    /* 未到达文件末尾 */
+}
+```
+
+### 8.2 ferror()
+
+```c
+#include <stdio.h>
+
+int ferror(FILE *stream);
+```
+
+测试错误标志:被设置返回非零值,否则返回 0。当对文件的 I/O 操作发生错误时,错误标志被设置。
+
+```c
+if (ferror(file)) {
+    /* 发生错误 */
+} else {
+    /* 未发生错误 */
+}
+```
+
+### 8.3 clearerr()
+
+```c
+#include <stdio.h>
+
+void clearerr(FILE *stream);
+```
+
+清除 `end-of-file` 标志和错误标志。调用 `feof()` 或 `ferror()` 校验后,通常需要清除标志,避免下次校验用到上一次的值。此函数没有返回值,调用总会成功。
+
+> 除 `clearerr()` 外,`fseek()` 成功时也会清除 `end-of-file` 标志。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+
+int main(void)
+{
+    FILE *fp = NULL;
+    char buf[20] = {0};
+
+    if (NULL == (fp = fopen("./testApp.c", "r"))) {
+        perror("fopen error");
+        exit(-1);
+    }
+
+    printf("文件打开成功!\n");
+
+    /* 将读写位置移动到文件末尾 */
+    if (0 > fseek(fp, 0, SEEK_END)) {
+        perror("fseek error");
+        fclose(fp);
+        exit(-1);
+    }
+
+    /* 从末尾读,必然读不到 10 字节 */
+    if (10 > fread(buf, 1, 10, fp)) {
+        if (feof(fp))
+            printf("end-of-file 标志被设置,已到文件末尾!\n");
+
+        clearerr(fp);   /* 清除标志 */
+    }
+
+    fclose(fp);
+    exit(0);
+}
+```
+
+---
+
+## 9. 格式化 I/O
+
+### 9.1 格式化输出
+
+C 库函数提供 5 个格式化输出函数,都是可变参函数,共同的固定参数是 `format`(格式控制字符串):
+
+```c
+#include <stdio.h>
+
+int printf(const char *format, ...);
+int fprintf(FILE *stream, const char *format, ...);
+int dprintf(int fd, const char *format, ...);
+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` 显式指定缓冲区大小,超出部分丢弃,更安全 |
+
+用法示例:
+
+```c
+printf("Hello World!\n");
+printf("%d\n", 5);
+
+fprintf(stderr, "Hello World!\n");   /* 写入标准错误 */
+fprintf(stderr, "%d\n", 5);
+
+dprintf(STDERR_FILENO, "Hello World!\n");
+dprintf(STDERR_FILENO, "%d\n", 5);
+
+char buf[20] = {0};
+sprintf(buf, "%d", 100);              /* 数字转字符串,自动加 '\0' */
+
+snprintf(buf, sizeof(buf), "%d", 100);
+```
+
+**返回值**:`printf`/`fprintf`/`dprintf`/`sprintf` 成功返回输出的字符数,失败返回负值;`snprintf` 若缓冲区足够大,返回写入缓冲区的字符数,发生错误返回负值。
+
+> `sprintf()` 可能造成缓冲区溢出,调用者有责任确保缓冲区足够大;`snprintf()` 用 `size` 显式限制,是更安全的选择。
+
+#### 格式控制字符串
+
+```text
+%[flags][width][.precision][length]type
+```
+
+只有 `%` 和 `type` 是必须的,其余可选。格式串由**普通字符**(原样输出)和**转换说明**组成,每个转换说明对应后续一个参数。
+
+**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` |
+
+**flags 标志**
+
+| 字符 | 名称 | 作用 |
+| ---- | ---- | ---- |
+| `#` | 井号 | `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` |
+
+**precision 精度**(以点号 `.` 开头)
+
+| 取值 | 说明 |
+| ---- | ---- |
+| 数字 | 十进制正数 |
+| `*` | 星号,精度由参数列表指定;如 `printf("%.*s\n", 5, "hello world")` → `hello` |
+
+对不同类型的含义:
+
+- **整型**(`d/i/o/u/x/X`):输出的最小数字个数,不足补前导零,超过不截断;会忽略 flags 的 `0`。如 `printf("%8.5d\n", 100)` → `   00100`(前面 3 个空格);
+- **浮点型**(`a/A/e/E/f/F`):小数点后的位数,默认 6 位,不足补后置 0,超过截断。如 `printf("%.8f\n", 520.1314)` → `520.13140000`;
+- **`g`/`G`**:最大有效位数;
+- **字符串**(`s`):最大可输出字符数,超过截断。如 `printf("%.5s\n", "hello world")` → `hello`。
+
+**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` | | | | |
+
+```c
+printf("%hd\n", 12345);    /* 以 short int 类型转换 */
+printf("%ld\n", 12345);    /* 以 long int 类型转换 */
+printf("%lld\n", 12345);   /* 以 long long int 类型转换 */
+```
+
+> 格式串不必死记,用时查询即可。原则是:转换说明与对应参数的数据类型要匹配,不匹配通常会编译报错或警告。
+
+### 9.2 格式化输出完整示例
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+
+int main(void)
+{
+    char buf[50] = {0};
+
+    printf("%d (%s) %d (%s)\n", 520, "我爱你", 1314, "一生一世");
+    fprintf(stdout, "%d (%s) %d (%s)\n", 520, "我爱你", 1314, "一生一世");
+    dprintf(STDOUT_FILENO, "%d (%s) %d (%s)\n", 520, "我爱你", 1314, "一生一世");
+
+    sprintf(buf, "%d (%s) %d (%s)\n", 520, "我爱你", 1314, "一生一世");
+    printf("%s", buf);
+
+    memset(buf, 0x00, sizeof(buf));
+    snprintf(buf, sizeof(buf), "%d (%s) %d (%s)\n",
+             520, "我爱你", 1314, "一生一世");
+    printf("%s", buf);
+
+    exit(0);
+}
+```
+
+### 9.3 格式化输入
+
+C 库函数提供 3 个格式化输入函数:
+
+```c
+#include <stdio.h>
+
+int scanf(const char *format, ...);
+int fscanf(FILE *stream, const char *format, ...);
+int sscanf(const char *str, const char *format, ...);
+```
+
+| 函数 | 数据来源 | 说明 |
+| ---- | -------- | ---- |
+| `scanf` | 标准输入 | 将用户输入的数据格式化转换 |
+| `fscanf` | FILE 指针指定文件 | 从文件中读取并转换;`fscanf(stdin, ...)` 等价于 `scanf` |
+| `sscanf` | 字符串 `str` | 从字符串缓冲区读取并转换 |
+
+`scanf` 从 format 最左端开始,每遇到一个转换说明便与下一个输入数据"匹配",匹配则继续,否则结束对后面输入的处理;转换后的数据存入对应的输入地址。每个输入地址都必须有一个转换说明与之一一对应。
+
+```c
+int a, b, c;
+scanf("%d %d %d", &a, &b, &c);   /* 终端阻塞,等待用户输入,回车后处理 */
+
+char *str = "5454 hello";
+char buf[10];
+int a;
+sscanf(str, "%d %s", &a, buf);
+```
+
+**返回值**:成功返回成功匹配和分配的输入项数量;较早匹配失败时该数目可能小于提供的数目甚至为 0;发生错误返回负值。
+
+#### 格式控制字符串
+
+```text
+%[*][width][length]type
+%[m][width][length]type
+```
+
+- `*`:按转换说明读取输入但**丢弃**,不需要提供对应指针参数;
+- `m`:只能与 `%s`、`%c`、`%[` 一起使用;加了 `m` 后调用者无需分配缓冲区,函数内部自动分配足够大小,并把地址通过对应指针参数返回(该指针应是指向 `char *` 变量的指针),用完后调用者应 `free()`。
+
+```c
+char *buf;
+scanf("%ms", &buf);
+/* ... 使用 buf ... */
+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)` |
+
+`[` 的规则:`%[a-z]` 匹配 a 到 z(含两端);`%[0-9]` 匹配数字;减号是连字符,放在两个字符之间不参与匹配;要匹配减号可放中括号旁,如 `%[-a-z]`;要排除字符用 `^` 放最前,如 `%[^a-z-]` 匹配除 a 到 z 及 `-` 之外的字符。
+
+**width 最大字符宽度**
+
+十进制整数,达到最大值或发现不匹配字符(以先发生者为准)时停止读取。大多数 type 会丢弃初始空白字符,且不计入最大宽度;对字符串转换,自动追加的终止符 `'\0'` 不计入最大宽度。
+
+```c
+scanf("%4s", buf);   /* 字符串不超过 4 个字符;输入 abcdefg,只取 abcd */
+```
+
+**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` | | |
+
+```c
+scanf("%hd", var);    /* short int */
+scanf("%hhd", var);   /* signed char */
+scanf("%ld", var);    /* long int */
+scanf("%f", var);     /* float */
+scanf("%lf", var);    /* double */
+scanf("%Lf", var);    /* long double */
+```
+
+### 9.4 格式化输入完整示例
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+int main(void)
+{
+    int a;
+    float b;
+    char *str;
+
+    printf("请输入一个整数:\n");
+    scanf("%d", &a);
+    printf("你输入的整数为: %d\n", a);
+
+    printf("请输入一个浮点数:\n");
+    scanf("%f", &b);
+    printf("你输入的浮点数为: %f\n", b);
+
+    printf("请输入一个字符串:\n");
+    scanf("%ms", &str);           /* 由 scanf 内部分配缓冲区 */
+    printf("你输入的字符串为: %s\n", str);
+
+    free(str);                    /* 释放字符串占用的内存空间 */
+    exit(0);
+}
+```
+
+调用 `scanf` 后终端会阻塞等待输入,输入完成按回车即可。标准输入默认行缓冲,所以要到按下回车(换行符)程序才继续。
+
+---
+
+## 10. I/O 缓冲
+
+系统 I/O 调用(文件 I/O)和标准 C 库 I/O(标准 I/O)在操作磁盘文件时都会对数据缓冲。理解两级缓冲是掌握本篇的关键。
+
+### 10.1 文件 I/O 的内核缓冲
+
+`read()`/`write()` 并不直接访问磁盘设备,而是在**用户空间缓冲区**和**内核缓冲区(kernel buffer cache)**之间复制数据。
+
+```c
+write(fd, "Hello", 5);   /* 仅把 5 字节拷贝到内核缓冲区,函数即返回 */
+```
+
+- `write()` 后数据只拷贝到内核空间缓冲区,函数返回;稍后某个时刻内核才把缓冲区数据写入磁盘。因此 `write()` 与磁盘操作**不同步**;
+- 若此期间其它进程 `read()` 该文件,内核会直接从缓冲区把数据返回;
+- 读文件同理:内核从磁盘读数据到内核缓冲区,`read()` 从缓冲区读,读完再读入下一段;
+- 这样设计是为了提高 I/O 速度和效率,避免每次 `write` 都发起磁盘操作,也减少内核操作磁盘的次数。
+
+内核缓冲区越大越好,Linux 内核本身对其大小没有固定上限,受限于物理内存总量。
+
+**刷新内核缓冲区**:强制把内核缓冲区数据写入磁盘,对某些应用(如数据库日志)很有必要。典型场景:向 U 盘拷贝文件后、拔 U 盘前执行 `sync`,就是把内核缓冲区数据更新到 U 盘硬件;不执行就拔,可能损坏文件。
+
+控制内核缓冲的系统调用:
+
+```c
+#include <unistd.h>
+
+int fsync(int fd);        /* 内容数据 + 元数据写入磁盘,写完才返回 */
+int fdatasync(int fd);    /* 仅内容数据写入磁盘,不含元数据,写完才返回 */
+void sync(void);          /* 刷新所有文件 I/O 内核缓冲区,无参数无返回值 */
+```
+
+- `fsync()`:将 `fd` 所指文件的**内容数据和元数据**写入磁盘,只有写入完成才返回;
+- `fdatasync()`:与 `fsync()` 类似,但**仅写入内容数据,不包括元数据**;
+- `sync()`:刷新所有文件 I/O 内核缓冲区,不对指定文件操作。
+
+> 元数据是记录文件属性的数据,如文件大小、时间戳、权限等,也存储在磁盘中。
+
+控制内核缓冲的 open 标志:
+
+```c
+fd = open(filepath, O_WRONLY | O_DSYNC);   /* 类似每次 write 后调用 fdatasync() */
+fd = open(filepath, O_WRONLY | O_SYNC);    /* 类似每次 write 后调用 fsync() */
+```
+
+> 频繁调用 `fsync()`/`fdatasync()`/`sync()`(或指定 `O_DSYNC`/`O_SYNC`)对性能影响极大,大部分应用不需要。
+
+**fsync 示例**(文件拷贝后同步):
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#define BUF_SIZE    4096
+#define READ_FILE   "./rfile"
+#define WRITE_FILE  "./wfile"
+
+static char buf[BUF_SIZE];
+
+int main(void)
+{
+    int rfd, wfd;
+    size_t size;
+
+    /* 打开源文件 */
+    rfd = open(READ_FILE, O_RDONLY);
+    if (0 > rfd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 打开目标文件:不存在则创建,存在则截断 */
+    wfd = open(WRITE_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0664);
+    if (0 > wfd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 拷贝数据 */
+    while (0 < (size = read(rfd, buf, BUF_SIZE)))
+        write(wfd, buf, size);
+
+    /* 对目标文件执行 fsync 同步 */
+    fsync(wfd);
+
+    close(rfd);
+    close(wfd);
+    exit(0);
+}
+```
+
+### 10.2 直接 I/O:绕过内核缓冲
+
+从 Linux 内核 2.4 起,允许应用在执行文件 I/O 时**绕过内核缓冲区**,从用户空间直接把数据传递到文件或磁盘设备,称为**直接 I/O(direct I/O)或裸 I/O(raw I/O)**。
+
+典型场景:测试磁盘设备读写速率的应用,需要保证 `read/write` 直接访问磁盘、不经过内核缓冲,否则结果误差大。但对大多数应用,直接 I/O 会**大幅降低性能**,因为内核缓冲区做了很多优化(顺序预读取、成簇磁盘块上执行 I/O、多进程共享高速缓存),直接 I/O 享受不到。
+
+在 `open` 时指定 `O_DIRECT` 标志即可(自 Linux 2.4.10 起生效):
+
+```c
+fd = open(filepath, O_WRONLY | O_DIRECT);
+```
+
+**直接 I/O 的三个对齐限制**:
+
+1. 用于存放数据的缓冲区,其内存起始地址必须以块大小的整数倍对齐;
+2. 写文件时,文件的位置偏移量必须是块大小的整数倍;
+3. 写入到文件的数据大小必须是块大小的整数倍。
+
+不满足任何一条,`write()` 都会以 `Invalid argument` 错误返回。块大小指磁盘设备的物理块大小,常见 512、1024、2048、4096 字节。
+
+查看磁盘分区块大小:
+
+```bash
+df -h                              # 查看根文件系统挂载在哪个分区,如 /dev/sda1
+tune2fs -l /dev/sda1 | grep "Block size"
+# Block size: 4096
+```
+
+**直接 I/O 示例**:
+
+```c
+/* 使用 O_DIRECT 需要定义 _GNU_SOURCE,否则编译提示 O_DIRECT 未定义 */
+#define _GNU_SOURCE
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+/* 数据缓冲区起始地址以 4096 字节对齐 */
+static char buf[8192] __attribute((aligned(4096)));
+
+int main(void)
+{
+    int fd;
+    int count;
+
+    /* 以直接 I/O 方式打开文件 */
+    fd = open("./test_file",
+              O_WRONLY | O_CREAT | O_TRUNC | O_DIRECT, 0664);
+    if (0 > fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    /* 循环 10000 次,每次写入 4096 字节,共约 40MB */
+    count = 10000;
+    while (count--) {
+        if (4096 != write(fd, buf, 4096)) {
+            perror("write error");
+            exit(-1);
+        }
+    }
+
+    close(fd);
+    exit(0);
+}
+```
+
+对照的**普通 I/O** 版本只是去掉 `O_DIRECT` 与对齐修饰:
+
+```c
+static char buf[8192];   /* 无需 4096 对齐 */
+
+int main(void)
+{
+    int fd;
+    int count;
+
+    fd = open("./test_file", O_WRONLY | O_CREAT | O_TRUNC, 0664);
+    if (0 > fd) {
+        perror("open error");
+        exit(-1);
+    }
+
+    count = 10000;
+    while (count--) {
+        if (4096 != write(fd, buf, 4096)) {
+            perror("write error");
+            exit(-1);
+        }
+    }
+
+    close(fd);
+    exit(0);
+}
+```
+
+对比测试(约 40MB):
+
+```bash
+time ./direct_io     # 约 2.7 秒,每次 write 都直接操作磁盘
+time ./normal_io     # 约 0.13~0.14 秒,只把数据拷到内核缓冲区
+```
+
+普通 I/O 比直接 I/O 快约 20 倍。原因:直接 I/O 每次 `write` 都直接对磁盘发起写操作,而普通方式只是把用户空间数据拷贝到内核缓冲区,硬件操作耗时远大于内存复制。
+
+> `_GNU_SOURCE` 宏用于开启/禁用 Linux 系统调用和 glibc 库函数的一些功能、特性,定义后意味着打开了所有特性。默认未定义,使用其控制的特性时会编译报错。定义方式有两种:
+>
+> ```c
+> #define _GNU_SOURCE          /* 直接在源文件定义 */
+> ```
+>
+> ```bash
+> gcc -D_GNU_SOURCE -o testApp testApp.c   /* 编译时用 -D 定义(全局宏) */
+> ```
+
+### 10.3 stdio 缓冲
+
+标准 I/O 在文件 I/O 基础上封装实现,但在效率、性能上要优于文件 I/O,原因是标准 I/O 维护了自己的缓冲区——**stdio 缓冲区**(用户空间缓冲区)。
+
+工作方式:标准 I/O 函数先把用户写入或读取的数据缓存在 stdio 缓冲区,再一次性通过文件 I/O 系统调用把 stdio 缓冲区数据写入内核缓冲区,或拷贝到应用程序的 `buf` 中。这样可以减少调用系统调用的次数,优化效率性能,也让编程者免于自行处理缓冲。
+
+**设置 stdio 缓冲**:`setvbuf()`、`setbuf()`、`setbuffer()`。
+
+```c
+#include <stdio.h>
+
+int setvbuf(FILE *stream, char *buf, int mode, size_t size);
+```
+
+| 参数 | 说明 |
+| ---- | ---- |
+| `stream` | FILE 指针,指定对应文件;每个文件都可设置自己的 stdio 缓冲区 |
+| `buf` | 不为 `NULL` 时指向 `size` 大小的内存区域作为缓冲区,应以动态或静态方式分配在堆上,**不要用栈上局部变量**;为 `NULL` 时 stdio 库自动分配(除非 `mode` 为非缓冲模式) |
+| `mode` | 缓冲区类型,见下表 |
+| `size` | 缓冲区大小 |
+
+| mode | 名称 | 行为 |
+| ---- | ---- | ---- |
+| `_IONBF` | 无缓冲 | 每个标准 I/O 函数立即调用 `write()`/`read()`,忽略 `buf` 和 `size`。**stderr 默认属于此类**,保证错误信息立即输出 |
+| `_IOLBF` | 行缓冲 | 输入或输出中遇到换行符 `'\n'` 时,标准 I/O 才执行文件 I/O 操作。**终端设备默认采用行缓冲**,如标准输入、标准输出 |
+| `_IOFBF` | 全缓冲 | 填满 stdio 缓冲区后才进行文件 I/O 操作。**默认普通磁盘上的常规文件常用这种模式** |
+
+**返回值**:成功返回 0,失败返回非 0 值并设置 `errno`。
+
+> 当 stdio 缓冲区数据被刷入内核缓冲区或被读取后,这些数据就不再存在于 stdio 缓冲区中了。
+
+```c
+#include <stdio.h>
+
+void setbuf(FILE *stream, char *buf);
+void setbuffer(FILE *stream, char *buf, size_t size);
+```
+
+- `setbuf()` 构建于 `setvbuf()` 之上,相当于 `setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ)`;`buf` 要么为 `NULL`(无缓冲),要么指向调用者分配的 `BUFSIZ` 字节缓冲区(`BUFSIZ` 定义于 `<stdio.h>`,通常为 8192);
+- `setbuffer()` 类似 `setbuf()`,但允许指定缓冲区大小,相当于 `setvbuf(stream, buf, buf ? _IOFBF : _IONBF, size)`。
+
+#### 行缓冲测试
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+int main(void)
+{
+    printf("Hello World!\n");
+    printf("Hello World!");     /* 无换行符 */
+
+    for ( ; ; )
+        sleep(1);
+}
+```
+
+运行后只有第一个 `printf` 显示出来,第二个没有。因为标准输出默认行缓冲,`printf` 的字符串先写入 stdio 缓冲区,只有输出换行符(或缓冲区填满)时才刷入内核缓冲区(写入终端)。第一个含 `\n` 已刷入,第二个没有换行符,仍缓存在 stdio 缓冲区,等待换行符才输出。
+
+同理,`scanf()` 会阻塞直到用户按回车,因为标准输入默认也是行缓冲。
+
+#### 无缓冲测试
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+int main(void)
+{
+    /* 将标准输出设置为无缓冲模式 */
+    if (setvbuf(stdout, NULL, _IONBF, 0)) {
+        perror("setvbuf error");
+        exit(0);
+    }
+
+    printf("Hello World!\n");
+    printf("Hello World!");
+
+    for ( ; ; )
+        sleep(1);
+}
+```
+
+两个 `Hello World!` 都能立即输出,且光标停在第二个后面(没有换行)。
+
+#### 刷新 stdio 缓冲区
+
+无论何种缓冲模式,任何时候都可用 `fflush()` 强制刷新:
+
+```c
+#include <stdio.h>
+
+int fflush(FILE *stream);
+```
+
+参数 `stream` 指定需要刷新的文件;设为 `NULL` 表示刷新所有 stdio 缓冲区。成功返回 0,否则返回 -1 并设置 `errno`。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+int main(void)
+{
+    printf("Hello World!\n");
+    printf("Hello World!");
+    fflush(stdout);             /* 刷新标准输出 stdio 缓冲区 */
+
+    for ( ; ; )
+        sleep(1);
+}
+```
+
+两次 `Hello World!` 都会显示。
+
+**自动刷新 stdio 缓冲区的时机**:
+
+1. **文件关闭时**:调用 `fclose()` 会自动刷新该文件的 stdio 缓冲区;
+2. **程序退出时**:程序终止时自动刷新。但**与退出方式有关**——使用 `exit()`、`return`、或不显式调用结束函数,会自动刷新;使用 `_exit()` 或 `_Exit()` 终止则**不会**刷新。
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+int main(void)
+{
+    printf("Hello World!\n");
+    printf("Hello World!");
+    fclose(stdout);      /* 关闭标准输出,自动刷新缓冲区 */
+
+    for ( ; ; )
+        sleep(1);
+}
+```
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+int main(void)
+{
+    printf("Hello World!\n");
+    printf("Hello World!");
+    /* 程序正常结束(return/exit),自动刷新 stdio 缓冲区 */
+    return 0;
+}
+```
+
+#### I/O 缓冲小结
+
+```mermaid
+flowchart TB
+    APP["应用程序调用标准 I/O 库函数<br/>fwrite / fread / printf"]
+    SB["stdio 缓冲区<br/>用户空间,由 stdio 库维护"]
+    KB["内核缓冲区<br/>内核空间"]
+    DISK["磁盘设备"]
+    APP -->|"写入"| SB
+    SB -->|"满足缓冲条件后调用文件 I/O(read/write)"| KB
+    KB -->|"内核最终发起读写"| DISK
+    SB -.->|"setvbuf 设置模式/大小/空间;fflush 强制刷新"| SB
+    KB -.->|"fsync/fdatasync/sync 刷新;O_SYNC/O_DSYNC;O_DIRECT 绕过"| KB
+```
+
+- 应用调用标准 I/O 库函数,把用户数据写入 stdio 缓冲区(用户空间);
+- 按缓冲模式满足条件时,stdio 库调用文件 I/O 把数据写入内核缓冲区(内核空间);
+- 最终由内核向磁盘发起读写;
+- 应用可用 `setvbuf` 设置 stdio 缓冲区模式/大小/空间,用 `fflush` 强制刷新;
+- 应用可用 `fsync`/`fdatasync`/`sync` 刷新内核缓冲区(或 `open` 时指定 `O_SYNC`/`O_DSYNC`),或用 `O_DIRECT` 绕过内核缓冲区。
+
+---
+
+## 11. 文件描述符与 FILE 指针互转
+
+在同一个文件上混合使用文件 I/O 与标准 I/O 时,需要在文件描述符和 FILE 指针之间转换,借助 `fdopen()` 和 `fileno()`:
+
+```c
+#include <stdio.h>
+
+int fileno(FILE *stream);
+FILE *fdopen(int fd, const char *mode);
+```
+
+| 函数 | 方向 | 说明 |
+| ---- | ---- | ---- |
+| `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 混合使用的缓冲陷阱
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+int main(void)
+{
+    printf("print");
+    write(STDOUT_FILENO, "write\n", 6);
+    exit(0);
+}
+```
+
+执行结果是先输出 `write`,再输出 `print`。原因:`printf` 是标准 I/O,受行缓冲控制,`"print"` 没有换行符,被缓存在 stdio 缓冲区;而 `write` 是文件 I/O,直接把 `"write\n"` 写入内核缓冲区并输出到终端;直到 `exit()` 时才刷新 stdio 缓冲区,把 `"print"` 输出。
+
+> 混合编程时要特别注意缓冲:文件 I/O 直接把数据写入内核缓冲区,标准 I/O 先写入 stdio 缓冲区,之后才通过 `write` 写入内核缓冲区。要保证顺序一致,必要时应先 `fflush()`。
+
+---
+
+## 12. 实验步骤与调试方法
+
+### 12.1 实验流程
+
+```bash
+mkdir -p ~/vscode_ws/4_chapter && cd ~/vscode_ws/4_chapter
+
+# 编译
+gcc -Wall -o testApp testApp.c
+
+# 运行
+./testApp
+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` |
+
+### 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 访问模式是否一致 |
+
+常用工具:
+
+```bash
+man 3 fopen          # 库函数手册
+man 3 setvbuf
+strace -e trace=write,read,fopen ./testApp   # 看 stdio 何时真正发起系统调用
+stdbuf -o0 ./testApp                          # 不改代码,强制标准输出无缓冲
+```
+
+---
+
+## 13. 跨平台对比:IMX6ULL vs STM32 vs RK3568
+
+> ⚠️ **来源说明**:本节不属于《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 与 RK3568 的标准 I/O 代码基本通用;迁移时注意架构位宽(32/64 位)对 `long`/`size_t` 的影响,以及交叉工具链 ABI(软浮点 `arm-linux-gnueabi` vs 硬浮点 `arm-linux-gnueabihf`),浮点格式化在软浮点 ABI 下更容易踩坑;
+- STM32 裸机没有操作系统提供的 `FILE` 与 stdio 缓冲,所谓"用 printf 调试串口"通常是通过重定向底层写函数实现,不能套用本篇的缓冲模型;
+- 嵌入式调试中,"printf 不见输出"最常见的原因就是**行缓冲 + 没有换行符**,在 Linux 应用(I.MX6ULL/RK3568)上尤其典型。
+
+---
+
+## 14. 面试精选
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+### Q1:标准 I/O 和文件 I/O 有什么区别?为什么标准 I/O 性能更好?
+
+**答**:标准 I/O 是标准 C 库函数,文件 I/O 是 Linux 系统调用;标准 I/O 由文件 I/O 封装而来(`fopen` 调 `open`、`fread` 调 `read`、`fwrite` 调 `write`);标准 I/O 围绕 FILE 指针,文件 I/O 围绕文件描述符。可移植性上标准 I/O 更好,因为各操作系统的系统调用接口差异大,而 C 库接口几乎一致。性能方面:标准 I/O 在用户空间维护了自己的 **stdio 缓冲区**,把多次小数据读写合并为较少的大块系统调用,减少系统调用次数,因此在性能和效率上通常优于文件 I/O;文件 I/O 在用户空间不带缓冲(内核另有 page cache)。
+
+### Q2:FILE 指针和文件描述符是什么关系?如何互相转换?
+
+**答**:FILE 指针用于标准 I/O 库函数,文件描述符用于文件 I/O 系统调用;FILE 指针的作用相当于文件描述符。FILE 是一个结构体(`struct _IO_FILE` 的 typedef),内部包含用于实际 I/O 的**文件描述符**、指向文件缓冲区的指针、缓冲区长度、当前缓冲区字节数、出错标志等。转换用 `<stdio.h>` 中的两个函数:`fileno(FILE *)` 由 FILE 指针得到 fd,`fdopen(int fd, const char *mode)` 由 fd 得到 FILE 指针,`mode` 必须与 fd 的访问模式一致,否则失败。混合使用两者时要特别注意缓冲,必要时先 `fflush()`。
+
+### Q3:stdio 缓冲有哪几种模式?`stdout`、`stderr`、普通文件各默认哪种?
+
+**答**:用 `setvbuf()` 设置,共三种:`_IONBF` 无缓冲(每次标准 I/O 立即调用 `write/read`)、`_IOLBF` 行缓冲(遇到换行符 `'\n'` 才执行文件 I/O)、`_IOFBF` 全缓冲(填满缓冲区才执行文件 I/O)。默认情况:**stderr 默认无缓冲**(保证错误信息立即输出);**标准输入、标准输出连接终端时默认行缓冲**;**普通磁盘上的常规文件默认全缓冲**。
+
+### Q4:什么情况下 stdio 缓冲区会被刷新?`_exit` 和 `exit` 有何区别?
+
+**答**:刷新 stdio 缓冲区的时机有三类:调用 `fflush(stream)` 强制刷新(参数为 `NULL` 时刷新所有);调用 `fclose()` 关闭文件时自动刷新;程序正常退出时自动刷新。`exit()` 与 `_exit()` 的区别正在于此:`exit()` 是标准 C 库函数,会执行清理工作(包括刷新 stdio 缓冲区)最后调用 `_exit()`;`_exit()`/`_Exit()` 是系统调用,直接终止进程,**不刷新 stdio 缓冲区**。因此用 `_exit()` 退出时,stdout 中未刷新的内容可能丢失。
+
+### Q5:文件 I/O 的内核缓冲和标准 I/O 的 stdio 缓冲分别是什么?如何控制?直接 I/O 为何要遵守对齐?
+
+**答**:**内核缓冲(page cache)**由内核维护,位于内核空间;`read/write` 只在用户空间与内核缓冲之间复制数据,不同步到磁盘。控制方式:`fsync()`(内容+元数据)、`fdatasync()`(仅内容)、`sync()`(全部),`open` 时指定 `O_SYNC`/`O_DSYNC`,或用 `O_DIRECT` 绕过。**stdio 缓冲**由标准 I/O 库维护,位于用户空间,把多次小 I/O 合并后再调用系统调用;用 `setvbuf`/`setbuf`/`setbuffer` 设置模式与大小,用 `fflush` 强制刷新。**直接 I/O 的对齐要求**:缓冲区起始地址、文件偏移量、写入数据大小都必须是磁盘块大小的整数倍;因为直接 I/O 绕过了内核缓冲,数据必须按块设备的粒度直接落盘,不满足任一条件 `write` 就返回 `Invalid argument`(EINVAL)。直接 I/O 适用于磁盘速率测试、数据库等特殊场景,普通应用使用会显著降低性能。
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第四章 标准I/O库

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

@@ -0,0 +1,659 @@
+---
+title: 文件属性与目录
+tags: [嵌入式Linux, Linux应用编程, 文件属性, stat, 目录, 权限, 硬链接, 软链接, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第五章 文件属性与目录"
+---
+
+# 文件属性与目录
+
+> 💡 **关联知识**:[[01-Linux应用编程基础/02-文件IO基础与深入探究]]、[[01-Linux应用编程基础/03-标准IO库]]、[[01-Linux应用编程基础/05-字符串处理与系统信息]]、[[01-Linux应用编程基础/面试-Linux应用编程基础]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/4. 目录操作与文件属性]]
+
+前几章讨论的都是围绕普通文件的打开、读写、关闭等 I/O 操作。本章转向 Linux 文件系统的其它特性:文件类型、`stat` 系列、属主、权限、时间戳、软硬链接、目录、删除与重命名。学完本章,你能用 C 代码实现 `ls -l`、`stat`、`chmod`、`ln`、`rm`、`mv`、`mkdir` 等命令的核心功能。
+
+---
+
+## 1. Linux 系统中的文件类型
+
+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 | 用于进程间通信 |
+
+**普通文件**分两类:**文本文件**(内容为 ASCII 字符,方便人阅读,如 `.c`/`.h`/`.sh`/`.txt`)和**二进制文件**(存真正的数字,如可执行文件、`.o`、`.bin`)。
+
+**设备文件**对应硬件设备,但**不占用磁盘**,由文件系统虚拟出来、一般由内存维护,关机即消失;字符设备通常在 `/dev/` 下,`/dev` 因此也称虚拟文件系统 devfs。**符号链接文件**内容指向另一个文件路径,对它的操作会被转移到所指向的文件。管道与套接字都用于进程间通信,后者可跨主机(网络通信)。
+
+```mermaid
+graph TD
+    F["Linux 文件类型"] --> R["- 普通文件"]
+    F --> D["d 目录"]
+    F --> C["c 字符设备"]
+    F --> B["b 块设备"]
+    F --> L["l 符号链接"]
+    F --> S["s 套接字"]
+    F --> P["p 管道"]
+    R --> R1["文本: .c .h .sh .txt"]
+    R --> R2["二进制: 可执行文件 .o .bin"]
+    C --> DV["/dev 虚拟设备文件, 不占磁盘, 关机消失"]
+    B --> DV
+```
+
+---
+
+## 2. stat / fstat / lstat
+
+```c
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+int stat(const char *pathname, struct stat *buf);
+int fstat(int fd, struct stat *buf);
+int lstat(const char *pathname, struct stat *buf);
+```
+
+| 函数 | 起点 | 说明 |
+| ---- | ---- | ---- |
+| `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` 无效 |
+
+### 2.1 struct stat 结构体
+
+`struct stat` 由内核定义,在 `<sys/stat.h>` 中声明,应用层可直接使用。
+
+```c
+struct stat
+{
+    dev_t st_dev;            /* 文件所在设备的 ID */
+    ino_t st_ino;            /* 文件对应 inode 节点编号 */
+    mode_t st_mode;          /* 文件对应的模式 */
+    nlink_t st_nlink;        /* 文件的链接数 */
+    uid_t st_uid;            /* 文件所有者的用户 ID */
+    gid_t st_gid;            /* 文件所有者的组 ID */
+    dev_t st_rdev;           /* 设备号(指针对设备文件) */
+    off_t st_size;           /* 文件大小(以字节为单位) */
+    blksize_t st_blksize;    /* 文件内容存储的块大小 */
+    blkcnt_t st_blocks;      /* 文件内容所占块数 */
+    struct timespec st_atim;  /* 文件最后被访问的时间 */
+    struct timespec st_mtim;  /* 文件内容最后被修改的时间 */
+    struct timespec 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` | 类型字段位掩码 |
+
+```c
+if (st.st_mode & S_IXUSR) { /* 所有者具有执行权限 */ }
+if ((st.st_mode & S_IFMT) == S_IFREG) { /* 普通文件 */ }   /* 写法 1 */
+if (S_ISREG(st.st_mode)) { /* 普通文件 */ }               /* 写法 2(推荐) */
+```
+
+判断宏全集:`S_ISREG` / `S_ISDIR` / `S_ISCHR` / `S_ISBLK` / `S_ISFIFO` / `S_ISLNK` / `S_ISSOCK`。
+
+### 2.3 struct timespec
+
+定义在 `<time.h>`,`time_t` 实质是 `long int`,表示从基准时间点经过的秒数(日历时间)。
+
+```c
+struct timespec { time_t tv_sec; syscall_slong_t tv_nsec; };  /* 秒 / 纳秒 */
+```
+
+```mermaid
+graph LR
+    A["stat(pathname, &st)"] --> B["struct stat"]
+    B --> C["st_mode: 类型+权限"]
+    B --> D["st_uid/st_gid: 属主"]
+    B --> E["st_size/st_ino: 大小/inode"]
+    B --> F["st_atim/mtim/ctim: 时间戳"]
+    B --> G["st_nlink: 链接数"]
+    C --> H["S_ISREG/S_ISDIR..."]
+    C --> I["S_IRUSR/S_IWOTH..."]
+```
+
+---
+
+## 3. 文件属主
+
+Linux 是多用户系统,每个文件关联一个用户(所有者)和一个用户组;系统通过 **UID** / **GID** 识别,而非用户名。
+
+| ID 类型 | 作用 |
+| ------- | ---- |
+| 实际用户 ID / 实际组 ID | 标识进程究竟是谁(执行该程序的用户) |
+| 有效用户 ID / 有效组 ID / 附属组 ID | 用于文件访问权限检查 |
+
+一般情况下进程的**有效用户 ID = 实际用户 ID**(绝大多数文件没有 set-user-ID 位)。`st_uid` / `st_gid` 记录文件的用户 ID 与组 ID。
+
+```c
+#include <unistd.h>
+int chown(const char *pathname, uid_t owner, gid_t group);
+int fchown(int fd, uid_t owner, gid_t group);
+int lchown(const char *pathname, uid_t owner, gid_t group);
+uid_t getuid(void);
+gid_t getgid(void);
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `pathname` / `fd` | 文件路径 / 文件描述符 |
+| `owner` / `group` | 新的用户 ID / 组 ID |
+
+返回值:成功 `0`,失败 `-1` 并设置 `errno`(如 `EPERM`)。修改限制:只有超级用户进程能更改文件的用户 ID;普通用户可将组 ID 改为自己所从属的任意附属组 ID(前提有效用户 ID 等于文件用户 ID);超级用户可将组 ID 改为任意值。只改其中一个 ID 时,另一个传入文件当前值(先用 `stat` 获取)。`getuid` / `getgid` 返回当前进程的**实际**用户/组 ID,加 `sudo` 时 `getuid()` 返回 0。
+
+---
+
+## 4. 文件访问权限
+
+`st_mode` 记录访问权限。**所有文件类型(目录、设备文件)都有访问权限**,并非只有普通文件。
+
+### 4.1 普通权限与特殊权限
+
+9 个普通权限位按所有者(U)、同组(G)、其它(O)划分,每类含读(r)、写(w)、执行(x),宏见 2.2 节。3 个特殊权限位:
+
+| 宏 | 值 | 含义 |
+| -- | -- | ---- |
+| `S_ISUID` | `04000` | set-user-ID 位 |
+| `S_ISGID` | `02000` | set-group-ID 位 |
+| `S_ISVTX` | `01000` | Sticky 位 |
+
+- 若文件设置了 set-user-ID 位,进程操作该文件时内核会把进程的**有效用户 ID** 设为该文件的用户 ID;
+- 若设置了 set-group-ID 位,内核会把进程的**有效组 ID** 设为该文件的组 ID;
+- Sticky 位等用途本书未展开。
+
+### 4.2 权限检查规则
+
+内核每次对文件读写执行都会做权限检查,参与检查的是进程的**有效用户、有效组、附属组**,对应文件的**所有者、所属组、其它用户**:
+
+1. 进程有效用户 ID == 文件所有者 ID → 以**所有者**角色检查;
+2. 否则,进程有效组 ID 或任一附属组 ID == 文件组 ID → 以**同组用户**角色检查;
+3. 否则 → 以**其它用户**角色检查;
+4. 进程有效用户 ID == 0(root)→ 无需检查,拥有最高权限。
+
+### 4.3 目录权限
+
+目录拥有与普通文件相同的权限位,但含义不同:
+
+| 目录权限 | 含义 |
+| -------- | ---- |
+| 读(r) | 可列出目录内容(如 `ls`) |
+| 写(w) | 可在目录下创建、删除文件 |
+| 执行(x) | 可访问目录下的文件(读写执行其中的文件) |
+
+结论:只读无执行时只能看到文件名、无法访问文件属性;有执行无读时知道文件名可访问、但不能列出目录内容;创建/删除文件需要目录的**写 + 执行**权限;要对文件读写执行,除文件本身权限外还需拥有**所在目录的执行权限**。
+
+### 4.4 access / chmod / fchmod / umask
+
+```c
+#include <unistd.h>
+int access(const char *pathname, int mode);   /* F_OK / R_OK / W_OK / X_OK, 可 | 组合 */
+
+#include <sys/stat.h>
+int chmod(const char *pathname, mode_t mode);
+int fchmod(int fd, mode_t mode);
+
+#include <sys/types.h>
+#include <sys/stat.h>
+mode_t umask(mode_t mask);
+```
+
+| 函数 | 说明 | 返回值 |
+| ---- | ---- | ------ |
+| `access` | 检查文件存在性/读/写/执行权限,任一不通过即失败 | 通过 `0`,否则 `-1` |
+| `chmod` / `fchmod` | 修改权限,`mode` 可用八进制或权限宏;链接文件改的是所指向文件 | 成功 `0`,失败 `-1` |
+| `umask` | 设置进程权限掩码,新建文件实际权限 = `mode & ~umask` | 返回旧 umask |
+
+修改权限要么是 root 进程,要么进程有效用户 ID 与文件用户 ID 匹配。umask 是**进程属性**、不能屏蔽特殊权限位、通常继承自父进程。例如 `mode=0777`、`umask=0002` 时实际权限为 `0775`。
+
+---
+
+## 5. 文件的时间属性
+
+| 字段 | 说明 |
+| ---- | ---- |
+| `st_atim` | 文件最后被访问的时间(如 `read`) |
+| `st_mtim` | 文件内容最后被修改的时间(如 `write`) |
+| `st_ctim` | 文件状态最后被改变的时间(inode 变更,如 `chmod`、`chown`) |
+
+### 5.1 utime / utimes
+
+```c
+#include <sys/types.h>
+#include <utime.h>
+int utime(const char *filename, const struct utimbuf *times);
+struct utimbuf { time_t actime; time_t modtime; };  /* 访问时间 / 内容修改时间 */
+
+#include <sys/time.h>
+int utimes(const char *filename, const struct timeval times[2]);
+struct timeval { long tv_sec; long tv_usec; };      /* 秒 / 微秒 */
+```
+
+`times` 传 `NULL` 则都设为当前时间。返回值:成功 `0`,失败 `-1` 并设置 `errno`。`utime` 精度到秒,`utimes` 精度到微秒;可显式修改 `atime` 与 `mtime`,**不能显式修改 `ctime`**(内核在 inode 变更时自动更新)。修改权限:超级用户进程;有效用户 ID 与文件用户 ID 匹配的进程;`times == NULL` 且对文件有写权限的进程。
+
+### 5.2 futimens / utimensat
+
+相比 `utime` / `utimes`:可到纳秒精度;可单独设置某一个时间戳;可独立将任一时间戳设为当前时间。
+
+```c
+#include <fcntl.h>
+#include <sys/stat.h>
+int futimens(int fd, const struct timespec times[2]);
+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` 指定值 |
+
+`utimensat` 的 `dirfd` 可为目录 fd 或 `AT_FDCWD`;`flags` 可为 `0` 或 `AT_SYMLINK_NOFOLLOW`(修改链接本身时间戳)。
+
+---
+
+## 6. 软链接与硬链接
+
+| 对比项 | 硬链接 | 软链接(符号链接) |
+| ------ | ------ | ------------------ |
+| inode | 与源文件**相同** | 与源文件**不同** |
+| 数据块 | 与源文件共享 | 存的是源文件路径 |
+| 关系 | 完全平等 | 主从关系 |
+| 源文件删除 | 仍可访问(链接数减 1) | 变悬空链接,指向无效路径 |
+| 链接数统计 | 计入 `st_nlink` | 不计入 |
+| 对目录 | 不能创建(root+底层支持除外) | 可以 |
+| 跨文件系统 | 要求同文件系统 | 可以 |
+| 目标不存在 | 不允许 | 允许(悬空链接) |
+
+`ln 源文件 链接文件` 创建硬链接,`ln -s 源文件 链接文件` 创建软链接;源文件本身也算一个硬链接。
+
+```c
+#include <unistd.h>
+int link(const char *oldpath, const char *newpath);      /* 硬链接 */
+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` |
+| `readlink` | `pathname` 必须是软链接;`buf` 缓冲区;`bufsiz` 大小 | 成功返回字节数,失败 `-1` |
+
+**重要**:不能对软链接用 `open` + `read` 读取其存储的路径——`open` 会打开所指向的文件,必须用 `readlink`。
+
+```mermaid
+graph TD
+    subgraph Hard["硬链接"]
+        H1["test_file (inode=100)"] --- I1["inode 100 nlink=3"]
+        H2["hard1 (inode=100)"] --- I1
+        H3["hard2 (inode=100)"] --- I1
+        I1 --- D1["数据块"]
+    end
+    subgraph Soft["软链接"]
+        S1["soft (inode=200)"] --- I2["inode 200 nlink=1"]
+        I2 --- D2["数据块: './test_file' 路径字符串"]
+        D2 -. 解析路径 .-> I1
+    end
+```
+
+---
+
+## 7. 目录
+
+- 普通文件 = inode 节点 + 数据块;
+- 目录 = inode 节点 + 目录块;目录块由多个**目录项**组成,每个目录项记录某文件的**文件名 + inode 编号**。
+
+```c
+#include <sys/stat.h>
+#include <sys/types.h>
+int mkdir(const char *pathname, mode_t mode);   /* 创建目录 */
+int rmdir(const char *pathname);                /* 删除空目录 */
+
+#include <sys/types.h>
+#include <dirent.h>
+DIR *opendir(const char *name);
+struct dirent *readdir(DIR *dirp);
+void rewinddir(DIR *dirp);
+int closedir(DIR *dirp);
+
+#include <unistd.h>
+char *getcwd(char *buf, size_t size);
+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` |
+
+```c
+struct dirent {
+    ino_t          d_ino;       /* inode 编号 */
+    off_t          d_off;       /* not an offset; see NOTES */
+    unsigned short d_reclen;    /* length of this record */
+    unsigned char  d_type;      /* type of file; not supported by all filesystem types */
+    char           d_name[256]; /* 文件名 */
+};
+```
+
+一般只关注 `d_ino` 和 `d_name`。`readdir` 返回 `NULL` 时用 `errno` 区分到末尾还是出错:
+
+```c
+errno = 0;
+direntp = readdir(dirp);
+if (NULL == direntp) {
+    if (0 != errno) { /* 出错 */ } else { /* 已到目录末尾 */ }
+}
+```
+
+进程的当前工作目录是解析**相对路径**的起点,一般继承自父进程。
+
+---
+
+## 8. 删除文件与重命名
+
+```c
+#include <unistd.h>
+int unlink(const char *pathname);      /* 删除文件(不含目录) */
+
+#include <stdio.h>
+int remove(const char *pathname);      /* 删除文件或空目录 */
+int rename(const char *oldpath, const char *newpath);  /* 重命名 */
+```
+
+`unlink` 移除目录项并把 inode 链接计数减 1:若还有其它硬链接仍可访问数据;只有链接计数为 0 且无进程打开时,inode 与数据块才被回收;若 `pathname` 是软链接,删除链接本身、不解引用。返回值:成功 `0`,失败 `-1` 并设置 `errno`。
+
+`remove` 内部判断:非目录调用 `unlink`,目录调用 `rmdir`;对软链接不解引用,删除链接本身。
+
+`rename` 只操作目录条目、不移动文件数据(inode 编号不变、数据块内容不移动),不影响指向该文件的其它硬链接,也不影响已打开该文件的进程。各种情况:
+
+- `newpath` 已存在则覆盖;
+- `newpath` 与 `oldpath` 指向同一文件则不变化(且成功);
+- 对两个参数中的软链接均不解引用:`oldpath` 是软链接则重命名该链接;`newpath` 是软链接则移除并覆盖;
+- `oldpath` 是文件(非目录)时 `newpath` 不能是目录路径;移到某目录下必须包含新文件名;
+- `oldpath` 是目录时 `newpath` 要么不存在,要么是空目录;
+- 两者必须在**同一文件系统**;不能对 `.` 和 `..` 重命名。
+
+---
+
+## 9. 完整可复制源码
+
+以下综合例程先创建 `test_file`、打印完整属性(`strftime` 格式化三个时间戳),再演示 `chmod`、`access`、`link`/`symlink`/`readlink`、遍历目录、`rename`、`unlink`,可直接在 Ubuntu 上编译运行。
+
+```c
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <errno.h>
+
+#define TEST_FILE "./test_file"
+#define HARD_LINK "./hard_link"
+#define SOFT_LINK "./soft_link"
+#define NEW_NAME  "./new_file"
+
+/* 根据 st_mode 打印文件类型 */
+static void print_file_type(mode_t mode)
+{
+    if (S_ISREG(mode))       printf("普通文件");
+    else if (S_ISDIR(mode))  printf("目录");
+    else if (S_ISCHR(mode))  printf("字符设备");
+    else if (S_ISBLK(mode))  printf("块设备");
+    else if (S_ISLNK(mode))  printf("符号链接");
+    else if (S_ISFIFO(mode)) printf("管道");
+    else if (S_ISSOCK(mode)) printf("套接字");
+    else                     printf("未知");
+}
+
+/* 把时间戳格式化为可读字符串 */
+static void time_to_str(const struct timespec *ts, char *out, size_t len)
+{
+    struct tm tm_buf;
+    localtime_r(&ts->tv_sec, &tm_buf);
+    strftime(out, len, "%Y-%m-%d %H:%M:%S", &tm_buf);
+}
+
+/* 打印 stat 结构体中的关键属性 */
+static void show_attr(const char *path)
+{
+    struct stat st;
+    char at[64], mt[64], ct[64];
+
+    if (-1 == stat(path, &st)) { perror("stat error"); return; }
+    printf("===== %s =====\n", path);
+    printf("inode=%ld  type=", (long)st.st_ino);
+    print_file_type(st.st_mode);
+    printf("  mode=0%o  nlink=%ld\n", st.st_mode & 07777, (long)st.st_nlink);
+    printf("uid=%d gid=%d size=%ld blksize=%ld blocks=%ld\n",
+           st.st_uid, st.st_gid, (long)st.st_size,
+           (long)st.st_blksize, (long)st.st_blocks);
+    time_to_str(&st.st_atim, at, sizeof(at));
+    time_to_str(&st.st_mtim, mt, sizeof(mt));
+    time_to_str(&st.st_ctim, ct, sizeof(ct));
+    printf("atime=%s\nmtime=%s\nctime=%s\n", at, mt, ct);
+}
+
+/* 遍历目录,打印文件名与 inode */
+static int list_dir(const char *dir_name)
+{
+    DIR *dirp = opendir(dir_name);
+    struct dirent *dir;
+
+    if (NULL == dirp) { perror("opendir error"); return -1; }
+    printf("----- dir %s -----\n", dir_name);
+    errno = 0;
+    while (NULL != (dir = readdir(dirp)))
+        printf("  %-20s inode=%ld\n", dir->d_name, (long)dir->d_ino);
+    if (0 != errno) { perror("readdir error"); closedir(dirp); return -1; }
+    printf("End of directory!\n");
+    closedir(dirp);
+    return 0;
+}
+
+int main(void)
+{
+    int fd;
+    char buf[128];
+    ssize_t n;
+    struct utimbuf utm;
+    time_t now;
+
+    /* 1. 创建并写入 test_file */
+    fd = open(TEST_FILE, O_RDWR | O_CREAT | O_TRUNC, 0664);
+    if (-1 == fd) { perror("open error"); exit(-1); }
+    if (write(fd, "Hello Linux file attribute\n", 27) < 0) perror("write error");
+    close(fd);
+    show_attr(TEST_FILE);
+
+    /* 2. 检查权限并修改权限为 0777 */
+    if (0 == access(TEST_FILE, R_OK | W_OK)) printf("access: 读写权限 OK\n");
+    else                                     printf("access: 权限不足\n");
+    if (-1 == chmod(TEST_FILE, 0777)) { perror("chmod error"); exit(-1); }
+
+    /* 3. 显式修改访问时间与内容修改时间 */
+    time(&now);
+    utm.actime = now; utm.modtime = now;
+    if (-1 == utime(TEST_FILE, &utm)) perror("utime error");
+
+    /* 4. 创建硬链接与软链接 */
+    if (-1 == link(TEST_FILE, HARD_LINK))    perror("link error");
+    if (-1 == symlink(TEST_FILE, SOFT_LINK)) perror("symlink error");
+    show_attr(TEST_FILE);
+    show_attr(HARD_LINK);   /* inode 与 test_file 相同 */
+    show_attr(SOFT_LINK);   /* inode 不同,大小为路径长度 */
+
+    /* 5. 用 readlink 读取软链接内容 */
+    memset(buf, 0x0, sizeof(buf));
+    n = readlink(SOFT_LINK, buf, sizeof(buf) - 1);
+    if (-1 == n) perror("readlink error");
+    else { buf[n] = '\0'; printf("soft_link 指向: %s\n", buf); }
+
+    /* 6. 遍历当前目录 */
+    list_dir(".");
+
+    /* 7. 重命名文件(inode 不变) */
+    if (-1 == rename(TEST_FILE, NEW_NAME)) perror("rename error");
+    show_attr(NEW_NAME);
+
+    /* 8. 删除链接与文件 */
+    if (-1 == unlink(SOFT_LINK)) perror("unlink soft error");
+    if (-1 == unlink(HARD_LINK)) perror("unlink hard error");
+    if (-1 == remove(NEW_NAME))  perror("remove error");
+
+    printf("done.\n");
+    exit(0);
+}
+```
+
+**逐段解释**:
+
+1. `print_file_type`:用 `S_ISREG` 等宏判断 `st_mode` 中的类型;比手工位运算可读性好。
+2. `time_to_str`:`localtime_r` 把 `time_t` 分解为 `struct tm`,`strftime` 按 `%Y-%m-%d %H:%M:%S` 格式化;`_r` 版本可重入、线程安全。
+3. `show_attr`:`stat` 取属性,`st_mode & 07777` 取低 12 位权限,`st_nlink` 展示硬链接数。
+4. `list_dir`:`opendir` → 循环 `readdir` → `closedir`;用 `errno = 0` + 返回值判断读到末尾还是出错。
+5. `main` 第 2 步:`access` 判权限,`chmod` 改权限(需所有者或 root)。
+6. `main` 第 3 步:`utime` 显式改 `atime`/`mtime`;注意 `ctime` 会被内核自动更新。
+7. `main` 第 4~5 步:`link` 后 `st_nlink` 加 1、inode 相同;`symlink` 后 inode 不同、`st_size` 为路径长度;`readlink` 才能读出软链接存储的路径。
+8. `main` 第 7 步:`rename` 后 inode 不变,验证只改目录条目。
+9. `main` 第 8 步:软链接用 `unlink` 删除链接本身;最后一个硬链接删除后数据块才回收。
+
+---
+
+## 10. 实验步骤与调试方法
+
+```bash
+gcc -Wall -o test_attr test_attr.c && ./test_attr
+stat test_file                          # 对照程序打印的 inode、权限、时间戳
+ls -li test_file hard_link soft_link    # 观察 inode 与链接数
+readlink soft_link                      # 对照 readlink() 读出内容
+```
+
+观察点:硬链接与源文件 inode 相同、`st_nlink` 增加;软链接 inode 不同,`ls -l` 显示 `l` 且带 `->`;`chmod 0777` 后 `ls -l` 权限变为 `rwxrwxrwx`。
+
+权限与属主实验:
+
+```bash
+./test_attr                     # 普通用户运行 chown 会 EPERM
+sudo ./test_attr                # root 身份才能改属主
+umask                           # 查看当前 shell 的 umask
+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` |
+
+调试技巧:出错立即 `perror()`;`strace -e trace=openat,stat,lstat,readlink,unlink ./test_attr` 观察系统调用;`ltrace` 观察 C 库函数;对同一软链接分别 `stat` / `lstat`,观察 `st_mode` 与 `st_ino` 差异。
+
+---
+
+## 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 |
+| 典型场景 | 应用编程、驱动配合 | 实时控制、低功耗 | 高性能边缘计算、多媒体 |
+
+要点:STM32 上的「文件」通常只是存储介质上的 FAT 记录,没有 inode、权限、链接概念;IMX6ULL/RK3568 是完整 Linux,文件属性体系完全一致,应用代码可无缝迁移。
+
+---
+
+## 12. 面试精选(5 题)
+
+### Q1:`stat`、`fstat`、`lstat` 有什么区别?什么场景用哪个?
+
+三者都获取文件属性并写入 `struct stat`。`stat(pathname)` 从路径出发、不需打开文件,若路径是符号链接会**解引用**得到所指文件属性;`fstat(fd)` 从文件描述符出发、需先 `open`,文件已打开时更自然,也能规避「检查后路径被替换」的 TOCTOU 风险;`lstat(pathname)` 与 `stat` 类似但**不解引用**,得到链接文件本身属性。典型用法:判断是否软链接用 `lstat` + `S_ISLNK`;查看真实文件属性用 `stat`;对已打开的 fd 用 `fstat`。
+
+### Q2:硬链接和软链接的本质区别是什么?为什么硬链接不能跨文件系统、不能指向目录?
+
+硬链接是多个目录项指向**同一个 inode**,地位平等,`st_nlink` 计数,删除一个只是计数减 1,计数为 0 且无进程打开才真正回收数据块。软链接是独立文件(独立 inode),数据块存目标路径字符串,靠路径解析找到目标,目标删除后成悬空链接。
+
+硬链接不能跨文件系统:inode 编号只在单个文件系统内唯一,跨文件系统无法用 inode 号定位同一文件。硬链接一般不能指向目录:若允许会形成目录环,导致 `find`、`du` 等遍历算法死循环,也会破坏目录树结构;只有超级用户在底层文件系统支持时才可能创建。
+
+### Q3:`access()` 检查权限有什么局限?为什么推荐用「打开再处理」替代?
+
+`access` 使用进程的**实际用户/组 ID**(real UID/GID)判断权限,而文件访问时内核用**有效用户/组 ID**(effective UID/GID)。当程序设置了 set-user-ID 位(`sudo` 或 SUID 程序)时两者可能不同,`access` 结果就不等于真正 `open` 的结果。此外检查与实际操作之间存在时间窗口,文件可能被替换(TOCTOU 竞态)。因此更安全的做法是直接用目标权限 `open`,根据 `errno`(`EACCES`/`EPERM`)判断;`access` 适合做提示性、非安全关键的检查。
+
+### Q4:`umask` 的作用是什么?`open(..., 0777)` 新建的文件权限一定是 0777 吗?
+
+`umask` 是**进程属性**,用于屏蔽新建文件/目录的权限位,最终权限为 `mode & ~umask`。所以 `open(..., 0777)` 的实际权限还取决于进程 umask,例如 umask=0002 时实际是 0775。umask 通常继承自父进程(如 shell),`umask()` 系统调用可修改并返回旧值;umask 不能屏蔽特殊权限位,也不影响已存在文件的权限(那是 `chmod` 的职责)。
+
+### Q5:文件有三个时间戳 atime/mtime/ctime,分别什么时候改变?能显式修改哪些?
+
+`atime`(访问时间)在读取文件内容时更新(如 `read`);`mtime`(内容修改时间)在文件内容改变时更新(如 `write`);`ctime`(状态改变时间)在 inode 变更时更新(如 `chmod`、`chown`、改链接数、`rename`),注意它不是「创建时间」。可显式修改的只有 `atime` 和 `mtime`:`utime`(秒级)、`utimes`(微秒级)、`futimens`/`utimensat`(纳秒级,可单独设置、可用 `UTIME_NOW`/`UTIME_OMIT`)。`ctime` 无法显式修改,任何导致 inode 变更的操作都会让内核自动更新它——这正是「状态最后被改变」的含义。
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第五章 文件属性与目录

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

@@ -0,0 +1,665 @@
+---
+title: 字符串处理与系统信息
+tags: [嵌入式Linux, Linux应用编程, 字符串, 正则表达式, 时间, proc文件系统, 内存分配, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第六章 字符串处理、第七章 系统信息与系统资源"
+---
+
+# 字符串处理与系统信息
+
+> 💡 **关联知识**:[[01-Linux应用编程基础/03-标准IO库]]、[[01-Linux应用编程基础/04-文件属性与目录]]、[[01-Linux应用编程基础/面试-Linux应用编程基础]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/1. C库文件IO]]、[[Linux+C+C++技术体系梳理/1. C语言基础/20. 字符串函数]]、[[Linux+C+C++技术体系梳理/1. C语言基础/21. 内存管理函数]]
+
+C 语言没有内建字符串类型,字符串处理依赖 C 库函数;应用程序又经常需要获取系统信息并向操作系统申请资源(堆内存)。本篇把**字符串处理**与**系统信息 / 系统资源**自包含地讲清楚。
+
+---
+
+## 1. 字符串输入 / 输出
+
+```c
+#include <stdio.h>
+int puts(const char *s);                 /* 输出字符串到 stdout,自动换行 */
+int putchar(int c);                      /* 输出一个字符到 stdout */
+int fputc(int c, FILE *stream);          /* 输出字符到指定流 */
+int fputs(const char *s, FILE *stream);  /* 输出字符串到指定流 */
+char *gets(char *s);                     /* 从 stdin 读一行(不安全,已废弃) */
+int getchar(void);                       /* 从 stdin 读一个字符 */
+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` |
+
+`printf` 是格式化输出,功能更强;`puts` 等更简单且同样使用 stdio 缓冲。`gets` 不检查缓冲区溢出,编译告警,应改用 `fgets`。
+
+**`gets` / `fgets` 与 `scanf("%s")` 的区别**
+
+- `gets` / `fgets` 允许空格、TAB,仅换行符作分隔;`scanf("%s")` 把空格、TAB、换行都当分隔符,一个 `%s` 只读一个词。
+- `gets` 取走并丢弃换行符;`scanf` **不取走**分隔符,残留在缓冲区影响下一次读取;`scanf("%c")` 会把空格、TAB、换行当正常字符读入。
+
+```mermaid
+graph TD
+    BUF["输入缓冲区"] --> G["gets: 读整行, 丢弃 \\n"]
+    BUF --> FG["fgets: 读整行(限长), 保留 \\n"]
+    BUF --> SC["scanf(%s): 遇分隔符停止, 保留分隔符"]
+    BUF --> GC["getchar/fgetc: 读 1 字符(含空格/换行)"]
+```
+
+---
+
+## 2. 字符串长度、拼接、拷贝、填充、比较、查找
+
+```c
+#include <string.h>
+size_t strlen(const char *s);                                    /* 长度,不含 '\0' */
+char *strcat(char *dest, const char *src);                       /* 拼接 */
+char *strncat(char *dest, const char *src, size_t n);            /* 拼接最多 n 字符 */
+char *strcpy(char *dest, const char *src);                       /* 拷贝(含 '\0') */
+char *strncpy(char *dest, const char *src, size_t n);            /* 拷贝最多 n 字符 */
+void *memset(void *s, int c, size_t n);                          /* 内存填充 */
+int strcmp(const char *s1, const char *s2);                      /* 比较 */
+int strncmp(const char *s1, const char *s2, size_t n);           /* 比较前 n 字符 */
+char *strchr(const char *s, int c);                              /* 正向查找字符 */
+char *strrchr(const char *s, int c);                             /* 反向查找字符 */
+char *strstr(const char *haystack, const char *needle);          /* 查找子串 */
+
+#include <strings.h>
+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` |
+
+**`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`。函数参数中的数组会退化为指针。
+
+`memcpy` / `memmove` / `bcopy` 也能拷贝(本质是内存拷贝):`memcpy` 要求源与目标不重叠,重叠用 `memmove`。
+
+### 2.1 strtok 字符串分割(扩展)
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+```c
+#include <string.h>
+char *strtok(char *str, const char *delim);
+```
+
+第一次传待分割字符串,之后传 `NULL`;按 `delim` 中任意字符分割。返回每个子串,无可分割时返回 `NULL`。会修改原字符串(分隔符改写为 `\0`),非线程安全,多线程用 `strtok_r`。
+
+```c
+char buf[] = "192.168.1.100";
+char *p = strtok(buf, ".");
+while (NULL != p) { printf("%s\n", p); p = strtok(NULL, "."); } /* 192 168 1 100 */
+```
+
+---
+
+## 3. 字符串与数字互转
+
+```c
+#include <stdlib.h>
+int atoi(const char *nptr);            long atol(const char *nptr);       long long atoll(const char *nptr);
+long int strtol(const char *nptr, char **endptr, int base);
+long long int strtoll(const char *nptr, char **endptr, int base);
+unsigned long int strtoul(const char *nptr, char **endptr, int base);
+unsigned long long int strtoull(const char *nptr, char **endptr, int base);
+double atof(const char *nptr);
+double strtod(const char *nptr, char **endptr);
+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` 系列跳过前导空格,遇数字或正负号开始,遇非数字或 `\0` 结束。`strtol` 的 `endptr` 若非 `NULL` 存第一个无效字符地址(无数字时存 `nptr` 并返回 0);`base=0` 时 `0x` 前缀为 16 进制、`0` 前缀为 8 进制。
+
+**数字转字符串**:用格式化 I/O,`sprintf` / `snprintf` 输出到缓冲区:
+
+```c
+char str[20];
+sprintf(str, "%d", 500);      /* "500" */
+sprintf(str, "%f", 500.111);  /* "500.111000" */
+snprintf(str, sizeof(str), "%u", 500);  /* 推荐,防溢出 */
+```
+
+```mermaid
+graph LR
+    S["字符串 \"0x1F4\""] -->|strtol base=16| L["long 500"]
+    S -->|atof/strtod| D["double"]
+    L -->|snprintf| S2["字符串 \"500\""]
+    D -->|sprintf %f| S2
+```
+
+---
+
+## 4. 给应用程序传参
+
+```c
+int main(int argc, char **argv) { /* 或 char *argv[] */ }
+```
+
+`argc` 是参数个数(**含程序自身路径**);`argv[0]` 为程序路径,`argv[1]` 起为真实参数;空格分隔,带空格的参数用引号包裹。`./app 112233` 时 `argc=2`,`argv[0]="./app"`,`argv[1]="112233"`。
+
+### 4.1 getopt 选项解析(扩展)
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+```c
+#include <unistd.h>
+int getopt(int argc, char * const argv[], const char *optstring);
+extern char *optarg;   /* 当前选项的参数 */
+extern int optind;     /* 下一个待处理参数的索引 */
+extern int optopt;     /* 未识别的选项字符 */
+```
+
+`optstring` 中字符后带 `:` 表示该选项需要参数,参数放入 `optarg`;解析完返回 `-1`。
+
+```c
+int opt;
+while ((opt = getopt(argc, argv, "f:n:")) != -1) {
+    switch (opt) {
+    case 'f': printf("file = %s\n", optarg); break;
+    case 'n': printf("num  = %s\n", optarg); break;
+    }
+}
+```
+
+---
+
+## 5. 正则表达式
+
+正则表达式描述字符串匹配模式,由**普通字符**与**元字符**组成。POSIX 场景流程:编译 → 匹配 → 释放。
+
+```c
+#include <sys/types.h>
+#include <regex.h>
+int regcomp(regex_t *preg, const char *regex, int cflags);
+int regexec(const regex_t *preg, const char *string, size_t nmatch,
+            regmatch_t pmatch[], int eflags);
+size_t regerror(int errcode, const regex_t *preg, char *errbuf, size_t errbuf_size);
+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` | 释放资源 | 无 |
+
+`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 次(扩展) |
+| `\` | 转义 | `\|` / `()` | 或 / 分组(扩展) |
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <regex.h>
+
+int main(int argc, char *argv[])
+{
+    regex_t reg; regmatch_t pmatch; char errbuf[64]; int ret;
+    const char *p; int length;
+
+    if (3 != argc) { fprintf(stderr, "usage: %s <regex> <string>\n", argv[0]); return 1; }
+    ret = regcomp(&reg, argv[1], REG_EXTENDED);       /* 1. 编译 */
+    if (ret) { regerror(ret, &reg, errbuf, sizeof(errbuf));
+               fprintf(stderr, "regcomp: %s\n", errbuf); return 1; }
+    p = argv[2]; length = strlen(argv[2]);
+    while (length > 0 && 0 == regexec(&reg, p, 1, &pmatch, 0)) {  /* 2. 逐个匹配 */
+        if (pmatch.rm_so == pmatch.rm_eo) { p++; length--; continue; } /* 空匹配 */
+        printf("%.*s\n", (int)(pmatch.rm_eo - pmatch.rm_so), p + pmatch.rm_so);
+        p += pmatch.rm_eo; length -= pmatch.rm_eo;
+    }
+    regfree(&reg);                                     /* 3. 释放 */
+    return 0;
+}
+```
+
+---
+
+## 6. 系统信息
+
+```c
+#include <sys/utsname.h>   int uname(struct utsname *buf);
+#include <sys/sysinfo.h>   int sysinfo(struct sysinfo *info);
+#include <unistd.h>        int gethostname(char *name, size_t len);
+long sysconf(int name);
+```
+
+```c
+struct utsname {            /* 成员均为字符数组 */
+    char sysname[];   /* 操作系统名称 */
+    char nodename[];  /* 主机名 */
+    char release[];   /* 内核版本 */
+    char version[];   /* 发行版本 */
+    char machine[];   /* 硬件架构 */
+#ifdef _GNU_SOURCE
+    char domainname[];
+#endif
+};
+struct sysinfo {
+    long uptime;            /* 启动后经过时间(秒) */
+    unsigned long loads[3]; /* 1,5,15 分钟负载 */
+    unsigned long totalram; /* 总可用内存 */
+    unsigned long freeram;  /* 未使用内存 */
+    unsigned long sharedram; unsigned long bufferram;
+    unsigned long totalswap; unsigned long freeswap;
+    unsigned short procs;   /* 当前进程数 */
+    unsigned long totalhigh; unsigned long freehigh;
+    unsigned int mem_unit;  /* 内存单元大小(字节) */
+    char _f[20-2*sizeof(long)-sizeof(int)]; /* Padding to 64 bytes */
+};
+```
+
+`uname` / `sysinfo` / `gethostname` 均成功返回 0、失败返回 -1 并设置 `errno`。内存实际字节数 = 字段值 × `mem_unit`。
+
+`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` | 系统节拍率 |
+
+---
+
+## 7. 时间与日期
+
+**基本概念**:GMT 是格林威治标准时间(零时区),北京时间(东八区)比它早 8 小时;UTC 是世界协调时间,比 GMT 更精准,编程中不必刻意区分;全球 24 个时区,中国统一用北京时间。时区配置在 `/usr/share/zoneinfo`,系统本地时间由 `/etc/localtime`(通常软链接到其中某文件)定义。
+
+**系统时钟与 RTC**:系统时钟由内核维护,开机时读取一次 RTC 作为初始值,关机不存在;RTC 有电池供电,关机仍计时,关机时内核把系统时钟写回 RTC。**jiffies** 是内核全局变量,记录自启动以来的节拍数;节拍率低(如 100Hz)精度低、中断负担小,默认 100Hz;`time()` / `gettimeofday()` 实质由 jiffies 换算。
+
+### 7.1 获取时间
+
+```c
+#include <time.h>
+time_t time(time_t *tloc);            /* 秒级,返回自 1970-01-01 00:00:00 UTC 起的秒数 */
+#include <sys/time.h>
+int gettimeofday(struct timeval *tv, struct timezone *tz);  /* 微秒级,tz 已废弃传 NULL */
+struct timeval { long tv_sec; long tv_usec; };
+```
+
+### 7.2 时间转换
+
+```c
+#include <time.h>
+char *ctime(const time_t *timep);                          char *ctime_r(const time_t *, char *);
+struct tm *localtime(const time_t *timep);                 struct tm *localtime_r(const time_t *, struct tm *);
+struct tm *gmtime(const time_t *timep);                    struct tm *gmtime_r(const time_t *, struct tm *);
+time_t mktime(struct tm *tm);
+char *asctime(const struct tm *tm);                        char *asctime_r(const struct tm *, char *);
+size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);
+
+struct tm {
+    int tm_sec;   /* 秒(0-60) */          int tm_mon;   /* 月(0-11) */
+    int tm_min;   /* 分(0-59) */          int tm_year;  /* 年(自 1900 起的年数) */
+    int tm_hour;  /* 时(0-23) */          int tm_wday;  /* 星期(0-6, 日=0) */
+    int tm_mday;  /* 日(1-31) */          int tm_yday;  /* 年内第几天(0-365) */
+    int tm_isdst; /* 夏令时 */
+};
+```
+
+| 函数 | 作用 | 可重入版本 |
+| ---- | ---- | ---------- |
+| `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。
+
+### 7.3 设置时间
+
+```c
+#include <sys/time.h>
+int settimeofday(const struct timeval *tv, const struct timezone *tz);  /* 需 root,tz 传 NULL */
+```
+
+```mermaid
+graph LR
+    T["time_t"] -->|localtime_r| TM["struct tm 本地"]
+    T -->|gmtime_r| UTC["struct tm UTC"]
+    T -->|ctime_r| CS["固定格式字符串"]
+    TM -->|mktime| T
+    TM -->|strftime| FS["自定义格式字符串"]
+    TM -->|asctime_r| AS["固定格式字符串"]
+    TV["struct timeval 秒+微秒"] -->|gettimeofday| T
+```
+
+---
+
+## 8. 进程时间
+
+进程时间 = 用户 CPU 时间 + 系统 CPU 时间;休眠不消耗 CPU,不计入。注意区分「时间总和」(生命周期,含休眠)与「进程时间」。
+
+```c
+#include <sys/times.h>
+clock_t times(struct tms *buf);
+struct tms {
+    clock_t tms_utime;  /* 用户 CPU 时间(节拍数) */   clock_t tms_cutime; /* 已死子进程用户时间总和 */
+    clock_t tms_stime;  /* 系统 CPU 时间(节拍数) */   clock_t tms_cstime; /* 已死子进程系统时间总和 */
+};
+#include <time.h>
+clock_t clock(void);    /* 总 CPU 时间, 除以 CLOCKS_PER_SEC 得秒; 不可用返回 -1 */
+```
+
+`times` 返回自过去某时间点起的节拍数,`节拍数 / 节拍率(_SC_CLK_TCK)` 即秒数,失败 -1。`clock` 不能区分用户/系统时间。
+
+---
+
+## 9. 随机数与休眠
+
+```c
+#include <stdlib.h>
+int rand(void);                     /* 返回 [0, RAND_MAX] 伪随机数 */
+void srand(unsigned int seed);      /* 设置种子, 常用 srand(time(NULL)) */
+
+#include <unistd.h>
+unsigned int sleep(unsigned int seconds);   /* 秒级; 被信号中断返回剩余秒数 */
+int usleep(useconds_t usec);                /* 微秒级; 成功 0, 失败 -1 */
+#include <time.h>
+int nanosleep(const struct timespec *req, struct timespec *rem); /* 纳秒级, 系统调用 */
+```
+
+未调用 `srand` 时默认种子为 1,每次运行序列相同;`srand(time(NULL))` 使序列每次不同。编程中只能得到伪随机数。休眠时进程**主动交出 CPU**、退出调度队列,是延时而非裸机 `nop` 空转;`nanosleep` 被中断返回 -1,剩余时间写入 `rem`。
+
+---
+
+## 10. 申请堆内存
+
+```c
+#include <stdlib.h>
+void *malloc(size_t size);              /* 分配 size 字节, 不初始化; 失败或 size=0 返回 NULL */
+void free(void *ptr);                   /* 释放 */
+void *calloc(size_t nmemb, size_t size);/* 分配 nmemb*size 字节并全部置 0 */
+int posix_memalign(void **memptr, size_t alignment, size_t size);
+void *aligned_alloc(size_t alignment, size_t size);
+void *valloc(size_t size);
+#include <malloc.h>
+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` |
+
+`calloc` 等价于 `malloc` + `memset`。进程终止时内核回收全部内存,但仍应显式 `free`:提升可读性、可维护性,及时归还避免内存泄漏。
+
+### 10.1 brk / sbrk(扩展)
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
+
+```c
+#include <unistd.h>
+int brk(void *addr);              /* 直接设置堆顶, 成功 0, 失败 -1(ENOMEM) */
+void *sbrk(intptr_t increment);   /* 相对移动堆顶, 返回旧堆顶; 失败返回 (void*)-1 */
+```
+
+`malloc` 内部通常基于 `brk` / `sbrk`,实际编程推荐用 `malloc` / `free`。
+
+```mermaid
+graph TD
+    S["堆内存申请"] --> M["malloc: 不初始化"]
+    S --> C["calloc: 置 0"]
+    S --> A["对齐分配: posix_memalign / aligned_alloc / memalign / valloc"]
+    M --> F["free: 显式释放"]
+    C --> F
+    A --> F
+    M -. 内部 .-> B["brk / sbrk"]
+```
+
+---
+
+## 11. proc 文件系统
+
+`/proc` 是**虚拟文件系统**,以文件方式向应用层暴露内核数据;动态创建、只存在于内存、不占磁盘(与 devfs 类似)。以 PID 命名的目录记录进程信息;虚拟文件含义:
+
+| 文件 | 含义 | 文件 | 含义 |
+| ---- | ---- | ---- | ---- |
+| `cmdline` | 内核启动参数 | `meminfo` | 物理内存与交换分区使用情况 |
+| `cpuinfo` | CPU 相关信息 | `modules` | 加载的模块列表 |
+| `iomem` | IO 设备内存使用 | `mounts` | 挂载的文件系统列表 |
+| `interrupts` | 被占用的中断及占用者 | `partitions` | 系统识别的分区表 |
+| `ioports` | IO 端口使用 | `swaps` | 交换分区利用情况 |
+| `kcore` | 物理内存映像,不可读 | `version` | 内核版本信息 |
+| `loadavg` | 系统平均负载 | `uptime` | 系统运行时间 |
+
+读取方式:`cat /proc/version`,或程序中 `open` + `read`:
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include <unistd.h>
+int main(void) {
+    char buf[512] = {0}; int fd = open("/proc/version", O_RDONLY);
+    if (-1 == fd) { perror("open error"); exit(-1); }
+    if (-1 == read(fd, buf, sizeof(buf))) { perror("read error"); exit(-1); }
+    puts(buf); close(fd); return 0;
+}
+```
+
+### 11.1 cpuinfo / meminfo / stat 字段(扩展)
+
+> ⚠️ **来源说明**:三个文件本身在教材 /proc 文件列表中已列出,字段含义解析为扩展知识。
+
+- `/proc/cpuinfo`:每逻辑 CPU 一段,字段 `processor`、`model name`、`cpu MHz`、`flags` 等。
+- `/proc/meminfo`:`MemTotal`、`MemFree`、`MemAvailable`、`Buffers`、`Cached`、`SwapTotal`、`SwapFree`(单位 kB)。
+- `/proc/stat`:`cpu` / `cpu0`… 各列依次 user、nice、system、idle、iowait、irq、softirq、steal;另有 `intr`、`ctxt`、`btime`、`processes`。常用于计算 CPU 利用率。
+
+---
+
+## 12. 完整可复制源码
+
+```c
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+#include <sys/time.h>
+#include <sys/times.h>
+#include <sys/utsname.h>
+#include <sys/sysinfo.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <regex.h>
+
+/* 字符串与数字转换 */
+static void demo_string(void)
+{
+    char dest[128] = "Linux "; char src[] = "string demo"; char num[32];
+    strncat(dest, src, 7);                       /* 拼接前 7 字符 */
+    printf("[str] strncat : %s\n", dest);
+    strncpy(dest, "copy me", 7); dest[7] = '\0'; /* n<=len 需手工补 '\0' */
+    printf("[str] strncpy : %s\n", dest);
+    printf("[str] strcmp  : %d\n", strcmp("ABC", "abc"));
+    printf("[str] strstr  : %s\n", strstr("I love my home", "home"));
+    snprintf(num, sizeof(num), "%d", atoi("500") * 2);
+    printf("[str] atoi/snprintf : %s\n", num);
+    printf("[str] strtol  : %ld\n", strtol("0x500", NULL, 16));
+    printf("[str] atof    : %lf\n", atof("100.0123"));
+}
+
+/* 正则: 提取所有数字 */
+static void demo_regex(void)
+{
+    regex_t reg; regmatch_t pm; char err[64];
+    const char *p = "abc 123 def 456"; int ret;
+    ret = regcomp(&reg, "[0-9]+", REG_EXTENDED);
+    if (ret) { regerror(ret, &reg, err, sizeof(err)); printf("[regex] %s\n", err); return; }
+    printf("[regex] matches:");
+    while (0 == regexec(&reg, p, 1, &pm, 0) && -1 != pm.rm_so) {
+        printf(" %.*s", (int)(pm.rm_eo - pm.rm_so), p + pm.rm_so);
+        p += pm.rm_eo;
+    }
+    printf("\n"); regfree(&reg);
+}
+
+/* 系统信息 */
+static void demo_sysinfo(void)
+{
+    struct utsname os; struct sysinfo si; char host[64];
+    if (0 == uname(&os)) printf("[sys] %s %s %s %s\n", os.sysname, os.release, os.machine, os.nodename);
+    memset(host, 0, sizeof(host));
+    if (0 == gethostname(host, sizeof(host))) printf("[sys] hostname = %s\n", host);
+    if (0 == sysinfo(&si)) {
+        printf("[sys] uptime=%lds totalram=%lu(x%d) freeram=%lu procs=%u\n",
+               si.uptime, si.totalram, si.mem_unit, si.freeram, si.procs);
+    }
+    printf("[sys] pagesize=%ld clk_tck=%ld open_max=%ld\n",
+           sysconf(_SC_PAGESIZE), sysconf(_SC_CLK_TCK), sysconf(_SC_OPEN_MAX));
+}
+
+/* 时间 */
+static void demo_time(void)
+{
+    time_t sec = time(NULL); struct tm t; char s[100]; struct timeval tv;
+    if ((time_t)-1 == sec) return;
+    localtime_r(&sec, &t); strftime(s, sizeof(s), "%Y-%m-%d %H:%M:%S", &t);
+    printf("[time] local = %s\n", s);
+    gmtime_r(&sec, &t); strftime(s, sizeof(s), "%Y-%m-%d %H:%M:%S", &t);
+    printf("[time] utc   = %s\n", s);
+    if (0 == gettimeofday(&tv, NULL)) printf("[time] %ld s + %ld us\n", tv.tv_sec, tv.tv_usec);
+}
+
+/* 进程时间 / 随机数 / 休眠 / 堆内存 / proc */
+static void demo_resource(void)
+{
+    struct tms tb; long tck = sysconf(_SC_CLK_TCK); char *mem; int fd; char pb[256];
+    if ((clock_t)-1 != times(&tb)) printf("[res] user cpu = %.4f s\n", tb.tms_utime / (double)tck);
+    printf("[res] clock() = %.4f s\n", clock() / (double)CLOCKS_PER_SEC);
+    srand(time(NULL));
+    printf("[res] rand = %d %d %d\n", rand() % 100, rand() % 100, rand() % 100);
+    puts("[res] sleep 1s ..."); sleep(1);
+    mem = (char *)malloc(1024);
+    if (mem) { memset(mem, 0, 1024); printf("[res] malloc 1024 at %p\n", (void *)mem); free(mem); }
+    fd = open("/proc/version", O_RDONLY);
+    if (fd >= 0) { memset(pb, 0, sizeof(pb)); if (read(fd, pb, sizeof(pb) - 1) > 0) printf("[res] %s", pb); close(fd); }
+}
+
+int main(int argc, char *argv[])
+{
+    int i;
+    printf("argc = %d\n", argc);
+    for (i = 0; i < argc; i++) printf("  argv[%d] = %s\n", i, argv[i]);
+    demo_string(); demo_regex(); demo_sysinfo(); demo_time(); demo_resource();
+    puts("done."); return 0;
+}
+```
+
+**逐段解释**:`demo_string` 用 `strncat`/`strncpy` 演示长度受控操作(`strncpy` 在 `n<=len` 时不带 `\0`,需手工补),`atoi`/`strtol`/`snprintf` 做数字互转;`demo_regex` 用 `regcomp` 编译 `[0-9]+`、`regexec` 循环提取、`regfree` 释放;`demo_sysinfo` 依次调用 `uname`/`gethostname`/`sysinfo`/`sysconf`,内存字节数 = 字段 × `mem_unit`;`demo_time` 用 `time`+`localtime_r`/`gmtime_r`+`strftime` 与 `gettimeofday`;`demo_resource` 用 `times`/`clock` 测进程时间、`srand`+`rand` 取随机数、`sleep` 休眠、`malloc`/`free` 管理堆、`open`+`read` 读 `/proc/version`。
+
+---
+
+## 13. 实验步骤与调试方法
+
+```bash
+gcc -Wall -o sysinfo_demo sysinfo_demo.c
+./sysinfo_demo hello world
+```
+
+交叉验证:
+
+```bash
+uname -a; hostname; date; date -u; free -b; getconf PAGE_SIZE; getconf CLK_TCK
+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` |
+
+调试技巧:`strace` 看系统调用、`ltrace` 看库函数、`valgrind --leak-check=full` 查泄漏;正则先用 `grep -E` 验证语法。
+
+---
+
+## 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 | 有,丰富 | 无 | 有 |
+
+要点:STM32 无 `/proc`、无 POSIX 正则、无时区/日历时间体系;IMX6ULL 与 RK3568 同属 Linux,本篇 API 通用。
+
+---
+
+## 15. 面试精选(5 题)
+
+### Q1:`strcpy` / `strncpy` / `memcpy` 有什么区别?各适用什么场景?
+
+**答案**:`strcpy` 复制以 `\0` 结尾的字符串并连 `\0` 一起复制,无长度限制;`strncpy` 最多复制 `n` 个字符,`n <= strlen(src)` 时不补 `\0`(需手工补),`n` 更大时用 `\0` 填充,适合固定长度字段;`memcpy` 是纯内存拷贝,不关心 `\0`,适合结构体/二进制数据,要求源与目标不重叠(重叠用 `memmove`)。关键:`str*` 以 `\0` 为界,`mem*` 以字节数为界。
+
+### Q2:`gets` 为什么被废弃?`fgets` 和 `scanf("%s")` 在读入换行符上有什么不同?
+
+**答案**:`gets` 不检查缓冲区大小,输入超长会栈溢出,C11 起被移除。`fgets` 最多读 `size-1` 字符,**把换行符读走并保留在字符串中**,末尾补 `\0`(可用 `s[strcspn(s,"\n")]='\0';` 去掉换行);`scanf("%s")` 遇空白停止且**不取走分隔符**,分隔符残留在缓冲区影响后续读取,也无法直接限长(可用 `%ns`)。读一行用 `fgets`,读单词可用 `scanf("%s")`。
+
+### Q3:`sizeof` 和 `strlen` 有什么区别?`char str[50]="abc"; char *p=str;` 结果分别是多少?
+
+**答案**:`sizeof` 是编译期操作符,算内存大小;`strlen` 是运行时库函数,算到 `\0` 的长度。`sizeof(str)=50`、`strlen(str)=3`、`sizeof(p)=8`(64 位)、`strlen(p)=3`。陷阱:函数参数中的数组退化为指针,`sizeof` 得到指针大小;`strlen` 要求以 `\0` 结尾,否则越界读取。
+
+### Q4:`regcomp` / `regexec` 的匹配流程是什么?`rm_so` / `rm_eo` 如何理解?
+
+**答案**:三步——`regcomp` 编译正则(失败用 `regerror` 取描述);`regexec` 执行匹配(0 匹配、`REG_NOMATCH` 不匹配);`regfree` 释放。`rm_so` 是匹配子串相对字符串起始的起始偏移,`rm_eo` 是结束偏移(左闭右开),子串长度 `rm_eo - rm_so`,内容 `str + rm_so`,`rm_so == -1` 表示未匹配。循环匹配时每次把指针前移 `rm_eo` 即可逐个提取。
+
+### Q5:`malloc` / `calloc` 有什么区别?进程退出时内核会回收内存,为什么还要 `free`?`sysinfo` 内存字段如何换算成字节?
+
+**答案**:`malloc(size)` 分配且不初始化,内容为垃圾数据;`calloc(nmemb,size)` 分配 `nmemb*size` 字节并全部置 0。仍需 `free`:显式释放提升可读性、可维护性;程序生命周期中内存常是阶段性需要,不及时释放会造成内存泄漏,常驻服务会持续吃内存直至 OOM。`sysinfo` 的 `totalram`/`freeram` 是内存单元数,实际字节数 = 字段 × `mem_unit`。
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第六章 字符串处理、第七章 系统信息与系统资源

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

@@ -0,0 +1,596 @@
+---
+title: 面试-Linux应用编程基础
+tags: [嵌入式Linux, 应用编程, 面试, 系统调用, 文件IO, 标准IO, 文件属性, 字符串, 系统信息]
+created: 2026-09-18
+updated: 2026-09-18
+---
+
+# 面试-Linux应用编程基础
+
+> 💡 **关联知识**:[[01-Linux应用编程基础/01-应用编程概念与开发环境]]、[[01-Linux应用编程基础/02-文件IO基础与深入探究]]、[[01-Linux应用编程基础/03-标准IO库]]、[[01-Linux应用编程基础/04-文件属性与目录]]、[[01-Linux应用编程基础/05-字符串处理与系统信息]]。
+
+覆盖《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 应用编程与裸机编程、驱动编程有什么区别?
+
+**答案要点**:裸机编程无操作系统、软硬件代码混在一起;驱动编程基于内核驱动框架、负责底层硬件操作;应用编程基于操作系统、运行在用户态、只实现用户逻辑。
+
+**详细解答**:以点亮 LED 为例:
+
+- 裸机:LED 硬件操作代码与用户逻辑在同一工程,代码直接跑在硬件上,无 OS;
+- 驱动:驱动属于内核的一部分,通过 `file_operations` 提供 open/write/release 等方法,运行在内核态;
+- 应用:只实现用户逻辑(`open`/`write`/`close` 设备文件),运行在用户态,与驱动分离、单独编译。
+
+应用与驱动通过系统调用(设备文件)交互,代码隔离。
+
+**追问**
+1. 用户态和内核态如何切换?(通过系统调用/中断/异常由用户态陷入内核态)
+2. 应用程序能直接访问硬件寄存器吗?为什么?(不能,需通过驱动/设备文件,否则破坏隔离与安全)
+### Q1.3 什么是文件描述符 fd?0、1、2 分别是什么?一个进程最多能打开多少文件?
+
+**答案要点**:fd 是一个非负整数,内核用它索引所有已打开文件;0/1/2 分别是标准输入/输出/错误;默认上限 1024(0~1023)。
+
+**详细解答**:`open` 成功返回 fd,所有 I/O 系统调用通过 fd 索引文件。fd 从 0 开始分配,总是取当前最小可用值;文件关闭后 fd 释放可被复用。0、1、2 默认被系统占用(stdin/stdout/stderr),所以程序首次 `open` 一般得到 3。
+
+```c
+#include <unistd.h>
+/* 可直接用 0/1/2,也可用宏 */
+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]` 起是真实参数。
+
+**详细解答**:原型 `int main(int argc, char **argv)`。执行 `./app 112233` 时 `argc=2`,`argv[0]="./app"`,`argv[1]="112233"`;参数以空格分隔,带空格的参数用引号。应用编程中还可用 `getopt` 解析选项。
+
+```c
+int main(int argc, char *argv[]) {
+    for (int i = 0; i < argc; i++) printf("argv[%d]=%s\n", i, argv[i]);
+    return 0;
+}
+```
+
+**追问**
+1. `argv` 最后一个元素是什么?(`argv[argc]` 为 `NULL`)
+2. 如何让程序接收选项参数?(用 `getopt`/`getopt_long`,参数放入 `optarg`)
+### Q1.5 glibc 是什么?如何确定当前系统的 glibc 版本?
+
+**答案要点**:glibc 是 GNU C 库、Linux 下的标准 C 函数库,通常以动态库 `libc.so.6` 形式提供。
+
+**详细解答**:glibc 构建在系统调用之上,向上提供标准 C 库函数。`libc.so.6` 一般是软链接,指向真正的库文件(如 `libc-2.23.so`,2.23 即版本号)。可直接运行共享库查询:
+
+```bash
+ls -l /lib/x86_64-linux-gnu/libc.so.6   # 看软链接指向
+/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` | 路径不是目录则失败 |
+
+```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。
+
+**详细解答**:`ssize_t read(int fd, void *buf, size_t count)`、`ssize_t write(int fd, const void *buf, size_t count)`。对普通文件,读写从当前偏移量开始,完成后偏移量自动后移相应字节数。`read` 在到达文件末尾时返回 0;若剩余数据不足 `count`,返回剩余数量。`write` 若磁盘满等情况可能返回小于 `count`,应循环写完。
+
+```c
+ssize_t n;
+while ((n = read(fd, buf, sizeof(buf))) > 0) {
+    ssize_t off = 0;
+    while (off < n) {
+        ssize_t w = write(fd2, buf + off, n - off);
+        if (w < 0) { perror("write"); break; }
+        off += w;
+    }
+}
+```
+
+**追问**
+1. `read` 返回 0 一定表示文件结束吗?(对普通文件是;对管道/终端等需结合场景)
+2. `write` 返回 0 代表什么?(未写入任何字节,通常表示异常或特殊设备行为)
+### Q2.3 `lseek` 的三个 `whence` 是什么?如何用它求文件大小?
+
+**答案要点**:`SEEK_SET` 相对文件头、`SEEK_CUR` 相对当前位置、`SEEK_END` 相对文件末尾;`lseek(fd, 0, SEEK_END)` 得到文件大小。
+
+**详细解答**:`off_t lseek(int fd, off_t offset, int whence)`,成功返回从文件头算起的新偏移量,失败 -1。偏移量可正可负。求大小:
+
+```c
+off_t size = lseek(fd, 0, SEEK_END);   /* 返回文件字节数 */
+lseek(fd, 0, SEEK_SET);                /* 重新定位到文件头 */
+```
+
+**追问**
+1. 空洞文件是什么?(`lseek` 跳过一段后写入,中间未写区域形成空洞,逻辑大小包含空洞,但不占实际块)
+2. 设备文件能 `lseek` 吗?(可以,但部分设备不支持,返回错误)
+### Q2.4 一个进程内多次 `open` 同一个文件会发生什么?
+
+**答案要点**:得到多个不同 fd;内存中只有一份动态文件(共享);各 fd 的读写偏移量相互独立(分别写),需用 `O_APPEND` 才能接续写。
+
+**详细解答**:多次 `open` 得到多个 fd,对应多个文件表,但文件表都指向同一个 inode,因此内存中只有一份文件数据。由于偏移量记录在文件表中,各 fd 的偏移量独立,`write` 时是**分别写**(互相覆盖);加 `O_APPEND` 后每次写前自动定位到末尾,变成**接续写**。多个进程打开同一文件同理。
+
+```c
+fd1 = open("f", O_RDWR | O_APPEND);
+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` | 「判断文件是否存在 + 创建」 |
+
+```c
+/* pread 不改变当前偏移量 */
+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 *`。
+
+**详细解答**:标准 I/O 在文件 I/O 之上封装,`fopen` 内部调 `open`、`fread` 内部调 `read`。`FILE` 结构包含 fd、缓冲区、偏移等。
+
+```c
+#include <stdio.h>
+int fd = fileno(fp);          /* FILE* -> fd */
+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` 全缓冲 | 缓冲区填满才刷出 | 磁盘普通文件 |
+
+```c
+setvbuf(stdout, NULL, _IONBF, 0);   /* 改为无缓冲 */
+setbuf(fp, NULL);                   /* 等价无缓冲 */
+```
+
+**追问**
+1. 为什么 `stderr` 默认无缓冲?(保证错误信息能立即输出)
+2. 为什么 `printf` 不带换行时不立即显示?(stdout 行缓冲,数据仍在 stdio 缓冲区)
+### Q3.3 如何强制刷新 stdio 缓冲区?哪些情况会自动刷新?
+
+**答案要点**:用 `fflush(stream)` 强制刷新(`NULL` 表示全部);文件关闭(`fclose`)时、程序正常退出时也会自动刷新。
+
+**详细解答**:
+
+```c
+printf("Hello World!");
+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 的 `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 绕过内核缓冲,直接访问磁盘。
+
+**详细解答**:文件 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` | 进程间通信 |
+
+**追问**
+1. 如何用编程方式判断类型?(`stat` 取 `st_mode`,用 `S_ISREG`/`S_ISDIR` 等宏)
+2. 设备文件为什么关机就消失?(由 devfs 虚拟产生,由内存维护,不写磁盘)
+### Q4.2 `stat`、`fstat`、`lstat` 有什么区别?
+
+**答案要点**:`stat` 从路径且解引用链接;`fstat` 从 fd;`lstat` 从路径且不解引用链接。
+
+**详细解答**:三者都填充 `struct stat`。`stat`/`lstat` 不需先打开文件,`fstat` 需先 `open`。对符号链接,`stat`/`fstat` 得到所指文件属性,`lstat` 得到链接本身属性。
+
+```c
+struct stat st;
+stat("link", &st);    /* 链接所指文件 */
+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)。
+
+**详细解答**:权限检查规则:有效用户 ID 等于文件所有者 ID 则以所有者身份检查;否则有效组或任一附属组等于文件组 ID 则以同组身份检查;否则以其它用户身份检查;有效用户 ID 为 0 则拥有最高权限。对文件读写执行还需拥有**所在目录的执行权限**;在目录下创建/删除文件需目录的写 + 执行权限。
+
+`umask`:新建权限 = `mode & ~umask`(如 `0777 & ~0002 = 0775`)。`access` 用**实际**用户/组判断,在 SUID 场景下结果可能不等于真正 `open`,且存在 TOCTOU 风险,推荐直接 `open` 后看 `errno`。
+
+```c
+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 | 相同 | 不同 |
+| 数据块 | 共享 | 存目标路径 |
+| 源文件删除 | 仍可访问 | 悬空 |
+| 链接数 | 计入 | 不计入 |
+| 目录 | 不能(一般) | 可以 |
+| 跨文件系统 | 不能 | 可以 |
+| 目标不存在 | 不允许 | 允许 |
+
+```c
+link("src", "hard");         /* 硬链接 */
+symlink("src", "soft");      /* 软链接 */
+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`。
+
+**详细解答**:`atime` 读文件时更新,`mtime` 写文件时更新,`ctime` 在 inode 变更时更新(`chmod`/`chown`/`rename`)。可显式修改 `atime`/`mtime`(`utime` 秒、`utimes` 微秒、`futimens`/`utimensat` 纳秒),`ctime` 不能显式修改。
+
+```c
+DIR *d = opendir(".");
+struct dirent *e;
+errno = 0;
+while ((e = readdir(d)) != NULL)
+    printf("%s %ld\n", e->d_name, (long)e->d_ino);
+if (errno) perror("readdir");
+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` 结尾的字符串 |
+| 时机 | 编译时 | 运行时 |
+| 结果 | 占用内存大小 | 字符串实际长度 |
+
+```c
+char str[50] = "abc";
+char *p = str;
+printf("%zu %zu %zu %zu\n", sizeof(str), strlen(str), sizeof(p), strlen(p));
+/* 输出: 50 3 8 3 (64 位系统) */
+```
+
+**追问**
+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 字节 | 不关心 | 结构体/二进制 |
+
+```c
+char d[16];
+strncpy(d, "hello world", 5); d[5] = '\0';  /* 手工补 '\0' */
+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` |
+
+```c
+char s[64] = "Linux ";
+strncat(s, "app", 3);                 /* "Linux app" */
+int r = strcmp("ABC", "abc");         /* < 0 (大写 ASCII 更小) */
+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`。
+
+**详细解答**:`atoi` 系列只支持十进制;`strtol`/`strtoll`/`strtoul`/`strtoull` 支持 `base` 2~36,`base=0` 时自动识别 `0x`(16 进制)/`0`(8 进制)前缀。`endptr` 可获取第一个无效字符地址。数字转字符串推荐 `snprintf` 防溢出。
+
+```c
+int i = atoi("500");                       /* 十进制 */
+long l = strtol("0x500", NULL, 16);        /* 16 进制 = 1280 */
+double d = atof("100.0123");
+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` | 释放资源 | 无 |
+
+`REG_EXTENDED` 启用扩展语法(`+`、`?`、`{}`、`|`、`()`)。`regmatch_t` 的 `rm_so`/`rm_eo` 是匹配子串的起止偏移(左闭右开)。
+
+```c
+regex_t reg; regmatch_t pm;
+regcomp(&reg, "[0-9]+", REG_EXTENDED);
+if (regexec(&reg, "abc123", 1, &pm, 0) == 0)
+    printf("%.*s\n", (int)(pm.rm_eo - pm.rm_so), "abc123" + pm.rm_so); /* 123 */
+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>` |
+
+```c
+struct sysinfo si;
+if (sysinfo(&si) == 0)
+    printf("totalram=%lu bytes\n", si.totalram * si.mem_unit);
+```
+
+内存字节数 = 字段值 × `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 记录系统节拍数。
+
+**详细解答**: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 → 字符串 | 自定义格式 |
+
+```c
+time_t t = time(NULL);
+struct tm tm;
+char s[100];
+localtime_r(&t, &tm);
+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` 只给总时间。
+
+**详细解答**:休眠不消耗 CPU、不计入进程时间,要与「时间总和」区分。`times(&tms)` 返回节拍数,`节拍数 / _SC_CLK_TCK` 得秒;`struct tms` 提供 `tms_utime`、`tms_stime`、子进程时间。`clock()` 返回总 CPU 时间,除以 `CLOCKS_PER_SEC` 得秒。
+
+```c
+struct tms tb; long tck = sysconf(_SC_CLK_TCK);
+times(&tb);
+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` 读取。
+
+**详细解答**:
+
+```c
+srand(time(NULL));                 /* 随机种子, 否则默认 1 */
+int n = rand() % 100;              /* [0,99] */
+sleep(1);                          /* 秒级, 主动交出 CPU */
+struct timespec ts = {3, 0};
+nanosleep(&ts, NULL);              /* 纳秒级, 系统调用 */
+char *p = malloc(1024);            /* 不初始化 */
+memset(p, 0, 1024); free(p);       /* 及时释放 */
+```
+
+`/proc` 下常见文件:`version`(内核版本)、`cpuinfo`、`meminfo`、`stat`、`uptime`、`cmdline` 等。
+
+**追问**
+1. 进程退出内核会回收内存,为什么还要 `free`?(避免阶段性命周期内的内存泄漏,提升可维护性)
+2. `sleep` 和裸机 `for` 循环延时的本质区别?(`sleep` 主动让出 CPU,后者一直占用 CPU 空转)
+
+---
+
+**内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第一章~第七章