--- 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 操作的一系列库函数的集合,函数定义都在头文件 `` 中。 标准 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 库函数
fopen/fread/fwrite"] S -->|"有 stdio 缓冲,封装"| F["文件 I/O 系统调用
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 数据结构定义在 `` 中。 --- ## 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 指针,在 `` 中定义: ```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 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 int fclose(FILE *stream); ``` | 参数 | 说明 | | ---- | ---- | | `stream` | FILE 类型指针 | **返回值**:成功返回 0;失败返回 `EOF`(也就是 -1),并设置 `errno`。 > `fclose()` 会在关闭前**自动刷新该文件的 stdio 缓冲区**。 --- ## 5. 读文件和写文件:fread() / fwrite() ```c #include 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 #include 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 #include 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 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 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 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 #include 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 void rewind(FILE *stream); /* 无返回值 */ ``` --- ## 8. 检查或复位状态:feof() / ferror() / clearerr() `fread()` 返回值小于 `nmemb` 时,无法区分是出错还是到达文件末尾,此时用下面两个函数判断。 ### 8.1 feof() ```c #include int feof(FILE *stream); ``` 测试 `end-of-file` 标志:标志被设置返回非零值,否则返回 0。当文件读写位置移动到文件末尾时,该标志会被设置。 ```c if (feof(file)) { /* 到达文件末尾 */ } else { /* 未到达文件末尾 */ } ``` ### 8.2 ferror() ```c #include int ferror(FILE *stream); ``` 测试错误标志:被设置返回非零值,否则返回 0。当对文件的 I/O 操作发生错误时,错误标志被设置。 ```c if (ferror(file)) { /* 发生错误 */ } else { /* 未发生错误 */ } ``` ### 8.3 clearerr() ```c #include void clearerr(FILE *stream); ``` 清除 `end-of-file` 标志和错误标志。调用 `feof()` 或 `ferror()` 校验后,通常需要清除标志,避免下次校验用到上一次的值。此函数没有返回值,调用总会成功。 > 除 `clearerr()` 外,`fseek()` 成功时也会清除 `end-of-file` 标志。 ```c #include #include 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 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 #include #include #include 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 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 #include #include 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 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 #include #include #include #include #include #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 #include #include #include #include #include /* 数据缓冲区起始地址以 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 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 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` 定义于 ``,通常为 8192); - `setbuffer()` 类似 `setbuf()`,但允许指定缓冲区大小,相当于 `setvbuf(stream, buf, buf ? _IOFBF : _IONBF, size)`。 #### 行缓冲测试 ```c #include #include #include int main(void) { printf("Hello World!\n"); printf("Hello World!"); /* 无换行符 */ for ( ; ; ) sleep(1); } ``` 运行后只有第一个 `printf` 显示出来,第二个没有。因为标准输出默认行缓冲,`printf` 的字符串先写入 stdio 缓冲区,只有输出换行符(或缓冲区填满)时才刷入内核缓冲区(写入终端)。第一个含 `\n` 已刷入,第二个没有换行符,仍缓存在 stdio 缓冲区,等待换行符才输出。 同理,`scanf()` 会阻塞直到用户按回车,因为标准输入默认也是行缓冲。 #### 无缓冲测试 ```c #include #include #include 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 int fflush(FILE *stream); ``` 参数 `stream` 指定需要刷新的文件;设为 `NULL` 表示刷新所有 stdio 缓冲区。成功返回 0,否则返回 -1 并设置 `errno`。 ```c #include #include #include 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 #include #include int main(void) { printf("Hello World!\n"); printf("Hello World!"); fclose(stdout); /* 关闭标准输出,自动刷新缓冲区 */ for ( ; ; ) sleep(1); } ``` ```c #include #include #include int main(void) { printf("Hello World!\n"); printf("Hello World!"); /* 程序正常结束(return/exit),自动刷新 stdio 缓冲区 */ return 0; } ``` #### I/O 缓冲小结 ```mermaid flowchart TB APP["应用程序调用标准 I/O 库函数
fwrite / fread / printf"] SB["stdio 缓冲区
用户空间,由 stdio 库维护"] KB["内核缓冲区
内核空间"] 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 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 #include #include 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,`` 全部可用 | 部分工具链有 `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 的**文件描述符**、指向文件缓冲区的指针、缓冲区长度、当前缓冲区字节数、出错标志等。转换用 `` 中的两个函数:`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库