目录
一.何为缓冲区
二. 缓冲区在哪
三. 模拟编码
一.何为缓冲区
缓冲区说白了就是一块内存区域,目的是为了提高使用者的效率以及减少C语言接口的使用频率~
下面我们用一则小故事来类比出缓冲区的功能~
张三为了给朋友李四庆祝生日快乐准备了份生日礼物~张三难道会横跨如此远的距离专门跑到新疆亲手给李四礼物吗?——这是不可能的~
这时候张三只需要下楼到顺丰站寄礼物就完事了,剩下只需要让顺丰帮张三把礼物送到位于新疆的顺丰区,然后张三再去取快递就可以了~
顺丰站就相当于我们的缓冲区,帮助我们存储数据再传递数据~而缓冲区呢有一个特点,它可以聚集数据一次性拷贝过去,提高整体的效率~
就像我们的生日礼物顺丰不可能单独一件一件发出去,肯定要等到一定量再一起发过去,这样效率才更高~
回到我们之前学习文件的时候,在描述文件的结构体中我们指出了里面包含了缓冲区
我们正常理解可能是这样的~C接口获取到内容数据后就会去调用系统调用接口,然后系统调用就会根据fb找到对应的文件结构体进而把数据写入到内核中的缓冲区~最后再由OS管理决定什么时候才刷新~
但这么频繁使用系统调用接口是有成本的,每获取一次数据都被进行一次C接口调用,系统接口调用会很麻烦~
这时候C接口也出现了缓冲区,而C接口函数也不会再通过频繁调用系统接口来传递数据,反而是把内容数据传递到缓冲区中,等到缓冲区满足一定的规则后再聚集数据一次性拷贝到系统接口中去,提高整体的效率~
在这个应用层中的缓冲区本质上就是用空间来换取时间效率~即减少了C接口的使用频率(去调用系统接口),也提高了使用者的效率(聚集拷贝数据)~
二. 缓冲区在哪
缓冲区就在名为FILE的结构体中,里面不仅仅封装了fd,还有缓冲区,并且是每一个文件都会有专属的缓冲区~
接下来我们就来证明一下~
#include <stdio.h> #include <unistd.h> #include <string.h> int main() { // 使用system call const char *s1 = "hello write\n"; write(1, s1, strlen(s1)); // 使用C语言接口 const char *s2 = "hello fprintf\n"; fprintf(stdout, "%s", s2); const char *s3 = "hello fwrite\n"; fwrite(s3, strlen(s3), 1, stdout); fork(); return 0; }
目前我们可以知道子进程肯定是不会去执行父进程代码的,因为fork在打印代码之后~
而由于父进程第一个是用系统调用接口,所以会直接走内核的缓冲区,OS会直接把该数据内容刷新出去写入磁盘中对应的文件~
而后面两个内容数据由于是C接口传递的应用层缓冲区,所以要遵守规则等待写满才可以刷新出去~
但是进程终究是要退出的,最后是通过进程退出来直接刷新出缓冲区~但恰恰是这里出了问题~
所以迷惑的点并不是少打印了一个write,而是多打印了后面两句语句~
三. 模拟编码
下面我们来模拟一下C标准库中的方法~
//filetest.c #include <stdio.h> #include <unistd.h> #include <string.h> #include "mystdio.h" const char* filename = "./log.txt"; int main() { myFILE* fp = my_fopen(filename,"w"); if(fp==NULL) return 1; const char* str = "hello\n"; my_fwrite(fp,str,strlen(str)); my_fclose(fp); return 0; }
//mystdio.c #include <string.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <stdlib.h> #include <unistd.h> #include "mystdio.h" //用我们模拟的接口去调用系统调用~ myFILE *my_fopen(const char *pathname, const char *mode) { //在调用系统接口open前我们需要先获取参数flag int flag = 0; if(strcmp(mode, "r") == 0) { flag |= O_RDONLY; } else if(strcmp(mode, "w") == 0) { flag |= (O_CREAT|O_WRONLY|O_TRUNC); } else if(strcmp(mode, "a") == 0) { flag |= (O_CREAT|O_WRONLY|O_APPEND); } else { return NULL; } int fd = 0; if(flag & O_WRONLY) { umask(0); fd = open(pathname, flag, 0666); } else { fd = open(pathname, flag); } if(fd < 0) return NULL; //my_fopen会得到一个myFILE*的指针 //所以我们在这里给该文件创建一个结构体 myFILE *fp = (myFILE*)malloc(sizeof(myFILE)); if(fp == NULL) return NULL; fp->fileno = fd; return fp; } int my_fwrite(myFILE *fp, const char *s, int size) { return write(fp->fileno,s,size); } void my_fclose(myFILE *fp) { close(fp->fileno); free(fp); }
//mystdio.h #pragma once #include <stdio.h> //#define SIZE 4096 typedef struct _myFILE { int fileno; }myFILE; myFILE *my_fopen(const char *pathname, const char *mode); int my_fwrite(myFILE *fp, const char *s, int size); void my_fclose(myFILE *fp);
这样我们就完成最基本的模拟了~可是这样会频繁去调用我们的系统接口~有没有减少该成本的办法呢?
当我们写入数据到文件调用my_fwrite的时候先不要着急调用系统接口,而是把数据拷贝到我们的缓冲区中~然后再等到缓冲区积累一定数据再统一拷贝给系统接口就可以很好提高效率了~
//mystdio.c #include <string.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <stdlib.h> #include <unistd.h> #include "mystdio.h" //用我们模拟的接口去调用系统调用~ myFILE *my_fopen(const char *pathname, const char *mode) { //在调用系统接口open前我们需要先获取参数flag int flag = 0; if(strcmp(mode, "r") == 0) { flag |= O_RDONLY; } else if(strcmp(mode, "w") == 0) { flag |= (O_CREAT|O_WRONLY|O_TRUNC); } else if(strcmp(mode, "a") == 0) { flag |= (O_CREAT|O_WRONLY|O_APPEND); } else { return NULL; } int fd = 0; if(flag & O_WRONLY) { umask(0); fd = open(pathname, flag, 0666); } else { fd = open(pathname, flag); } if(fd < 0) return NULL; //my_fopen会得到一个myFILE*的指针 //所以我们在这里给该文件创建一个结构体 myFILE *fp = (myFILE*)malloc(sizeof(myFILE)); if(fp == NULL) return NULL; fp->fileno = fd; fp->cap = SIZE; fp->pos = 0; //这里我们默认设置为行刷新 fp->flush_mode = LINE_FLUSH; return fp; } const char *toString(int flag) { if(flag & NONE_FLUSH) return "None"; else if(flag & LINE_FLUSH) return "Line"; else if(flag & FULL_FLUSH) return "FULL"; return "Unknow"; } //查看结构体基本属性 void DebugPrint(myFILE *fp) { printf("outbufer: %s\n", fp->outbuffer); printf("fd: %d\n", fp->fileno); printf("pos: %d\n", fp->pos); printf("cap: %d\n", fp->cap); printf("flush_mode: %s\n", toString(fp->flush_mode)); } void my_fflush(myFILE*fp) { if(fp->pos==0)return; //把缓冲区的内容交给系统接口 write(fp->fileno,fp->outbuffer,fp->pos); fp->pos = 0; } int my_fwrite(myFILE *fp, const char *s, int size) { //写入缓冲区中 memcpy(fp->outbuffer+fp->pos,s,size); fp->pos+=size; //判断是否需要刷新 if((fp->flush_mode&LINE_FLUSH)&&fp->outbuffer[fp->pos-1]=='\n') { my_fflush(fp); } if((fp->flush_mode&FULL_FLUSH)&&fp->outbuffer[fp->pos-1]=='\n') { my_fflush(fp); } return size; } void my_fclose(myFILE *fp) { //进程关闭前自动刷新缓冲区 my_fflush(fp); close(fp->fileno); free(fp); }
//mystdio.h #pragma once #include <stdio.h> #define SIZE 4096 #define NONE_FLUSH (1<<1) #define LINE_FLUSH (1<<2) #define FULL_FLUSH (1<<3) typedef struct _myFILE { //定义输入缓冲区 char inbuffer[SIZE]; //定义输出缓冲区 char outbuffer[SIZE]; //当前缓冲区写入位置 int pos; //缓冲区当前容量大小 int cap; //缓冲区刷新模式 int flush_mode; int fileno; }myFILE; myFILE *my_fopen(const char *pathname, const char *mode); int my_fwrite(myFILE *fp, const char *s, int size); void my_fclose(myFILE *fp); void my_fflush(myFILE *fp); void DebugPrint(myFILE *fp);
//filetest.c #include <stdio.h> #include <unistd.h> #include <string.h> #include "mystdio.h" const char* filename = "./log.txt"; int main() { myFILE *fp = my_fopen(filename, "w"); if(fp == NULL) return 1; int cnt = 5; char buffer[64]; while(cnt) { //每一次把字符串写入buffer中 snprintf(buffer, sizeof(buffer), "helloworld,hellobit,%d ", cnt--); //获取写入的数据 my_fwrite(fp, buffer, strlen(buffer)); //打印属性,查看每一次循环的情况 DebugPrint(fp); sleep(2); } //创建子进程,看是否会有拷贝两次的问题 fork(); //强制刷新缓冲区 my_fclose(fp); return 0; }
最终我们发现在模式为行刷新但没有\n的情况下我们在进程离开后成功让缓冲区刷新,并且符合父子进程之间的写时拷贝,让刷新出来的内容重复了两次~