Linux文件IO——缓冲区磁盘上的文件管理

前言

什么是缓冲区?

  • 缓冲区是内存空间上的一小段内存,我们平常在写程序的时候,其实是很难感知到缓冲区的存在的,接下来看一段代码,可以很好地体现缓冲区的存在。
#include<stdio.h>
#include<unistd.h>
int main()
{printf("这是我关于缓冲区的一次小测试");sleep(2);return 0;
}

运行后

789721af98364a54a4693dbab0234c5d.png

2秒后

0f855b0f451f468faf696a7b9d3b03a8.png

按照我们对于平常代码执行顺序的理解,我们应该先打印printf中的文本信息,在执行sleep函数休眠2秒,但实际并不是这样,事实恰恰相反。

对上述代码做一次改动,再次运行

#include<stdio.h>
#include<unistd.h>
int main()
{printf("这是我关于缓冲区的一次小测试\n");sleep(2);return 0;
}

我们添加在文本信息后添加了换行符"\n",

结果

8957a6a36be34eef91a0659c914b2fa0.png

  这一次,符合我们心中的预期,先打印文本信息,在进行休眠。

  这其实就是缓冲区在发挥作用,有人说,缓冲区让我对代码执行产生误解,我摸不清代码怎么执行了,为什么要存在它?

  为什么会有缓冲区,缓冲区的作用是什么?这就是今天我们所探究的。

缓冲区

缓冲区是什么?

  • 缓冲区是内存空间上的一段内存,大小由操作系统分配。
  • 一个暂时存储数据的区域 

缓冲区存在于哪里?

  • 缓冲区存在于内存空间

1.为什么要有缓冲区呢?

  缓冲区是一个暂时存储数据的地方,当数据足够,就会向目标地点进行输送,我们现实生活中也存在这样的地方,就是我们日常的快递站。

  当你需要将礼物送给你的朋友,而你们相距甚远的时候,比如一个在江西,一个在黑龙江,你会亲自将礼物送到他手上吗?

b7d0c19fd42f4da2a672f42128627b9d.jpeg

 

  答案显然是否定的,那样成本不仅高,还会耗费你大量的时间,这时候,我们都会选择——寄快递,让快递员替我们将礼物送出,当你将快递交给快递站,他们也不会立刻将你的快递发出,送一个快递,对他们来说和让你自己送没有区别,不过耗费的不是你的时间和成本,快递站也有他们自己的寄送规则。

快递站寄送规则

  • 立即寄送
  • 等到一定数量寄送
  • 存满快递寄送

以上三种规则,第一种时间快,但成本太大,收益几乎没有,甚至倒贴;

第二种,不仅能有收益,还能保障效率;

第三种,牺牲时间换取收益,收益提高了,但用户等待的时间很长。

快递站的这三种寄快递规则,也就相当于我们缓冲区的推送(刷新)数据规则

  1. 实时刷新
  2. 行刷新
  3. 存满刷新

通过上述,我们理解的缓冲区的作用

  • 提高效率,暂时存储数据,待到数据数量达到一定时,刷新推送数据到磁盘文件当中,避免了多次读写带来的时间损耗,降低效率                         

缓冲区的运作过程

   当一个文件被进程打开的时候,它就不存在于磁盘之上,而是存在于内存之上,这是因为操作系统会将其属性和内容均加载到内存上,属性我们知道,操作系统内核会形成一个struct file类型的结构体来保存属性。

那文件内容呢?

  •   被操作系统拷贝到缓冲区当中,内核文件管理不仅会创建文件结构体,还会为文件内容分配一段内存空间,也就是我们的缓冲区,将文件内容放入其中,并为文件结构体添加相关指针,用来管理这一段内存空间。526bfb9dec18496eb0379b538c8d4134.jpeg
  •   当进程对文件内容进行操作的时候,实际并不是对磁盘上的文件进行操作,而是对内存上的文件进行操作,修改的,增加的,或者删除的内容,都是在对内存上缓冲区里面的内容操作,当操作结束,关闭文件,结束进程,会自动刷新缓冲区,将缓冲区内的内容拷贝到磁盘当中。cd0f3563968d4daab67e2164acfc1d93.jpeg

  所以,缓冲区的运作过程

  1. 文件被打开,文件内容从磁盘上拷贝到缓冲区当中
  2. 用户进程对文件内容操作,实际是对内存中缓冲区中的内容操作
  3. 关闭文件,用户进程结束,缓冲区刷新,其中的数据被重新拷贝到磁盘文件中。

