【Linux】第二十三站:缓冲区

文章目录

  • 一、一些奇怪的现象
  • 二、用户级缓冲区
  • 三、用户级缓冲区刷新问题
  • 四、一些其他问题
    • 1.缓冲区刷新的时机
    • 2.为什么要有这个缓冲区
    • 3.这个缓冲区在哪里?
    • 4.这个FILE对象属于用户呢?还是操作系统呢?这个缓冲区,是不是用户级的缓冲区呢?
    • 5.为什么前面的图④中C语言系列接口打印了两次?

一、一些奇怪的现象

首先我们需要先注意这两个函数,即fwrite和fread函数

image-20231129192011392

注意这两个函数中

fread中,表示从stream流中读取size个单位的nmemb大小的数据放入ptr处。注意这里的返回值返回的是成功读取的个数,即与size是类似的

fwrite中,表示向stream流中写入size个单位的nmem大小的数据从ptr中。注意这里的返回值返回的是成功写入的个数,与size是类似的

而下面这个函数中

image-20231129192613897

它的意思是向fd文件描述符对应的文件中,写入buf位置的count个字节,这里的返回值返回的是写入成功字节的个数,与count是类似的

当我们的代码为如下的时候

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
    const char* fstr = "hello fwrite\n";
    const char* str = "hello write\n";
    
    //C语言
    printf("hello printf\n");  //stdout-->1
    fprintf(stdout,"hello fprintf\n");//stdout-->1
    fwrite(fstr,strlen(fstr),1,stdout);//stdout-->1
    //操作系统提供的systemcall
    write(1,str,strlen(str)); //1
    return 0;
}

最终运行结果为,我们将这个运行结果称作为①

image-20231129194141307

如果我们不变代码,而是在运行的时候加上重定向,那么最终运行结果为如下,我们将其称之为②

image-20231129194602070

如果我们将代码改为如下,即在最后加上一个fork

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
    const char* fstr = "hello fwrite\n";
    const char* str = "hello write\n";
    
    //C语言
    printf("hello printf\n");  //stdout-->1
    fprintf(stdout,"hello fprintf\n");//stdout-->1
    fwrite(fstr,strlen(fstr),1,stdout);//stdout-->1
   // close(1);
    //操作系统提供的systemcall
    write(1,str,strlen(str)); //1
    fork();
    return 0;
}

那么最终运行结果为如下,我们记作③

image-20231129194946113

同样是这段代码,我们对其做一个重定向,我们记作④

image-20231129195226031

最终如下图所示

image-20231129195812606

对于一二三而言,都是非常容易理解的,唯独二我们也许会好奇为什么系统调用接口会提前打印。

对于第四个,我们会发现,对于C语言的接口而言都打印了两遍,而对于系统调用的接口而言,是不受到这些影响的,依然只打印一次。虽然我们不知道为什么C语言的接口被打印了两遍,但是我们知道这个一定与fork有关

如果我们的代码是这样的

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
    const char* fstr = "hello fwrite\n";
   // const char* str = "hello write\n";
    
    //C语言
    printf("hello printf\n");  //stdout-->1
    fprintf(stdout,"hello fprintf\n");//stdout-->1
    fwrite(fstr,strlen(fstr),1,stdout);//stdout-->1
    close(1);
    //操作系统提供的systemcall
   // write(1,str,strlen(str)); //1
   // fork();
    return 0;
}

那么运行结果为

image-20231129203256910

如果我们的代码是这样的,将所有的\n都给去掉,

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
    const char* fstr = "hello fwrite";
   // const char* str = "hello write\n";
    
    //C语言
    printf("hello printf");  //stdout-->1
    fprintf(stdout,"hello fprintf");//stdout-->1
    fwrite(fstr,strlen(fstr),1,stdout);//stdout-->1
    close(1);
    //操作系统提供的systemcall
   // write(1,str,strlen(str)); //1
   // fork();
    return 0;
}

运行结果为,什么内容也没有了,我们将下图记作⑤

image-20231129203523217

如果我们紧接着将close给去掉了

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
    const char* fstr = " hello fwrite";
   // const char* str = "hello write\n";
    
    //C语言
    printf("hello printf");  //stdout-->1
    fprintf(stdout,"hello fprintf");//stdout-->1
    fwrite(fstr,strlen(fstr),1,stdout);//stdout-->1
   //close(1);
    //操作系统提供的systemcall
   // write(1,str,strlen(str)); //1
   // fork();
    return 0;
}

