[linux仓库]性能加速的隐形引擎:深度解析Linux文件IO中的缓冲区奥秘

🌟 各位看官好,我是egoist2023

🌍 Linux == Linux is not Unix !

🚀 今天来学习C语言缓冲区和内核缓存区的区别以及缓存类型。

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享给更多人哦!

目录

书接上文

引入

什么是缓冲区

为什么要引入缓冲区

缓冲类型

FILE结构体

扩展

cout和cerr分开

将文件内核缓冲区刷新到磁盘

语言级缓冲区的意义

总结


书接上文

上文我们深入拆解了 Linux “一切皆文件” 设计哲学的核心支撑:从进程task_struct中的files_struct结构出发,明确了文件描述符(FD)作为 “数组下标” 的本质与分配规则;更通过struct filefile_operations结构体的分析,揭示了 Linux 如何以 “上层统一接口(如read/write)、下层设备差异化实现” 的 “类 C++ 多态” 思路,让进程能以一致视角访问异构资源,极大简化了开发复杂度。

然而,当我们聚焦于这些统一接口的实际 I/O 执行细节时会发现:用户空间与内核空间的频繁切换、高速 CPU 与低速外设(磁盘、网络卡等)的速度鸿沟、以及 “单次小数据 I/O” 带来的硬件调度低效,都会成为制约资源访问性能的关键瓶颈。为了解决这些矛盾、在 “统一接口” 的基础上进一步优化 I/O 效率,Linux 引入了缓冲区(Buffer/Cache) 机制 —— 它既是衔接 “用户进程” 与 “底层文件 / 设备” 的核心中间层,也是 “一切皆文件” 哲学在性能层面的重要延伸。接下来,我们将深入解析缓冲区的设计逻辑、实现机制及其在 I/O 流程中的关键作用。

引入

根据前面所学,一个进程要打开文件必须要通过OS进行打开,操作系统为了管理所打开的⽂件,都会为这个⽂件创建⼀个file结构体,内部就要加载一个文件的属性和内容,属性加载到文件属性 ,内容加载到文件缓冲区中。这里的缓冲区指的是文件内核缓冲区。

进程·柒章节,我们讲了三种退出场景,对exit和_exit做了对比,exit属于库函数,终止进程时会主动刷新缓冲区。这里的缓冲区指的是C语言库提供的缓冲区。

什么是缓冲区

缓冲区是内存空间的⼀部分。也就是说,在内存空间中预留了⼀定的存储空间,这些存储空间⽤来缓冲输⼊或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输⼊设备还是输出设备,分为输⼊缓冲区和输出缓冲区。

简单来说,缓冲区的本质就是一段内存空间

为什么要引入缓冲区

这里提供一段小故事供大家进行理解:

事件一:假期之余,张三买了一台大疆action5pro的运动相机记录生活。李四恰巧这几天要出外旅游,打算租台相机拍拍vlog,听闻朋友张三买了台action5pro。李四给张三呼电话:老朋友啊!听闻你这几天买了台相机,这几天打算出外玩玩,可否借你相机一用。热情的张三对朋友也是极为真诚,直接答应了,说道:李四啊!在家里等我一下,我现在开车就把相机送你那。

张三到李四的家费了半天的时间。

事件二:待张三回来后,发现街拍套装忘给李四送过去了,但此时他自己也有事情要做。张三想了一想,要不把自己的东西放菜鸟驿站上寄送,由快递员代替他讲街拍套装送到李四那的菜鸟驿站。

快递员从张三的菜鸟驿站到李四的菜鸟驿站也是需要花费半天的时间。只不过这个动作主体由张三替换成了快递员。这意味着什么呢?张三可以在这段本是要他送货的时间转移到了快递员,允许自己做更多的工作了。

事件三:我们清楚菜鸟驿站的货并不是一收到货就马上派送的,可以等货多了再进行派送。

相机和街拍套装就是数据,是缓存的角色;

菜鸟驿站就是所谓的缓冲区;

将数据给菜鸟本质就是拷贝;

菜鸟驿站允许等货多了再进行派送就是允许数据在缓冲区中积压。这样做的目的一次就可以刷新多次数据,变相减少IO次数。

