【Linux修炼】13.缓冲区

news2024/11/27 16:46:28

在这里插入图片描述每一个不曾起舞的日子,都是对生命的辜负。

缓冲区的理解

  • 一. C接口打印两次的现象
  • 二. 理解缓冲区问题
    • 为什么要有缓冲区
    • 缓冲区刷新策略的问题
    • 所说的缓冲区在哪里?指的是什么缓冲区?
  • 三. 解释打印两次的现象
  • 四. 模拟实现
  • 五. 缓冲区与OS的关系

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

#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

当添加一个fork()后:

#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

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

二. 理解缓冲区问题

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

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

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

为什么要有缓冲区

下面举个场景:

image-20230109144936010

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

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

因此我们所调用的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

#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

#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

#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

五. 缓冲区与OS的关系

image-20230110215110787

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

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

总结:

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ThinkPHP 表单验证使用

对前端或表单请求的数据&#xff0c;一定要做校验&#xff0c;而使用ThinkPHP 验证器则可以事半功倍。 可以使用validate助手函数&#xff08;或者封装验证方法&#xff09;进行验证。TP版本6.1。 目录 验证场景 验证器 创建验证器 定义规则和提示 数据验证 独立验证&…

Arbotix使用

内容学自赵虚左的视频及资料 需求描述: 控制机器人模型在 rviz 中做圆周运动 1.安装 Arbotix 方式1:命令行调用 sudo apt-get install ros-<<VersionName()>>-arbotix <<VsersionName()>> 替换成当前 ROS 版本名称 添加 arbotix 所需配置文件 # …

Web原型设计规范

上篇文章为大家介绍了app端在进行原型设计时的设计规范&#xff0c;本篇将继续为大家介绍一下Web端&#xff08;这里主要指网页端&#xff09;的设计规范。其实web端的设计规范并没有像app端那样多&#xff0c;因为展示的空间比较大&#xff0c;所有要求也就没有那么严苛。 电脑…

Spring_事务

事务的主要内容 事务定义 特性&#xff1a;ACID 并发时产生的问题 事务的隔离级别 锁 事务的传播特性 异常处理 超时 只读事务 TransactionDefinition 并发时产生的问题 一个数据库可以允许多个客户端同时访问&#xff0c;即并发的方式访问数据库。数据库中的同一个数据可能同…

2023年12306购票平台自动化购票终|解决乘客选择与车票提交(附自动化购票完整源代码与演示视频)

目录 一、说明 1.1、背景 1.2、说明 二、步骤 2.1、切换视角检索乘车乘客 2.2、选择乘客 2.3、关闭学生票选择界面 2.4、提交订单 2.5、选择座位并确认 三、完整代码与视频演示 3.1、完整源代码如下 3.2、视频演示代码运行 四、结果 4.1、代码运行结果 五、总结…

windows获取iOS设备信息

依赖环境&#xff1a; 1.python3.6以上版本&#xff0c; 2.配置python的系统环境变量。 3.python已经安装pip。 安装tidevice: 1.打开cmd&#xff0c;输入命令pip3 install -U "tidevice[openssl]"如图所示&#xff0c;安装成功。 2.查看tidevice版本号&#xff0c…

网络超火的音效素材、BGM,全在这里了。

推荐几个超好用的音效素材网站&#xff0c;全网火爆的音效、BGM这里都能找到&#xff0c;自媒体、视频剪辑小伙伴必备&#xff01;建议收藏&#xff01; 1、菜鸟图库 https://www.sucai999.com/audio.html?vNTYwNDUx 菜鸟图库是一个综合性素材网站&#xff0c;站内涵盖设计、…

vector模拟实现之迭代器失效及深浅拷贝的问题

vector模拟实现 Tips&#xff1a;new申请空间不用判断&#xff0c;因为失败的话会抛异常。 STL源代码中vector的私有成员变量如下&#xff1a; private:iterator _start;//首元素iterator _finish;//最后一个有效数据的下一个&#xff0c;-_start为sizeiterator _endofstora…

6-3分布散度的9个梯度

( A, B )---1*30*2---( 1, 0 )( 0, 1 ) 让网络的输入只有1个节点&#xff0c;AB各由9张二值化的图片组成&#xff0c;排列组合A和B的所有可能性&#xff0c;固定收敛误差为7e-4&#xff0c;统计收敛迭代次数&#xff0c;并比较迭代次数的变化规律。 差值结构 A-B 迭代次数 …

Huawei Matebook X Pro 2018 Space Gray电脑 Hackintosh 黑苹果efi引导文件