运行结果为如下,我们可以看到已经有东西打印出来了

image-20231129203733405

如果我们将代码改为下面的

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
    //const char* fstr = " hello fwrite";
    const char* str = "hello write";
    
    //C语言
  //  printf("hello printf");  //stdout-->1
  //  fprintf(stdout,"hello fprintf");//stdout-->1
  //  fwrite(fstr,strlen(fstr),1,stdout);//stdout-->1
   
    write(1,str,strlen(str)); //1
    close(1);
    //操作系统提供的systemcall
   // fork();
    return 0;
}

那么最终运行结果为如下,我们可以看到打印出来东西了,我们将下面的记作⑥

image-20231129204051641

所以最终情况汇总如下

image-20231129204257545

我们发现上面了很多的奇怪的现象了

二、用户级缓冲区

我们知道像printf/fprintf/fwrite/fputs/…这些C式的接口,他们最终的底层一定是调用write这个系统调用的,但是为什么他们之前出现了上面很多奇怪的现象呢?

而我们知道这些C式的接口一定会将数据写入缓冲区的

所以说这个缓冲区一定不在操作系统内部!!!不是系统级别的缓冲区!

因为一旦,我们将这些数据写入到对应的操作系统中,如下图所示

image-20231129205628436

它在close的时候就一定可以找到对应的文件,比如我们关闭的是1号文件。然后将对应的数据刷新到磁盘中。直接就可以刷新了。它应该是可以看到结果的,可是根据我们的第⑤号运行结果中可以看出来,它是没有看到的,所以一定是不在操作系统内部的。

而第六图中,write可以看到,是因为它直接写到了系统级别的缓冲区,当close的时候,就会刷新系统级别的缓冲区。

所以说,我们所说的缓冲区都是语言层的。

image-20231129211436897

所以说,我们的数据写入的时候,都会先写入到这个C语言层面的缓冲区,随后在通过write写入到系统级别的缓冲区

如下所示才是正确的缓冲区流向

image-20231129211708156

所以这样就解释了前面的五和六两张图的情况

就是因为printf/fprintf这些函数会去使用C提供的一个语言层面的缓冲区,然数据都写到这里来了,而我们最后直接close的时候只能刷新内核级别的缓冲区,所以最终什么结果也没有。而前面的write些往内核里面去写入的,所以最终close的时候会刷新里面的缓冲区,从而使得数据打印出来

所以最终就解释了下面的这个现象

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main()
{
    const char* fstr = " hello fwrite";
    const char* str = "hello write";
    
    //C语言
    printf("hello printf");  //stdout-->1
    fprintf(stdout,"hello fprintf");//stdout-->1
    fwrite(fstr,strlen(fstr),1,stdout);//stdout-->1
   
    write(1,str,strlen(str)); //1
    close(1);
    //操作系统提供的systemcall
   // fork();
    return 0;
}

最终运行结果为

image-20231129212353646

就是因为,前面的三个接口都是写入到了C语言层面上的缓冲区了,并没有写入到内核级别的缓冲区,而write是会写入到内核级别的缓冲区的,而close只会刷新内核级别的缓冲区,而关闭以后,原先在C语言级别缓冲区的数据由于1号文件被关闭了,而我们在程序结束的时候确实是会刷新C语言级别的缓冲区,不过在刷新这个缓冲区的时候,wirte要利用1号文件写入到内核中。可是此时1号文件已经被关闭了,所以无法写入。

而我们知道显示器的文件的刷新方案是行刷新,所以在printf执行完,就会立即遇到\n,将数据进行刷新

所以刷新的本质就是将数据通过1+write写入到内核中

而上面的C语言层面的缓冲区就是用户级缓冲区

我们还记得之前的_exit和exit其实是有区别的

exit是c语言的接口,它能看到这个用户级别缓冲区,可以对其进行刷新(fflush(stdout)),然后调用_exit进程退出

而_exit是一个系统调用,看不到这个用户级缓冲区,它就直接关闭了程序了。

目前我们可以认为,只要将数据刷新到了内核,数据就可以到硬件了

所以说,上层所谓的fflush等等,都是通过调用write将数据写入到缓冲区中。

三、用户级缓冲区刷新问题

  1. 无缓冲:直接刷新
  2. 行缓冲:不刷新,直到遇到\n才刷新-------比如显示器
  3. 全缓冲:缓冲区满了,才刷新-----------比如文件写入到普通文件的时候

