【Linux】深入理解缓冲区

目录

什么是缓冲区

为什么要有缓冲区

缓冲区刷新策略

缓冲区在哪里

 手动设计一个用户层缓冲区


什么是缓冲区

缓冲区本质上一块内存区域,用来保存临时数据。缓冲区在各种计算任务中都广泛应用,包括输入/输出操作、网络通信、图像处理、音频处理等。

这块内存区域是由谁提供的呢,缓冲区在哪里呢?可以继续向下看.

这里先告诉答案,是C标准库提供的.

为什么要有缓冲区

缓冲区用于解决数据传输速度不匹配或不稳定的问题,并提高数据处理的效率。

当从硬盘读取大量数据时,将数据直接传输到内存中可能会导致读写速度不匹配(内存速度快,而硬盘读取速度慢,这是相对来说的),从而导致性能瓶颈。为了缓解这个问题,可以引入一个缓冲区,先将一部分数据读取到缓冲区中,然后再从缓冲区逐步读取数据到内存中,以平衡数据传输速度。

这里有个很合适的例子来解释:

例如你和你的朋友在两个不同的大学,相差大概500公里,有一天你想送一些书给你的朋友,此时你可以选择骑自行车,亲自骑行去送这些书,礼轻情意重嘛,加上中途休息,然后由于速度慢,花了大概一周的时间才到,送了之后然后又骑回自己的学校,又花了一周的时间,一共过了两周完整的工作才完成,耗时太长

假设此时你学聪明了,既然那么慢,那么直接坐高铁去送,可来回一共都500多了,这都比这些书的价值多了,即成本太高了.

可以把以上这些书看做资源,这种模式叫做写透模式.

此时你想到,可以寄快递来送这些书啊,价格便宜,而且两三天就到了,这多实惠,于是你把这些书交给了顺丰 快递,过了两三天,你的朋友在手机上给你说,说我收到这些书了,然后这样就成功的把资源交到了对方的手中。这个顺丰快递在这里扮演的角色便是缓冲区. 

顺丰 拿到你的快递也不是立马就送,而是等待数量足够多时,再一次性开始运输,这相当于是一种缓冲区的刷新策略.

缓冲区刷新策略

刷新策略主要有以下3种:

1.立即刷新

2.行刷新(行缓冲),遇到\n刷新

3.满刷新(全缓冲),指的是将输入或输出的数据完全存储在缓冲区中,然后再进行传输或处理。

当然也会有一些特殊情况:

1.用户强制刷新(fflush)

2.进程退出

遇到以上两种情况时,必须马上从刷新缓冲区的数据,而不要按照之前的刷新策略继续等待.

所以缓冲策略 = 一般情况 + 特殊情况.


一般而言,行缓冲的设备文件 --- 显示器

全缓冲的设备文件 --- 磁盘文件

所有的设备,永远倾向于全缓冲 --> 缓冲区满了再刷新 --> 需要更少次数的IO操作 -->更少次数的外设访问(相当于提高了整机效率).

有同学可能有疑问,比如10行数据,每一行有100个字节,虽然10行最后再一起刷新,只进行了一次的外设访问,但是数据量很多啊,1000个字节,而按行刷新虽然刷新了10次,但每次数据量少啊,那为什么外设访问次数越少越好呢?

这是因为和外部设备IO的时候,数据量的大小不是主要矛盾,你和外设预备IO的过程是最耗费时间的.

比如你和别人借钱,往往沟通的过程要耗费很长时间,而转账的过程只需要几秒,这同样的道理.

那我们直接改成全缓冲不就行了吗?这样效率不就高了吗,还要什么行缓冲.

其实这些策略,都是根据实际情况做的妥协:

例如行缓冲就是针对于显示器,是给用户看的,一方面要照顾效率,另一方面也要照顾用户体验.

而平常我们打开的一些文本文件便是全缓冲,等到用户全部写完再一次性进行保存.

有了这些缓冲区和策略,便可以提高数据处理的效率.

缓冲区在哪里

为了解决这个问题,我们可以先写以下代码:

int main()    
{    
    //C语言提供的接口    
    printf("hello,printf\n");    
    fprintf(stdout,"hello,fprintf\n");    
    const char* s = "hello,fputs\n";    
    fputs(s,stdout);    
    
    //系统接口    
    const char* ss = "hello,write\n";    
    write(1,ss,strlen(s));    
    //注意这里有一个创建子进程
    fork();  
}

我们这里把创建子进程fork函数放在了最后,这有什么用呢?放在最后子进程什么都没有执行就结束了,有什么意义呢?

我们先把代码运行一下:

 可以看到,这些都是正常输出的,没有任何问题.

但我们此时创建一个log.txt文件,然后把输出结果重定向到它里面,然后cat看卡里面的内容:

很奇怪的现象:我们发现除了系统接口的只输出了一次C语言提供的函数都输出了两次 .

这是什么原因呢?


根据我们上面所讲的缓冲区的刷新策略:

我们直接运行程序是向显示器中打印,采用的是行刷新策略,而我们重定向到文件中,向文件中打印,便成了全缓冲策略.

1.如果是向显示器中打印,那么采用的是行刷新策略,那么最后执行fork的时候,所有的数据都已经刷新完成了,此时再执行fork就没有意义了.

2.如果对程序进行了重定向,即此时要向文件中打印,此时刷新策略便隐式的变成了全缓冲

\n换行符便没有了意义.

fork的时候,一定是函数已经执行完了,但是数据还没有刷新! 这些数据在当前进程对应的C标准库中的缓冲区里. 这些数据是父进程的.

代码执行完了,并不代表数据已经刷新了,fork之后子进程和父进程执行return 0的时候,即要把数据刷新的时候,要发生写时拷贝,这样就有了两份数据,然后分别输出到文件中。

所以就出现了C语言标准库输出的函数打印了两次,而系统接口打印了一次。

因为系统接口是直接写入到了文件中,而不用经过缓冲区。

此时,更加让我们确信了一个事实缓冲区一定不是操作系统提供的而是由C语言标准库提供的!因为如果是操作系统提供的,那么这个系统接口也应该输出两次,而不是只有一次。


那么它在哪里呢?

我们再加一条代码:

    fflush(stdout);

 我们再次运行,然后将其输出到log.txt文件中:

我们发现此时都只输出了一条语句,而不是两条语句了. 

经过上面的讲解,我相信大家也能明白了,fflush已经强制将缓冲区的内容刷新了出来,此时缓冲区已经是空的了,然后再执行fork,父子进程缓冲区都是空的,所以也没有数据刷新了,这样才各自只打印了一条语句。

那么注意的是,我们fflush是C语言提供的,参数我们只提供了一个stdout,那么它是如何找到缓冲区的呢?

在C语言中,打开文件对应函数是fopen.它的函数原型如下:

它的函数返回值是FILE*,即一个struct file,里面不仅封装了fd,而且包含了该文件fd对应的语言层的缓冲区结构_IO_FILE。

_IO_FILE的内部结构大致如下:

 

 所以C语言缓冲区存在FILE结构体里.

这里需要说明一下的是,内核中也有缓冲区,只不过是内核缓冲区,和用户级缓冲区(例如C语言提供的缓冲区)两个各自独立,互不影响.

 手动设计一个用户层缓冲区

我们知道缓冲区被封装在一个file结构体中,而且里面还有文件描述符fd等,所以首先我们要创建一个结构体来封装这些数据.

#define NUM 1024                                                                                                                      
struct MyFILE_{    
 int fd;//文件描述符
 char buffer[NUM];//缓冲区
 int end;//当前缓冲区的结尾    
};    
typedef struct MyFILE_ MyFILE;    

同样的,为了使用,还有四个接口来使用,分别为fopen_,fputs_,fflush_,fclose_这四个接口。

先来说fopen_,这个主要有两个参数,第一个参数是打开的文件路径pathname,第二个是打开模式mode(r,r+,w,w+,a,a+),最后返回MyFILE结构体.

这六种模式不全部一一写,只写一种w模式,够我们使用即可。

方法思路是:首先调用系统open()接口,然后传入参数,打开模式为O_WRONLY,O_TRUNC,O_CREAT,然后接受返回的fd

如果fd大于等于0,说明打开成,然后此时再为FILE结构体开空间并初始化,并将FILE中的fd等于刚才open所得到的fd

MyFILE* fopen_(const char* pathname,const char* mode)
{
  assert(pathname);
  assert(mode);
  
  MyFILE* fp = NULL;
  if(strcmp(mode,"r") == 0)
  {

  }
  else if(strcmp(mode,"r+") == 0)
  {

  }
  else if(strcmp(mode,"w") == 0)
  {
    int fd = open(pathname,O_WRONLY | O_TRUNC | O_CREAT,0666);
    if(fd >= 0)
    {
      fp = (MyFILE*)malloc(sizeof(MyFILE));
      memset(fp,0,sizeof(MyFILE));
      fp->fd = fd;

    }
    
  }
  else if(strcmp(mode,"w+") == 0)
  {

  }
  else if(strcmp(mode,"a") == 0)
  {

  }
  else if(strcmp(mode,"a+") == 0)
  {

  }
  return fp;
}

接下来是fputs_,主要作用是向指定文件描述符fd中(本质上还是向缓冲区中写入)写入内容。所以第一个参数是要写入的内容,第二个参数是MyFILE结构体.