读写⽂件时,如果不会开辟对⽂件操作的缓冲区,直接通过系统调⽤对磁盘进⾏操作(读、写等),那么每次对⽂件进⾏⼀次读写操作时,都需要使⽤读写系统调⽤来处理此操作,即需要执⾏⼀次系统调⽤,执⾏⼀次系统调⽤将涉及到CPU状态的切换,即从⽤⼾空间切换到内核空间,实现进程上下⽂的切换,这将损耗⼀定的CPU时间,频繁的磁盘访问对程序的执⾏效率造成很⼤的影响。

为了减少使⽤系统调⽤的次数,提⾼效率,我们就可以采⽤缓冲机制。⽐如我们从磁盘⾥取信息,可以在磁盘⽂件进⾏操作时,可以⼀次从⽂件中读出⼤量的数据到缓冲区中,以后对这部分的访问就不需要再使⽤系统调⽤了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作⼤ 快于对磁盘的操作,故应⽤缓冲区可⼤ 提⾼计算机的运⾏速度。⼜⽐如,我们使⽤打印机打印⽂档,由于打印机的打印速度相对较慢,我们先把⽂档输出到打印机相应的缓冲区,打印机再⾃⾏逐步打印,这时我们的CPU可以处理别的事情。可以看出,缓冲区就是⼀块内存区,它⽤在输⼊输出设备和CPU之间,⽤来缓存数据。它使得低速的输⼊输出设备和⾼速的CPU能够协调⼯作,避免低速的输⼊输出设备占⽤CPU,解放出CPU,使其能够⾼效率⼯作。

缓冲类型

由于我们的缓冲区是允许数据进行积压的,那么肯定有自身的一套积压规则存在:

标准I/O提供了3种类型的缓冲区。

  • 全缓冲区:这种缓冲⽅式要求填满整个缓冲区后才进⾏I/O系统调⽤操作。对于磁盘⽂件的操作通常使⽤全缓冲的⽅式访问。
  • ⾏缓冲区:在⾏缓冲情况下,当在输⼊和输出中遇到换⾏符时,标准I/O库函数将会执⾏系统调⽤操作。当所操作的流涉及⼀个终端时(例如标准输⼊和标准输出),使⽤⾏缓冲⽅式。因为标准I/O库每⾏的缓冲区⻓度是固定的,所以只要填满了缓冲区,即使还没有遇到换⾏符,也会执⾏I/O系统调⽤操作,默认⾏缓冲区的⼤⼩为1024。
  • ⽆缓冲区:⽆缓冲区是指标准I/O库不对字符进⾏缓存,直接调⽤系统调⽤。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。

除了上述列举的默认刷新⽅式,下列特殊情况也会引发缓冲区的刷新:

  1. 缓冲区满时;

  2. 执行fflush语句;

验证stderr是否带缓冲区:

int main() 
{close(2);int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (fd < 0) {perror("open");return 0;}perror("hello world");close(fd);return 0;
}

将2号⽂件描述符重定向⾄⽂件,由于stderr没有缓冲区,“hello world”不⽤fflush就可以写⼊⽂件。

cat log.txt

hello world : Success

FILE结构体

printf("hello linux,hello everyone!");
sleep(3);
return 0;

在上面这段代码中,printf执行完毕后,并未打印到显示器上,而是3s后显示。那么在sleep期间,我们的数据在哪呢?毫无疑问,在缓存区中。

是什么缓冲区呢?C语言库提供的缓冲区。那这个缓冲区又在哪呢?FILE结构体中。

  1. struct FILE 本质是一个结构体
  2. C语言上,输入输出格式化
  3. C访问文件,都是通过FILE访问的,包括stdin,stdout, stderr!

FILE结构体内部为我们维护:语言级别的缓冲区空间!

struct FILE
{int fd;char *inbuffer;char *outbuffer;
}

如何理解printf,scanf 的格式化过程?

int a = 123456;
printf("%d", a);
  1. 格式化
  2. 格式化结果刷新到FILE缓冲区中
  3. 检测是否需要刷新
  4. 满足条件时调用write系统调用刷新到文件内核缓冲区中