总结:对文件内容的操作,本质就是文件内容数据的来回拷贝。

缓冲区的刷新规则

通过上面的学习

我们知道缓冲区的刷新的三个规则

  • 即时刷新
  • 行刷新
  • 全满刷新

在我们开头的代码中,为什么加换行符之前不刷新,加了换行符后立刻刷新呢?

除了等待进程退出,自动刷新缓冲区

我们还有两种方式刷新缓冲区

  • 使用换行符号强制刷新
  • 使用flush函数强制刷新

什么时候行刷新,什么时候全满刷新呢?

  • 一般对于显示器文件,我们采用行刷新的策略
  • 对于磁盘文件,我们一般全满刷新。

来看一个样例

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{fprintf(stdout,"C: hello fprintf\n");printf("C: hello printf\n");fputs("C: hello fputs\n",stdout);const char*str="system call:hello write";write(1,str,strlen(str));fork();return 0;
}

当我们向显示器当中打印的时候

结果:

0b754e69ef2f47d5bb5aa348cd1f431b.png

当我们向文件当中打印的时候

结果:

0c4dff89d9a844478c4b29211f8848bd.png

这是为何?

  1. 当我们向显示器中打印信息的时候,为了照顾用户体验,显示器的刷新方式默认为行刷新,并且每个打印语句都有\n,当执行到fork的时候,这时候的缓冲区已经被刷新完了,缓冲区当中已经没有数据了。
  2. 当我们重定向到文件中的时候,刷新方式变成了全缓冲,即等到缓冲区数据到达一定体量再刷新数据,切刷新方式为全缓冲的时候,缓冲区也会变大,这时候代码执行到fork语句的时候,缓冲区内还有数据。
  3. 子进程和父进程共享代码和数据,如果执行到fork函数的时候,父进程缓冲区中还有数据,子进程也会一并将其继承下来。


  所以,当父子进程都退出的时候,父进程缓冲区的数据被刷新,子进程缓冲区的数据被刷新,子进程缓冲区的数据是继承父进程的,所以会重复打印文本信息。

C式缓冲区和内核缓冲区

 当我们重定向,将信息输出到指定文件中的时候

acbcc618e79e4666adec2fcc0a18b759.png

  我们可以发现,C语言函数接口的语句,每个都打印了两次,而系统接口函数的文本信息仅仅只打印了一次,这是为什么?

  •      利用子进程会继承父进程数据的特点,我们也可以让子进程继承缓冲区中的数据,实现两次打印的效果,但这都是针对C语言函数接口而言的,对于系统函数接口write,它写入的文本信息其实并不在父进程的缓冲区当中,而是在内核缓冲区,直接和操作系统打交道的缓冲区,而父进程和子进程的缓冲区,则是C语言自己维护的缓冲区。
  •     也就是说,此时共有三个缓冲区,父进程一个C语言缓冲区,子进程一个C语言缓冲区,一个内核缓冲区,父子进程的缓冲区中的数据是一样的,重复的,内核缓冲区的数据则是系统调用接口write写入的,独一份的,当进程退出的时候,父子进程缓冲区数据被刷入内核缓冲区,再和内核缓冲区的数据一同被刷新拷贝到磁盘文件当中。44368a9d0965483e916b0eedb0569866.jpeg

  缓冲区的作用就是提高效率,解决各个设备读写速度不匹配带来的低速拖累高速的情况,C式缓冲区存在的目的也是为了减少用户进程与内核缓冲区的交互,希望数据积攒到一定体量再写入内核缓冲区,就像内核缓冲区和磁盘文件的存储关系一样。

  如果想直接写入内核缓冲区,就使用操作系统提供的系统调用接口,它们会直接对内核缓冲区读写,如read和write;如果想写入C式缓冲区,则使用C语言提供的调用接口,如printf和scanf。


总结

  • 无论是C式缓冲区还是内核缓冲区,它们的存在目的都是为了避免频繁读写,从而提高整体效率
  • 使用C调用接口,读写经过C式缓冲区;使用系统调用接口,读写跳过C式缓冲区,直接进入内核缓冲区。

磁盘上的文件管理

文件分为被打开、未被打开两种状态,被打开的文件加载到内存空间当中,由内核操作系统的文件系统同一管理,那未被打开的文件呢?

  • 未被打开的文件存在于磁盘之上,由磁盘的文件系统管理 