主要思路是先将传入的内容拷贝到MyFILE中的buffer缓冲区中,然后再更新end的长度。

此时我们再判断fd是0,1,2还是其他文件,这里我们只实现了fd=1的情况

当fd=1时,先判断缓冲区中最后一个字符是否是'\0',如果是,则write写入到1中,即刷新缓冲区的内容,并将end置为0.

若不是,则无需任何操作.

void fputs_(const char* message, MyFILE* fp)
{
  assert(message);
  assert(fp);

  strcpy(fp->buffer+fp->end,message);                                                                                                 
  fp->end += strlen(message);

  //for debug
  //printf("%s\n",fp->buffer);
  //暂时没有刷新,刷新策略是是来执行的呢? 用户通过执行C标准库中的代码逻辑,来完成刷新动作
  //效率提高体现在哪里呢?因为C提供了缓冲区,那么我们就通过策略,减少了IO次数的执行次数,不是数据量!
  if(fp->fd == 0)
  {
    //标准输入
  }
  else if(fp->fd == 1)
  {
    //标准输出
    if(fp->buffer[fp->end-1] == '\n')
    {
      //fprintf(stderr,"fflush: %s",fp->buffer);
      write(fp->fd,fp->buffer,fp->end);
      fp->end = 0;
    }
  }
  else if(fp->fd == 0)
  {
    //标准错误
  }
  else
  {
    //其他文件
  }
}

接下来是fflush_,这个函数主要作用是强制刷新缓冲区中的内容.

主要思路是:先判断缓冲区内容是否为空,若不为空,则write写入到编号为fd文件中,然后调用syncfs函数将数据写入磁盘,最后将end置为0.

void fflush_(MyFILE* fp)
{
  assert(fp);
  if(fp->end != 0)
  {
    //暂且认为刷新了 -- 其实是把数据写到了内核
    write(fp->fd,fp->buffer,fp->end);
    syncfs(fp->fd);//将数据写入到磁盘
    fp->end = 0;
  }
}

最后一个是fclose_,这个函数作用是关闭文件,并刷新缓冲区内容.

这个比较简单,直接复用刚才的fflush_刷新缓冲区和调用close函数关闭文件即可

void fclose_(MyFILE* fp)
{
  assert(fp);
  fflush_(fp);
  close(fp->fd);
  free(fp);

}

 关于缓冲区的内容到这里就讲解完了,如果有疑问或者错误的地方,欢迎评论区或私信 提出或指正哦

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/47317.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【前端笔记】本地运行cli项目报错ERR_OSSL_EVP_UNSUPPORTED