所以fprintf/fwrite这些接口最后写入到的都是C缓冲区,然后根据一定的刷新策略调用write接口,最后写到操作系统中

image-20231130094018559

有了上面的理解,我们就可以直到,像fflush接口中,里面绝对使用了write函数

image-20231130094419721

因为只有write才可以将数据写入到操作系统内核中。

四、一些其他问题

1.缓冲区刷新的时机

在行缓冲的时候,就比如像显示器写入的时候策略就是行缓冲

对于全缓冲,就比如往普通文件写入时候就是全缓冲

还有一种缓冲区刷新的时机是进程退出的时候

image-20231130094859162

虽然我们没有刷新但是,此时是进程退出,会自动刷新C语言缓冲区

image-20231130095344614

但是要注意,如果我们在退出之前,关闭了1号文件,那么就无法完成刷新了。因为write接口无法写入到1号文件了

2.为什么要有这个缓冲区

  1. 解决效率问题-----用户的效率问题(我们只需要将数据移交给缓冲区即可,至于如何放到操作系统里面,不需要我们去自己完成,由缓冲区去完成。类似于我们将包裹交给快递公司。但是最后快递公司一般不会只有一件包裹就去递送,而是有了很多件包裹以后,或者今天要关门了,会去统一的配送,这就类似于缓冲区的刷新)
  2. 配合格式化(我们之前的printf函数中,我们打印的时候需要先将数据给格式化再写入缓冲区中,比如printf(“hello %d”,123)这行代码中,我们写入到缓冲区的时候已经变为了"hello 123")

这个缓冲区,我们一般把他叫做文件流,在C++中也是叫做文件流。

因为我们之前的fprintf/fwrite这些接口会将数据源源不断的写入到缓冲区中。然后这些缓冲区的数据会源源不断的刷新到操作系统中。

而这个过程就是源源不断的往里面写,然后源源不断的删除。

有进有出。所以就有了流的概念,就是文件流。

3.这个缓冲区在哪里?

我们直到,像fflush,fprintf这些接口始终绕不开FILE这个东西

image-20231130101934024

image-20231130102004366

而FILE是一个结构体,而它里面必须要封装fd

FILE里面其实还有对应打开文件的缓冲区字段和维护信息

所以说,其实,这个这个所谓的stdout,就是将hello world给放到stdout所对应文件中的缓冲区中,随后便会通过write写入到操作系统中

image-20231130102431007

所以这个缓冲区就在FILE里面

就比如我们现在打开了10个文件,就有10个缓冲区了。每个文件都有对应的缓冲区

所以每一个文件都会通过它的自己的缓冲区,刷新到其对应的文件上。

4.这个FILE对象属于用户呢?还是操作系统呢?这个缓冲区,是不是用户级的缓冲区呢?

这个FILE对象一定属于用户。语言都属于用户层。

所以这个缓冲区,就是用户级缓冲区

所以我们就知道了,下面这个函数返回的为很么要是FILE*了

image-20231130103324137

fopen是libc.so库中提供的接口。

这个fopen在底层会去调用open系统调用,帮我们建立内核级别的文件对象。然后拿到文件描述符,在语言层给我们malloc(FILE),所以返回的是FILE*

5.为什么前面的图④中C语言系列接口打印了两次?

即在下面代码中,出现了下面的情况

#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
    const char* fstr = "hello fwrite\n";
    const char* str = "hello write\n";
    
    //C语言
    printf("hello printf\n");  //stdout-->1
    fprintf(stdout,"hello fprintf\n");//stdout-->1
    fwrite(fstr,strlen(fstr),1,stdout);//stdout-->1
   
    write(1,str,strlen(str)); //1
    //操作系统提供的systemcall
     fork();
    return 0;
}

对于没有重定向的,我们很清楚打印流程

image-20231130105707411

而我们一旦使用重定向,那么缓冲方案变为了全缓冲,因为全缓冲是在普通文件写入的时候才使用的。

也就是说,遇到\n不在刷新了,而是等缓冲区被写满才刷新

所以我们前面用C接口写的这三行数据也写不满缓冲区,所以会留在缓冲区里面。因为缓冲区并没有被刷新

而最后的write系统调用就会直接写入到操作系统中,故而会先打印出来。

