title: 标准 IO 库 tags: [
嵌入式Linux,
Linux应用编程,
标准IO,
FILE,
fopen,
fread,
fwrite,
格式化IO,
stdio缓冲,
fileno,
IMX6ULL,
] created: 2026-09-18 updated: 2026-09-18
💡 关联知识:[[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 的混用。
标准 I/O 库是标准 C 库中用于文件 I/O 操作的一系列库函数的集合,函数定义都在头文件 <stdio.h> 中。
标准 I/O 构建于文件 I/O 之上:
fopen() 内部利用系统调用 open() 打开文件;fread() 内部利用 read() 读文件;fwrite() 内部利用 write() 写文件。那为什么还要设计标准 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["磁盘设备"]
文件 I/O 的所有函数(open/read/write/lseek)围绕文件描述符进行;标准 I/O 的所有函数围绕 FILE 指针进行。
使用标准 I/O 库打开或创建文件时,返回一个指向 FILE 类型对象的指针(FILE *),该指针与被打开/创建的文件关联,后续标准 I/O 操作都围绕它进行。
<stdio.h> 中。| 设备 | 含义 | 通常对应 |
|---|---|---|
| 标准输入 | 系统标准输入设备 | 键盘 |
| 标准输出 | 输出正常信息的设备 | 显示器 |
| 标准错误 | 显示错误信息的设备 | 显示器 |
用户通过标准输入与系统交互:进程从标准输入(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 中用stdin、stdout、stderr表示标准输入、输出、错误。
#include <stdio.h>
FILE *fopen(const char *path, const char *mode);
| 参数 | 说明 |
|---|---|
path |
文件路径,可为绝对路径或相对路径 |
mode |
字符串,指定对该文件的读写权限 |
返回值:成功返回指向 FILE 类型对象的指针,与打开/创建的文件关联,后续标准 I/O 操作围绕它进行;失败返回 NULL 并设置 errno。
| 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 |
fopen() 只有 path 和 mode 两个参数,没有参数指定新建文件权限。当 mode 为 "w"、"w+"、"a"、"a+" 之一且文件不存在时,新文件的权限有一个默认值:
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH (0666)
fopen(path, "r"); /* 只读打开 */
fopen(path, "r+"); /* 可读可写打开 */
fopen(path, "w"); /* 只写打开,长度截断为 0,不存在则创建 */
#include <stdio.h>
int fclose(FILE *stream);
| 参数 | 说明 |
|---|---|
stream |
FILE 类型指针 |
返回值:成功返回 0;失败返回 EOF(也就是 -1),并设置 errno。
fclose()会在关闭前自动刷新该文件的 stdio 缓冲区。
#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);
| 参数 | 说明 |
|---|---|
ptr |
存放读取数据的缓冲区 |
size |
每个数据项的字节大小 |
nmemb |
读取数据项的个数(总共读取 nmemb * size 字节) |
stream |
FILE 指针 |
返回值:成功返回读取到的数据项数目(不一定等于字节数,除非 size == 1);若发生错误或到达文件末尾,返回值小于 nmemb。fread() 本身无法区分是错误还是文件末尾,需用 ferror() 或 feof() 判断。
| 参数 | 说明 |
|---|---|
ptr |
待写入数据的缓冲区 |
size |
每个数据项的字节大小 |
nmemb |
写入数据项的个数 |
stream |
FILE 指针 |
返回值:成功返回写入的数据项数目(不一定等于字节数,除非 size == 1);出错时返回值小于 nmemb(或等于 0)。
指定数据大小的方式与系统调用不同:
fread/fwrite用nmemb(数据项个数)* 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 的printfFILE *重定向 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-gnueabivs 硬浮点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库