接下来,进入磁盘的文件管理学习!

 磁盘物理结构

首先来认识一下,磁盘

e8d27ce60f004b2f90f492817fccb039.png

磁盘由许多盘片组成,每个盘片由有正反两个面,每个面分配一个磁头,也就是说,有多少个盘面,就有多少个磁头。

俯瞰盘片

  • 盘片分成许多空心圆,每个空心圆的边缘都是一圈磁道。
  • 磁道平均分,每一段磁道就是扇区,扇区是磁盘存储数据的基本单位。
  • 越内的扇区越小,但存储的数据量不变,因此,它的数据密度更大。

注:扇区是磁盘存储数据的基本单位,大小一般为512个字节。

 CHS方法

  1. 定位磁道
  2. 定位盘片
  3. 定位扇区

磁盘逻辑结构

如图,将卷成圆形的磁带抽出来,就是一条长带。

同样,我们也可以将磁盘看成卷起来的磁带,将其扯出来,也是一条长带。

这是一种线性结构,因此,我们可以将磁盘看成一个数组,一个元素就是一个扇区,对文件的查找,就可以转换成对数组元素的查找!


磁盘的大小很大,而一个扇区的大小仅仅只有512字节,如果真正按照以扇区作为一个数组元素的方式划分,那么数组元素的数量将达到恐怖的数字,这个数量也是我们不愿意看到的。

借用现实生活中,我们分省分市的划分方法,为了更好地管理文件,磁盘管理系统对文件采用分区分组的管理方式。

先将磁盘分成几个大区,再在一个大区中分组,最后组中存储文件。

组中又分为几个块,这些块就是磁盘文件管理的核心,我们认识一下

  • Data blocks:存储文件内容的块区,该块区中有许多数据块,数据块用来存储对应文件的内容,每个数据块大小为4KB(8个扇区)
  • inode Table:存储的是struct inode结构体,每个结构体大小为128字节,该结构体中保存的是文件的inode编号,属性,数据块指针(指向Data blocks中的数据块,找到文件内容)
  • inode Bitmap:文件位图,来判断一个文件是不是存在,如果该文件存在,对应比特位为1,否则为0。
  • Block Bitmap:用来判断对应的数据块是否被占用,如果被占用,对应比特位为1,否则为0.
  • Group Descriptor Table:存储的是当前组的信息,如属性,数据块使用量,文件量,组大小。
  • Super Block:存放文件系统本身的结构信息。记录的信息主要有:大区中block和inode的总量,未使用的block和inode的量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息,Super Block的信息被破坏,该大区的文件系统结构就被破坏了
  • 一般一个分区中,有好几个组都有Super Block,防止分区文件系统结构被破坏后无法被修复。

在组中,我们不止一次见到了inode。那么inode是什么?

  • inode是文件编号,一个inode代表一个文件,它是文件的唯一标识。

inode虽然是文件的唯一标识,但我们在平常Linux文件操作中,却从未见过,一般都是靠文件名来查找,操作文件,这是为什么?

  • inode是文件的唯一标识符,这是对内核而言的,内核通过inode对文件进行定位。
  • 当文件被创建的时候,内核会为其分配inode,并为其文件名和inode建立映射关系,这个映射关系会被存储到该文件的目录文件当中。
  • 当用户层使文件名操作文件,内核操作系统会先在该目录下找到对应目录关系,用inode替换文件名,再进行后续操作。

Linux操作系统怎么在磁盘中查找文件?

  • 依靠文件路径查找文件,对于一个文件而言,文件路径是唯一的。

原理:

  • 文件系统管理分区首先要进行挂载才能被使用,挂载点一般是一个目录,后序将该目录当成入口访问某个分区上的文件。
  • 查找文件,依靠的是文件路径,通过目录进入分区,在通过文件名和inode的映射关系找到文件。

删除或者修改一个文件的本质是什么?

  • 删除一个文件的本质,就是将其inode Bitmap中的比特位置为0,将其Block Bitmap中的比特位置0,这样在逻辑上,该文件就被删除了,当该inode被重新使用,数据块内容被覆盖,才是真正被删除。这也为恢复文件提供机会,前提是保存了inode。
  • 修改一个文件的本质是通过struct inode结构体中的数据块指针找到对应的数据块,修改数据块中的内容。