文件内核缓冲区的刷新方式比较特殊,是单独的执行流,这里不做考虑。

由此我们也可以回答上一章节留下的疑惑,为什么close该文件后,并没有显示打印的内容。

int main()
{close(1);int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);if(fd<0){perror("fd");return 1;}printf("hello file,fd:%d\n",fd); // stdout -> 1close(fd);return 0;
}

一般向普通文件写入是全缓冲。此时程序还没结束,文件被关闭了,而我们的内容还在FILE内缓冲区中,没有刷新到文件内核缓冲区中,所以没有显示打印内容。

那为什么fflush刷新呢?想必底层是调用write系统调用,实际上就是如此。

fflush底层调用write系统调用,将FILE内缓冲区强制刷新到文件内核缓冲区中,文件内核缓冲区在刷新出来给我们看到。(由语言到内核的过程)

扩展

int main()
{//C提供const char *s1 = "hello printf\n";printf(s1);const char *s2 = "hello fprintf\n";fprintf(stdout,s2);const char *s3 = "hello fwrite\n";fwrite(s3,strlen(s3),1,stdout);//系统const char *s4 = "hello write[syscall]\n";write(1,s4,strlen(s4));//创建子进程fork();return 0;
}

在上面这段程序当中, 执行过程是:调用printf、fprintf、fwrite三个C库函数和系统调用write,再创建一个子进程,我们来观察两种情况:

情况1:向显示器进行写入 --> 刷新策略:行刷新

由于是行刷新策略,当遇到换⾏符时语言级缓冲区会自动刷新到文件内核缓冲区中,因此是有规律地刷新到文件内核缓冲区中,此时创建子进程,之后程序结束需要情况语言级缓冲区,但缓冲区上并没有数据。最终文件内核缓冲区再刷新出去,向显示器写入数据,给我们看到符合我们需求的结果。

情况2:向文件进行写入 --> 刷新策略:全缓冲

但当向文件进行写入时,打印结果并不是符合我们所想的,为什么同样的代码会有两种不同的结果呢?

首先,向文件进行写入一般的刷新策略是全缓冲,前三个都是C语言提供的库函数,因此都是向FILE内缓冲区进行写入,而write是系统调用,写到文件内核缓冲区中,因此先看到write[syscall]这个结果并不例外。

但是为什么有两段同样的打印结果呢?

write结束后,此时会调用fork创建子进程,之后程序就结束了。由于程序结束了,就需要清空缓冲区,而此时我们是父子进程啊,而子进程清空缓冲区实际上就是修改数据啊,修改就要发生写时拷贝,并不会影响父进程的缓冲区。父进程再进行清空缓冲区,呈现出刷新两次的结果。(即父子进程各自执行退出逻辑,刷新自己的缓冲区)

cout和cerr分开

将文件内核缓冲区刷新到磁盘

int fsync(int fd);

强制将指定文件描述符(fd)对应的文件的所有已修改数据和元数据从操作系统缓存(如页缓存)刷新到磁盘。

语言级缓冲区的意义

  • IO相关函数与系统调⽤接⼝对应,并且库函数封装系统调⽤,所以本质上,访问⽂件都是通过fd访问的,而调用系统调用是有成本的(浪费时间)。
  • 就拿C语言的malloc来说,我们在造vector轮子时,说过扩容尽量往1.5倍和2倍去靠,是为了减少扩容次数,频繁扩容只是原因之一,并不是重点。malloc底层调用了系统调用,这意味着会频繁调用系统调用,要花费操作系统的时间。
  • C语言为什么要提供缓冲区呢?FILE结构体内的缓冲区允许积压,加速IO函数的调用频率。而使用C语言IO接口,提高了效率,进而使单位时间内,执行C代码行数就变多了,从而也反向提高了IO接口的效率。

总结

