> 技术文档 > 深入理解 Linux 磁盘文件存储:inode、扇区与数据块_linux 数据存储

深入理解 Linux 磁盘文件存储:inode、扇区与数据块_linux 数据存储

目录

认识磁盘

磁盘的逻辑抽象

文件系统

格式化

具体情况具体分析

软硬链接

总结


在之前的博客中了解了这么多打开的文件(内存级文件),现在我们再来了解一下文件没有打开是什么样子的?没有被打开的文件是存放在磁盘中的,而我们无非关心的就是以下这些问题:

  1. 路径问题(如何能找到这个文件在磁盘中的位置)
  2. 存储问题(这个文件在磁盘中是如何存储的,总得有个像我们去菜鸟驿站找快递一样的\"编号\"吧,不然大海捞针怎么可以。 )
  3. 获取的问题(之前的博客中就有所了解,文件 = 文件的内容+ 文件的属性,所以我们的磁盘存储文件 = 存储文件的内容 + 存储文件的属性,我们如何获取这两部分内容呢?)

Linux在磁盘中存储文件是以数据块为单位的,而文件的属性是用inode进行存储的(关于数据块和inode在下面的内容中讲解),所以这就表明Linux的文件在磁盘上存储时,属性和内容是分开存储的。说完这个前提条件,现在我们就正式进入磁盘文件的世界。

认识磁盘

这就是我们的机械磁盘,大家现在笔记本电脑中装的都是SSD固态硬盘,这中SSD固态硬盘基于闪存的技术,没有像机械硬盘这些物理部件,依靠电子信号而非物理运动就可以进行读写信息,所以他的读写效率非常块,但是也比较贵,相比之下,传统的机械硬盘需要通过旋转磁头以及盘面来进行数据定位并读取,所以速度就比较缓慢了,但是机械磁盘的价格便宜,所以性价比对我们普通人高一点。

我们传统的机械磁盘如图就有多个磁头,多个盘面,马达等组件构成(注:多个磁头在移动的时候是一起移动的,盘面也是如此),而盘面会被划分为许多同心圆环,这些圆环称为磁道,而每个磁道又被切分为许多小块,每一个小块就是扇区,这一个扇区的大小通常是512B,而我们的多个盘片叠合在一起,同一个同心圆环就构成了一个柱面。(大家可能会说我有时候读的数据也不多,可能也用不了512B,我能不能就读到我想要的大小呢?答案是不可能,我们都磁盘必须按照一个块的大小进行分配,即使你读一个字节,磁盘也会将整个扇区给你读入内存,然后操作系统在内存中取你要的那个字节,这就好比我们谈恋爱的时候,你可能喜欢她的外貌,不喜欢她的身材,或者你喜欢她的身材,不喜欢她的外貌,无论你喜欢她的哪一个部分,只要你喜欢她,就得喜欢她这个人得全部,总不能你把她大卸八块了,就留下你喜欢的吧,那你可真是太残忍了。还有人可能会问那给了我那么多,我又有什么用呢?给你多余的那部分就是属于你的了,这样当你对你想要访问的数据进行写入的时候,这个时候多余的空间就可以供你使用,也算是提前开辟空间为你使用一样。)

硬盘的数据是以 扇区为最小存储单位,物理地址由三个坐标描述:

  1. 柱面:同一半径上的所有磁道组成一个柱面。

  2. 磁头:决定使用哪一面盘片(双面盘片有两个磁头)。

  3. 扇区:在磁道上的具体分段,通常是 512B 或 4KB。

一个扇区的物理地址传统上是 (柱面号, 磁头号, 扇区号);也叫 CHS 寻址.符合我们机械磁盘的运转顺序。

所以我们定位一个区域的过程就是,首先,我们的磁头根据要访问的数据块,使得这些磁头沿着这些盘面的先移动到我们要访问的柱面的位置(确定柱面);确定了柱面之后,我们再根据要访问的数据块选择哪一个盘面,这个时候我们的控制器就会激活对应的磁头(选择磁头,也就是确定盘面);接着就是根据我们所访问的数据块,等待我们的磁盘进行旋转,知道旋转到相应的扇区,这个时候我们的磁头通过电磁感应与磁盘表面的磁性物质相互交互,这样就可以转换为数字信号,这样就获取到了相应的数据,接着就可以传递给我们的主机。

磁盘的逻辑抽象

接下来对磁盘进行逻辑抽象。

对磁盘进行逻辑抽象之前,我不知道大家有没有见过磁带,我小时候就非常讨厌这个东西,因为这个东西就是我小时候英语作业的象征(小时候经常英语老师就会布置作业给我们说,回家好好听磁带,听完让你家长签字),这对于我这个专一喜欢中华文化的人,让我学习英语,简直比杀了我还难受,好了,废话不多说。其实这个磁带我们把它拆开,再把它拉直,就可以看到这个是线性的,在这个线性上就有许多0、1序列,就是存储我们的数据内容的,所以物理上看,它好像是圆形的,其实逻辑上,我们可以把它看成线性的;这样一类比,其实我们的磁盘也是如此,我们也可以将我们的磁盘逻辑上抽象理解为一个线性的结构

所以最后我们的磁盘就被我们抽象为一个基于扇区的数组,这样我们的每一个扇区都有下标,但是我们的磁盘是需要通过CHS进行寻址的,现在抽象为了基于扇区的一个个数组,我该如何反转回去呢?现在假设我们有2个盘面,每个盘面有20个磁道,每个磁道上有100个扇区,假设现在我访问的是这个抽象出来的逻辑扇区数组下标为2888的扇区时,磁盘的CSH分别是多少?

扇区号(S): 2888 / ( 20 * 100 )  =  1  , 2888 % ( 20 * 100 )  =  888  , 888 % 100 = 88
所以S = 88

磁头号(H): 2888 / ( 20 * 100 )  =  1
H = 1

柱面号(C): 888 / 100 = 8
C = 8

所以这样我们就将一个逻辑扇区地址,转换成了CHS地址,同理CHS地址转换为逻辑扇区地址也就是乘以相对应的各个盘面,磁道以及扇区的个数,这样就可以进行逻辑扇区地址(LBA地址)和CHS地址之间的相互转化。这样操作系统使用的就是逻辑扇区地址(LBA)地址,通过转化为CHS地址,这样就可以就我们的操作系统完全不需要关心磁盘是什么结构,只需要对逻辑扇区地址进行操作就可以转换为磁盘的任何一个扇区,进而达到对数据的读取。

CPU跟外设“打交道”的过程。。首先,CPU要知道自己要找哪个外设,于是通过I/O地址确定目标;然后,它把要做的事情,比如读数据还是写数据,通过总线告诉外设;外设收到命令后,会先看看自己能不能处理,如果忙,就让CPU等一会儿,或者用中断告诉CPU什么时候能开始;等外设准备好了,数据就可以传输了——读操作时CPU拿数据,写操作时CPU送数据;最后,操作完成,CPU和外设都更新好状态,为下一次交流做准备。

文件系统

接下来我们就来了解文件系统

现在我们已经了解了在操作系统中我们的磁盘就相当于一个以扇区为大小的数组,假设我们磁盘512GB,而如果我们的扇区大小是512B,这就相当于1073741824个扇区,这么大的扇区我们哪里能管理的过来,所以就可以效仿我们国家一样,按省划分,最后都有中央管理,所以我们也可以将我们的磁盘进行分区,最后统一管理,那么什么是分区呢?相信大家肯定都了解,我们的电脑中会有C盘和D盘,这就是分区。而对于我们的操作系统只要关心好每一个分区的起始和结束位置就可以了。这样接下来我们就可以将这个大一块磁盘进行分区使用,每个分区获得一块大小,接下来在分区内就可以使用分区内的空闲扇区块,最后转换为CSH地址就放到了磁盘上。所以接下来我们就把这么大一个磁盘划分为一个个分区,接下来只需要管理好一个分区,其他的分区采用相同的办法就都管理好了。那分区这么管理呢?这就是我们接下俩要一起了解的内容。

磁盘的部分还有一个Boot Block ,这个块负责的是我们电脑的开机程序,其实我们的内存中有一块特殊区域(ROM)在关机后仍能保存数据。开机时,系统首先执行ROM中的程序,该程序随后会读取磁盘上的Boot Block。Boot Block包含启动代码和分区表信息,通过这些信息可以定位并加载活动分区的引导块(相当于Windows系统中的C盘)。值得注意的是,活动分区引导块的第一个块中存储的正是关键的启动代码。这就是Boot Block。简单了解即可。现在我们开始研究这个分区内容,只要将这个分区管好,其他分区也是相同的道理,这样整个磁盘我们也就一清二楚了。

由图可知,在Linux下,一个分区就被分为了这样几个模块,分别是Super Block , Group Descriptor Table , Block Bitmap , inode Bitmap , inode Table ,Data Blocks

Data Blocks是存储文件内容的区域,当我们读取文件系统的内容时,在读取文件系统时,通常会以固定大小的块为单位进行读取,常见的块大小为4KB,这也是文件系统的基本存储单元(这个意思就是磁盘的最小访问单位是物理扇区(通常为512B);而我们的文件系统则不会,因为512B毕竟太小了,我们现在动不动一个文件就是好几M,所以文件系统会采用更大的块大小(如4KB)来提高大文件访问效率)

inode Table是存放所有文件的所有属性的,每个文件的属性都被封装在inode结构体中,以inode进行存储,单个inode的大小为128B,所有文件的inode就构成了这个inode Table。这样每个文件就有一个唯一的inode编号。而文件内容使用了多少个块,且这些块在Data Blocks中的哪里存放都在inode结构体中。(注:在Linux的文件属性(inode)中不包含文件的名称,在Linux的系统中标识一个文件使用的是inode)

这样通过inode编号,我们就可以找到inode结构体,再根据inode结构体中的数据块表,我们就可以找到文件的内容,完成了inode到数据内容的映射。

而block这个数组就和上下两幅图中的任意一种,都是前几个数组成员为直接地址块(也就是直接映射的就是数据块),接下来的几个就为一级间接地址块,就是将我们的一整个数据块当作索引块(也就是不存放数据,而是存放文件的块号),然后通过这个索引块取映射我们的数据块(我们的数字是整形,占4个B,而我们的一个磁盘块为512B,所以这个样这个磁盘块中就可以存放256个数据块的位置,所以我们就可以通过这个一级间接地址块就可以使用256个数据块),同理还有二级间接地址块也是这样的地址,我们可以再将地址块再放入一个数据块中,这样这个数据块中就是存放的是一级间接地址快,这样,通过这个二级间接地址块,我们就可以访问(256*256)个数据块,这样完全不必担心我们的文件放不下的问题。

block Bitmap这个块的作用就是帮我们记录哪些块被使用,而哪些块没有被使用,由于磁盘中的数据块数量庞大(磁盘512GB,而如果我们的扇区大小是512B,这就相当于1073741824个扇区)将近10亿个数据块,这么大的数字我们必须采用一种高效的方法来跟踪块的使用状态,而哪些没有被使用,这样才可以给我们的操作系统再创建文件的时候可以快速分配空闲的数据块,而我们的磁盘面对这种情况使用的就是位图(用比特位的位置完成于数据块的映射)这种数据结构(也就是用一个比特位(b)代表一个磁盘块)仅需约128MB的空间就可以表示这512G磁盘的所有块的状态,相比其他数据结构更节省空间。

inode Bitmap也是如此,有多少个文件就有多少个inode,所以我们也得将这些文件管理起来,方便我们快速找到,同时也要考虑一定的节省空间,所以我们的inode Bitmap也是通过比特位的位置与inode的编号映射起来,从而表示文件的inode是否是有效的。 

所以明白了这一点,我们就可以知道当我们在删除一个文件的时候,我们并不需要将文件数据块的内容置空,我们只需要拿到文件的inode,找到文件的inode,在通过inode Bitmap确定文件的inode是否有效,然后再通过inode Table中进行查询,获取inode和数据块的映射关系,找到对应的块号之后将其对应的block Bitmap中对应的比特位的位置进行置0即可,最后再将inode Bitmap中所对应的比特位置0即可,这样就达到了删除的效果。

仅仅只需要将文件数据块所对应的block Bitmap中的比特位置0就可以代表整个数据块已经是空闲的了。

Group Descriptor Table描述的是整个block group基本的使用情况,存储了该块组的核心元数据,例如:

  • 这个块组的 数据块位图 在哪里?

  • 这个块组的 inode 位图 在哪里?

  • 这个块组的 inode 表 在哪里?

  • 这个块组还有多少空闲 inode / 空闲块?

Super Block描述的是文件系统的基本信息,它相当于文件系统的“身份证”和“目录大纲”,告诉内核:

  • 这个文件系统有多大?

  • 每个块组有多少 inode、多少数据块?

  • 位图和 inode 表在哪里?

  • 文件系统版本、状态、挂载次数等。

记录的信息主要有:block 和 inode的总量, 未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。

这个Super Block不一定每一个分区中都有,可能之后有几个分区有用作备份,为了就是防止万一只有一个Super Block,这个Super Block被损坏了,这影响的可不仅仅只是这一个分区,而是整个文件系统,所以为了避免这一个情况的发生,所以会在之后的分区中也会有Super Block当做备份,万一第一个Super Block受损,我们还可以通过其他分区备份的Super Block继续运行。

总而言之就是在一个文件系统里,真正存放用户数据的地方是 inode table 和 data blocks
但在管理这些资源之前,系统必须要有“目录本”和“索引表”来描述和管理它们:

  • Super Block(超级块):描述整个文件系统的全局属性(总共有多少 inode、多少块、每组多大、状态等)。

  • GDT / DST(Group Descriptor Table,块组描述符表):描述每个块组内部 inode table 和位图的所在位置、空闲数量等。

  • Block Bitmap:记录数据块的使用情况(某个 bit=0 表示空闲,1 表示已分配)。

  • Inode Bitmap:记录 inode 的使用情况。

这几个数据结构就像“目录、索引和清单”,让内核能高效找到空闲 inode、空闲数据块。

格式化

当我们在执行格式化时,文件系统工具会在分区上写入必要的数据,完成如下操作:

  1. 写入超级块

    • 在分区偏移 1024 字节的位置写入主 Super Block;

    • 根据文件系统类型还会在若干块组里写入 Super Block 的副本。

  2. 写入 GDT(块组描述符表)

    • 紧跟在 Super Block 后面写入每个块组的 Group Descriptor;

    • 每个描述符都记录该组的 block bitmap、inode bitmap、inode table 的起始位置。

  3. 初始化 block Bitmap

    • 设置哪些块已经被占用(例如:超级块、GDT、自身的位图、inode table 占用的块);

    • 剩余的数据块标记为空闲。

  4. 初始化 inode Bitmap

    • 标记根目录的 inode(通常是 inode 2)已分配;

    • 其他 inode 标记为空闲。

  5. 写入 inode Table

    • 为每个 inode 预留固定的空间(比如每个 inode 128B);

    • 初始化根目录 inode。

通过这样的方式,让分区从“裸磁盘空间”变成一个“可用的文件系统。

具体情况具体分析

接下来我们就来整理以下以下几个问题的答案

  1. 新建一个文件,系统做了什么?
  2. 删除一个文件,系统做了什么?
  3. 查看一个文件,系统做了什么?
  4. 修改一个文件,系统做了什么?

新建一个文件,首先我们查看GDT中该分组的inode的使用情况,然后查看inode Bitmap(GDT中已记录它的起始位置),然后逐位扫描inode位图,直到找到第一个空闲的inode,然后将其由0置1,接着再找到inode Table(GDT中已记录它的起始位置)定位该inode并分配给该文件,最后将相关的文件属性填进去。当要进行写入文件内容的时候,我们的系统调用接口write的参数都会让我们指明这个所些内容的大小,所以就可以根据所写内容的大小分配相应的数据块,然后从block Bitmap中依次扫描,找到空闲的数据块,并将这些数据块由0置1,然后将这写数据块的信息填入它的inode表中,然后直接跳转过去,至此就可以将相应的数据填入对应的数据块中。这就是新建一个文件所经历的过程。

删除一个文件,我们可以根据inode,找到对应的数据块的块号,然后在block Bitmap中将其对应的比特位由1置0,然后再将该inode在inode Bitmap中的比特位也由0置1,这样就完成了文件的删除。

查找一个文件,我们首先根据inode,在inode Bitmap中查看该inode的比特位是否为1,然后再在inode Table中找到该inode,通过inode中对数据块的映射,再再block Bitmap查看这个数据块对应的比特位是否为1,然后我们就可以找到该文件的内容。

修改一个文件,首先我们要知道我们想要修改时文件的何种信息,是文件的属性,还是文件的内容,如果是文件的属性,我们只需要找到对应的inode就可以进行修改,如果是要对内容进行修改,我们只需要通过inode中对数据块的映射关系,如果数据块的大小不够,我们会再次从我们的block Bitmap中重新查找,最后再将新分配的数据块,通过这个inode再次进行映射,这样我们的数据也就修改成功了。

那说了这么多,我们都必须要找到这个inode就可以完成以上所有的操作,但是有个问题就是我要如何找到这个inode呢?我们平时使用的都是文件名,从来没有用过inode,那我们是如何找到inode?

这个问题的答案其实就是与我们的目录有关,无论我们这么创建文件,我们都需要指明这个文件的存放位置,比如read系统调用参数中就要我们指明该文件的所属位置在哪里,所以我们可以大致猜一猜,我们一定是通过这个文件的所属位置在哪里(也就是这个文件的所属目录),就可以完成文件名到inode的映射,事实也就是如此,我们现在其实可以思考一个问题就是目录是文件吗?目录当然是文件了,我们在使用Windows除了在目录中新建文本文件,也经常会新建文件夹,这也就是一个创建文件的过程,所以目录也是文件,所以既然目录也是文件,那么目录也就有自己的inode,也会有自己对应的属性。我们可以看看Linux中的目录文件。(也顺便看看普通文件)

所以显而易见,目录文件也是文件,也有自己的inode,那么目录有没有内容呢?当然有了,目录既然是文件,那就一定有他对应的内容,那么目录的内容是什么呢?目录的内容就是你在该目录下创建的文件和该文件对应的自己的inode编号 。

所以我们现在就明白了为什么在同一个目录下不能有同一个文件名,这就是因为,我们的文件名与自己的inode是一个键值对,所以在同一个目录下只能有一个主键,否则一旦出现两个相同的文件名,我们就无法精准定位到底哪个文件对应的哪个inode了。现在,我们就知道了,我们只需要我们想要获取一个文件的inode,只需要通过该文件的目录所对应的内容就可以获取到了。所以我们获取一个文件的inode,举个例子就是

访问 /home/user/test.txt 时,操作系统查找 inode 的过程如下:

  1. 起点:从根目录 inode 开始(在文件系统中根目录 inode 是特殊的:系统固定知道它的 inode 号)
  2. 第一层查找:在根目录的数据块中检索 home 目录项,获取其 inode
  3. 第二层查找:进入 home 目录的 inode,在其数据块中查找 user 目录项,获取其 inode
  4. 最终查找:在 user 目录的 inode 数据块中定位 test.txt 文件,获取其 inode

这个过程揭示了目录文件的本质:它们是用于索引下一层级 inode 的映射表。

这样我们未打开的文件(磁盘级文件)我们也就了解清楚,结合之前内存级文件的内容,这就是我们整个文件系统,相信了解到这里大家应该是一目了然了,接下来,我们再来看看软硬连接这个概念,相信对于大家来说就好理解多了。

软硬链接

#include int main(){ printf(\"hello world !\\n\"); return 0;}

根据这个现象我们可以得到一个明显的结论就是软连接是一个独立的文件,因为软连接具有独立的inode,而我们的硬链接则有点不同。

我们可以看到硬链接不是一个独立的文件,因为它没有独立的inode。且我们ls命令中的这个数字则代表我们的硬链接数。了解了这些,现在我们来看看我们是如何理解这个软硬链接的。

所谓的硬链接,就是在特定的目录下的数据块中新增文件名和指向文件名的inode编号的映射关系,那么这个硬链接数又是什么呢?其实在我们的inode属性中还有一个字段就是专门记录这个硬链接数的,这个字段就是引用计数,作用就是表明现在有多少个文件名指向我,所以其实在删除文件的时候,我们只是将对应的目录中的数据块中该文件名与其对应的inode进行删除,然后我们找到该文件对应的inode,将这个inode中的引用计数进行减1,如果这个引用计数的值仍然大于0,则不删除该inode,知道将这个引用计数的值减为0时,我们的就对该inode进行真正的删除。

但是还有一个问题就是如图这种情况,为什么我们在创建普通文件的时候,它的硬链接是1,而我们创建目录文件的时候,它的硬链接数确实是2呢?

其实这个问题的答案根据这副图就显而易见了,这是因为在我们的目录中有两个隐藏文件,一个是(.)代表当前目录,而(..)代表上级目录,而这个(.)就是我们创建的目录文件的硬链接。

还有一点就是我们在建立链接的时候可以给目录文件创建软链接,但是不可以为我们的文件创建硬链接。

可以创建软连接是为了方便我们打开使用,但是为什么不能创建硬链接呢?

这样一看其实答案已经非常明显了,因为我们如果给目录创建硬链接的话,就会造成一个环形图,这样当我们进行查找一个文件的inode时,就会造成无休止的死循环,这样就会导致文件找不找的到先不说,操作系统就进入一个无休止的循环中,永远停不下来,所以我们是不可以给我们的目录文件创建硬连接的。

那么这个软连接是一个独立的inode,那么我们该如何理解软连接呢?这个软连接既然有自己的inode,那么也就有自己的属性和数据,属性我们可以理解,无非就是这个文件的大小,创建时间等等,那么这个软连接的数据内容是什么呢?其实这个软链接的数据内容就是“目标文件的路径字符串”,所以我们的原文件有什么数据信息,这个软连接通过这个目标文件的路径都可以找到并访问,所以我们将这个软链接进行删除对原文件也不会有什么影响,但是一旦删除了原文件,这个软连接保存的目标文件的路径也就没什么用了,所以这就相当于和大家谈恋爱的时候是一样的,你的女朋友看到你天天打游戏,气不打一处来,一狠心,就把你的快捷方式就删除了,甚至于清空了回收站,而你小子则在心里偷偷的笑,明白自己的游戏还在。这就是软连接的好处了。

文件系统的总结(将打开的文件和文件系统的文件产生关联)

通过本文,我们从硬件到软件层面,循序渐进地揭开了 Linux 文件系统在磁盘上的存储机制:从扇区、柱面、CHS 与 LBA 定位,到超级块、块组描述符表、bitmap,再到 inode 与数据块的映射,以及目录查找和文件操作的全过程。掌握这些概念,就像拥有了磁盘的“全景地图”,能够清晰理解文件是如何被系统高效管理和访问的。

如果把硬盘比作一栋大楼,每个扇区就是一间小房间,inode 是门牌号,bitmap 是前台的入住登记簿,那么每一次文件创建、删除或移动,系统就像细心的管理员,有条不紊地安排每个房间的入住和搬迁。软连接和硬连接,则像房间的“亲友通行证”,让你灵活访问文件而不打乱原有房间的安排。

理解这些幕后机制,不仅能在操作系统和文件系统学习中更有信心,也为性能优化、异常处理和深入探索现代文件系统打下坚实基础。未来,你还可以继续了解 ext4、XFS 等不同文件系统的“大楼装修风格”,或者研究 SSD 这个“新型公寓”里奇妙的空间管理。走得越深,你会发现,原来磁盘里的世界,比你想象的还要精彩!

 首先,我想向大家提出一个问题,我们的操作系统会不会对我们的内存进行管理呢?我们可以猜一下,我们的操作系统会对我们之前提到的进程,以及文件系统都会进行管理,那么对于我们的内存,也肯定不会厚此薄彼,操作系统都会对其进行管理。而且我们的老师在上课的时候进场会给我我们讲什么缺页中断,也就是我们的程序通过页表进行访问目标模块时,该目标模块尚未调入内存,这个时候就会发生缺页中断,给我们的进程分配一定的内存空间,然后将数据拷贝到内存;之后我们还了解写时拷贝,当父子进程对同一块空间进行写入的时候,就会给子进程重新分配内存空间,并将父进程的数据进行拷贝;在进程控制的时候,我们的子进程在进行程序替换的时候,会将新程序的代码和数据加载到内存中,然后修改子进程的页表,讲子进程的代码和数据指向新程序的代码和数据;而现在在文件系统中,将文件描述符,打开一个文件时,我们的操作系统会管理这个文件,给这个文件创建内核结构struct file,然后我们还要创建相应的文件缓冲区等等,这些所有的工作都绕不开一件事情,就是操作系统就必须给我们提供一个模块——内存管理模块

现在,我们就来了解一下物理内存是如何和磁盘进行交互的,首先,我们的操作系统是将我们的物理内存划分为一个个基本单位——页框,大小为4KB,而我们平时下载的小电影,以及玩的游戏等等放在磁盘上其实也是按照4KB进行划分的(也可以理解为将8个512B的物理块同时进行读取和访问),这样我们的物理内存和磁盘进行数据交互的时候就可以以4KB进行交互,现在即使我们想要修改其中的一个比特位,我们都会将这4KB的数据全部加载到内存之后才能对其进行修改,这是为什么呢,主要是因为我们的磁盘是一些机械设备,在访问的时候由于物理设备的原因,访问周期一定会很慢,所以我们在访问的时候1次读取4KB的内容和分几次读取4KB相比,肯定是一次读取4KB所消耗的时间更短一点,也更高效一点,现在你可能会说我现在就要读取100B的数据,操作系统这样直接给我读100B的数据肯定相对比这读4KB的数据要块的多,但是这是你理想的情况下,也有可能你要读取的100B还在这4KB的不同区域内,可能还需要磁盘进行更细粒度的访问,甚至在操作系统中也要提供更精细的方法,但是你能保证你过一段时间之后你不想访问接下来的数据吗?答案是当然不回来,现在我们就可以想象一下,你现在正在修改你的课程论文,操作系统先给你加载一个页的内容,当你想看下一页的数据时,操作系统再给你加载下一页的内容,作为新时代快节奏生活下的我们,我们早就开始问候这个软件的程序员了,这个无辜的程序员也是躺着也中枪,所以只多一部分内容,这种不太符合我们的现实情况,所以程序员为了不让你问候他,一般在你进行读取的时候,他会根据局部性原理,将你目前想要访问的信息以及接下来的可能访问的信息(你访问信息的后面一大堆数据)也就都给你加载到内存,所以不管你接下来读取这100B之后的数据你进不进行访问,操作系统都会将这4KB的内容都给你加载进来,这样才是XX保卫战的合适做法 ,这也就是操作系统基于局部性原理的预加载机制,这样既可以在硬件层面减少I/O的次数,同时也可以在软件层面这种预加载机制,可以是我们的整体效率得到提升。

那么操作系统是如何管理内存的呢?首先,我们要明确一点就是,我们的操作系统既可以看到虚拟内存空间,也可以看到物理内存,而我们之前能看到的虚拟地址空间就是我们的操作系统让我们看到的,所以操作系统是可以看到物理内存的,不然他如何给我们的虚拟地址空间分配真实的物理地址。

那么操作系统是如何知道哪些页框是“脏”数据(修改过的数据),哪些页框已经被分配了,目前这些物理内存已经被使用了多少了等等这些问题,那么操作系统是如何管理的?答案就是先描述再组织,(假设我们的物理内存现在是4GB,而我们的页框是4KB,所以我们的操作系统就会有1,048,576的页框)所以我们操作系统为了维护这些页框,定义了一个struct page的结构体,里面存放的就是page页所必要的属性信息,所以接下来我们对内存的管理就变为了对数组的管理,同样由于是数组是有下标的,这样我们的page页也就有了页号,这样我们就可以通过页号访问到对应的page页(假如我们现在需要访问0x11223344的物理地址,这个时候只需要让(11223344&FFFFF000)进行按位与的操作,我们就可以获得相应的页号,从而访问对应的page页,所以所有申请内存的动作,都是在访问page数组!

这就是操作系统管理我们内存模块时描述的结构体,其第一个成员变量flags就可以让我们知道哪些页框是“脏”数据(修改过的数据),哪些页框已经被分配了,目前这些物理内存已经被使用了多少了等等这些问题,那么这是怎么做到的呢?答案就是利用宏比特位级别的标志位传递方式,这样我们就可以利用这一个flags字段知道该页的情况信息。而第二个成员变量count则就是我们的引用计数,它的使用情况就比如之前我们的父子进程在刚刚创建的时候还没有发生写时拷贝的时候,我们父子进程的代码和数据都是共享的,所有这里的引用计数就可以表示现在有两个进程一起指向我等等这样的功能。

其中还有一个成员变量是lru(LRU),咱们作为一个计算机的学生应该对这个东西现在不能说是了如指掌,但起码只要我们是听过操作系统老师讲课的话,无论老师讲的好坏,他肯定会给你提到这个LRU,这个就是最近最少使用,当我们进程被分配的页使用完的时候,这个时候就需要进行页替换,那么应该替换哪个页呢,这个就是根据这个LRU算法确定,从而进行页面替换。

其实在我们的操作系统启动的时候,我们的操作系统会把每个磁盘分组中管理信息(super block,GDT,block bitmap,inode bitmap)预加载到我们的内存中,同样可能每个分区使用的文件系统不一样,面对这种情况,操作系统肯定也是会维护一张链表,然后将每一个分区的管理信息放进去,这样每个分区的具体情况,我们的操作系统也是可以知道的,简而言之,就是文件系统中的信息会在启动操作系统的预加载到内存中。而当我们的进程在打开一个文件之后,这个进程既然可以打开这个文件,必然是这个文件知道这个文件的路径和文件名,因为我们在使用系统调用接口的时候,我们已经指明了这个文件的路径和文件名,这个时候由于我们每一个分区的管理系统在启动的时候就加载到了内存,所以我们只需要根据这个路径在相关的分区找到对应的目录,然后在目录的数据块中,根据我们提供的文件名,就可以找到对应文件的inode信息,就可以找到这个文件了,而我们对于这个打开的文件,关心的就是文件的属性和信息,而我们的文件属性的具体内容存放在操作系统的一个struct inode的结构体中,之前我们提到的存放文件属性信息struct file中只存放了少量信息,更加具体的信息在这个struct inode的结构体,而在struct file中可以通过不断的映射,从而找到struct inode的结构体。

这样我们就找到了我们文件属性存放的具体位置。

现在我们再看文件的数据信息,而且在之前我们还会将我们的文件通过fopen调用打开,然后通过fprintf先写入到C标准库提供的缓冲区中,然后通过系统调用给我们分配的一个fd文件描述符,最后指向struct file,那么最后这些数据是如何写入到磁盘的呢?现在我们通过struct file找到文件的属性了,那文件的内容是如何操作的呢?

答案就是在struck inode中有个成员变量是struct address_space,然后在struct address_space的成员变量page_tree,而这个树的叶子节点就是存放的一个个struct page页内存对象,而我们的struct page就是4KB大小,所以其实最后我们在用户空间的C标准库提供的缓冲区最后都是写入到了这个struct page页内存对象中,而这些个叶子节点(struct page)就构成了文件页的缓冲区,也就是我们在操作系统中那个笼统的概念:内核缓冲区。最后,我们只需要将这些struct page页中的内容刷新到我们的磁盘即可。

所以总而言之就是在Linux中,我们的每一个进程,打开的文件的都要有自己的inode属性和自己的文件页缓冲区(内核缓冲区)

奶粉品牌