Linux系统编程学习 NO.14——缓冲区的概念、模拟实现Cstdio库

news2025/2/21 18:41:29

用户缓冲区

先介绍一下关于用户缓冲区的周边知识。

fread和fwrite的返回值

在这里插入图片描述
谈一谈fread和fwrite的返回值,如果写入/读取文件成功,fread或fwrite的返回值指的是实际写入/读取的内存块数量(实际的nmemb的大小)。假如fwrite写入的size是5字节,写的内存块数量为8,全部写入成功,fwrite的返回值就是8。假如fwrite写入的size是5字节,写的内存块数量为8,写入成功的内存块数量为5,fwrite的返回值就是5。

缓冲区刷新

下面我写一段代码,分别用不同的库函数和系统调用在显示器上输出一些内容。然后再将在显示器的内容重定向到文件中。

在这里插入图片描述
在这里插入图片描述
下面我微调一下代码。
在这里插入图片描述

在这里插入图片描述
上图代码在文件操作调用接口后加了一句fork(),创建了一个子进程。为什么将输出到显示器文件的内容重定向到普通文件lg.txt。为什么输出结果是这样的呢?带着疑问继续接着向下看。

在这里插入图片描述

在这里插入图片描述
上图代码中,在文件操作调用后关闭了1号fd,此时只有系统调用接口write写的内容成功写入了lg.txt文件中。这是为什么呢?首先,可以肯定的是C语言提供了用户级别的缓冲区将C接口对应写入1号文件的重定向内容给缓存了。但是1号fd已经被close掉了,这些内容还没来得及通过write写到系统文件缓冲区。所以,我们在上层也就没有看到lg.txt有C接口输出重定向的内容。而write接口是系统调用,它会直接将内容写入内核的文件缓冲区内,当1号fd close后,文件缓冲区的内容就已经刷新到了磁盘普通文件上了。而close是系统调用接口,它并不知道它的上层还有用户级别的缓冲区,所以它不等C接口调用write将数据写入内核缓冲区,就已经把文件关掉了,所以,普通文件lg.txt上没有C接口写入的输出重定向内容。在前面进程退出时,谈了exit和_exit。其中_exit调用,printf输出内容没法显示,这是因为_exit是系统调用,它不关心你上层用户级的缓冲区的存在,不等你将内容写入内核缓冲区就把文件描述符给关闭了。而exit是C语言提供的接口,它不仅会调用_exit退出之前调用fflush方法将用户级缓冲取得内容刷新到内核文件缓冲区里,所以exit调用前printf会正常输出内容。
在这里插入图片描述

下面我将所有字符串的’\n’去掉,请看看发生的现象。

在这里插入图片描述
在这里插入图片描述
通过上图代码和结果可以发现,不仅lg.txt文件上没有输出C接口的写入结果,居然连显示器都没有输出对应的写入结果。为什么显示器上没有输出结果呢?因为显示器是行刷新的刷新策略。即当printf执行写入时,若遇到’\n’就会立刻将数据刷新到内核缓冲区,然后刷新到磁盘上的显示器文件,显示器上也就输出对应的字符串信息。

目前暂且认为当数据到达内核缓冲区后,就可以直达硬件。有了这个认知接下来继续谈一谈上层(语言层)。当用户使用fprintf、fputs、fwrite等接口时,数据会先被写入C语言维护的用户级缓冲区中。在合适的时机,调用系统调用write写入到内核缓冲区中。

通过上面的样例,可以提炼出用户刷新缓冲区的行为本质就是调用系统调用接口write将用户缓冲区数据写入内核缓冲区。就用户层缓冲区刷新这一话题再谈一谈周边的问题。如刷新的方式分为三种,如无缓冲刷新、行缓冲刷新以及全缓冲刷新。无缓冲刷新即上层写入数据,写入完成后直接将用户缓冲区的内容写入内核缓冲区。 行缓冲刷新即在写入换行符’\n’缓冲区才会刷新,否则不刷新。全缓冲刷新即当缓冲区内空间写满了才去刷新。

显示器文件的刷新就是行刷新的经典应用,为什么printf输出的字符串带’\n’哪怕1号fd被close也能输出出来字符串,是因为行缓冲区刷新的策略。它会在执行完printf这一行代码就将C缓冲区内容刷新到内核缓冲区。