本文深入探讨了Linux系统中缓冲区的设计原理与实现机制。文章从Linux"一切皆文件"的哲学出发,分析了缓冲区作为衔接用户进程与底层设备的关键中间层,如何通过减少系统调用次数、平衡CPU与低速I/O设备的速度差异来提升系统性能。内容涵盖缓冲区的三种类型(全缓冲、行缓冲、无缓冲)、FILE结构体实现原理,并通过具体代码示例演示了不同缓冲策略下的I/O行为差异。文章特别解析了C语言缓冲区与内核缓冲区的交互机制,包括缓冲区刷新时机、父子进程间的缓冲区复制问题等,最终阐明了缓冲区设计在平衡统一接口与高效I/O访问之间的重要意义。

 

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

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

相关文章

一、计算机的数据存储

计算机的世界只有0和1。 1.1 进制 十进制整数->二进制整数&#xff1a;除2倒取余二进制->十进制&#xff1a;权值相加法 结论&#xff1a;1位8进制值 3位二进制值&#xff0c;1位十六进制值 4位二进制值 public class JinZhiDemo {public static void main(String[]…

SpringBoot集成XXL-JOB保姆教程

第一步&#xff1a; 下载xxl-job源码到本地&#xff0c;地址如下&#xff1a; xxl-job: 一个分布式任务调度平台&#xff0c;其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线&#xff0c;开箱即用。 第二步&#xff1a; 创建…

Debezium日常分享系列之:Debezium 3.2.2.Final发布

Debezium日常分享系列之&#xff1a;Debezium 3.2.2.Final发布Debezium CoreConnector启动时出现难以理解的错误临时阻塞快照失败可能导致数据丢失的问题修复Debezium for OracleDebezium CoreConnector 启动时出现难以理解的错误 我们解决了一个问题&#xff0c;即连接器会因…

Zoom AI 技术架构研究:联合式方法与多模态集成

一、研究背景与概述 在当今数字化转型加速的背景下,人工智能技术正深刻改变企业协作与沟通方式。作为全球领先的视频会议平台,Zoom 已从单纯的通信工具转型为全面的生产力平台,而其 AI 技术架构是这一转变的核心驱动力。本报告将深入分析 Zoom 的 AI 技术架构,特别是其创新…

排序-快速排序 O(n log n)

快排&#xff1a;1、设定一个中间值 q[ lr >>1 ] , 让左右区间来比较2、左边通过 i 依次比较&#xff0c;如果比这个中间值小&#xff0c;就继续 , 直到不符合3、右边通过 j-- 依次比较&#xff0c;如果比这个中间值大&#xff0c;就继续 &#xff0c;直到不符合4、两边…

【Proteus仿真】定时器控制系列仿真——LED小灯闪烁/流水灯/LED灯带控制/LED小灯实现二进制

目录 0案例视频效果展示 0.1例子1&#xff1a;基于AT89C51单片机的定时器控制小灯闪烁 0.2例子2&#xff1a;基于AT89C51单片机的定时器T0流水灯 0.3例子3&#xff1a;基于AT89C51单片机的定时器控制LED灯带 0.4例子4&#xff1a;基于AT89C51单片机的定时器控制LED闪烁 0…

进阶向:密码生成与管理工具

密码生成与管理工具&#xff1a;从零开始的完全指南在现代数字生活中&#xff0c;密码是保护个人信息和账户安全的第一道防线。随着网络服务的普及&#xff0c;每个人平均需要管理数十个不同账户的密码。一个强大且独特的密码通常应包含12个以上字符&#xff0c;混合大小写字母…

解决 Gitee 中 git push 因邮箱隐私设置导致的失败问题

解决 Gitee 中 git push 因邮箱隐私设置导致的失败问题 在使用 Git 向 Gitee 远程仓库推送代码时&#xff0c;可能会遇到因邮箱隐私设置引发的 git push 失败情况。最近我就碰到了&#xff0c;现在把问题现象、原因和解决方法分享出来。 一、错误现象 执行 git push -u origin …

Flutter的三棵树

“三棵树”是 Flutter 渲染和构建UI的核心机制&#xff0c;理解它们对于掌握 Flutter 至关重要。这三棵树分别是&#xff1a; Widget 树 Element 树 RenderObject 树 它们协同工作&#xff0c;以实现 Flutter 的高性能渲染和高效的响应式编程模型。 Flutter 是声明式的UI&…

同一台nginx中配置多个前端项目的三种方式

