**摘要:**本文通过手写一个简易的 C 语言标准 I/O 库(mystdio),深入讲解文件描述符(fd)与用户态缓冲区的工作原理。文章从准备工作开始,依次模拟实现了 fopen、fwrite、fflush、fclose 等核心接口,重点剖析了行刷新、全刷新与强制刷新三种刷新策略的区别,并通过测试代码直观展示了不同刷新方式下数据写入文件的时机差异。最后对未打开文件在磁盘上的存储方式等问题做了铺垫,为后续深入理解文件系统打下基础。
目录
1.准备工作
我们需要创建一系列文件mystdio.h,mystdio.c,test.c,Makefile:

mystdio.h:C语言标准头文件;mystdio.c:C语言标准头文件的具体实现。
test.c:代码的测试;Makefile:方便编译。
我们可以在Makefile写下:
bash
testc:mystdio.c test.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f testc
在mystdio.h里写:
cpp
#ifndef __MYSTDIO_H__
#define __MYSTDIO_H__
//将来打开的文件刷新方式
//不刷新,数据直接刷新到内核,不经过用户缓冲区(stderr)
#define NONE_FLUSH 1
//行刷新,遇到'\n'就刷新(类似显示器的stdout)
#define FLUSH_LINE 2
//全刷新,缓冲区满了才刷新(类似重定向到文件的stdout)
#define FLUSH_FULL 4
//缓冲区大小
#define SIZE 1024
//设置初始权限
#define UMASK 0666
//模拟libc库中的FILE
typedef struct _MY_IO_FILE
{
int fileno;//底层文件描述符(open返回的fd)
int flag;//刷新模式:NONE_FLUSH/FLUSH_LINE/FLUSH_FULL
//输出缓冲区,暂存待写入的数据
char outbuffer[SIZE];
int curr;//当前缓冲区已写入的字节数(写指针位置)
int cap;//缓冲区总容量(通常等于SIZE)
}MyFILE;
//打开文件:内部调用 open(),并初始化 MyFILE 结构体
MyFILE* my_fopen(const char* filename,const char* mode);
//关闭文件:先刷新缓冲区,再调用 close(),最后释放 MyFILE
void my_fclose(MyFILE* fp);
//写数据:先把数据拷入 outbuffer,再根据 flag 判断是否刷新
int my_fwrite(const char* s,int size,MyFILE* fp);
//强制刷新:把 outbuffer 中 curr 字节的数据 write 到内核,并重置 curr
void my_fflush(MyFILE* fp);
#endif
因此,我们需要在以该文件为基础,在mystdio.c里完善该代码!
我们先完善一下stdio.c的代码:
cpp
#include "mystdio.h"
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<unistd.h>
//模拟实现fopen
MyFILE* my_fopen(const char* filename,const char* mode);
//模拟实现fclose
void my_fclose(MyFILE* fp);
//模拟实现fwrite
int my_fwrite(const char* s,int size,MyFILE* fp);
//模拟实现fflush
void my_fflush(MyFILE* fp);
然后在此基础上完善其他的部分!
2.fopen的模拟实现
cpp
//模拟实现fopen
MyFILE* my_fopen(const char* filename,const char* mode)
{
int fd = -1;
//先判断文件打开方式是什么
if(strcmp(mode,"w") == 0)
{
//不存在就新建,如果存在就打开,并清空写入
fd = open(filename,O_CREAT | O_WRONLY | O_TRUNC,UMASK);
}
else if(strcmp(mode,"r") == 0)
{
//只读
fd = open(filename,O_RDONLY);
}
else if(strcmp(mode,"a") == 0)
{
//不存在就新建,如果存在就打开,并追加写入
fd = open(filename,O_CREAT | O_WRONLY | O_APPEND,UMASK);
}
else if(strcmp(mode,"a+") == 0)
{
//不存在就新建,如果存在就打开,可读可写,追加写入
fd = open(filename,O_CREAT | O_RDWR | O_APPEND,UMASK);
}
else
{}
//如果打开失败
if(fd<0)
return NULL;
//打开成功
//申请空间
MyFILE* fp = (MyFILE*)malloc(sizeof(MyFILE));
//申请空间失败
if(!fd)
return NULL;
fp->fileno = fd;
//设置刷新方式按行刷新
fp->flag = FLUSH_LINE;
fp->curr = 0;
//总容量为SIZE
fp->cap = SIZE;
//对字符串进行清空操作(字符串以0结尾)
fp->outbuffer[0]=0;
return fp;
}
现在我们先不做测试,等一会再做测试!
3.fwrite的基本实现
fwrite的本质:把用户的数据拷贝到缓冲区里,而拷贝方式有很多种,也就有很多种写法,因此,我们可以使用memcpy来进行!
cpp
#include <string.h>
void *memcpy(void *dest, const void *src, size_t n);
dest:目标内存地址(拷贝到哪里);src:源内存地址(从哪里拷贝);n:要拷贝的字节数。
功能:从 src 指向的内存地址开始,连续拷贝 n 个字节到 dest 指向的内存地址。不检查重叠,也不关心内存中存放的是什么类型的数据。
cpp
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
fd:文件描述符;buf:指向要写入数据的内存缓冲区;count:要写入的字节数。
返回值 :ssize_t 类型(在 64 位 Linux 上通常是 long)。
-
成功 :返回实际写入的字节数 。这个值可能小于
count(部分写)。 -
失败 :返回
-1,并设置errno来指示具体错误
cpp
//模拟实现fwrite
int my_fwrite(const char* s,int size,MyFILE* fp)
{
//fwrite的本质:
//拷贝,把用户数据拷贝到C语言的缓冲区
//fp->outbuffer:结构体定义的缓冲区数组
//fp->curr:当前缓冲区已经使用的字节数
//这两个参数相加代表下一个可以写入数据的空闲起始位置
//size:要写入的字节数
memcpy(fp->outbuffer+fp->curr,s,size);
//我们还需要更新位置
fp->curr += size;
//开始刷新,只有符合刷新条件才刷新!
//fp->flag & FLUSH_LINE:代表为行刷新此时为真
//fp->outbuffer[fp->curr-1]代表最后一个字符
//行刷新
if((fp->flag & FLUSH_LINE) && fp->outbuffer[fp->curr-1] == '\n')
{
//开始刷新
write(fp->fileno,fp->outbuffer,fp->curr);
//清零
fp->curr = 0;
}
else if((fp->flag & FLUSH_FULL) && fp->curr == fp->cap)
{
//开始刷新
write(fp->fileno,fp->outbuffer,fp->curr);
fp->curr = 0;
}
else
{
//无缓冲
//直接调用系统调用
//此时我们不考虑
//write(fp->fileno,fp->outbuffer,fp->curr);
}
return size
}
4.fflush的模拟实现
cpp
//模拟实现fflush
void my_fflush(MyFILE* fp)
{
//由于fflush是根据我们的规则做刷新,因此,直接用上面一样的代码就行
if((fp->flag & FLUSH_LINE) && fp->outbuffer[fp->curr-1] == '\n')
{
//开始刷新
write(fp->fileno,fp->outbuffer,fp->curr);
//清零
fp->curr = 0;
}
else if((fp->flag & FLUSH_FULL) && fp->curr == fp->cap)
{
//开始刷新
write(fp->fileno,fp->outbuffer,fp->curr);
fp->curr = 0;
}
else
{
//无缓冲
}
}
但是我们也可以加个标志位更好,因此,在mystdio.h加:
cpp
//标志位
#define FORCE 1
#define NORMAL 2
而且我不想把方法暴露给外部,只能自己在内部定义,因此,代码改为:
cpp
//模拟实现fflush
//不想把它暴露给外部,加static
static void my_fflush_core(MyFILE* fp,int force)
{
//如果内部的数据没有,就不刷新
if(fp->curr <= 0)
return;
//强制刷新
//就把所有内容刷新
if(force == FORCE)
{
write(fp->fileno,fp->outbuffer,fp->curr);
fp->curr = 0;
}
else
{
//否则走正常条件
//由于fflush是根据我们的规则做刷新,因此,直接用上面一样的代码就行
if((fp->flag & FLUSH_LINE) && fp->outbuffer[fp->curr-1] == '\n')
{
//开始刷新
write(fp->fileno,fp->outbuffer,fp->curr);
//清零
fp->curr = 0;
}
else if((fp->flag & FLUSH_FULL) && fp->curr == fp->cap)
{
//开始刷新
write(fp->fileno,fp->outbuffer,fp->curr);
fp->curr = 0;
}
else
{
//无缓冲
}
}
}
此外,我们对外部还要提供一个接口fflush:
cpp
//给外部提供的接口
void my_fflush(MyFILE* fp)
{
//因为调用这个函数时,必定是强制刷新
my_fflush_core(fp,FORCE);
}
5.fwrite的最终实现
因为fflush的逻辑和fwrite基本类似,我们只需要判断是否满足刷新条件就行:
cpp
//模拟实现fwrite
int my_fwrite(const char* s,int size,MyFILE* fp)
{
//fwrite的本质:
//拷贝,把用户数据拷贝到C语言的缓冲区
//fp->outbuffer:结构体定义的缓冲区数组
//fp->curr:当前缓冲区已经使用的字节数
//这两个参数相加代表下一个可以写入数据的空闲起始位置
//size:要写入的字节数
memcpy(fp->outbuffer+fp->curr,s,size);
//我们还需要更新位置
fp->curr += size;
//检测是否要刷新
//直接调用fflush函数即可
my_fflush_core(fp,NORMAL);
return size;
}
6.fclose的模拟实现
cpp
//模拟实现fclose
void my_fclose(MyFILE* fp)
{
//判断fp是否合法,只有不小于0才行
if(fp->fileno >= 0)
{
//不论如何都需要强制刷新
//把数据从用户->缓冲区
my_fflush(fp);
//要求操作系统把内核的数据立马刷新到外设
//把数据从缓冲区->内核
fsync(fp->fileno);
//关闭文件
close(fp->fileno);
//释放空间
free(fp);
}
}
7.测试代码
我们要测试直接写一段打开文件并写入再关闭文件的代码即可:
cpp
#include "mystdio.h"
#include<string.h>
#include<unistd.h>
int main()
{
MyFILE* fp = my_fopen("log.txt","w");
//打开文件失败
if(fp == NULL)
return 1;
const char* s = "hello myfile \n";
int cnt = 20;
while(cnt--)
{
my_fwrite(s,strlen(s),fp);
sleep(1);
}
my_fclose(fp);
return 0;
}
此时我们的代码应该的结果是:一行一行刷新且一直刷新!
我们新开一台机器,持续运行:
bash
while :; do cat log.txt; sleep 1; done
然后在原来机器上运行有(第5秒的结果):

