[ Linux ] 缓冲区的理解 以及简易模拟实现封装C标

在输出重定向的时候为什么必须fflush(stdout)才能将内容刷新到指定文件呢?我们当时回答是因为存在缓冲区。那么本篇文章我们将重点了解认识一下缓冲区。

0.什么是缓冲区?

缓冲区的本质就是一段内存。 那么这段内存在哪里呢?我们接下来将会说明这个问题。

1.为什么要有缓冲区?

我们举个例子来理解这个概念:

假设你在北京大学上学,你的朋友在上海交通大学上学,你有10本书想给你的朋友,你打算怎么将这些书送给你的同学呢?

第一种方式:你自己带着10本书从北京到上海,亲自送给你的朋友。但是这种方式成本明显过于大,并且耽误你的时间。因此我们通常是采用第二种方式。

第二种方式:你在北京大学门口菜鸟驿站将10本书打包成快递发给你在上海交通大学的朋友。当你发送完快递后你就什么也不用管了,静静地等着你朋友收到快递的消息即可。

因此这个快递存在的最大价值是解放你的时间。这里快递存在意义等同于缓冲区的意义。

缓冲区的意义:

解放使用缓冲区的进程时间。缓冲区的存在可以集中处理数据刷新,减少IO的次数,从而达到提高整机的效率。2.缓冲区在哪里?

我们使用一段代码来理解

#include <stdio.h>#include <string.h>#include <fcntl.h>#include <unistd.h>#include <sys/types.h>#include <sys/stat.h>int main(){ printf(“hello printf”);// stdout -> 1 const char* msg = “hello write”; write(1,msg,strlen(msg)); sleep(5); return 0;}

printf内部封装了write,而printf不显示的原因是因为printf的内容在缓冲区内,当sleep时,内容存在在缓冲区内,当我们不带’\n’时,不会被理解刷新出来,数据被暂存在缓冲区内。

但是我们看到hello write被立马刷新,那么printf封装了write,那么这个缓冲区在哪里呢?

我们通过现象可以回答的是这个缓冲区一定不在write内。因此这个缓冲区只能是语言提供的(C语言)。因此这个缓冲区是一个语言级别的缓冲区。

那么我们来具体深挖一下缓冲区的位置.stdout的返回值是FILE,FILE内部有struct结构体,结构体内封装了很多的属性,其中包括上篇我们提到的文件描述符fd,除此之外还有该File对应的语言级别的缓冲区!

printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,

都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是 C,所以由C标准库提供

我们也可以一起看看FILE结构体

//在/usr/include/libio.hstruct _IO_FILE{ int _flags; /* High-order word is _IO_MAGIC; rest is flags. */#define _IO_file_flags _flags //缓冲区相关 /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char *_IO_read_ptr; /* Current read pointer */ char *_IO_read_end; /* End of get area. */ char *_IO_read_base; /* Start of putback+get area. */ char *_IO_write_base; /* Start of put area. */ char *_IO_write_ptr; /* Current put pointer. */ char *_IO_write_end; /* End of put area. */ char *_IO_buf_base; /* Start of reserve area. */ char *_IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; //封装的文件描述符#if 0 int _blksize;#else int _flags2;#endif _IO_off_t _old_offset; /* This used to be _offset but it’s too small. */#define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */ _IO_lock_t *_lock;#ifdef _IO_USE_OLD_IO_FILE};3.缓冲区的刷新策略3.1 刷新策略问题

刷新策略说白了就是什么时候刷新?

常规无缓冲(立即刷新)行缓冲(逐行刷新)显示器文件全缓冲(缓冲区写满再刷新) 块设备对应的文,磁盘文件特殊进程退出用户强制刷新(fflush)

4.奇怪的问题

结合上面的之后,下面的这段代码的执行结果是什么?

#include <stdio.h>#include <string.h>#include <fcntl.h>#include <unistd.h>#include <sys/types.h>#include <sys/stat.h>int main(){ const char *str1 = “hello printf\n”; const char *str2 = “hello fprintf\n”; const char *str3 = “hello fputs\n”; const char *str4 = “hello write\n”; //C库函数 printf(str1); fprintf(stdout,str2); fputs(str3,stdout); //系统接口 write(1,str4,strlen(str4)); //是调用完了上面的代码才执行的fork fork(); return 0;}

我们运行上述代码后,将结果重定向到log.txt内部,为什么会有7条消息?


