前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【Linux修炼】13.缓冲区

【Linux修炼】13.缓冲区

作者头像
每天都要进步呀
发布2023-03-28 12:27:00
1.8K0
发布2023-03-28 12:27:00
举报
文章被收录于专栏:C++/Linux

缓冲区的理解

一. C接口打印两次的现象

代码语言:javascript
复制
#include<stdio.h>  
#include<string.h>  
#include<unistd.h>
int main()                              
{                                       
    //C接口                             
    printf("hello printf");              
    fprintf(stdout, "hello fprintf\n");         
    const char* fputsString = "hello fputs\n";  
    fputs(fputsString, stdout);         

    //系统接口                              
    const char* wstring = "hello write\n";  
    write(1, wstring, strlen(wstring));  
    return 0;                                                                  
}            

先看看这段代码的结果:

image-20230106120309824
image-20230106120309824

当添加一个fork()后:

代码语言:javascript
复制
#include<stdio.h>  
#include<string.h>  
#include<unistd.h>
int main()                              
{                                       
    //C接口                             
    printf("hello printf");              
    fprintf(stdout, "hello fprintf\n");         
    const char* fputsString = "hello fputs\n";  
    fputs(fputsString, stdout);         

    //系统接口                              
    const char* wstring = "hello write\n";  
    write(1, wstring, strlen(wstring));  
    // 代码结束之前,进行创建子进程
    fork();
    return 0;                                                                  
}            
image-20230106121511060
image-20230106121511060

直接运行仍是正常的现象,但当重定向到log.txt中,C接口的打印了两次,这是什么原因呢?带着疑问继续探讨:

二. 理解缓冲区问题

  • 缓冲区本质就是一段内存

那么既然有了本质前提,那么就有这几个方面要思考:

  1. 缓冲区是谁申请的?
  2. 缓冲区属于谁?
  3. 为什么要有缓冲区?

为什么要有缓冲区

下面举个场景:

image-20230109144936010
image-20230109144936010

张三和李四是好朋友,一天张三想给李四一个包裹,但是张三在四川,李四在北京,如果张三亲自去送包裹,实际上会占用张三大量的时间,而且也不现实,所以为了不占用张三自己的时间,就把包裹送到快递公司让其送到李四那里。

image-20230109150054484
image-20230109150054484

现实生活中,快递行业的意义就是节省发送者的时间,而对于这个例子来说,四川就相当于内存,发送者张三相当于进程,包裹就是进程需要发送的数据,北京就相当于磁盘,李四就是磁盘上的文件,那么可以看成这样:

image-20230109150623814
image-20230109150623814

在冯诺依曼体系中,我们知道内存直接访问磁盘这些外设的速度是相对较慢的,即正如我们所举的例子一样,张三亲自送包裹会占用张三大量的时间,因此顺丰同样属于内存中开辟的一段空间,将我们在内存中已有的数据拷贝到这段空间中,拷贝函数就直接返回了,即张三接收到顺丰的通知就离开了。在执行你的代码期间,顺丰对应的内存空间的数据也就是包裹就会不断的发送给对方,即发送给磁盘。而这个过程中,顺丰这块开辟的空间就相当于缓冲区。

那么缓冲区的意义是什么呢?——节省进程进行数据IO的时间。这也就回答了第三个问题为什么要有缓冲区。

  • 在上述的过程中,拷贝是什么,我们在fwrite的时候没有拷贝啊?因此我们需要重新理解fwrite这个函数,与其理解fwrite是写入到文件的函数,倒不如理解fwrite是拷贝函数,将数据从进程拷贝到“缓冲区”或者外设中!

那我们送的包裹何时会发送出去呢?即我们的数据什么时候会到磁盘中呢?这就涉及到缓冲区刷新策略的问题:

缓冲区刷新策略的问题

上述我们提到,张三的包裹送到了顺丰,但是当张三再次来到顺丰邮寄另一个包裹时,发现之前的包裹还在那里放着,毫无疑问,张三会去找工作人员理论:为什么这么长时间还没有发?而工作人员这时也解释:我们的快递是通过飞机运的,如果只送你这一件包裹,路费都不够!因此可以看出,快递不是即送即发,也就是说数据不是直接次写入外设的。

那么如果有一块数据A,一次写入到外设,还有一块数据B多次少批量写入外设,A和B谁效率最高呢?