实际上,我们这样看确实不怎么方便,建议大家去到自己的机器上执行该代码更好观察!
如果我们不是按照行刷新,那么就可以把代码改为这样:
cpp
#include "mystdio.h"
#include<string.h>
#include<unistd.h>
int main()
{
MyFILE* fp = my_fopen("log.txt","w");
//打开文件失败
if(fp == NULL)
return 1;
//行刷新
//const char* s = "hello myfile \n";
//全刷新
const char* s = "hello myfile ";
int cnt = 20;
while(cnt--)
{
my_fwrite(s,strlen(s),fp);
sleep(1);
}
my_fclose(fp);
return 0;
}
此时我们在另外一台机器上先删掉log.txt然后再执行刚刚的指令有:
一旦执行左边的指令,就出现了:

此时的那个cat:log.txt:No such file or directory这个语句也不打印了,直到最后:

当我们左边程序执行完的一瞬间,右边的语句就直接打印出来了!!!
我们还可以测试在以a的形式打开文件的时候的程序是否正确,这个可以自己去测试!
实际上,这篇博客以及上两篇博客都是在讲解基础文件I/O,而这些都是在讲解:fd(文件描述符),全部都是在说:一个被打开的文件和进程如何如何!
我们之前说过:文件分为:打开的文件和未打开的文件。
那么未打开的文件我们有很多问题:
①为什么打开的文件要带上文件路径,即便不带,我们也要拿cwd来找到当前文件,为什么要路径?
②打开文件时在操作系统底层做了什么?
③对于文件来讲,如果文件没有被打开,那它应当放在哪里?
如果一个文件没有被打开,它一定是在类似磁盘这样的存储设备上的(磁盘是永久性存储介质,即掉电不易失,而内存叫做电易失性存储介质),因此磁盘通常是对文件进行永久保存性的方案!
因此,需要了解后续内容,请见下篇博客!
8.代码总结
代码已经提交到Gitee上,如果想要更快速查看可以去:
mylibc · 谭旦0000/test_for_csdn - 码云 - 开源中国
或者直接看下面代码:
bash
//Makefile
testc:mystdio.c test.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f testc
cpp
//mystdio.h
#ifndef __MYSTDIO_H__
#define __MYSTDIO_H__
//将来打开的文件刷新方式
//不刷新,数据直接刷新到内核,不经过用户缓冲区(stderr)
#define NONE_FLUSH 1
//行刷新,遇到'\n'就刷新(类似显示器的stdout)
#define FLUSH_LINE 2
//全刷新,缓冲区满了才刷新(类似重定向到文件的stdout)
#define FLUSH_FULL 4
//缓冲区大小
#define SIZE 1024
//设置初始权限
#define UMASK 0666
//标志位
#define FORCE 1
#define NORMAL 2
//模拟libc库中的FILE
typedef struct _MY_IO_FILE
{
int fileno;//底层文件描述符(open返回的fd)
int flag;//刷新模式:NONE_FLUSH/FLUSH_LINE/FLUSH_FULL
//输出缓冲区,暂存待写入的数据
char outbuffer[SIZE];
int curr;//当前缓冲区已写入的字节数(写指针位置)
int cap;//缓冲区总容量(通常等于SIZE)
}MyFILE;
//打开文件:内部调用 open(),并初始化 MyFILE 结构体
MyFILE* my_fopen(const char* filename,const char* mode);
//关闭文件:先刷新缓冲区,再调用 close(),最后释放 MyFILE
void my_fclose(MyFILE* fp);
//写数据:先把数据拷入 outbuffer,再根据 flag 判断是否刷新
int my_fwrite(const char* s,int size,MyFILE* fp);
//强制刷新:把 outbuffer 中 curr 字节的数据 write 到内核,并重置 curr
void my_fflush(MyFILE* fp);
#endif
cpp
//mystdio.c
#include "mystdio.h"
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<unistd.h>
//模拟实现fopen
MyFILE* my_fopen(const char* filename,const char* mode)
{
int fd = -1;
//先判断文件打开方式是什么
if(strcmp(mode,"w") == 0)
{
//不存在就新建,如果存在就打开,并清空写入
fd = open(filename,O_CREAT | O_WRONLY | O_TRUNC,UMASK);
}
else if(strcmp(mode,"r") == 0)
{
//只读
fd = open(filename,O_RDONLY);
}
else if(strcmp(mode,"a") == 0)
{
//不存在就新建,如果存在就打开,并追加写入
fd = open(filename,O_CREAT | O_WRONLY | O_APPEND,UMASK);
}
else if(strcmp(mode,"a+") == 0)
{
//不存在就新建,如果存在就打开,可读可写,追加写入
fd = open(filename,O_CREAT | O_RDWR | O_APPEND,UMASK);
}
else
{}
//如果打开失败
if(fd<0)
return NULL;
//打开成功
//申请空间
MyFILE* fp = (MyFILE*)malloc(sizeof(MyFILE));
//申请空间失败
if(!fd)
return NULL;
fp->fileno = fd;
//设置刷新方式按行刷新
fp->flag = FLUSH_LINE;
fp->curr = 0;
//总容量为SIZE
fp->cap = SIZE;
//对字符串进行清空操作(字符串以0结尾)
fp->outbuffer[0]=0;
return fp;
}
//模拟实现fclose
void my_fclose(MyFILE* fp)
{
//判断fp是否合法,只有不小于0才行
if(fp->fileno >= 0)
{
//不论如何都需要强制刷新
//把数据从用户->缓冲区
my_fflush(fp);
//要求操作系统把内核的数据立马刷新到外设
//把数据从缓冲区->内核
fsync(fp->fileno);
//关闭文件
close(fp->fileno);
//释放空间
free(fp);
}
}
//模拟实现fflush
//不想把它暴露给外部,加static
static void my_fflush_core(MyFILE* fp,int force)
{
//如果内部的数据没有,就不刷新
if(fp->curr <= 0)
return;
//强制刷新
//就把所有内容刷新
if(force == FORCE)
{
write(fp->fileno,fp->outbuffer,fp->curr);
fp->curr = 0;
}
else
{
//否则走正常条件
//由于fflush是根据我们的规则做刷新,因此,直接用上面一样的代码就行
if((fp->flag & FLUSH_LINE) && fp->outbuffer[fp->curr-1] == '\n')
{
//开始刷新
write(fp->fileno,fp->outbuffer,fp->curr);
//清零
fp->curr = 0;
}
else if((fp->flag & FLUSH_FULL) && fp->curr == fp->cap)
{
//开始刷新
write(fp->fileno,fp->outbuffer,fp->curr);
fp->curr = 0;
}
else
{
//无缓冲
}
}
}
//给外部提供的接口
void my_fflush(MyFILE* fp)
{
//因为调用这个函数时,必定是强制刷新
my_fflush_core(fp,FORCE);
}
//模拟实现fwrite
int my_fwrite(const char* s,int size,MyFILE* fp)
{
//fwrite的本质:
//拷贝,把用户数据拷贝到C语言的缓冲区
//fp->outbuffer:结构体定义的缓冲区数组
//fp->curr:当前缓冲区已经使用的字节数
//这两个参数相加代表下一个可以写入数据的空闲起始位置
//size:要写入的字节数
memcpy(fp->outbuffer+fp->curr,s,size);
//我们还需要更新位置
fp->curr += size;
//检测是否要刷新
//直接调用fflush函数即可
my_fflush_core(fp,NORMAL);
return size;
//开始刷新,只有符合刷新条件才刷新!
//fp->flag & FLUSH_LINE:代表为行刷新此时为真
//fp->outbuffer[fp->curr-1]代表最后一个字符
//行刷新
//if((fp->flag & FLUSH_LINE) && fp->outbuffer[fp->curr-1] == '\n')
//{
// //开始刷新
// write(fp->fileno,fp->outbuffer,fp->curr);
// //清零
// fp->curr = 0;
//}
//else if((fp->flag & FLUSH_FULL) && fp->curr == fp->cap)
//{
// //开始刷新
// write(fp->fileno,fp->outbuffer,fp->curr);
// fp->curr = 0;
//}
//else
//{
// //无缓冲
// //直接调用系统调用
// //write(fp->fileno,fp->outbuffer,fp->curr);
//}
//return size
}
cpp
//test.c
#include "mystdio.h"
#include<string.h>
#include<unistd.h>
int main()
{
MyFILE* fp = my_fopen("log.txt","w");
//打开文件失败
if(fp == NULL)
return 1;
//行刷新
//const char* s = "hello myfile \n";
//全刷新
const char* s = "hello myfile ";
int cnt = 20;
while(cnt--)
{
my_fwrite(s,strlen(s),fp);
sleep(1);
}
my_fclose(fp);
return 0;
}