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

时间:2022-11-10 20:02:10

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

0.什么是缓冲区?

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

1.为什么要有缓冲区?

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

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

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

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

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

缓冲区的意义:

  1. 解放使用缓冲区的进程时间。
  2. 缓冲区的存在可以集中处理数据刷新,减少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;
}

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

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.h
struct _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 刷新策略问题

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

常规

  1. 无缓冲(立即刷新)
  2. 行缓冲(逐行刷新)显示器文件
  3. 全缓冲(缓冲区写满再刷新) 块设备对应的文,磁盘文件

特殊

  1. 进程退出
  2. 用户强制刷新(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条消息?

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


答:当我们重定向后,本来要把显示在显示器的文件重定向到指定文件时,缓冲区的刷新策略由行缓冲(显示器文件)切换成了全缓冲(磁盘文件)。答案一定是和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 0x2

typedef 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标准库


我们也可以模拟进程退出

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

(本篇完)