一定是A最高。一块数据写入到外设,需要外设准备,如果多次写入外设,每一次外设进行的准备都会占用时间,而积攒到一定程度一次发送到外设,外设的准备次数就会大幅减少,效率也会提高。因此,为了在不同设备的效率都是最合适的,缓冲区一定会结合具体的设备,定制自己的刷新策略:

  1. 立即刷新,无缓冲
  2. 行刷新,行缓冲(显示器)\n就会刷新,比如_exit和exit
  3. 缓冲区满 全缓冲 (磁盘文件)

当然还有两种特殊情况

  1. 用户强制刷新:fflush
  2. 进程退出 ——>进程退出都要进行缓冲区刷新

所说的缓冲区在哪里?指的是什么缓冲区?

文章开始时我们提到了C语言接口打印两次的现象,毫无疑问,我们能够从中获得以下信息:

  1. 这种现象一定和缓冲区有关
  2. 缓冲区一定不在内核中(如果在内核中,write也应该打印两次)

因此我们之前谈论的所有的缓冲区,都指的是用户级语言层面给我们提供的缓冲区。这个缓冲区在stdout,stdin,stderr->FILE* ,FILE作为结构体,其不仅包括fd,缓冲区也在这个结构体中。所以我们自己要强制刷新的时候,fflush传入的一定是文件指针,fclose也是如此,即:fflush(文件指针),fclose(文件指针)

通过查看:vim /usr/include/libio.h

image-20230109165050557
image-20230109165050557

因此我们所调用的fscanf,fprintf,fclose等C语言的文件函数,传入文件指针时,都会把相应的数据拷贝到文件指针指向的文件结构体中的缓冲区中。

即缓冲区也可以看做是一块内存,对于内存的申请:无非就是malloc new出来的。

因此在这里我们也就能回答最初的三个问题:

  1. 缓冲区是谁申请的?用户(底层通过malloc/new)
  2. 缓冲区属于谁?属于FILE结构体
  3. 为什么要有缓冲区?节省进程进行IO的时间

三. 解释打印两次的现象

有了缓冲区的理解,现在就足以解释打印两次的现象:

由于代码结束之前,进行创建子进程:

  1. 如果我们不进行重定向,看到四条消息 stdout默认使用的是行刷新,在进程进行fork之前,三条C函数已经将数据进行打印输出到显示器上(外设),也就是说FILE内部的缓冲区不存在对应的数据。
  2. 如果进行了重定向>,写入的就不是显示器而是普通文件,采用的刷新策略是全缓冲,之前的三条C显示函数,虽然带了\n,但是不足以将stdout缓冲区写满!数据并没有被刷新,而在fork的时候,stdout属于父进程,创建子进程时,紧接着就是进程退出!无论谁先退出,都一定会进行缓冲区的刷新(就是修改缓冲区)一旦修改,由于进程具有独立性,因此会发生写时拷贝,因此数据最终会打印两份。
  3. write函数为什么没有呢?因为上述的过程都与write无关,write没有FILE,用的是fd,没有C对应的缓冲区。

因此如上就是对于现象的解释。

四. 模拟实现

所以呢?缓冲区应该如何理解呢?和OS有什么关系呢?下面就通过写一个demo实现一下行刷新:touch myStdio.h;touch myStdio.c;touchmain.c

myStdio.h

代码语言:javascript
复制
#pragma once
#include<stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define SIZE 1024
#define SYNC_NOW    1
#define SYNC_LINE   2
#define SYNC_FULL   4

typedef struct _FILE{
    int flags; //刷新方式
    int fileno;
    int cap; //buffer的总容量
    int size; //buffer当前的使用量
    char buffer[SIZE];
}FILE_;


FILE_ *fopen_(const char *path_name, const char *mode);
void fwrite_(const void *ptr, int num, FILE_ *fp);
void fclose_(FILE_ * fp);
void fflush_(FILE_ *fp);

myStdio.c

代码语言:javascript
复制
#include "myStdio.h"

FILE_ *fopen_(const char *path_name, const char *mode)
{
    int flags = 0;
    int defaultMode=0666;

    if(strcmp(mode, "r") == 0)
    {
        flags |= O_RDONLY;
    }
    else if(strcmp(mode, "w") == 0)
    {
        flags |= (O_WRONLY | O_CREAT |O_TRUNC);
    }
    else if(strcmp(mode, "a") == 0)
    {
        flags |= (O_WRONLY | O_CREAT |O_APPEND);
    }
    else
    {
        //TODO
    }
    int fd = 0;

    if(flags & O_RDONLY) fd = open(path_name, flags);
    else fd = open(path_name, flags, defaultMode);
    if(fd < 0)
    {
        const char *err = strerror(errno);
        write(2, err, strlen(err));
        return NULL; // 为什么打开文件失败会返回NULL
    }
    FILE_ *fp = (FILE_*)malloc(sizeof(FILE_));
    assert(fp);

    fp->flags = SYNC_LINE; //默认设置成为行刷新
    fp->fileno = fd;
    fp->cap = SIZE;
    fp->size = 0;
    memset(fp->buffer, 0 , SIZE);

    return fp; // 为什么你们打开一个文件,就会返回一个FILE *指针
}

