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

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

目录

0.什么是缓冲区?

1.为什么要有缓冲区?

2.缓冲区在哪里?

3.缓冲区的刷新策略

3.1 刷新策略问题

常规

特殊

4.奇怪的问题

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


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;
}

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条消息?


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

我们也可以模拟进程退出

(本篇完)

  • 15
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白又菜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值