创建一个文件的过程是怎么样的?

  • 权限检查:
    • 在创建文件之前,内核会检查当前用户是否有在目标目录中创建文件的权限。
    • 这通常涉及读取目录的权限位和当前用户的权限。
  • 分配inode和数据块:
    • 如果权限检查通过,文件系统会为新文件分配一个inode,用于存储文件的属性(如文件大小、权限、时间戳等)。
    • 同时,文件系统还会为新文件分配必要的数据块,用于存储文件内容。
  • 更新目录结构
    • 文件系统会在目标目录中为新文件创建一个目录项,将文件名与inode关联起来。
    • 这通常涉及更新目录的数据块,以包含新文件的条目。

寄语

   本次文章介绍了缓冲区和磁盘上的文件管理,希望其中的一些观点能够帮助大家学习,缓冲区我个人认为难度较大,难以理解,其中有问题的地方希望大家能够指出来,在评论区进行讨论。

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

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

相关文章

Java中如何去自定义一个类加载器

之前写过一篇&#xff0c;关于 类加载器和双亲委派的文章&#xff0c;里边提到过可以根据自己的需要&#xff0c;去写一个自定义的类加载器&#xff0c;正好有人问这个问题&#xff0c;今天有时间就来手写一个自定义的类加载器&#xff0c;并使用这个自定义的类加载器来加载一个…

X86 RouterOS 7.18 设置笔记六:端口映射(IPv4、IPv6)及回流问题

X86 j4125 4网口小主机折腾笔记五&#xff1a;PVE安装ROS RouterOS X86 RouterOS 7.18 设置笔记一&#xff1a;基础设置 X86 RouterOS 7.18 设置笔记二&#xff1a;网络基础设置(IPV4) X86 RouterOS 7.18 设置笔记三&#xff1a;防火墙设置(IPV4) X86 RouterOS 7.18 设置笔记四…

代码随想录|二叉树|21合并二叉树

leetcode:617. 合并二叉树 - 力扣&#xff08;LeetCode&#xff09; 题目 给定两个二叉树&#xff0c;想象当你将它们中的一个覆盖到另一个上时&#xff0c;两个二叉树的一些节点便会重叠。 你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠&#xff0c;那么…

LDR6500在Type-C转DP视频双向互传方案

LDR6500在Type-C转DP视频双向互传方案中扮演着核心角色&#xff0c;以下是对该方案的详细解析&#xff1a; 一、LDR6500芯片概述 LDR6500是乐得瑞科技针对USB Type-C标准中的Bridge设备而开发的USB-C DRP&#xff08;Dual Role Port&#xff0c;双角色端口&#xff09;接口USB…

Vue3中 ref 与 reactive区别

ref 用途: ref 通常用于创建一个响应式的基本类型数据&#xff08;如 string、number、boolean 等&#xff09;&#xff0c;但它也可以用于对象或数组 返回值: ref 返回一个带有 .value 属性的对象&#xff0c;访问或修改数据需要通过 .value 进行 使用场景: …

CRM企业客户关系管理系统产品原型方案

客户关系管理系统&#xff08;CRM&#xff09;是企业产品应用中的典范&#xff0c;旨在通过信息技术和互联网技术提升企业核心竞争力&#xff0c;优化企业与顾客在销售、营销和服务方面的互动。本作品提供了一套通用型的CRM系统原型模板&#xff0c;涵盖数据管理、审批流程、统…

【算法】 【c++】字符串s1 中删除所有 s2 中出现的字符

【算法】 【c】字符串s1 中删除所有 s2 中出现的字符 eg&#xff1a; s1:“helloworld” s2:“wd” 删除后&#xff1a;s1:“helloorl” 1 双循环匹配并删除–>时间复杂度O(n^2) string 里面的删除函数–>erase std::string::erase 是 C 标准库中用于删除字符串中字符…

利用委托用户控件、窗体之间传值 c#

获取数据方&#xff08;usercontrol111&#xff09;声明 public Func<Tuple<int, int>> GetCurrentResult { get; set; }获取数据方调用 var val GetCurrentResult?.Invoke() ?? new Tuple<decimal, decimal>(0, 0);数据发送方声明与赋值 usercontrol111…

【3-14 STC-pair超级详细的解说】

1. pair的定义和结构 • 基础概念&#xff1a;考察对std::pair模板类的理解&#xff0c;包括其头文件&#xff08;<utility>&#xff09;和基本语法&#xff08;pair<T1, T2>&#xff09;。 • 成员访问&#xff1a;测试对first和second成员变量的使用能力。 • 构…