而普通文件的刷新策略是全缓冲刷新,那为什么printf()不带’\n’也能进行刷新呢?因为进程在退出时,会刷新缓冲区。但是若在printf()不带’\n’情况下,在下面的代码关闭了1号fd,相应的显示器上就不会输出printf的内容,因为在进程退出时,1号fd被关闭,所以无法使用write往内核写入,显示器文件也就不会收到数据。全缓冲刷新策略从OS的角度出发,是效率最高的刷新侧率。这就好比,在我们网购的时候,通常卖家都是会在每天的傍晚和早上发货。这是因为,间隔半天时间,将两次发货时间间隔内所有买家的包裹统一交给快递公司的专员。这样对卖家和快递公司双方的时间效率方面是最优的。对于买家来说,则是牺牲了一定的时间,但是这并不妨碍网购的便捷。全缓冲刷新侧率就是站在OS的视角上效率最高的方式。

为什么需要C语言需要提供缓冲区

最核心的一点就是提高用户层的效率。如果不提供缓冲区来缓存用户层写入的数据,直接将用户层的数据采取无缓冲侧率直接写入操作系统内核中,系统调用将高频次被调用。但是,操作系统中同一时刻跑着许许多多的进程,而每一个进程都会有读写数据的操作,。这势必导致操作系统的整体运行效率不佳,并且高频次的系统调用还会影响内核运行的稳定性。同时,用户层的进程执行代码时还需要等待每一次读写操作,这也导致进程的运行从逻辑层面看是串行的,整体的运行效率也不佳。而C语言提供的缓冲区很好的解决了进程运行从逻辑上看是串行的问题。上层的接口,将读写的数据先交给到语言级别的缓冲区中,然后继续执行自己的代码。此时进程运行的逻辑就变成了并行,效率较高。对于操作系统而言,它只需要定期以某种刷新策略,将语言级别缓冲区的内容通过系统调用将写入到内核,或是从内核缓冲区用系统调用读入语言级别缓冲区。这样操作系统的运行效率也会提高。

缓冲区还配合了printf、fprintf这个类C语言接口进行可对数据的格式化操作。将** 数据格式化后提高了与内核缓冲区数据交换的效率和数据交换的灵活性**。

C语言提供的缓冲区在哪里呢?

C语言提供的文件缓冲区在我们前面学习fd时谈到的FILE结构体里。这也是为什么我们在使用C语言提供的文件操作接口如fflush、fprintf时,需要传的是FILE*的指针。以fprintf为例,当我们要在显示器上输出内容时,我们传的是stdout,而stdout本质是一个FILE对象的指针,它指向的地址内部就有维护的缓冲区空间和缓冲区信息字段。第二个参数接收的是格式化字符串的指针,fprintf的函数内一定会将格式化字符串的内容写入到stdout内部的缓冲区中,并填写对应的缓冲区信息字段。然后,根据上层指定的不同刷新策略将FILE结构体管理的缓冲区内的数据通过系统调用写入到磁盘对应的文件上。

这个C语言的缓冲区具体有多少个呢?答案是我们打开多少个文件,他就有多少个。因为每一个打开的文件都会创建对应的FILE对象,每个FILE对象不仅需要维护不同的fd,还需要维护不同的缓冲区和缓冲区信息字段。 所以FILE是属于用户层的,它不属于系统层。当我们调用fopen这类的C接口时,系统层面上它会调用系统调用open从磁盘上以特定选项打开一个文件。然后,语言层面上fopen会malloc创建FILE对象,其中包含fd、缓冲区这类的成员。最后返回给用户层的是FILE*。

解释一下上面的问题

在这里插入图片描述

在这里插入图片描述

为什么直接运行程序时,打印顺序是正常的代码顺序呢?因为C语言将写入显示器数据的刷新策略定位行刷新,即碰到’\n’就刷新。所以,代码以正常的顺序打印内容。

下面再解释一下为什么进行重定向后文件内的内容如图例。首先,系统调用接口的数据被提前写入到lg.txt中,因为调用write在调用时会让数据从内核缓冲区刷新到磁盘上。C接口如何在系统调用后写入文件呢?因为往文件写入数据,C语言缓冲区采取的缓冲刷新策略是全缓冲刷新的方式,即将缓冲去打满时,才会调用write将缓冲区内的数据写入内核,再由OS将内核缓冲区数据写入磁盘。显然这几句代码还不至于打爆缓冲区。刷新到内核是因为进程退出了。至于为什么写入两份,是因为fork创建了子进程。而父进程退出时,将C语言缓冲区的数据写入内核,此时缓冲区进行了读写操作,触发了子进程的写实拷贝机制。此时,子进程将父进程的代码和数据各自拷贝私有一份。当子进程退出时,又将它对应缓冲区的内容刷新到了内核。所以,C接口的内容又刷新了一遍。这也是上面实验现象的原理。