硬件型号驱动情况主板Huawei Matebook X Pro 2018 Space Gray处理器Intel Core i7-8550U已驱动内存16 GB LPDDR4 2133 MHz已驱动硬盘LiteON SSD PCIe NVMe 512 GB [CA3-8D512]已驱动显卡NVIDIA GeForce MX150 (Disabled) / Intel(R) UHD Graphics 620已驱动声卡瑞昱ALC256 英特…

微积分——导数和切线问题

目录 1. 切线(Tangent Line)问题 2. 函数的导数(derivative) 3. 函数的可微性(differentiability)与连续性(Continuity) 1. 切线(Tangent Line)问题 微积分的出现源于17世纪欧洲数学家们正在研究解决的四个主要的问题&#xff1a; (1) 切线(tangent line)问题&#xf…

使用Alexnet实现CIFAR10数据集的训练

如果对你有用的话&#xff0c;希望能够点赞支持一下&#xff0c;这样我就能有更多的动力更新更多的学习笔记了。&#x1f604;&#x1f604; 使用Alexnet进行CIFAR-10数据集进行测试&#xff0c;这里使用的是将CIFAR-10数据集的分辨率扩大到224X224&#xff0c;因为在测试…

第03讲:Docker 容器的数据卷

一、什么是数据卷 数据卷是宿主机中的一个目录或文件&#xff0c;当容器目录或者文件和数据卷目录或者文件绑定后&#xff0c;对方的修改会立即同步&#xff0c;一个数据卷可以被多个容器同时挂载&#xff0c;一个容器也可以被挂载多个数据卷&#xff0c;数据卷的作用:容器数据…

基于遥感卫星影像水体提取方法综述

水体提取分类依据及基础 水体提取分类依据 水体提取的方法很多,很多学者也进行了分类,大体上有一个分类框架,主要是基于光学影像的分类,比如王航等[7]将水体提取分成3类,分别是基于阈值法、分类器法和自动化法; 李丹等[8]更深一步进行总结,引入近些年发展火热的基于雷达影像数…

Redisson自定义序列化

配置RedissonClientBean public RedissonClient redissonClient() {Config config new Config();// 单节点模式SingleServerConfig singleServerConfig config.useSingleServer();singleServerConfig.setAddress("redis://127.0.0.1:6379");singleServerConfig.set…

LeetCode二叉树经典题目(六):二叉搜索树

目录 28. LeetCode617. 合并二叉树 29. LeetCode700. 二叉搜索树中的搜索 30. LeetCode98. 验证二叉搜索树 31. LeetCode530. 二叉搜索树的最小绝对差 32. LeetCode501. 二叉搜索树中的众数 33. LeetCode236. 二叉树的最近公共祖先​ 28. LeetCode617. 合并二叉树 递归&…

Hi3861鸿蒙物联网项目实战:智能安防报警

华清远见FS-Hi3861开发套件&#xff0c;支持HarmonyOS 3.0系统。开发板主控Hi3861芯片内置WiFi功能&#xff0c;开发板板载资源丰富&#xff0c;包括传感器、执行器、NFC、显示屏等&#xff0c;同时还配套丰富的拓展模块。开发板配套丰富的学习资料&#xff0c;包括全套开发教程…

Windows11 系统打开IE浏览器的方式(完整版)

前言 大家好&#xff0c;好久不见&#xff01; 1、最近疯狂加班&#xff0c;旧电脑不太给力&#xff0c;换了新电脑&#xff0c;嘎嘎开心&#xff1b;开心之余发现新电脑是Win11系统的&#xff0c;但是IE浏览器找不到了&#xff0c;由于我的某些工作需要用到IE浏览器&#xf…

Vue2前端路由(vue-router的使用)、动态路由、路由和视图的命名以及声明式和编程式导航

目录 一、vue2的前端路由&#xff08;vue-router&#xff09; 1、路由&#xff1a;页面地址与组件之间的对应关系 2、路由方式&#xff1a;服务器端路由、前端路由 3、前端路由&#xff1a;在前端维护一组路由规则&#xff08;地址和组件之间的对应关系&#xff09;&#xf…

【UE4 第一人称射击游戏】34-制作一个简易计时器

上一篇&#xff1a;【UE4 第一人称射击游戏】33-创建一个迷你地图本篇效果&#xff1a;可以看到左上角有个简易的关卡计时器在倒计时步骤&#xff1a;打开“FPSHUD”&#xff0c;拖入一个图像控件图像选择“Timer_Backing”&#xff0c;尺寸改为4719拖入3个文本控件大小为1210字…