|
|
@@ -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库
|