模拟实现C文件库

模拟实现

模拟实现一份Cstdio库主要对于前面所学知识的一个提炼,本意是为了更好地学习,相应的模拟实现的库也比较简陋。下面就以一份demo代码来介绍一下需要模拟实现的结构以及相应的接口。
在这里插入图片描述

需要实现的接口有_fopen、_fwrite、_fflush以及_fclose。下面就采用声明实现分离的方式来模拟实现stdio库。首先就是需要创建一个.h文件和一个.c文件。紧接着,在.h文件中进行防止重复包含头文件的预处理。再定义一个文件结构体,别的先不管,对应的文件描述符字段是一定要有的。再声明一下上面的四个接口。
在这里插入图片描述

然后,在.c文件中对声明的接口进行实现。先实现模拟实现一下fopen接口。fopen其实是对系统调用open进行了封装。所以,使用open前需要包含系统调用open所需的头文件。

接下来第一步,使用系统调用open打开文件,这里模拟实现,仅仅实现"w"、“a"以及"r"这三种打开模式。需要定义两个整型变量。一个用于传递对应的打开方法,一个用于获取open返回的文件描述符。先通过_fopen的第二个参数flag来进行一个判断,若传入的参数为"w”,说明是以写方式打开,open对应需要参入的对应的flags为O_CREAT| O_WRONLY | O_TRUNC。若传入的参数为"a",说明是以追加写方式打开,open对应需要参入的对应的flags为O_CREAT | O_WRONLY | O_APPEND。若传入的参数为"r",说明是以读方式打开,open对应需要参入的对应的flags为O_RDONLY。紧接着对打开失败做一下特殊处理。

第二步就是使用malloc开辟动态内存,用动态内存创建_FILE结构体成员指针,并使用open返回的fd来初始化对应文件结构体的fileno字段。最后将创建_FILE结构体成员指针返回上层。

在这里插入图片描述

模拟实现了fopen,下面再实现一下fclose。fclose的实现思路如下,使用系统调用close将文件结构体指针的fileno字段所对应的文件描述符关闭,然后free释放掉文件结构体指针指向的堆区空间。

在这里插入图片描述

模拟实现fwrite实现思路就是通过调用系统调用write完成对文件的写入。

在这里插入图片描述

接下来对上面实现的接口进行一个简单的测试。
在这里插入图片描述
下面以追加写的方式再验证一下。
在这里插入图片描述

上面我在linux系统下以linux系统提供的系统调用接口封装了一套类似于Cstdio库。可以窥探触C语言代码是如何做到跨平台的。跨平台是根据条件编译裁剪出不同系统下的对应系统调用接口代码。若是fopen跑在windows环境下,底层就是使用了windows提供的系统调用。若是fopen跑在macOS环境下,底层就是使用了macOS提供的系统调用。这也是为什么跨平台项目的代码复用性需要有保证。

上面模拟实现部分,将文件操作的基本功能搭建出来了。下面引入语言级别的缓冲区,语言级别的缓冲区的知识覆盖量之广我无法一一讲解,所以就是以一个捡漏demo的方式进行一个模拟实现。然后谈一谈与它相关的周边知识。

首先,为了简单模拟就以数组模拟的方式来模拟实现缓冲区。
在这里插入图片描述

这里插一嘴题外话,为什么常说键盘显示器是字符设备?因为我们使用scanf从键盘读取到的内容都是字符,虽然在上层看了是键盘获取到的字符写入到整型变量中,那是因为scanf在格式控制上动了手。归根结底键盘还是字符设备,将它读取到的内容转化成整型势必需要借助缓冲区,而从缓冲区读取数据的格式决定了数据被上层以哪种类型拿走。

下面就在代码中引入缓冲区以及对应的刷新策略,使用数组模拟缓冲区,并且定义了三种刷新策略。
在这里插入图片描述
下面调整一下对应的接口。
在这里插入图片描述
修改一下fwrite接口的处理逻辑,将数据先缓存到缓冲区中。模拟不同的刷新策略进行对数据的操作。这里由于使用后静态数组模拟的原因,没有对异常情况做处理。以及对于行刷新策略的完善程度还有待考究,这里先通过实验简单看一看我们模拟实现的缓冲区。
在这里插入图片描述
分别以行刷新和全刷新来看。
在这里插入图片描述

