前言
有了前面的基础我们知道c语言的缓冲区本质上就是FILE结构体的一部分,我们每次使用stdout将数据打印到屏幕上时,本质上是先将数据拷贝到FILE结构体的缓冲区中,然后再拷贝到内核缓冲区中也就是file结构体里面的缓冲区,最后再刷新到屏幕上面,在前面的学习中我们是使用fopen函数打开文件,fwrite函数将数据输出到文件里面,fflush函数刷新FILE结构体里面的缓冲区,fclose函数关闭打开的文件,这些函数都是c语言提供的函数,而我们知道c语言是用户级语言,这些c语言函数在底层是调用系统的接口来实现的功能,比如说open函数,write函数,close函数等等,那么本篇文章我将带着大家通过这些系统调用接口来实现一个属于自己的缓冲区
准备工作
首先创建三个文件:mystdio.h(这个文件里面存放各种函数和结构体的声明) mystdio.c(这个文件里面存放.h文件里面的函数实现) main.c(这个文件则是用来测试main.c文件里面的函数实现是否是正确),那么这里的操作就如下:
在linux系统通过fd也就是一个数组的下标来往指定的文件里面写入内容,所以使用write函数时会返回一个正整数,使用c语言的fopen函数打开一个文件时会返回一个FILE结构体,这个结构体里面就会装对应文件在linux系统中的fd和一个缓冲区以及缓冲区对应的各种属性比如说:缓冲区的大小,缓冲区的占用率,缓冲区的刷新方式等等,既然我们要自己模拟实现缓冲区的话是不是也得自己创建一个结构体来存放文件的fd和对应的缓冲区以及对应的属性啊,那么这里的代码如下:
typedef struct FILE_
{
int file_fd;//文件的fd
int capacity;//缓冲区的容量
int size;//缓冲区已经被使用的
int flags;//缓冲区刷新的方式
char buffer[SIZE];//缓冲区
}FILE_;
file_fd中记录了文件的fd,capacity记录缓冲区的容量,size记录缓冲区中已经装了多少个字符,因为c语言中有三种缓冲区刷新方式:立即缓冲,行缓冲和全缓冲,那么这里就得在结构体中创建一个flags来记录当前缓冲区中数据的刷新方式,字符数组buffer就是缓冲区,使用写入函数就是将数据写入到这个数组里面,既然这里有刷新方式的话,那么我们就可以定义三个标记位来分别代表立即刷新,行刷新和满刷新,那这里的代码如下:
#pragma once
#define SIZE 1024
#define SYNC_NOW 1
#define SYNC_LINE 2
#define SYNC_FULL 4
#define size 1024
typedef struct FILE_
{
int file_fd;//文件的fd
int capacity;//缓冲区的容量
int size;//缓冲区已经被使用的
int flags;//缓冲区刷新的方式
char buffer[SIZE];//缓冲区
}FILE_;
结构体创建完之后,我们还得创建一些函数来对结构体做一些操作,类似于c语言的fwrite函数,fclose函数,fflush函数,比如说下面的代码:
FILE_ *fopen_(const char* path_name,const char* mode);
void fwrite_(const void*ptr,int num,FILE_*fp);
void fclose_(FILE_*fp);
void fflush_(FILE_*fp);
这些函数参数都是根据c语言文件函数模仿写的的,那么接下来我们就要模拟实现这些函数。那么mystdio.h文件里面的代码就如下:
#pragma once
#define SIZE 1024
#define SYNC_NOW 1
#define SYNC_LINE 2
#define SYNC_FULL 4
#define size 1024
typedef struct FILE_
{
int file_fd;//文件的fd
int capacity;//缓冲区的容量
int size;//缓冲区已经被使用的
int flags;//缓冲区刷新的方式
char buffer[SIZE];//缓冲区
}FILE_;
FILE_ *fopen_(const char* path_name,const char* mode);
void fwrite_(const void*ptr,int num,FILE_*fp);
void fclose_(FILE_*fp);
void fflush_(FILE_*fp);
当然这里有些后面的用到的函数头文件没有包含并不是完整的代码,大家注意一下。
fopen_函数的实现
c语言的fopen函数在底层调用操作系统的open函数来打开文件,那么我们这里也采用同样的方式来实现打开文件的功能,首先来看看open函数的介绍
第一个参数是文件的名字也就是你要打开哪个文件,第二个参数就是以什么样的形式来打开一个文件,c语言有很多种打开文件的方式,但是我们这里实现的open函数就采用平时使用的最多的几个打开方法:w(写的方式打开),r(读的方式打开),a(追加的方式打开),fopen_函数底层是通过调用操作系统的open函数来实现,但是open函数有标志位参数不同的标志位有着不同的功能,所以在fopen_函数的开头就得创建一个flags来表示打开文件的方式,然后通过else if语句来赋予flags不同的标记位,这里赋予标记位的方法就是直接将flags与等标记位即可,比如说下面的代码:
FILE_* fopen_(const char * path_name,const char* mode)
{
int flags=0;
int defaultmode =0666;
if(strcmp(mode,"r")==0)
{
flags|=O_RDONLY;
}
else if(strcmp(mode,"w")==0)
{
flags|=O_CREAT|O_TRUNC|O_WRONLY;
}
else if(strcmp(mode,"a")==0)
{
flags|=O_CREAT|O_TRUNC|O_APPEND;
}
else
{
printf("打开文件的方式错误\n");
return NULL;
}
}
有了标记位之后就可以使用open函数打开文件,因为用写和追加的方式打开文件的话会存在创建文件的情况,创建文件得给这个文件添加对应的起始权限,而以读的方式打开文件的话却不存在创建文件的情况,所以我们这里得再添加一个if else语句用来区分open打开文件时是否需要创建文件,并且open调用完成之后会返回该文件的文件描述符,所以我们这里还得创建一个整型变量来接收open函数的返回值,如果返回值大于0的话表明打开文件成功了,如果返回值小于0的话表明打开文件失败了,那么这时我们得在屏幕上告诉使用者文件打开失败了,那这里告诉使用者的方法就是调用write函数向文件描述符为2的文件里面输出对应的错误信息,那这一步的功能就如下:
int fd=0;
if(flags&O_RDONLY)
{
fd=open(path_name,flags);
}
else
{
fd=open(path_name,flags,defaultmode);
}
if(fd<0)
{
const char*err=strerror(errno);
write(2,err,strlen(err));
return NULL;
}
打开文件成功之后我们就得创建一个FILE_结构体来描述新打开的文件,将文件的fd以及文件的缓冲区的大小等信息进行填入,因为这个结构体在出了函数之后还要被使用,所以我们就使用malloc在堆上动态开辟该结构体,将结构体的信息填充完毕之后就返回结构体的地址,那么完整的代码就如下:
FILE_* fopen_(const char * path_name,const char* mode)
{
int flags=0;
int defaultmode =0666;
if(strcmp(mode,"r")==0)
{
flags|=O_RDONLY;
}
else if(strcmp(mode,"w")==0)
{
flags|=O_CREAT|O_TRUNC|O_WRONLY;
}
else if(strcmp(mode,"a")==0)
{
flags|=O_CREAT|O_TRUNC|O_APPEND;
}
else
{
printf("打开文件的方式错误\n");
return NULL;
}
int fd=0;
if(flags&O_RDONLY)
{
fd=open(path_name,flags);
}
else
{
fd=open(path_name,flags,defaultmode);
}
if(fd<0)
{
const char*err=strerror(errno);
write(2,err,strlen(err));
return NULL;
}
FILE_* fp=(FILE_*)malloc(sizeof(FILE_));
assert(fp);
fp->flags=SYNC_LINE;
fp->capacity=SIZE;
fp->size=0;
fp->file_fd=fd;
memset(fp->buffer,0,SIZE);
return fp;
}
fflush_函数模拟实现
fflush_函数的作用就是将文件缓冲区中的内容输出到文件里面,那这里我们就可以使用write函数进行输出,然后将文件结构体里面的size置为0即可,那这个函数实现的代码就如下:
void fflush_(FILE_*fp)
{
if(fp->size>0)
{
write(fp->file_fd,fp->buffer,fp->size);
fp->size=0
}
}
fclose_函数模拟实现
fclose_函数就是关闭我们打开的文件,在关闭文件之前首先得将文件缓冲区里面的内容进行刷新,然后使用操作系统的close函数关闭打开的文件,那么这里的代码就如下:
void fclose_(FILE_*fp)
{
fflush_(fp);
close(fp->file_fd);
}
fwrite_函数模拟实现
最后就是fwrite_函数,这个函数就是往文件里面写入内容,我们先来看看这个函数的参数声明:
void fwrite_(const void*ptr,int num,FILE_*fp);
第一个参数ptr表明要往文件里面写入的字符串,第二个参数表明要写入文件的字符个数,第三个参数则是往哪个文件里面进行写入,将内容写入到文件之前得先将内容写入到文件的缓冲区里面,然后再根据刷新的策略来判断该如何将数据刷新到文件里面,那这里就先使用memcpy函数将字符串拷贝进缓冲区里面由于我们这里是简易的实现缓冲区,所以不考虑缓冲区溢出的情况,将数据拷贝进缓冲区之后我们就要判断缓冲区刷新的策略,那这里就得使用else if语句来分情况进行处理,那这里的代码就如下:
void fwrite_(const void* ptr,int num ,FILE_ *fp)
{
memcpy(fp->buffer+fp->size,ptr,num);
fp->size+=num;
if(fp->flags&SYNC_NOW)
{
}
else if(fp->flags&SYNC_FULL)
{
}
else if(fp->flags&SYNC_LINE)
{
}
else
{
}
}
对于满缓冲这里就直接调用write函数将buffer中的数据输出到文件里面然后将fp中的size置为0即可,那这里的代码就如下:
if(fp->flags&SYNC_NOW)
{
write(fp->file_fd,fp->buffer,fp->size);
fp->size=0;
}
对于满刷新策略就只用加个判断语句如果缓冲区的size等于capacity的话就调用write函数清空一下缓冲区,那么这咯的代码就如下:
else if(fp->flags&SYNC_FULL)
{
if(fp->size==fp->capacity)
{
write(fp->file_fd,fp->buffer,fp->size);
fp->size=0;
}
}
由于我们这里只是简单的实现我们就不考虑特殊的情况,我们这里假设传递过来的字符串只有结尾有出现\n的可能,这样的话对于行缓冲我们只用判断一下缓冲区的最后一个字符是否是\n即可,那这里完整的代码就如下:
void fwrite_(const void* ptr,int num ,FILE_ *fp)
{
memcpy(fp->buffer+fp->size,ptr,num);
fp->size+=num;
if(fp->flags&SYNC_NOW)
{
write(fp->file_fd,fp->buffer,fp->size);
fp->size=0;
}
else if(fp->flags&SYNC_FULL)
{
if(fp->size==fp->capacity)
{
write(fp->file_fd,fp->buffer,fp->size);
fp->size=0;
}
}
else if(fp->flags&SYNC_LINE)
{
if(fp->buffer[fp->size-1]=='\n')
{
到这里我们的缓冲区模拟实现就完成,接下来我们就测试一下上面写的代码是否符合我们的预期。
测试代码
这里我们就测试一下行刷新,测试的代码如下:
#include"mystdio.h"
int main()
{
FILE_* fp=fopen_("tmp","w");
fwrite_("abcdefghijklmn",15,fp);
fclose_(fp);
return 0;
}
这段代码的运行结果如下:
可以看到这里是正常运行的,因为字符串的末尾会自动添加\n,所以我们将测试的代码进行一下修改:
#include"mystdio.h"
int main()
{
const char* ptr="abcd";
FILE_* fp=fopen_("tmp","w");
fwrite_(ptr,4,fp);
sleep(10);
fwrite_(ptr,5,fp);
fclose_(fp);
return 0;
}
这段代码的运行结果如下:
一开始tmp文件里面没有任何数据,因为采用行缓冲的策略,等过了10秒再往文件里面输入内容,由于内容的末尾存在\n,所以文件里面就会出现内容:
那么这就是基本缓冲区的模拟实现,当然这些代码有很多的不足,但是本篇文章的目的是通过这些代码来让大家理解缓冲区的工作原理,并不是真正的实现。