报错原因 Node版本>17.x,本地npm run 起项目后会发现终端报错,具体有以下2块关键信息: Error: error:0308010C:digital envelope routines::unsupported和 opensslErrorStack: [ error:03000086:digital envelope routines::initializa…

Python补充笔记5-模块化、文件

目录 一、模块 二、模块的导入 三、python中的包​编辑 四、常用的内容模块 五、第三方模块的安装与使用 六、编码格式的介绍 七、文件读写的原理 八、常用的文件打开模式 ​九、文件对象的常用方法 十、with语句​编辑 十一、os模块的常用函数 十二、os.path模块的常用方法​编…

TCP协议如何实现可靠传输

TCP最主要的特点 TCP是面向连接的运输层协议,在无连接的、不可靠的IP网络服务基础之上提供可靠交付的服务。为此,在IP的数据报服务基础之上,增加了保证可靠性的一系列措施。 TCP最主要的特点: TCP是面向连接的输出层协议 每一条…

如何启用路由器dhcp?快解析如何内网穿透?

一、什么是DHCP? 动态主机设置协议(DHCP)是一种使网络管理员能够集中管理和自动分配 IP 网络地址的通信协议。在网络中,每个联网设备都需要分配独有的 IP 地址。并当有新计算机移到网络中的其它位置时,能自动收到新的…

微服务——http客户端Feign

目录 Restemplate方式调用存在的问题 Feign的介绍 基于Feign远程调用 Feign自定义配置 修改日志方式一(基于配置文件) 修改日志方式二(基于java代码) Feign的性能优化 连接池使用方法 Feign_最佳实践分析 方式一: 方式二 实现Feign最佳实践(方式二) 两种解决方案 Re…

【数据结构】实验九:二叉树

实验九 二叉树 一、实验目的与要求 1)理解二叉树的类型定义; 2)掌握二叉树的存储方式及基于存储结构的基本操作实现; 二、 实验内容 1. 二叉树的结点定义如下: struct TreeNode { int m_nvalue; TreeNode* m_…

【梯度下降在波士顿房价预测中的应用】

数据准备 我们首先需要加载波士顿房价数据集。该数据集包含房屋特征信息和对应的房价标签。 import pandas as pd import numpy as npdata_url "http://lib.stat.cmu.edu/datasets/boston" raw_df pd.read_csv(data_url, sep"\s", skiprows22, headerN…

“可以黑掉整个宇宙”的Metasploit Framework

0x01、 简述 Metasploit Framework(MSF)是一款开源安全漏洞检测工具,他带有数千个已知的软件漏洞,目前人在持续更新。Metasploit可以用来信息收集、漏洞探测、漏洞利用等渗透测试的全流程,被安全社区冠以“可以黑掉整个宇宙”之名。在普通的…

又一“邪恶版”ChatGPT出现,专为网络犯罪而生

最近,Hackread 分享了一个恶意聊天机器人 WormGPT 的详细信息,该聊天机器人是为帮助网络犯罪分子进行非法活动而创建的。现在,暗网上又出现了一个名为 FraudGPT 的聊天机器人。这是一个基于订阅的人工智能聊天机器人,可以为网络犯…

解密C++多态机制:发挥对象的多样性,实现更加智能的程序设计

目录 一.多态1.多态的用处2.多态的实现3.虚函数4.override 和 final5.重载重写与重定义6.虚函数表 一.多态 1.多态的用处 众所周知C语言的三大特性:封装、多态、继承。其中多态就是去完成某个行为,但是会根据不同的对象产生不同的状态,所以…

C语言每天一练:输出杨辉三角

题目:请输出以下杨辉三角(要求输出前10行) 列: 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 1 5 10 10 5 1 ...... 题解析:不了解杨辉三角的可以百度看一下,大概是这样的,咱们就可以解…

SpringBoot集成kafka全面实战

本文是SpringBootKafka的实战讲解,如果对kafka的架构原理还不了解的读者,建议先看一下《大白话kafka架构原理》、《秒懂kafka HA(高可用)》两篇文章。 一、生产者实践 普通生产者 带回调的生产者 自定义分区器 kafka事务提交…

微信联系人批量删除功能如何操作?删除的联系人如何恢复?

继微信推出了朋友圈置顶功能后,微信又推出了"批量删除好友的功能" ,具体的操作步骤如下: 第一步 是点击聊天界面上的搜索框"搜索" 第二步 "搜索"排序字母,点击"更多联系人" 第三步 搜…

openEuler 22.03 LTS系统搭建局域网上网代理服务器

生产网环境的一个痛点就是与外网隔离,内网的服务器如果需要进行补丁升级或者下载更新软件,比较困难,本文讲解在生产网中能访问公网的openEuler 22.03 LTS系统服务器上搭建代理服务器,供内网其它服务器连接公网,同时介绍…

SpringBoot实战(二十三)集成 SkyWalking

目录 一、简介二、拉取镜像并部署1.拉取镜像2.运行skywalking-oap容器3.运行skywalking-ui容器4.访问页面 三、下载解压 agent1.下载2.解压 四、创建 skywalking-demo 项目1.Maven依赖2.application.yml3.DemoController.java 五、构建启动脚本1.startup.bat2.执行启动脚本3.发…

hive之文件格式与压缩

hive文件格式: 概述: 为Hive表中的数据选择一个合适的文件格式,对提高查询性能的提高是十分有益的。Hive表数据的存储格式,可以选择text file、orc、parquet、sequence file等。 文本文件: 文本文件就是txt文件&…

【SpirngCloud】分布式事务解决方案

【SpirngCloud】分布式事务解决方案 文章目录 【SpirngCloud】分布式事务解决方案1. 理论基础1.1 CAP 理论1.2 BASE 理论1.3 分布式事务模型 2. Seata 架构2.1 项目引入 Seata 3. 强一致性分布式事务解决方案3.1 XA 模式3.1.1 seata的XA模式3.1.2 XA 模式实践3.1.3 总结 4. 最终…

JavaScript --简介

目录 JS可以用来做什么? JS在前端中几种写法: 1. 文件引用: 2. 页面样式 3. 行内样式 集中常见的弹框: JS基本语法: 变量: 常量: 数据类型: 基本数据类型: 引用数据类型&#xff1a…

微信批量删除好友怎么删除

微信好友太多想要批量删除不知道怎么删除,相信这个问题也困扰了不少人。那么怎样才能批量的删除微信好友?其实不难,可以通过新建标签删除的方式来实现批量删除好友。 怎么批量删除 微信批量删除好友的具体步骤如下: 1、新建标签 首…

《零基础入门学习Python》第056讲:论一只爬虫的自我修养4:网络爬图

今天我们结合前面学习的知识,进行一个实例,从网络上下载图片,话说我们平时闲来无事会上煎蛋网看看新鲜事,那么,熟悉煎蛋网的朋友一定知道,这里有一个 随手拍 的栏目,我们今天就来写一个爬虫&…
最新文章