所以截止至此,已经解释了为什么前面的二号图中,顺序会出现问题的现象,因为最后进程退出的时候,会强制刷新。

我们可以用这段代码,来验证一下结果

image-20231130110913183

运行结果为如下

image-20231130111809720

而我们后面将代码加上一个fork以后。

而我们此时,write接口已经写入了。那些C语言系列的接口的数据还在缓冲区中,此时我们fork以后,会创建子进程,共享代码,然后数据以写时拷贝的方式存在。

而我们这个缓冲区就是用户级缓冲区,是我们FILE的一部分,也就是说,就相当于在堆里面的一个数据。而父进程在想要刷新的时候,其实本质就是清空缓冲区,也就是要修改。所以此时会发生写时拷贝,父子进程就对这段缓冲区各自拥有了一份。所以我们最终就发现了这段数据被刷新了两份。

所以我们就看到了C接口被刷新了两次。

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

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

相关文章

STM32 自定义UART数据格式(串口通信点亮LED实验)

起始位&#xff1a;0xaa告诉机器我们要开始传输数据了。 校验位&#xff1a;等于前几项数据位的相加。 结束位&#xff1a;结束传输。 自定义UART数据格式&#xff1a; 1》CPU与CPU之间 2》外设与CPU之间 这里举例&#xff0c;利用串口调试助手发送一串数据&#xff0c;…

Java数据结构之《循环队列》题目

一、前言&#xff1a; 这是怀化学院的&#xff1a;Java数据结构中的一道难度中等的一道编程题(此方法为博主自己研究&#xff0c;问题基本解决&#xff0c;若有bug欢迎下方评论提出意见&#xff0c;我会第一时间改进代码&#xff0c;谢谢&#xff01;) 后面其他编程题只要我写完…

11.26电梯控制器设计分析

项目三 电梯控制器设计&#xff08;*****&#xff09; 设计一个多楼层的电梯控制器系统&#xff0c;并能在开发板上模拟电梯运行状态。可以利用按键作为呼叫按键&#xff0c;数码管显示电梯运行时电梯所在楼层&#xff0c;led灯显示楼层叫梯状态。 就是初始默认在1楼&#xff0…

【MySQL】MySQL安装 环境初始化

MySQL安装 MYSQL官网 安装完成后,傻瓜下一步即可 配置一下环境变量即可 (1) 初始化MySQL, 管理员身份运行 mysqld --initialize-insecure(2) 注册 mysqld mysqld -install# 如果记录以前的版本执行下面指令 mysqld -remove(3) 启动MySQL服务 // 启动mysql服务 net start …

Nginx配置文件全解析【深度剖析细节】

文章目录 &#x1f4a5; 简介&#x1f4ab; 基本结构&#x1f349; 事件处理器&#x1f96d; 配置分析&#x1f34f; 配置示例 &#x1f349; HTTP服务器&#x1f96d; 配置分析&#x1f34f; 配置示例 &#x1f349; 虚拟主机 &#x1f34a; 优化&#x1f354; 总结 &#x1f…

卡码网15 .链表的基本操作III

链表的基础操作III 时间限制&#xff1a;1.000S 空间限制&#xff1a;128MB 题目描述 请编写一个程序&#xff0c;实现以下链表操作&#xff1a;构建一个单向链表&#xff0c;链表中包含一组整数数据。 1. 实现在链表的第 n 个位置插入一个元素&#xff0c;输出整个链表的…

计算机基础知识62

模型层回顾&#xff1a;基本使用 # 模型层有orm框架&#xff1a;对象关系映射 数据库中&#xff1a;一个个表 &#xff1a;user表&#xff0c;book表&#xff0c;一条条的记录 程序中&#xff1a;一个个类&#xff0c;一个个对象 数据库中一张表---->程序中一个…

【JavaSE】:String类(一):基本使用方法

String类 一.String类的基本构成二.字符串比较1.比较相等2.比较大小 三.字符串查找四.字符串转换五.字符串替换六.字符串的拆分七.字符串的截取八.其他操作方法 在C语言中已经涉及到字符串了&#xff0c;但是在C语言中要表示字符串只能使用字符数组或者字符指针&#xff0c;可以…

【软件推荐】卸载360软件geek;护眼软件flux;

卸载360软件geek f.lux: software to make your life better (justgetflux.com) 卸载完扫描残留 护眼软件 hf.lux: software to make your life better (justgetflux.com)https://justgetflux.com/https://justgetflux.com/

