Linux系统编程:缓冲区的概念和工作原理

目录

一. 什么是缓冲区

1.1 缓冲区的概念

1.2 缓冲区存在的价值

二. 缓冲区的刷新策略

三. 缓冲区的提供者和所在的位置

3.1 代码测试缓冲区的提供者

3.2 缓冲区的位置和工作原理

四. 缓冲区的简单模拟实现

五. 总结 


一. 什么是缓冲区

1.1 缓冲区的概念

缓冲区,就是一段存储空间。当进程要向外部设备中写数据,并不是直接将数据写入到外部设备,而是会先将待读写的数据写到缓冲区,当缓冲区的数据积累到一定量时,再集中将缓冲区中的数据写到外部设备,这样就可以提高IO效率。

进程从外部设备中读数据时,同样也存在一段输入缓冲区。

图1.1 缓冲区

1.2 缓冲区存在的价值

当进程执行将数据写到外部设备的操作时,有两种IO模式:

  1. 写透模式(WT):直接将数据写到外部设备
  2. 写回模式(WB):先将数据写到缓冲区,当缓冲区的数据达到一定量时,再集中写到外部设备

不同硬件设备的访问速度遵循的这样的规律:CPU > 内存 > 外部设备(磁盘、显示器、网卡等),这样就造成了如果采用写透模式,进程会频繁多次的访问外部设备,降低效率。

这里通过收发快递的例子来类比缓冲区,我们要寄快递并不是直接将快递送到收件人手上,而是将快递先寄存到菜鸟驿站,当菜鸟驿站中的待发送快递达到一定量时,再集中发送。我们寄出去的快递就好比要进行读写的数据,菜鸟驿站就好比缓冲区。相对于直接将快递送到收件人手上,菜鸟驿站将一批快递集中发送,效率会大幅提高。缓冲区的存在,会很大程度上提高整机效率,最重要的是提高用户响应速度。

或许有人会问,无论是采用写透模式,还是写回模式,进程向外部设备中写数据的数据量是一样的,为什么有缓冲区效率就变高了呢?注意:当进程与外部设备进行IO操作时,数据量并不是主要问题所在,准备IO的过程,是最消耗时间的,缓冲区的存在在数据量不变的条件下,减少了访问外部设备的次数,这样就提高了整机效率。

二. 缓冲区的刷新策略

缓冲区的刷新策略,分为以下三种:

  1. 立即刷新
  2. 行刷新(行缓冲):遇到'\n'就刷新缓冲区
  3. 满刷新(全缓冲):当缓冲区存满数据,无法容纳新数据时,就刷新缓冲区。

当然,缓冲区的刷新也存在两种特殊情况,需要立即刷新:

  1. 用户强制刷新:fflush
  2. 进程终止

一般而言,显示器会采用行缓冲策略,其他的设备基本都采用全缓冲策略。

为了减少进程与外部设备的IO次数,提高效率,所有设备都倾向于采用全缓冲。但是,显示器相对特殊,它不仅要提高效率,还要兼顾用户体验,因此采用行缓冲。

三. 缓冲区的提供者和所在的位置

3.1 代码测试缓冲区的提供者

我们猜测,缓冲区的提供者有两种可能:C/C++标准库、操作系统

通过图3.1的代码进行测试获取结论,在代码中先后调用C语言的IO函数printf、fprintf、fputs以及系统接口write向标准输出中打印信息,所有输出的信息都已'\n'结尾,在代码末尾通过fork创建子进程。代码的运行结果见图3.1的右侧,如果直接./test.exe执行代码,那么所有要输出的信息都在屏幕上输出了一次,而如果将进程运行结果输出重定向到log.txt文件中,C语言IO接口printf、fprintf、fputs打印的内容被输出了两次,而系统接口write打印的内容依旧只被输出了一次。

如果缓冲区由OS提供,那么就不应当出现输出重定向到log.txt时,C接口内容输出两次而系统接口内容输出一次的现象。

因此,我们可以断定:缓冲区由C/C++标准库提供。

至于为什么不进行输出重定向时无论C语言IO函数还是系统接口内容都输出一次,则是由于向显示器输出数据时采用行刷新策略,执行fork操作时,缓冲区已经没有了内容。

图3.1 代码测试缓冲区提供者

针对运行图3.1所示代码的允许结果,做出下面的总结:

  • 如果向显示器中输出数据,那么遵循行刷新的策略,当函数执行到fork时,缓冲区内的数据已经全部被刷新了,因此子进程不会再向外部设备输出数据。
  • 如果将输出重定向到磁盘文件,那么就遵循满刷新的规则,函数执行的fork时,即使C语言IO函数已经执行了,也只是将数据写到了缓冲区中,并没有真正输出到外部设备。缓冲区中的数据也属于父进程数据,子进程在运行过程中要对父进程数据进行写时拷贝以保证进程的独立性,那么父进程缓冲区的数据会被拷贝一份给子进程缓冲区,父子进程都会执行将缓冲区数据刷新到外部设备的操作,而缓冲区由C标准库提供,因此C语言IO函数的内容会两次输出到外部设备,而系统接口只输出一次到外部设备。

注意:上面提到的只是C/C++标准库提供的用户级缓冲区,实际上,OS也会提供内核级缓冲区。

3.2 缓冲区的位置和工作原理

