Linux 文件缓冲区
我们要理解文件缓冲区 我们先来看一段代码
int main()
{
const char *fstr = "fwrite_fstr ";
const char *str = "fwrite_str\n\n";
const char *str1="操作系统的接口带\n\n";
const char *fstr1="操作系统的接口不带\n";
const char *fstr2 = "write_fstr ";
const char *str2="write_str\n\n";printf("C语言的接口不带\n ");
printf("printf ");
fprintf(stdout, " fprintf ");
fwrite(fstr, strlen(fstr), 1, stdout);printf("C语言的接口带\n\n");
printf("printf\n");
fprintf(stdout, " fprintf\n");
fwrite(str, strlen(str), 1, stdout);write(1, str1, strlen(str1));
write(1, str2, strlen(str2));write(1, fstr1, strlen(fstr1));
write(1, fstr2, strlen(fstr2));fork();
return 0;
}
Linux 中 IO 分为带缓冲的标准 IO(如 printf、fprintf、fwrite)和无缓冲的系统调用 IO(如 write),且缓冲行为会因输出目标(终端 / 文件)变化:
| IO 类型 | 缓冲类型 | 刷新时机 |
| 标准 IO (printf 等) | 行缓冲(输出到终端时) | 遇到 \n、缓冲满、程序结束时刷新 |
| 标准 IO (printf 等) | 全缓冲(输出到文件时) | 缓冲满、程序结束时刷新(\n 不触发刷新) |
| 系统调用 (write) | 无缓冲 | 调用后立即输出,无缓冲延迟 |
1. ./test
1. 标准 IO 的 “行缓冲” 导致内容 “攒批输出”
printf、fprintf、fwrite属于带缓冲的标准 IO,在终端输出时采用行缓冲策略—— 只有遇到\n、缓冲区满或程序结束时才会刷新(输出到终端)。
第一块标准 IO 代码(“C 语言的接口不带” 部分):printf、fprintf、fwrite都没有显式\n,因此内容会先存入内存缓冲区,不立即输出。
第二块标准 IO 代码(“C 语言的接口带” 部分):printf("printf\n"); 包含\n,触发行缓冲刷新,此时会将第一块的缓冲内容 + 第二块的printf\n 一起输出,形成 “C语言的接口不带\n printf fprintf fwrite_fstr C语言的接口带\nprintf” 的连续内容。
第二块的fprintf(stdout, " fprintf\n"); 因前面的\n已刷新缓冲,且自身带\n,所以会立即输出,显示为 “ fprintf”。
2. 系统调用write的 “无缓冲” 导致内容 “实时输出”
write是无缓冲的系统调用,调用后会立即将数据输出到文件描述符(此处stdout对应终端),不受行缓冲限制。
“操作系统的接口带” 部分:write(1, str1, ...) 和 write(1, str2, ...) 会立即输出,其中str包含\n,所以 “fwrite_str\n” 会换行显示。
“操作系统的接口不带” 部分:write(1, fstr1, ...) 和 write(1, fstr2, ...) 也会立即输出,无延迟。
3. fork()
fork会复制父进程的地址空间(包括stdout的缓冲区)。
但在本代码中,fork执行前,所有标准 IO 的缓冲区已因\n被刷新,write也已直接输出,因此父子进程的缓冲区为空,程序退出时无额外重复输出。
4.如果前面的没有\n刷新会怎么样???
int main()
{printf("printf只输出一次 ");fflush(NULL);fork();return 0;
}
fork之后会有父子两个进程
当父子进程执行到return 0正常退出时,标准 IO 库会自动触发「缓冲区刷新」(这是标准 IO 的退出清理机制)
当父子进程各自return0、触发stdout缓冲区刷新时,需要修改缓冲区的状态(这里就是把缓冲区内容输入到文件中)—— 这个「修改操作」会触发 写实拷贝;
所以这个时候子进程会拷贝一份和父进程一模一样的缓冲区
然后再刷新这个缓冲区 把内容输到文件中(这个步骤底层是调用write函数实现的)
这也是为什么"printf只输出一次 "这个字符串会被打印两次了
2. ./test>log.txt
重定向(./test>log.txt)的影响 当输出重定向到文件时,标准 IO 的缓冲类型从 “行缓冲” 变为 “全缓冲”,而系统调用 write 的无缓冲特性不受影响。
这直接导致输出顺序和可见性变化
系统调用 write:无缓冲,调用后立即将内容写入文件,所以 hello write 会先出现在 log.txt 中。
标准 IO (printf等):全缓冲模式下,若输出没有 \n 且缓冲未 “满”,会等到程序结束时才一次性刷新。
因此,标准 IO 的内容会在 write 之后输出,顺序与直接运行时(终端行缓冲)相反。
代码中所有标准 IO 操作(printf等)未调用fflush,且输出量未填满缓冲区,因此在 fork 执行前,所有标准 IO 内容都暂存在父进程的stdout缓冲区中,未写入文件。
fork之后
当父子进程执行到return 0正常退出时,标准 IO 库会自动触发「缓冲区刷新」(这是标准 IO 的退出清理机制)
当父子进程各自return0、触发stdout缓冲区刷新时,需要修改缓冲区的状态(这里就是把缓冲区内容输入到文件中)—— 这个「修改操作」会触发 写实拷贝;
所以这个时候子进程会拷贝一份和父进程一模一样的缓冲区
然后再刷新这个缓冲区 把内容输到文件中(这个步骤底层是调用write函数实现的)
这也是为什么 C语言的接口printf fprintf这些会被打印两次 并且位于write后面
那么我们可以主动刷新缓冲区吗 答案是可以的
3.fflush
主动刷新缓冲区的函数是fflush
当缓冲区满、遇到换行符(行缓冲,如 stdout)、或调用 fclose 时,stdio 库会自动调用 write,将缓冲区数据写入内核;
而 fflush 的作用是 “手动触发这个过程”:不管缓冲区是否满,强制让 stdio 库调用 write,把当前缓冲区的数据推送到内核。
检查目标 FILE* 流的 stdio 缓冲区(用户态)是否有未写入的数据;
若有数据:调用 write(或 pwrite 等系统调用),将缓冲区数据推送到内核缓冲区;
若没有数据:直接返回成功,不调用任何系统调用(包括 write)。
int fflush(FILE *stream);适用性:fflush 对全缓冲和行缓冲都适用,无论当前流是全缓冲(如重定向到文件的stdout)还是行缓冲(如直接输出到终端的stdout),调用 fflush(stream) 都会强制将 stream 对应的缓冲区内容立即写入设备或文件。
1. 参数
FILE *stream: 指向 FILE 类型结构体的指针,代表要刷新的标准 IO 流。
常见取值包括:
stdout:标准输出流(如终端、重定向文件);
stderr:标准错误流;
自定义的文件流(如通过 fopen 打开的文件指针);
特殊值 NULL:此时 fflush 会刷新所有打开的输出流的缓冲区(仅对输出流有效,输入流如 stdin 无意义)。
2.返回值
成功:返回 0;
失败:返回 EOF,并设置全局变量 errno 以指示错误原因(例如流指针无效、IO 操作失败等)。
#include <stdio.h>
#include <unistd.h>
int main()
{printf("printf只输出一次 ");fflush(NULL); fork();return 0;
}
总结一下:
操作系统的接口write是直接输出 没有缓冲
但是c语言的文件接口比如printf fprintf
C语言会为其提供一个缓冲区
当缓冲区满足一定条件后(行缓冲 全缓冲)
再调用write一起输出
我们可以用代码验证这一点
int main()
{const char* str = "write ";const char* str1 = "fwrite ";write(1, str, strlen(str));close(1);printf("printf ");fwrite(str1, strlen(str1), 1, stdout);fork();return 0;
}
我们知道./test是行缓冲 ./test>log.txt是全缓冲
但是我的代码中没有\n 也就是
无论是./test 还是./test>log.txt
fwrite和fprintf发内容 在return 0之前都是在C语言的提供的缓冲区中
当遇到return 0的时候
父进程和子进程都要
调用write函数刷新缓冲区
但是由于stdout被close(1)关闭了
这个时候调用write函数失败
自然无法打印结果了
思考:这个代码有没有触发写实拷贝???
首先 我们要明确 return0的时候会刷新缓冲区
会调用write函数
但是由于stdout已经关闭了
所以write函数会调用失败
无法对享可写页面实现修改
所以就不会触发写实拷贝
但是要注意:
如果你先fork后
再执行 fwrite printf等等C语言的函数
就会触发写实拷贝
因为C语言提供的用户缓冲区 本质上是共享可写的
当你子进程printf或者fwrite本质上是对这个缓冲区进行修改
所以就会触发写实拷贝
这个用户缓冲区位于FILE结构体中
我们以fopen函数为例