INFINI Labs 产品更新 | 修复 Easysearch 跨集群复制索引同步问题,Gateway 内存异常增长等问题

INFINI Labs 产品又更新啦~&#xff0c;本次更新主要对 Easysearch、Gateway、Console、Agent 等产品功能进行优化和相关 Bug 修复&#xff0c;解决了内存异常增长等问题&#xff0c;以下是详细说明。 INFINI Easysearch v1.6.2 INFINI Easysearch 是一个分布式的近实时搜索与…

YOLOv8改进 | 2023 | AKConv轻量级架构下的高效检测(可变核卷积)

一、本文介绍 本文给大家带来的改进内容是AKConv&#xff08;可变核卷积&#xff09;是一种创新的卷积神经网络操作&#xff0c;它旨在解决标准卷积操作中的固有缺陷&#xff08;采样形状是固定的&#xff09;&#xff0c;AKConv的核心思想在于它为卷积核提供了任意数量的参数…

最小化安装 Neokylin7.0 用于搭建 Hadoop 集群

文章目录 环境搭建背景虚拟机创建和环境配置安装过程注意事项虚拟机设置软件选择KOUMP系统分区网络和主机名打开以太网&#xff0c;并记录信息配置 IPv4修改主机名 创建用户 hadoop完全分布式搭建-CSDN博客 环境搭建背景 为什么不从hadoop100或者hadoop101开始&#xff0c;而是…

【Python表白系列】这个情人节送她一个漂浮的爱心吧(完整代码)

文章目录 漂浮的爱心环境需求完整代码详细分析系列文章 漂浮的爱心 环境需求 python3.11.4PyCharm Community Edition 2023.2.5pyinstaller6.2.0&#xff08;可选&#xff0c;这个库用于打包&#xff0c;使程序没有python环境也可以运行&#xff0c;如果想发给好朋友的话需要这…

外包搞了6年,技术退步明显......

先说情况&#xff0c;大专毕业&#xff0c;18年通过校招进入湖南某软件公司&#xff0c;干了接近6年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试&#xf…

对 Vision Transformers 及其基于 CNN-Transformer 的变体的综述

A survey of the Vision Transformers and its CNN-Transformer based Variants 摘要1、介绍2、vit的基本概念2.1 patch嵌入2.2 位置嵌入2.2.1 绝对位置嵌入(APE)2.2.2 相对位置嵌入(RPE)2.2.3卷积位置嵌入(CPE) 2.3 注意力机制2.3.1多头自我注意(MSA) 2.4 Transformer层2.4.1 …

【精选】VulnHub Shuriken-1 (超详细过程思路)

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【java】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收藏 …

spring @Autowired 和 @Qualifier 配合使用实现按名称属性赋值源码

一、调用过程 1、QualifierAnnotationAutowireCandidateResolver.java用于解析注解候选类&#xff0c;在构造器中往qualifierTypes集合中添加注解类。 2、执行逻辑 &#xff08;1&#xff09;、接着上篇博客【Spring之Autowired 属性多实现和单实现源码解析 】AutowiredAnnot…

【话题】程序员养生指南(AI生成)

目录 程序猿可能出现的职业病有哪些&#xff1f; 如何预防和对付这些职业病&#xff1f; 一、颈椎病的预防 二、神经衰弱的调适 三、肩周炎的防护 四、视力下降的保护 五、饮食与运动的重要性 六、消化系统职业病的预防 程序员养生心得&#xff1a;呵护健康&#xff0c…

每天五分钟计算机视觉:经典的卷积神经网络之VGG-16模型

VGG-16 Vgg16是牛津大学VGG组提出来的,相比于AlexNet来说,AlexNet的一个改进是采用连续的几个4*3的卷积核来代替AlexNet中的较大的卷积核(11*11,5*5)。前面我们也说过了使用小卷积核是优于大的卷积核的,因为多层非线性层可以增加网络深度来保证学习到更加复杂的模式,而且代…

Vlan配置

需求 1 PC1和PC3所在接口为Access接口 PC2/4/5/6处于同一网段&#xff0c;其中pc2可以访问pc4/5/6 PC4可以访问pc5&#xff0c;但不能访问pc6 PC5不能访问PC6 2 PC1/3与PC2/4/5/6不再同一网段 3 所有PC通过DHCP获取IP地址&#xff0c;且PC1/3可以正常访问PC2/4/5/6 R1 [V200R00…