答:当我们重定向后,本来要把显示在显示器的文件重定向到指定文件时,缓冲区的刷新策略由行缓冲(显示器文件)切换成了全缓冲(磁盘文件)。答案一定是和fork()有关系。我们可以这样理解,当str1,str2,str3把数据打印到文件里,此时已经重定向到log.txt,数据不会立即刷新,而变成了全缓冲,所以前三条信息暂存在了log.txt缓冲区内部,当我们调用fork()时,fork()要创建子进程,fork之后父子进程同时退出,退出之后父子进程就要刷新缓冲区了,而刷新的本质就是把缓冲区的数据写入到操作系统内部,并清空缓冲区。这里的缓冲区是自己的FILE内部维护的,属于父进程内部的数据区域,当我们刷新的时候,代码和数据要发生写时拷贝,因此这份代码父进程刷一份,子进程刷一份,因此我们就看到了有2个str1,2个str2,2个str3刷到了log.txt。

5.模拟实现一下自己封装C标准库

我们写的是样例代码不代表全部的标准的实现。从代码层面上理解一下原理

#include <stdio.h>#include <string.h>#include <stdlib.h>#include <assert.h>#include <unistd.h>#include <sys/stat.h>#include <sys/types.h>#include <fcntl.h>#define NUM 1024#define NONE_FLUSH 0x0#define LINE_FLUSH 0x1#define FULL_FLUSH 0x2typedef struct _MyFILE{ int _fileno; char _buffer[NUM]; int _end; int _flags;//fflush method}MyFILE;MyFILE *my_fopen(const char* filename,const char*method){ assert(filename); assert(method); int flags = O_RDONLY; if(strcmp(method,”r”) == 0) { } else if(strcmp(method,”r+”) == 0) {} else if(strcmp(method,”w”) == 0) { flags = O_WRONLY | O_CREAT |O_TRUNC; } else if(strcmp(method,”w+”) == 0) {} else if(strcmp(method,”a”) == 0) { flags = O_WRONLY | O_CREAT |O_APPEND; } else if(strcmp(method,”a+”) == 0) {} int fileno = open(filename,flags,0666); if(fileno < 0) { return NULL; } MyFILE *fp = (MyFILE*)malloc(sizeof(MyFILE)); if(fp == NULL ) return fp; memset(fp,0,sizeof(MyFILE)); fp->_fileno = fileno; fp->_flags |= LINE_FLUSH; fp->_end = 0; return fp;}void my_fflush(MyFILE* fp){ assert(fp); if(fp->_end > 0) { write(fp->_fileno,fp->_buffer,fp->_end); fp->_end =0; syncfs(fp->_fileno); }}void my_fwrite(MyFILE* fp,const char* start,int len){ assert(fp); assert(start); assert(len>0); // abcde->追加 strncpy(fp->_buffer+fp->_end,start,len);//将数据写入缓冲区 fp->_end += len; if(fp->_flags & NONE_FLUSH){} if(fp->_flags & LINE_FLUSH) { if(fp->_end > 0 && fp->_buffer[fp->_end-1] == ‘\n’) { write(fp->_fileno,fp->_buffer,fp->_end); fp->_end = 0; syncfs(fp->_fileno); } } if(fp->_flags & FULL_FLUSH){}}void my_fclose(MyFILE* fp){ my_fflush(fp); close(fp->_fileno); free(fp);}int main(){ MyFILE * fp = my_fopen(“log.txt”,”w”); if(fp == NULL) { printf(“my_fopen error\n”); return 1; } const char *msg = “hello my_file 11111111\n”; my_fwrite(fp,msg,strlen(msg)); printf(“hello my_file 11111111消息立即刷新\n”); sleep(3); const char *mssg = “hello 222222222”; my_fwrite(fp,mssg,strlen(mssg)); sleep(3); printf(“写入了一个不满足条件的字符串hello 222222222\n”); const char *msssg = “hello 33333333”; my_fwrite(fp,msssg,strlen(msssg)); sleep(3); printf(“写入了一个不满足条件的字符串hello 33333333\n”); const char *mssssg = “end\n”; my_fwrite(fp,mssssg,strlen(mssssg)); printf(“写了一个满足条件的字符串end\n”); sleep(3); const char *msssssg = “aaaaaaa”; my_fwrite(fp,msssssg,strlen(msssssg)); printf(“写了一个满足条件的字符串aaaaaaa\n”); sleep(1); my_fflush(fp); sleep(3); my_fclose(fp); return 0;}

我们也可以模拟进程退出

(本篇完)

鸟儿爱美,不仅需要羽毛之美,还需要鸣声婉转之美;

[ Linux ] 缓冲区的理解 以及简易模拟实现封装C标

相关文章:

你感兴趣的文章:

标签云: