目录
头文件:
接下来就是设计这四个函数:Mystdio.c
重点讲一讲_fflush函数的底层实现原理:
所以数据内容的经过如下:
总结:
执行——测试写好的这4个函数:
运行结果:
修改测试代码:
运行结果:
回顾上文,我讲述了关于Linux文件系统中关于缓冲区的含义和理解,用一个特殊案例表明了我们所了解到的缓冲区是C语言库函数中特有的,而系统调用函数没有。 此外就是C库缓冲区的刷新策略,共有三种:立即刷新、行缓冲、全缓冲.....
Linux文件系统的缓冲区问题
那么接下来我就根据学到的缓冲区,通过在VIM编译器中自己简单手撕一下四个函数:fopen、fclose、fwrite、fflush函数的底层实现,因为这4个函数都是C库函数,都有缓冲区,那么我们就可以去深入了解一下这些函数到底是怎么做到将内容写入到指定文件中去的。
头文件:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<errno.h>
//定义缓冲区刷新策略
#define cash_now 1 //立即刷新
#define cash_line 2 //行缓冲
#define cash_all 4 //全缓冲
#define SIZE 1024
//重命名结构体
typedef struct _FILE{
int _fileno; //文件描述符
int _flag; //缓冲区刷新策略
int _cap; //缓冲区最大容量
int _size; //当前缓冲区的存在的字节数
char _buf[SIZE]; //缓冲区——数组
}_FILE;
//函数声明
_FILE* _fopen(const char* path,const char* mode);
void _fwrite(void* ptr,int num,_FILE* fp);
void _fclose(_FILE* fp);
void _fflush(_FILE* fp);
因为缓冲区的位置在FILE结构体的内部,我们手撕的话,需要自己创建一个类似FILE的结构体,在该结构体中上面5个是必要的成员变量,_fileno是用于使用系统调用函数open的返回值,_flag可以自己设置缓冲区的刷新策略:cash_now、cash_line、cash_all;_cap和_size就是用于缓冲区的容量和当前值,而最重要的就是_buf缓冲区了。我们写的内容全都要靠它进行传递。
然后我来介绍一下我们要手撕的C库函数:
1.fopen函数用于打开文件,对其进行读/写。
2.fwrite函数用于将写好的内容送入缓冲区中。
3.fflush函数用于强制刷新缓冲区的内容到指定流中。
4.fclose函数用于关闭文件,最终清理缓冲区残留内容。
接下来就是设计这四个函数:Mystdio.c
#include"Mystdio.h"
#include<string.h>
_FILE* _fopen(const char* path,const char* mode){
int flags=0;
if(strcmp(mode,"r")==0){
flags=flags |O_RDONLY;
}
else if(strcmp(mode,"w")==0){
flags=flags |O_WRONLY |O_CREAT |O_TRUNC;
}
else if(strcmp(mode,"a")==0){
flags=flags | O_WRONLY |O_CREAT |O_APPEND;
}
else{
//剩余的r+,w+,....
}
int power=0666; //设置文件权限
int fd=0; //文件描述符
if(flags& O_RDONLY){ //读文件的文件描述符
fd=open(path,flags);
}
else{ //写文件的文件描述符
fd=open(path,flags,power);
}
//判断文件是否被打开
//情况1:打开文件失败
if(fd<0){
const char* str=strerror(errno);
write(2,str,strlen(str)); //将失败原因写到缓冲区中
return NULL; //返回空
}
//情况2:打开文件成功
else{
_FILE* fp=(_FILE*)malloc(sizeof(_FILE));
assert(fp); //断言fp一定申请结构体对象成功
//为结构体对象赋初值
fp->_cap=SIZE;
fp->_size=0;
fp->_fileno=fd;
fp->_flag=cash_line;
memset(fp->_buf,0,SIZE); //初始化数组——缓冲区
return fp; //函数返回值
}
}
//强制刷新缓冲区函数
void _fflush(_FILE* fp){
if(fp->_size>0){ //表明当前缓冲区中还有内容
//将缓冲区的内容写入文件中
write(fp->_fileno,fp->_buf,fp->_size);
fp->_size=0;
}
//重点:
syncfs(fp->_fileno);
}
//写入文件函数
void _fwrite(void* ptr,int num,_FILE* fp){
memcpy(fp->_buf+fp->_size,ptr,num);
fp->_size+=num;
//判断缓冲区的刷新策略
if(fp->_flag==cash_now){ //立即刷新—— //将缓冲区的内容写入文件中
write(fp->_fileno,fp->_buf,fp->_size);
fp->_size=0;
}
else if(fp->_flag==cash_line){ //行缓冲——需要遇到'\n'才会刷新
if(fp->_buf[fp->_size-1]=='\n'){
//将缓冲区的内容写入文件中
write(fp->_fileno,fp->_buf,fp->_size);
fp->_size=0;
}
}
else if(fp->_flag==cash_all){ //全缓冲——需要写满缓冲区才会刷新
if(fp->_size==fp->_cap){
//将缓冲区的内容写入文件中
write(fp->_fileno,fp->_buf,fp->_size);
fp->_size=0;
}
}
else{
//...
}
}
//关闭文件函数
void _fclose(_FILE* fp){
_fflush(fp);
close(fp->_fileno);
}
由上面各个函数的底层实现可知:fopen、fclose、fwrite函数的底层实现是都由open、close、wirte等系统调用函数封装和优化才形成的。
重点讲一讲_fflush函数的底层实现原理:
我们说的缓冲区是C语言库中给我们提供的缓冲区,也就是说我们使用fprintf、fput、fwrite函数写的内容是会先写到C语言中的缓冲区,然后根据fopen函数中的FILE*类型指针的文件描述符经过OS操作系统将缓冲区内容刷新并且送往磁盘文件中。
而在送往磁盘文件的过程中,C语言的缓冲区被刷新并不是直接送到文件中,它会经过内核缓冲区,没听错!OS中也有自己的缓冲区,只不过这个缓冲区我们根本不会用到,内核缓冲区的作用就是将各个进程对文件进行读写的内容暂时集中起来放在这里,相当于一个大型仓库,也是用于数据的传递。内核缓冲区的刷新策略与之前说的C语言给我们提供的缓冲区刷新策略不同,不能用C缓冲区的刷新方式去看待内核缓冲区。
所以我们在使用fpinrtf、fputs、fwrite函数将想要写入的内容传入文件时,它们会先被放到C缓冲区中,然后执行fflush(stdout);语句:
fflush:标准IO函数(如fread,fwrite等)会在内存中建立缓冲,该函数刷新内存缓冲,将内容写入内核缓冲,要想将其真正写入磁盘,还需要调用fsync。(即需要先调用fflush,然后再调用fsync,否则不会起作用)。
强制刷新C缓冲区内容,也只是刷新到了内核缓冲区中。等达到它的刷新要求或者采用fsync()函数后,这些数据才会被真正写入磁盘文件中。
所以数据内容的经过如下:
总结:
fflush:强制刷新C库缓冲区内容到系统的内核缓冲区。
fsync:强制刷新内核缓冲区的数据到指定的文件流中。
Linux对10文件的操作分为:
1.不带缓存:open read,write等。posix系统标准,在用户空间没有缓冲,在内核空间还是进行了缓存的。数据----->内核缓存区---->磁盘.
假设内核缓存区长度为100字节且需要写满后才会刷新。当我们调用 ssize t write (int fd,const void * buf,size tcount);写操作时,设每次写入count=10字节,那么我们要调用10次这个函数才能把这个缓存区写满,没写满时数据还是在内核缓冲区中,并没有写入到磁盘中,内核缓存区满了之后或者执行了fsync (强制写入硬盘) 之后,才进行实际的I/O操作,把数据写入文件上。
2.带缓存区: fopen fwrite fputs、fread等,是c标准库中定义的。数据----->C库缓存区----->内核缓存区---->磁盘。
假设C库缓存区长度为50字节,内核缓存区100字节,我们用标准C库函数fwrite(); 将数据写入到这个C缓存区中,每次写10字节,需要写5次到C缓冲区才会满或者直接调用 fflush( ) ),才将数据写到内核存区, 直到内核缓存区满了之后或者执行了fsync();之后,才进行实际的I/O操作,将内核缓冲区的数据写入磁盘上。
执行——测试写好的这4个函数:
#include<stdio.h>
#include"Mystdio.h"
#include<string.h>
int main(){
_FILE* fp= _fopen("log.txt","w");
if(fp==NULL){
perror("open file");
return -1;
}
char* str="一骑红尘妃子笑\n";
int cnt=10;
while(1){
_fwrite(str,strlen(str),fp);
sleep(1);
printf("cnt:%d\n",cnt--);
if(cnt%3==0){
_fflush(fp);
}
if(cnt==0){
break;
}
}
_fclose(fp);
return 0;
}
在测试案例中,我通过循环的方式,使用自己写的_fwrite函数将字符串写入文件中,因为使用了\n字符,所以该情况为行缓冲刷新策略,每写入缓冲区一行字符串,就立即刷新到文件中去。
运行结果:
修改测试代码:
将字符的\n字符去掉,再次汇编链接生成可执行文件.
运行结果:
该结果为全缓冲的刷新策略,去掉了\n字符后,字符串会被一直留在缓冲区中,除非是调用_fclose函数或者缓冲为满,才会被刷新到文件中去。