void fwrite_(const void *ptr, int num, FILE_ *fp)
{
    // 1. 写入到缓冲区中
    memcpy(fp->buffer+fp->size, ptr, num); //这里我们不考虑缓冲区溢出的问题
    fp->size += num;

    // 2. 判断是否刷新
    if(fp->flags & SYNC_NOW)
    {
        write(fp->fileno, fp->buffer, fp->size);
        fp->size = 0; //清空缓冲区
    }
    else if(fp->flags & SYNC_FULL)
    {
        if(fp->size == fp->cap)
        {
            write(fp->fileno, fp->buffer, fp->size);
            fp->size = 0;
        }
    }
    else if(fp->flags & SYNC_LINE)
    {
        if(fp->buffer[fp->size-1] == '\n') // abcd\nefg , 不考虑
        {
            write(fp->fileno, fp->buffer, fp->size);
            fp->size = 0;
        }
    }
    else{

    }
}

void fflush_(FILE_ *fp)
{
    if( fp->size > 0) write(fp->fileno, fp->buffer, fp->size);
    fsync(fp->fileno); //将数据,强制要求OS进行外设刷新!
    fp->size = 0;
}

void fclose_(FILE_ * fp)
{
    fflush_(fp);
    close(fp->fileno);
}

main.c

代码语言:javascript
复制
#include "myStdio.h"

int main()
{
    FILE_ *fp = fopen_("./log.txt", "w");
    if(fp == NULL)
    {
        return 1;
    }
    int cnt = 10;
    const char *msg = "hello bit ";
    while(1)
    {
        fwrite_(msg, strlen(msg), fp);
        fflush_(fp);
        sleep(1);
        printf("count: %d\n", cnt);
        //if(cnt == 5) fflush_(fp);
        cnt--;
        if(cnt == 0) break;
    }
    fclose_(fp);

    return 0;
}
image-20230110004308970
image-20230110004308970

五. 缓冲区与OS的关系

image-20230110215110787
image-20230110215110787

我们所写入到磁盘的数据hello bit是按照行刷新进行写入的,但并不是直接写入到磁盘中,而是先写到操作系统内的文件所对应的缓冲区里,对于操作系统中的file结构体,除了一些接口之外还有一段内核缓冲区,而我们的数据则通过file结构体与文件描述符对应,再写到内核缓冲区里面,最后由操作系统刷新到磁盘中,而刷新的这个过程是由操作系统自主决定的,而不是我们刚才所讨论的一些行缓冲、全缓冲、无缓冲……,因为我们提到的这些缓冲是在应用层C语言基础之上FILE结构体的刷新策略,而对于操作系统自主刷新策略则比我们提到的策略复杂的多(涉及到内存管理),因为操作系统需要考虑自己的存储情况而定,因此数据从操作系统写到外设的过程和用户毫无关系。

所以一段数据被写到硬件上(外设)需要进行这么长的周期:首先通过用户写入的数据进入到FILE对应的缓冲区,这是用户语言层面的,然后通过我们提到的刷新的策略刷新到由操作系统中struct file*的文件描述符引导写到操作系统中的内核缓冲区,最后通过操作系统自主决定的刷新策略写入到外设中。如果OS宕机了,那么数据就有可能出现丢失,因此如果我们想及时的将数据刷新到外设,就需要一些其他的接口强制让OS刷新到外设,即一个新的接口:int fsync(int fd),调用这个函数之后就可以立即将内核缓冲区的数据刷新到外设中,就比如我们常用的快捷键:ctrl + s

总结:

因此以上我们所提到的缓冲区有两种:用户缓冲区和内核缓冲区,用户缓冲区就是语言级别的缓冲区,对于C语言来说,用户缓冲区就在FILE结构体中,其他的语言也类似;而内核缓冲区属于操作系统层面,他的刷新策略是按照OS的实际情况进行刷新的,与用户层面无关。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-01-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 缓冲区的理解
  • 一. C接口打印两次的现象
  • 二. 理解缓冲区问题
    • 为什么要有缓冲区
      • 缓冲区刷新策略的问题
        • 所说的缓冲区在哪里?指的是什么缓冲区?
        • 三. 解释打印两次的现象
        • 四. 模拟实现
        • 五. 缓冲区与OS的关系
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档