机器人触觉的意义

机器人触觉的重要性 触觉在机器人领域至关重要&#xff0c;尤其是在自主操作、精细操控、人机交互等方面。虽然视觉和语音技术已高度发展&#xff0c;但机器人在现实世界中的操作仍然受限&#xff0c;因为&#xff1a; 视觉有局限性&#xff1a;仅凭视觉&#xff0c;机器人难…

RabbitMQ消息持久化与Lazy模式对比分析

RabbitMQ消息持久化与Lazy模式对比分析 在RabbitMQ中&#xff0c;消息持久化与Lazy模式是两种不同的机制&#xff0c;分别针对消息可靠性、存储优化等不同维度设计。以下从六个层面进行深度对比&#xff1a; 一、核心目标与作用对象差异 维度消息持久化&#xff08;delivery_…

Search-R1 、 R1-Searcher 和 Search-O1

原文链接:https://i68.ltd/notes/posts/20250307-search-r1/ Search-R1 DeepSeek团队开发的SEARCH-R1模型通过强化学习&#xff0c;让AI学会了自主搜索信息并将其与推理过程无缝结合&#xff0c;性能提升高达26%高效、可扩展的RL训练框架&#xff0c;用于推理和搜索引擎调用&…

linux 命令 tail

tail 是 Linux 中用于查看文件末尾内容的命令&#xff0c;常用于日志监控和大文件快速浏览。以下是其核心用法及常见选项&#xff1a; 基本语法 tail [选项] 文件名 常用选项 显示末尾行数 -n <行数> 或 --lines<行数> 指定显示文件的最后若干行&#xff08;…

某乎x-zse-96加密算法分析与还原

文章目录 1. 写在前面2. 接口分析3. 加密分析4. 算法实现 【&#x1f3e0;作者主页】&#xff1a;吴秋霖 【&#x1f4bc;作者介绍】&#xff1a;擅长爬虫与JS加密逆向分析&#xff01;Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致…

Java常用算法

一、排序算法 排序算法是计算机科学中最基础的算法之一&#xff0c;用于将一组数据按照特定顺序排列。 1.1 冒泡排序&#xff08;Bubble Sort&#xff09; 通过重复遍历列表&#xff0c;比较相邻元素并交换位置&#xff0c;直到列表有序。时间复杂度&#xff1a;O(n)。 pub…

ubuntu 24 安装 python3.x 教程

目录 注意事项 一、安装不同 Python 版本 1. 安装依赖 2. 下载 Python 源码 3. 解压并编译安装 二、管理多个 Python 版本 1. 查看已安装的 Python 版本 2. 配置环境变量 3. 使用 update-alternatives​ 管理 Python 版本 三、使用虚拟环境为项目指定特定 Python 版本…

【后端】【django】Django 自带的用户系统与 RBAC 机制

Django 自带的用户系统与 RBAC 机制 Django 自带的用户系统&#xff08;django.contrib.auth&#xff09;提供了 身份验证&#xff08;Authentication&#xff09; 和 权限管理&#xff08;Authorization&#xff09;&#xff0c;能够快速实现 用户管理、权限控制、管理员后台…

怎样使用Modbus转Profinet网关连接USB转485模拟从站配置案例

怎样使用Modbus转Profinet网关连接USB转485模拟从站配置案例 Modbus转profinet网关可以将Modbus协议转化为profinet协议&#xff0c;以实现设备之间的数据交互。在实际使用过程中&#xff0c;我们需要使用Modbus协议进行设备通讯&#xff0c;而profinet协议则是用于工业自动化…

5.编译链接和宏**

1. 宏&#xff08;考察很多&#xff09;-要求轻松实现宏&#xff0c;很容易出错 #define 机制包括了一个规定&#xff0c;允许把参数替换到文本中&#xff0c;这种实现通常称为宏或定义宏。 下面是宏的声明方式&#xff1a; #define name(参数列表) 内容 参数列表的左括号必…

如何搭建一个适配微信小程序,h5,app的uni-app项目

在vscode搭建 uni-app 项目&#xff08;Vue 3 Vite Pinia uView Plus&#xff09; 一、环境准备 1. 安装 Node.js 确保已安装 Node.js&#xff08;需≥14版本&#xff09;&#xff0c;可通过以下命令检查版本&#xff1a; node -v2. 安装 VSCode 从 VSCode 官网 下载并…