跳到主要内容
Linux 文件操作核心:缓冲区机制与文件描述符原理 | 极客日志
C
Linux 文件操作核心:缓冲区机制与文件描述符原理 Linux 文件操作中,C 标准库通过用户级缓冲区减少系统调用次数以提升效率。解析文件描述符与缓冲区的交互机制,对比 printf/fwrite/write 的输出差异及刷新策略(无缓冲、行缓冲、全缓冲)。通过模拟实现简易 C 文件库,展示_fopen、_fwrite、_fclose 等函数如何封装 open/write/close 系统调用,阐明进程退出时缓冲区刷新的关键逻辑。
灭霸 发布于 2026/3/15 更新于 2026/4/26 12 浏览一、文件描述符
1、重新认识缓冲区
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main () {
const char * fstr = "hello fwrite\n" ;
const char * str = "hello write\n" ;
fprintf (stdout , "hello fprintf\n" );
fwrite(fstr, strlen (fstr), 1 , stdout );
write(1 , str, strlen (str));
fork();
return 0 ;
}
代码的结果为直接输出时显示正常输出,输出到文件中时 C 语言的接口输出了 2 次,系统调用的函数输出了 1 次。
原因 :向显示器输出为行缓冲方式会依次输出到显示器中。当向文件中输出时,缓冲方式由行缓冲变成了全缓冲。即遇到 \n,不在刷新,而是等缓冲区被写满才刷新。
首先 write 通过缓冲区直接刷新,文件刷新为全缓冲,C 接口的函数的内容被存储在 C 语言提供的用户级缓冲区中,fork 之后创建子进程,父子进程的数据共享,子进程进行写时拷贝,C 语言提供的缓冲区里的数据被拷贝了 2 份,在进程退出时,C 语言的缓冲区被刷新。
#include <stdio.h>
#include <string.h>
#include
{
* fstr = ;
* str = ;
( );
sleep( );
( , );
sleep( );
fwrite(fstr, (fstr), , );
sleep( );
write( , str, (str));
sleep( );
;
}
<unistd.h>
int
main
()
const
char
"hello fwrite\n"
const
char
"hello write\n"
printf
"hello printf\n"
2
fprintf
stdout
"hello fprintf\n"
2
strlen
1
stdout
2
1
strlen
5
return
0
前 6 秒时 C 接口的函数被存储在 C 语言的缓冲区中,write 函数通过缓冲区直接写入文件中,当进程结束时刷新缓冲区将数据放入文件中。文件刷新为全缓冲,\n 不会刷新缓冲区,而是等缓冲区写满才会被刷新。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main () {
const char * fstr = "hello fwrite\n" ;
const char * str = "hello write\n" ;
fprintf (stdout , "hello fprintf\n" );
fwrite(fstr, strlen (fstr), 1 , stdout );
close(1 );
write(1 , str, strlen (str));
fork();
return 0 ;
}
和第 1 次代码的结果不同,添加 close 后,C 语言的接口输出了 1 次,系统调用的接口没有输出。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main () {
const char * fstr = "hello fwrite" ;
printf ("hello printf" );
fprintf (stdout , "hello fprintf" );
fwrite(fstr, strlen (fstr), 1 , stdout );
close(1 );
return 0 ;
}
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main () {
const char * str = "hello fwrite" ;
write(1 , str, strlen (str));
close(1 );
return 0 ;
}
代码没有 \n,和 C 语言的接口不同,系统调用的接口可以输出。
总结
缓冲区一定不在操作系统内部,不是系统级别的缓冲区。write 可以输出是因为 write 直接将字符串写入了缓冲区中,close 对显示没有影响。
C 语言会提供一个用户级缓冲区,C 语言的接口函数传递的数据实际上储存在 C 语言提供的用户级缓存区中,当调用 \n、fclose 等可以刷新缓冲区时,才会刷新 C 语言提供的缓冲区,并调用 write 函数,将数据写入操作系统中。
C 语言的文件操作绕不开 FILE,FILE 中包含文件描述符 fd,FILE 里面还有对应打开文件的缓冲区字段。这个 FILE 对象属于用户,语言都属于用户层。
显示器的文件刷新方案是行刷新,所以在 printf 执行完成后就会立即遇到 \n 的时候,将数据进行刷新。刷新的本质是将数据通过 1 + write 通过 write 接口写入到内核中。
目前我们认为,只要将数据刷新进入了内核中,数据就会被刷新进入硬件中。
2、exit 和 _exit exit 是 C 语言的接口,退出时会刷新 C 语言提供的缓冲区,然后调用 _exit 退出,_exit 是系统调用,直接释放进程不会对数据进行刷新。
3、缓冲区的刷新问题
(1)无缓冲 无缓冲区是一种写透模式,不要在缓冲区中做出各种数据残留,直接刷新,不能等待,没有进行各种刷新策略。
(2)行缓冲 不刷新直到遇到 \n 才会刷新。默认向显示器输出采用行刷新是因为显示器是给人看的,人每次看数据符合每次看一行的习惯,需要尽可能快的把数据刷新出来。
(3)全缓冲 缓冲区满了才会刷新缓冲区。在向普通文件写入时为了提高效率刷新时不需要实时观看所以采用全缓冲。
根据这 3 种情况来决定什么时候调用 write 接口的问题。fflush 的底层会封装 write。缓冲区中存储的数据越多,效率越高。
4、进程退出 #include <stdio.h>
#include <string.h>
#include <unistd.h>
int main () {
printf ("hello world" );
return 0 ;
}
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main () {
printf ("hello world" );
close(1 );
return 0 ;
}
printf("hello world");若未包含换行符 \n 且程序未结束或未手动刷新,数据会暂时留在缓冲区中,不会立即显示在终端上。当使用 close(1) 后,文件描述符被关掉,会导致缓冲区中的数据未来得及刷新就丢失,最终无法在显示器上显示。
5、C 语言提供缓冲区的原因
解决效率问题,硬件设备的读写速度远慢于 CPU 和内存的处理速度。缓冲区用于临时存储待输出或待读取的数据。程序先将数据写入缓冲区,待缓冲区满、触发特定条件或程序结束时,再一次性将缓冲区数据写入硬件设备。这大幅减少了硬件 I/O 的次数,从而提升整体效率。
标准 I/O 库通过缓冲区批量处理数据,将多次小的 I/O 请求合并为一次大的请求,从而减少系统调用的次数,降低开销。
优化用户交互体验。数据暂存在缓冲区,直到遇到换行符 \n 才刷新到屏幕。这种设计符合人类阅读习惯,用户更希望看到完整的一行内容,而非字符逐个零散显示。
二、模拟实现 C 文件标准库
1、为什么要'造轮子'? 我们平时写 C 语言文件操作时,习惯直接调用标准库的 fopen、fwrite、fclose - - - 这些函数好用,但你有没有想过:
为什么 fwrite 不是'写了就立刻到文件'?
FILE 结构体里到底存了什么?
标准库是怎么跟操作系统交互的?
这是一个简化版的 C 文件操作库:手动实现了 _fopen、_fwrite、_fflush、_fclose,没有依赖标准库的 stdio.h,而是直接调用操作系统的底层接口(open、write、close),还加入了核心的缓冲区机制。FILE 中的缓冲区的意义是使用 C 语言的接口更快,节省时间。
2、核心原理 在拆解代码前,先搞懂 3 个核心概念:系统调用、缓冲区、刷新策略 - - - 这是所有高级语言文件操作的'通用逻辑'。
(1)系统调用与用户层封装 Linux 操作系统提供了最底层的文件操作接口,称为系统调用(如 open、write、close)。但系统调用的'开销很高' - - - 每次调用都要从用户态切换到内核态,频繁调用会拖慢程序。
C 标准库的 stdio 系列函数(fopen、fwrite)本质上是对系统调用的封装,核心目的是:在用户层加一层'缓冲区',减少系统调用的次数,从而提高 IO 效率。
用 _FILE 结构体封装文件描述符(fileno)和缓冲区;
_fopen 封装 open 系统调用;
_fwrite 先写缓冲区,满了再调用 write;
_fclose 先刷新缓冲区,再调用 close。
(2)缓冲区 为什么缓冲区能提高效率?
举个例子:
如果要写 1000 个字符到文件,直接调用 write 需要 1000 次系统调用;但如果先把字符存到 1024 字节的缓冲区,满了再调用 1 次 write,系统调用次数从 1000 降到 1 - - - 效率天差地别。
代码里,缓冲区通过 FILE 结构体的 outbuffer(字符数组)和 out_pos(当前缓冲区已用长度)实现,缓冲区大小由 SIZE 宏定义为 1024 字节。
(3)缓冲刷新的 3 种策略 缓冲区的数据不会一直存着,需要在特定时机'刷新'到磁盘,代码里定义了 3 种刷新策略(通过 flag 控制):
FLUSH_NOW :无缓冲,写入后立即刷新
FLUSH_LINE :行缓冲,遇到 \n 时刷新
FLUSH_ALL :全缓冲,缓冲区满了才刷新。
3、代码拆解
(1)头文件 myfile.h 头文件的作用是'对外声明' - - - 定义结构体、宏、函数接口,让其他文件能复用。
#ifndef __MYFILE_H__
#define __MYFILE_H__
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <unistd.h>
#define SIZE 1024
#define FLUSH_NOW 1
#define FLUSH_LINE 2
#define FLUSH_ALL 4
typedef struct IO_FILE {
int fileno;
int flag;
char outbuffer[SIZE];
int out_pos;
}_FILE;
_FILE*_fopen(const char * filename, const char * flag);
int _fwrite(_FILE* fp, const char * msg, int len);
void_fclose(_FILE* fp);
void_fflush(_FILE* fp);
#endif
用 #ifndef MYFILE_H 防止头文件被重复包含(比如 main.c 和 myfile.c 都包含 myfile.h,编译时不会报错)。
_FILE 结构体只实现了输出缓冲区,输入缓冲区(inbuffer/in_pos)被注释了。
fileno 是核心:操作系统通过文件描述符识别文件,所有底层操作都依赖它。
(2)文件 myfile.c myfile.c 负责把 myfile.h 声明的接口落地,核心是 4 个函数:_fopen、_fwrite、_fflush、_fclose。
1. _fopen _fopen 的作用是 根据用户传入的'打开模式'(flag),调用 open 系统调用创建文件描述符,再分配并初始化 _FILE 结构体。
#include "myfile.h"
#define FILE_MODE 0666
_FILE*_fopen(const char * filename, const char * flag){
assert(filename);
int open_flags = 0 ;
int fd = -1 ;
if (strcmp (flag,"w" )==0 )
{
open_flags = O_CREAT|O_WRONLY|O_TRUNC;
fd = open(filename, open_flags, FILE_MODE);
}
else if (strcmp (flag,"a" )==0 )
{
open_flags = O_CREAT|O_WRONLY|O_APPEND;
fd = open(filename, open_flags, FILE_MODE);
}
else if (strcmp (flag,"r" )==0 )
{
open_flags = O_RDONLY;
fd = open(filename, open_flags);
}
else {return NULL ;
}
if (fd == -1 ){return NULL ;}
_FILE* fp = (_FILE*)malloc (sizeof (_FILE));
if (fp == NULL )
{
return NULL ;
}
fp->fileno = fd;
fp->flag = FLUSH_ALL;
fp->out_pos = 0 ;
return fp;
}
FILE_MODE 0666 :文件权限的八进制表示,rw-rw-rw-(所有者、组、其他用户都有读写权限),但实际权限会受 umask 影响(比如默认 umask 0022,实际权限会变成 0644);
open 的标志组合 :不同模式对应不同的标志,比如 w 模式必须加 O_TRUNC(清空文件),a 模式必须加 O_APPEND(追加);
内存分配检查 :malloc 可能失败(比如内存不足),必须检查 fp == NULL,否则后续操作会崩溃。
2. _fwrite _fwrite :不直接写文件,而是先把数据拷贝到缓冲区,再根据 flag 判断是否需要刷新到磁盘。
int _fwrite(_FILE* fp, const char * s, int len){
memcpy (&fp->outbuffer[fp->out_pos], s, len);
fp->out_pos += len;
if (fp->flag & FLUSH_NOW)
{
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0 ;
}
else if (fp->flag & FLUSH_LINE)
{
if (fp->outbuffer[fp->out_pos -1 ]=='\n' )
{
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0 ;
}
}
else if (fp->flag & FLUSH_ALL)
{
if (fp->out_pos == SIZE)
{
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0 ;
}
}
return len;
}
缓冲区溢出风险 :当前代码直接 memcpy,如果 fp->out_pos + len > SIZE(比如剩余缓冲区只有 50 字节,要写 100 字节),会导致数据溢出 outbuffer,破坏内存 - - - 这是严重 bug,后续优化会解决。
刷新策略的实际效果 :当前 _fopen 默认用 FLUSH_ALL,即使写入的字符串有 \n,也不会立即刷新,只会在缓冲区满(1024 字节)或 _fclose 时刷新。
3. _fflush _fflush 的作用是'不管缓冲区是否满、是否有 \n',强制把缓冲区的数据写到文件,常用于 fclose 前或需要立即落盘的场景。
void_fflush(_FILE* fp){
if (fp->out_pos > 0 )
{
write(fp->fileno, fp->outbuffer, fp->out_pos);
fp->out_pos = 0 ;
}
}
判断缓冲区是否有数据(out_pos > 0),有就调用 write,然后重置 out_pos。
4. _fclose _fclose 是'收尾工作':必须先刷新缓冲区(避免数据残留),再关闭文件描述符,最后释放 _FILE 结构体的内存。
void_fclose(_FILE* fp){
assert(fp);
_fflush(fp);
close(fp->fileno);
free (fp);
}
必须先刷新再关闭 :如果不调用 _fflush,缓冲区中未刷的数据会随着 fp 被 free 而丢失。
资源释放顺序 :先释放用户层资源(缓冲区数据),再释放内核层资源(文件描述符),最后释放内存。
4、运行与验证 代码运行多次,会继续追加内容(因为打开模式是 a)。
5、完整代码展示 #include "myfile.h"
#define FILE_MODE 0666
_FILE*_fopen(const char * filename, const char * flag){
assert(filename);
int f = 0 ;
int fd = -1 ;
if (strcmp (flag,"w" )==0 ){
f =(O_CREAT|O_WRONLY|O_TRUNC);
fd = open(filename,f,FILE_MODE);
}
else if (strcmp (flag,"a" )==0 ){
f =(O_CREAT|O_WRONLY|O_APPEND);
fd = open(filename,f,FILE_MODE);
}
else if (strcmp (flag,"r" )==0 ){
f = O_RDONLY;
fd = open(filename,f);
}
else {return NULL ;}
if (fd == -1 ){return NULL ;}
_FILE* fp = (_FILE*)malloc (sizeof (_FILE));
if (fp == NULL ){return NULL ;}
fp->fileno = fd;
fp->flag = FLUSH_ALL;
fp->out_pos = 0 ;
return fp;
}
int _fwrite(_FILE* fp, const char * s, int len){
memcpy (&fp->outbuffer[fp->out_pos],s,len);
fp->out_pos += len;
if (fp->flag & FLUSH_NOW){
write(fp->fileno,fp->outbuffer,fp->out_pos);
fp->out_pos = 0 ;
}
else if (fp->flag & FLUSH_LINE){
if (fp->outbuffer[fp->out_pos-1 ]=='\n' ){
write(fp->fileno,fp->outbuffer,fp->out_pos);
fp->out_pos = 0 ;
}
}
else if (fp->flag & FLUSH_ALL){
if (fp->out_pos == SIZE){
write(fp->fileno,fp->outbuffer,fp->out_pos);
fp->out_pos = 0 ;
}
}
return len;
}
void_fflush(_FILE* fp){
if (fp->out_pos > 0 ){
write(fp->fileno,fp->outbuffer,fp->out_pos);
fp->out_pos = 0 ;
}
}
void_fclose(_FILE* fp){
assert(fp);
_fflush(fp);
close(fp->fileno);
free (fp);
}
#ifndef __MYFILE_H__
#define __MYFILE_H__
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <unistd.h>
#define SIZE 1024
#define FLUSH_NOW 1
#define FLUSH_LINE 2
#define FLUSH_ALL 4
typedef struct IO_FILE {
int fileno;
int flag;
char outbuffer[SIZE];
int out_pos;
}_FILE;
_FILE*_fopen(const char * filename, const char * flag);
int _fwrite(_FILE* fp, const char * msg, int len);
void_fclose(_FILE* fp);
void_fflush(_FILE* fp);
#endif
#include "myfile.h"
#define filename "test.txt"
int main () {
_FILE* fp = _fopen(filename,"a" );
if (fp == NULL ){return 1 ;}
const char * msg = "hello world\n" ;
int cnt = 10 ;
while (cnt){
_fwrite(fp,msg,strlen (msg));
sleep(1 );
cnt--;
}
_fclose(fp);
return 0 ;
}
main:main.c myfile.c
gcc -o $@ $^ -std=c99
.PHONY :clean
rm -f main
三、总结 当你跑通模拟 C 库的代码,那些文件操作的困惑早已有了答案:printf 等 \n、fwrite 藏缓冲、close (1) 丢数据,本质都是'用户层与内核层交互逻辑'的体现。Linux 学习从不是记接口,而是抓本质:想清'数据在哪个缓冲','通过哪个描述符到内核',多数问题会迎刃而解。继续打磨代码、追问底层,'懂原理'的每一步,都会让后续进阶更扎实。
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online