在这里插入图片描述
当刷新策略设置成全缓冲刷新时,进程退出后,缓冲区的内容并没有如愿刷新到文件中。这是因为,我们在关闭文件描述符时强制刷新缓冲区内容。下面就模拟实现_fflush接口来进一步完善一下代码。_fflush的实现思路就是将缓冲区内的数据通过write系统调用写入到对应文件中。

在这里插入图片描述
在这里插入图片描述

缓冲区的意义

为什么需要缓冲区呢?本质上是解决效率层面上的意义。假如上层用户每调用一次fwrite这样的接口都需要调用底层的系统调用时,势必造成系统资源极大程度的浪费。缓冲区的作用是囤积一批数据,在某些条件成立时,再通过系统调用接口进行缴费,这样减少系统调用的调用次数,可以有效的提高系统运行的效率。这就好比快递驿站每收到一个快递都直接将快递送往区域的统一收集派发站点,这样运行效率肯定非常低下。若是,在早晚时间段囤积的包裹统一送往区域的统一收集派发站点,这样一两趟就送完一天的快递,效率上肯定是更加好了。通过模拟实现缓冲区,可以比较直观感受到这一结论,这也是为什么上面花了一些篇幅来进行代码和实验的描述。

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

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

相关文章

某手sig3-ios算法 Chomper黑盒调用

Chomper-iOS界的Unidbg 最近在学习中发现一个Chomper框架,Chomper 是一个模拟执行iOS可执行文件的框架,类似于安卓端大名鼎鼎的Unidbg。 这篇文章使用Chomper模拟执行某手的sig3算法,初步熟悉该框架。这里只熟悉模拟执行步骤以及一些常见的…

MySQL版本选择与安装

MySQL版本选择与安装 MySQL 5.5 优点: 稳定性:5.5版本是长期支持(LTS)版本,因此它非常稳定,被广泛部署在生产环境中。 兼容性:与旧版本的MySQL和各种应用程序有很好的兼容性。 缺点: 过时:…

【飞行器原理学习】——1. 机翼及机翼参数

飞行器原理学习——1.机翼 一、 概述 飞机的各种机翼是飞机的控制面 通过铰链、钢索、液压等方式连接在机身上 操纵面运动时,会改变机翼的弧度和形状,使流经的空气发生偏转,从而影响空气动力的大小。使飞机围绕着3轴运动 二、机翼的操纵面…

TS语言自定义脚手架

初始化 新建文件夹初始化命令 npm init -ytsc --initnpm i types/nodenpm i typescript# 处理别名npm i -D tsc-alias -y 表示选项都为yes 安装ts相关依赖 新建相关文件 bin 文件夹 src文件夹 commands 文件夹 (命令 utils 文件夹 (封装方法) index.t…

lab4 CSAPP:Cachelab

写在前面 最简单的一集 实验室分为两个部分。在A部分中,实现一个缓存模拟器。在B部分中,编写一个矩阵针对高速缓存性能优化的转置功能。 感觉是比较经典的问题,之前在体系结构的课程中接触过,终于能通过lab实操一下了。 实验目…

VScode C语言学习开发环境;运行提示“#Include错误,无法打开源文件stdio.h”

C/C环境配置 参考: VS Code 配置 C/C 编程运行环境(保姆级教程)_vscode配置c环境-CSDN博客 基本步骤 - 安装MinGW-W64,其包含 GCC 编译器:bin目录添加到环境变量;CMD 中输入gcc --version或where gcc验证…

雷龙CS SD NAND(贴片式TF卡)测评体验

声明:非广告,为用户体验文章 前段时间偶然获得了雷龙出品的贴片式 TF 卡芯片及转接板,到手的是两片贴片式 nand 芯片搭配一个转接板,其中有一片官方已经焊接好了,从外观来看,正面和背面设计布局合理&#x…

伯克利 CS61A 课堂笔记 11 —— Mutability

本系列为加州伯克利大学著名 Python 基础课程 CS61A 的课堂笔记整理,全英文内容,文末附词汇解释。 目录 01 Objects 02 Example: Strings Ⅰ Representing Strings: the ASCII Standard Ⅱ Representing Strings: the Unicode Standard 03 Mutatio…

DEX-EE三指灵巧手:扩展AI与机器人研究的边界