FILE 结构体的内存分配:“在语言层给我们 malloc (FILE)” ,当调用 fopen 时,libc.so 会在用户态的堆内存中动态分配 FILE 结构体(通过 malloc 实现)。
这个结构体包含了文件的缓冲、状态、关联的文件描述符等关键信息,是标准 I/O 库管理文件的核心载体。
fopen 借助 C 标准库(libc.so)分配 FILE 结构体的内存,并封装了底层系统调用,为开发者提供了 “用 FILE* 操作文件” 的便捷抽象。
fopen 本身会间接调用类似 open 的系统调用来打开文件,再通过 FILE 结构体封装这些底层细节,让开发者无需直接操作文件描述符和系统调用,只需通过 FILE* 就能完成文件的读写、缓冲管理等操作。
举个例子
当调用 fprintf(stdout, "hello world\n"); 时,stdout 是一个 FILE* 类型的指针(代表标准输出流)。fprintf 会先将数据写入 stdout 对应的用户态缓冲区(由 FILE 结构体管理)。
最后再由write写入内核缓冲区
write函数不会触发写实拷贝 但是会被打印两次
但是write函数没有缓冲区 本质上是直接
“将用户空间的数据,写入内核空间的文件描述符(fd)对应的内核缓冲区”(如文件的页缓存、终端的内核缓冲区),其操作不涉及 “修改用户空间内存”:
int main()
{const char* str = "write ";fork();write(1, str, strlen(str));return 0;
}