通过对语言的学习,我们知道C语言提供了FILE类,用于描述文件的属性信息,进程所打开的每个文件,都有一个FILE*指针与之对应,指向描述这个文件属性信息的结构体。

在FILE结构体中,会存储文件描述符fd,也会有指向该文件对应的缓冲区的指针,FILE的定义见代码3.2,可见C会为每个打开的文件都申请缓冲区,用于该文件的读写。

结论:缓冲区就是一段内存空间,每一个被进程打开的文件都有与之对应的缓冲区。

代码3.2:FILE结构体的定义 

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

我们可以据此推断,当调用C/C++的IO函数向外部设备(磁盘文件)输出数据时,数据会被先写入目标磁盘文件的缓冲区,当缓冲区的数据积累到了一定的量时,再调用系统接口write,将缓冲区的数据写操作系统的内核缓冲区,在合适的时间,内核缓冲区的数据就会被刷新到外部设备,具体流程参考图3.2。这样相比于每次调用C/C++的IO函数都直接使用系统接口write,效率会有所提高。

从外部设备读取数据时,C/C++标准库提供的缓冲区的工作原理与向外部设备写数据时类似。

图3.2 缓冲区的工作原理

四. 缓冲区的简单模拟实现

只需要在自定义的struct myFILE中定义一段缓冲区,在写文件时先将内容写入到缓冲区中,在缓冲区满、文件关闭或用户强制刷新缓冲区时,调用系统write函数刷新缓冲区,即可模拟实现简单的缓冲区。

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/fcntl.h>

#define NUM 10

struct myFile
{
    int fd;       //文件描述符
    char buffer[NUM]; //缓冲区 
    int end;
};

typedef struct myFile MyFILE;

MyFILE* fopen_(const char* path, const char* mode)
{
    assert(path);
    assert(mode);

    MyFILE* ret = NULL;
    
    int fd = -1;
    if(strcmp(mode, "w") == 0)
    {
        //只写、清空、没有就创建文件
        fd = open(path, O_WRONLY|O_CREAT|O_TRUNC, 0666);
    }
    else if(strcmp(mode, "r") == 0)
    {
        fd = open(path, O_RDONLY);  //只写打开
    }
    else if(strcmp(mode, "a") == 0)
    {
        fd = open(path, O_WRONLY|O_CREAT|O_APPEND, 0666);
    }
    else if(strcmp(mode, "w+") == 0)
    {
        fd = open(path, O_RDWR|O_CREAT|O_TRUNC, 0666);
    }
    else if(strcmp(mode, "r+") == 0)
    {
        fd = open(path, O_RDWR);
    }
    else if(strcmp(mode, "a+") == 0)
    {
        fd = open(path, O_RDWR|O_CREAT|O_APPEND, 0666);
    }
    else
    {
        printf("mode error!\n");
    }


    if(fd >= 0)                                                                                                      
    {
        ret = (MyFILE*)malloc(sizeof(MyFILE));
        memset(ret, 0, sizeof(MyFILE));
        ret->fd = fd;
    }

    return ret;
}

//缓冲区刷新函数
void fflush_(MyFILE* pf)
{
    size_t n = strlen(pf->buffer);
    //fprintf(stdout, "%s\n", pf->buffer);
    write(pf->fd, pf->buffer, n);
    memset(pf->buffer, 0, NUM);
    pf->end = 0;
}

//文件关闭函数
void close_(MyFILE* pf)
{
    fflush_(pf);
    close(pf->fd);
    free(pf);
}

void fputs_(const char* s, MyFILE* pf)
{
    int len = (int)strlen(s);   //要写入的字符数
    while(pf->end + len >= NUM)
    {
        int n = NUM - pf->end - 1;   //实际可以读取的字符数
        strncpy(pf->buffer + pf->end, s, n);  //字符数拷贝到缓冲区
        //fprintf(stdout, "%s\n", pf->buffer);
        fflush_(pf);   //缓冲区满,强制清理
        sleep(1);
        len -= n;   //还没有拷贝的字符数
        s += n;
    }


    strcpy(pf->buffer, s);
    pf->end = len;

    if(pf->end != 0 && (pf->fd == 1 || pf->fd == 2))
    {
        //标准输出和标准错误(显示屏)采用行刷新策略
        if(s[strlen(s) - 1] == '\n')
        {
            fflush_(pf);
        }
    }
}

int main()
{
    MyFILE* pf = fopen_("log.txt", "w");
    dup2(pf->fd, 2);

    //int fd = open("log2.txt", O_RDONLY);
    //printf("%d\n", fd);

    const char* s1 = "hello world, hello linux, hello everyone\n";
    fputs_(s1, pf);

    const char* s2 = "zhangHHHHHHHHHH\n";
    fputs_(s2, pf);

    close_(pf);
    return 0;
}

五. 总结 

  • 缓冲区是一段存储空间,用于充当进程和外部数据IO操作时的中间介质,以协调外部设备、内存和CPU访问速度的差异,提高整机效率。
  • 缓冲区的刷新策略分为立即刷新、行刷新和满刷新。所有设备都倾向于满刷新,但显示器为了用户体验采用行刷新。
  • 用户级别的缓冲区由C/C++标准库提供,每个打开的文件都有一块与之对应的缓冲区,FILE结构体中存有指向缓冲区的指针。当进程调用C/C++库函数与外部设备进行IO操作时,数据会被先存入缓冲区,然后再调用系统接口write/read进行读写。
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值