DEX-EE三指灵巧手,由Shadow Robot与Google DeepMind合作开发,以其先进技术和设计,正在引领AI与机器人研究的新趋势。其高精度传感器和灵活的机械手指,能够捕捉复杂的环境数据,为强化学习实验提供了可靠支持。 Shadow R…

在ubuntu上用Python的openpyxl模块操作Excel的案例

文章目录 安装模块读取Excel数据库取数匹配数据和更新Excel数据 在Ubuntu系统的环境下基本职能借助Python的openpyxl模块实现对Excel数据的操作。 安装模块 本次需要用到的模块需要提前安装(如果没有的话) pip3 install openpyxl pip3 install pymysql在操作前,需…

【STM32】外部时钟|红外反射光电开关

1.外部时钟 单片机如何对外部触发进行计数?先看一下内部时钟,内部时钟是接在APB1和APB2时钟线上的,APB1,APB2来自stm32单片机内部的脉冲信号,也叫内部时钟。我们用来定时。同样我们可以把外部的信号接入单片机,来对其…

深入了解 DevOps 基础架构:可追溯性的关键作用

在当今竞争激烈的软件环境中,快速交付强大的应用程序至关重要。尽管如此,在不影响质量的情况下保持速度可能是一项艰巨的任务,这就是 DevOps 中的可追溯性发挥作用的地方。通过提供软件开发生命周期 (SDLC) 的透明视图…

Django+Vue3全栈开发实战:从零搭建博客系统

文章目录 1. 开发环境准备2. 创建Django项目与配置3. 设计数据模型与API4. 使用DRF创建RESTful API5. 创建Vue3项目与配置6. 前端页面开发与组件设计7. 前后端交互与Axios集成8. 项目优化与调试9. 部署上线10. 总结与扩展10.1 项目总结10.1.1 技术栈回顾10.1.2 项目亮点 10.2 扩…

深度学习之图像回归(一)

前言 图像回归任务主要是理解一个最简单的深度学习相关项目的结构,整体的思路,数据集的处理,模型的训练过程和优化处理。 因为深度学习的项目思路是差不多的,主要的区别是对于数据集的处理阶段,之后模型训练有一些小…

解决 Mac 只显示文件大小,不显示目录大小

前言 在使用 mac 的时候总是只显示文件的大小,不显示文件夹的大小,为了解决问题可以开启“计算文件夹”。 步骤 1.进入访达 2.工具栏点击“显示”选项,点击 “查看显示选项” 3.勾选 显示“资源库"文件夹 和 计算所有大小 或者点击…

从零开始学习PX4源码9(部署px4源码到gitee)

目录 文章目录 目录摘要1.gitee上创建仓库1.1 gitee上创建仓库PX4代码仓库1.2 gitee上创建子仓库2.固件在gitee部署过程2.1下载固件到本地2.2切换本地分支2.3修改.gitmodules内容2.4同步子模块仓库地址2.5同步子模块仓库地址更新(下载)子模块3.一级子模块和二级子模块的映射关…

2025年AI数字人大模型+智能家居HA引领未来(开源项目名称:AI Sphere Butler)

介绍 开源项目计划:AI Sphere Butler 打造全方位服务用户生活的AI全能管家——代号**“小粒”**(管家名称可以随意自定义) GitHub地址:https://github.com/latiaoge/AI-Sphere-Butler 项目名称:AI Sphere Butler&…

UGUI RectTransform的SizeDelta属性

根据已知内容,SizeDelta offsetMax - offsetMin 1.锚点聚拢情况下 输出 那么此时SizeDelta就是UI元素的长宽大小 2. 锚点分散时 引用自此篇文章中的描述 揭秘!anchoredPosition的几何意义! SizeDelta offsetMax - offsetMin (rectMax…

三甲医院网络架构与安全建设实战

一、设计目标 实现医疗业务网/卫生专网/互联网三网隔离 满足等保2.0三级合规要求 保障PACS影像系统低时延传输 实现医疗物联网统一接入管控 二、全网拓扑架构 三、网络分区与安全设计 IP/VLAN规划表 核心业务配置(华为CE6865) interface 100G…

机器学习笔记——常用损失函数

大家好,这里是好评笔记,公主号:Goodnote,专栏文章私信限时Free。本笔记介绍机器学习中常见的损失函数和代价函数,各函数的使用场景。 热门专栏 机器学习 机器学习笔记合集 深度学习 深度学习笔记合集 文章目录 热门…