7

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

 1 year ago
source link: https://blog.51cto.com/xingyuli/5841940
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

在输出重定向的时候为什么必须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. 用户强制刷新(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标准库_缓冲区_02

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

我们也可以模拟进程退出

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

(本篇完)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK