03-标准IO库.md 49 KB


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 缓冲区,带缓存,性能效率更优 用户空间不带缓存
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)。标准输出和标准错误都对应终端屏幕,标准输入对应键盘。

每个进程启动后默认打开这三个文件,得到三个文件描述符:

/* 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> 中定义:

/* 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 中用 stdinstdoutstderr 表示标准输入、输出、错误。


4. 打开文件:fopen()

#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() 只有 pathmode 两个参数,没有参数指定新建文件权限。当 mode"w""w+""a""a+" 之一且文件不存在时,新文件的权限有一个默认值:

S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH   (0666)

4.3 使用示例

fopen(path, "r");    /* 只读打开 */
fopen(path, "r+");   /* 可读可写打开 */
fopen(path, "w");    /* 只写打开,长度截断为 0,不存在则创建 */

4.4 fclose() 关闭文件

#include <stdio.h>

int fclose(FILE *stream);
参数 说明
stream FILE 类型指针

返回值:成功返回 0;失败返回 EOF(也就是 -1),并设置 errno

fclose() 会在关闭前自动刷新该文件的 stdio 缓冲区


5. 读文件和写文件:fread() / fwrite()

#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);若发生错误或到达文件末尾,返回值小于 nmembfread() 本身无法区分是错误还是文件末尾,需用 ferror()feof() 判断。

5.2 fwrite()

参数 说明
ptr 待写入数据的缓冲区
size 每个数据项的字节大小
nmemb 写入数据项的个数
stream FILE 指针

返回值:成功返回写入的数据项数目(不一定等于字节数,除非 size == 1);出错时返回值小于 nmemb(或等于 0)。

指定数据大小的方式与系统调用不同:fread/fwritenmemb(数据项个数)* size(每项大小),而 read/write 直接用一个 size 参数。

写一个结构体可以有两种等价写法:

> 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` 定义于 `<stdio.h>`,通常为 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 库函数<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

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,<stdio.h> 全部可用 部分工具链有 printf,但通常无文件流;FatFs 提供 f_open/f_read 完整 glibc/aarch64,全部可用
流对象 FILE *(stdin/stdout/stderr) FILE(裸机);RTOS 下可能有 retarget 的 printf FILE *
重定向 freopendup2 可把 stdout 重定向到文件/串口 通过重写 _write/fputcprintf 指向串口 同 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 封装而来(fopenopenfreadreadfwritewrite);标准 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 缓冲有哪几种模式?stdoutstderr、普通文件各默认哪种?

:用 setvbuf() 设置,共三种:_IONBF 无缓冲(每次标准 I/O 立即调用 write/read)、_IOLBF 行缓冲(遇到换行符 '\n' 才执行文件 I/O)、_IOFBF 全缓冲(填满缓冲区才执行文件 I/O)。默认情况:stderr 默认无缓冲(保证错误信息立即输出);标准输入、标准输出连接终端时默认行缓冲普通磁盘上的常规文件默认全缓冲

Q4:什么情况下 stdio 缓冲区会被刷新?_exitexit 有何区别?

:刷新 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库