目录 第一种方式:配置多个二级域名 第二种方式:配置端口转发(不推荐) 第三种方式:同一个server中基于location配置(重点讲解) 第一种方式:配置多个二级域名 一个域名下面申请多个二级域名,每个二级域名配置一个vue前端项目,这个很好配置,在这里不再详细说明。 …

第二家公司虽然用PowerBI ,可能更适合用以前的QuickBI

第二家公司虽然用PowerBI &#xff0c;可能更适合用以前的QuickBI现在回想一下&#xff0c;第二家公司数据源是MySQL &#xff0c;常规报表是用excel报表&#xff0c;另外还做了一张能发布到web的看板供运营使用。基于基本情况&#xff0c;quickbi 的早期版本是合适的&#xff…

STM32 USBx Device HID standalone 移植示例 LAT1466

关键字&#xff1a;USBx&#xff0c; Device, HID&#xff0c;standalone 1.设计目的 目前 USBx Device standalone 的官方示例较少&#xff0c;不过使用 STM32CubeMX 可以快速地生成 USBx Device 相关类的示例工程&#xff0c;会很方便大家的开发。这里以 NUCLEO-H563 为例&…

python创建并写入excel文件

大家好&#xff0c;这里是七七&#xff0c;今天来跟大家分享一个python创建并写入一个excel文件的小例子&#xff0c;话不多说&#xff0c;开始介绍。首先我们来看一下这一小段代码。import openpyxl# 创建一个新的 Excel 工作簿workbook openpyxl.Workbook()# 获取当前活动的…

react native 出现 FATAL EXCEPTION: OkHttp Dispatcher

react native 出现 FATAL EXCEPTION: OkHttp Dispatcher 报错信息FATAL EXCEPTION: OkHttp DispatcherProcess: , PID: 8868java.lang.NoSuchMethodError: No virtual method toString(Z)Ljava/lang/String; in class Lokhttp3/Cookie; or its super classes (declaration of o…

sentinel实现控制台与nacos数据双向绑定

有两种方式可以实现&#xff1a;Springboot客户端做相应配置&#xff08;推荐&#xff09;修改sentinel-dashboard的源码一、Springboot客户端做相应配置&#xff08;推荐&#xff09;1、添加依赖<dependency><groupId>com.alibaba.csp</groupId><artifac…

Kubernetes (k8s)

Kubernetes (k8s) 以下是一份 ​Kubernetes (k8s) 基础使用教程&#xff0c;涵盖从环境搭建到核心操作的完整流程&#xff0c;附详细命令和示例&#xff1a; &#x1f680; ​一、环境准备&#xff08;3种方式&#xff09;​​ ​1. 本地开发环境&#xff08;推荐&#xff09;​…

三打ANSYS HFSS

2. 激励方式&#xff08;端口&#xff09;详细对比分析在HFSS中&#xff0c;“激励方式”和“端口”这两个词经常混用&#xff0c;但严格来说&#xff0c;“端口”是实现“激励”的一种最主要的方式。端口类型工作原理适用情况优点缺点波端口 (Wave Port)默认首选。计算端口的固…

3.python——数据类型转换

python的数据类型转换分为两种&#xff1a; 隐式转换&#xff1a;自动完成 显式转换&#xff1a;用类型函数转换 隐式转换 # 自动转为浮点数 num_int 123 num_flo 1.23num_new num_int num_flo显式转换 整型 x int(1) # x 输出结果为 1 y int(2.8) # y 输出结果为 2 z …

迅为RK3568开发板OpenHarmonyv3.2-Beta4版本测试-命令终端

将串口连接到开发板的调试串口&#xff0c;进入 OpenHarmony 系统后&#xff0c;会自动进入 OpenHarmony终端&#xff0c;如下图所示&#xff1a;

【面试题】介绍一下BERT和GPT的训练方式区别?

BERT(双向编码器): 预训练任务: 掩码语言模型(MLM):随机掩盖15%的token,其中: 80%替换为[MASK] 10%替换为随机token 10%保持原样 下一句预测(NSP):判断两个句子是否连续(后续版本已移除) 训练特点: 使用双向Transformer编码器 同时利用左右上下文信息 适合理解类任…