【Linux】磁盘 | 文件系统 | inode

🪐🪐🪐欢迎来到程序员餐厅💫💫💫

主厨:邪王真眼

主厨的主页:Chef's blog

所属专栏:青果大战linux

总有光环在陨落,总有新星在闪烁


模电好难啊,不知不觉怎么就要期末考试了,还有六级考试,哦哭了


前言

我们之前讲的文件都是被打开的文件,或者说被加载到内存中的文件,但事实上计算机中没有被打开的文件比被打开的文件数量多得多,操作系统是如何管理这些没被打开的文件的呢?显然不会是一股脑往硬盘里一塞就啥也不管了, 今天我们就来好好介绍一下。


磁盘

在了解硬盘中的文件之前,我们要先来了解一下磁盘

计算机中存储数据有两种方式,,一种是存放在内存中,一种是存放在硬盘中,我们常说的电脑是512+16,手机是256+12,这里的大数字"256"、"512"就是硬盘的空间大小(单位G),小数字就是内存的空间大小(单位G)。

内存的特点叫做掉电即丢失,也就是说当你的电脑没电了那你内存中存储的数据会直接清空,要想保存数据就要一直开着电脑,这显然是不可能的,这时就要让硬盘发挥作用了,硬盘的不仅空间比内存大得多,而且他的数据存储是掉电不丢失的,除此之外价格也便宜很多。

硬盘分为机械磁盘和SSD(固态硬盘)

上图中,左侧是磁盘,右侧是SSD。

  1. 机械磁盘是我们的计算机中唯一一个机械设备,IO交互慢

  2. 固态硬盘IO效率、功耗、噪音大小上明显比机械磁盘更好,

事实上我们现在的PC上大多都是固态硬盘,那么为什么我们还要讲磁盘,而不是SSD呢?

首先虽然性能上有明显差异,但是SSD和磁盘的文件系统大框架是一样的,只是SSD多了些优化。

其次,对于企业来说他们要在服务器上安装硬盘,单块硬盘就是几个TB,并且一台机器可以安装几十块,这些硬盘主要是起到存储数据的作用,对他的IO交互速度、噪音什么的没什么要求,为了最大可能的节约成本,当然就要用机械磁盘了。


磁盘的物理结构

一个磁盘是由多个盘面组成的,我们先单拿出一个盘面讲解

在上图中我们可以看到很多圆环,每条圆环都是一条磁道,在工作时,中心的马达会带动磁盘高速旋转(一分钟几千转),磁头可以在磁头臂的帮助下移动到不同的磁道,这样一来磁头就可以读取任何一张盘面的任何地方了,当然磁头不是贴着盘面的,不然会刮坏盘面,而是以极近的距离挨着它。

机械磁盘是通过磁头在高速旋转的盘片表面进行数据的读写操作。盘片表面涂有磁性材料,磁头可以改变盘片上磁性材料的磁极方向来写入数据。例如,当磁头通过电流产生磁场时,就可以在盘片的磁性涂层上产生对应的磁极排列,这些不同的磁极排列就代表了 0 和 1 这样的二进制数据(例如规定N极为1,S为0)。而在读取数据时,磁头会检测盘片上磁性材料的磁极方向,从而将其转换为计算机能够识别的电信号。

磁盘是由多个盘面组成的,每个盘面都有一个磁头,用于写该面的数据,注意每个盘面正反都可以读写,也都各有一个磁头。

我们将不同盘面的相同大小的磁道看作一起,就可以当作一个柱面。


数据的存储

每个圆环都是一个磁道,而每个磁道又被分为了许多扇区,

扇区是磁盘IO的最小单位,一般固定是512字节

当然请注意,越是远离圆心,一个扇区所占面积就越大,为了使得扇区的数据空间都是512,所以外围的磁密度也会减小。

CHS寻址

  1. 第一步确定在哪个Cylinder(柱面),
  2. 第二步确定是哪个磁道,要用哪个Header(磁头)
  3. 第三步确定在哪个Sector(扇区)

数据的逻辑结构

相信不少朋友小时候都玩过这个东西,揪着一边然后把它整个磁带全部撤出来,最后自然是按不回去了,不难看出磁带里面也是一圈一圈缠绕的,被扯出来后则是一根直线。

事实上,这不就是二维数据转化为一维数组吗,我们的磁盘也是同样,于是乎在逻辑结构上我们可以把它从一个CHS三维的数组,转化为一个一维数组,

注意CHS中计算数量,扇区号是从下标1开始算的,磁道号和柱面号是下标0开始

在这个逻辑结构基础上,先人又发明了LAB寻址,

LAB下标=柱面号*单个柱面的扇区数+磁道号*单个磁道的扇区数+扇区号-1(最后减一是因为LAB下标从0开始算)

但是一个扇区的大小只有512字节,这个单位太小了,经过大量时间后人们发现当OS与磁盘单次OI以1kb、2kb、4kb、8kb为单位时效率高,其中又以4kb最为常用,于是我们接下来都是对4kb为单位的OS进行讨论。

我们把八个扇区看作一个数据块,这个块就是4字节,即OS与磁盘IO的单位


文件系统

inode

属性会以结构体类型struct_inode的方式被构建出来,一个文件只有一个inode,他是文件属性的集合。

一个inode一般为128字节(某些OS下是256),他的主要内容如下

struct inode {
	unsigned long		i_ino;
	umode_t			i_mode;
	uid_t			i_uid;
	gid_t			i_gid;
	loff_t			i_size;
};
  1. i_ino:该inode结构体的编号

  2. i_mode:该文件的权限和类型(文件还是普通文件或其他的类型)

  3. uid:文件拥有者

  4. i_gid:文件拥有组

  5. i_size:文件大小

使用指令 ls -i即可查看文件的inode编号

请注意,在linux中文件名属性没有在inode中保存,


分区&分组管理

我们的PC一般是1T或者512G,而服务器可达几十TB,如此巨大的空间会存储大量的数据,那么如何对这么多数据进行管理呢?思路很简单,分治,分而治之。

这里的分区对应的就是我们windows里的分盘,C盘、D盘、E盘,不同的盘可以是不同的文件系统,这样可以防止出现一个文件系统挂了,文件全部出问题的情况。

此时我们要做的就是如何管理好一个组的问题了,只要一个组管理好了,其他组就借鉴他的管理方法就可以了。


Group内容

我们先看看一个组里都有什么

inode Table

用于存储该组中的文件的inode,

Data blocks

以块为单位保存该组中的文件的内容,一份文件所占用块数是"文件大小/4kb"(向上取整)。

加入一个文件只有一个字节,就是是1/4k(向上取整),要给他分配一个数据块。

如果是大小为6kb,那就是6/4(向上取整),要分配两个数据块。

inode Bitmap&Block Bitmap

我们把inode Table中的空间以一个inode的大小(128字节)分为一份一份的,如何确定某一份空间有没有被使用呢,如何确定Data Block中的一个块(4字节)有没有被使用,靠的就是他们俩Bitmap。

这是两个位图,对于inode Bitmap第i个比特位为0表示第i份空间还没被使用,第i个比特位为1,表示第i份空间被使用了,Block Bitmap的第i个比特位负责表示第i个数据块是否被用了

当然了,虽然我们这里用的是位图,但是我们之前说了磁盘数据的IO要以块为单位,所以哪怕你只修改bitmap中的一个比特位,也要把一个块大小的Bitmap都读进去。

OS在给inode分配空间时,不是一定是连续的,加入第一份空间被使用了,第二份还没有,那么下次存储inode不一定会用第二份,而可能是第三、第五、第一千份空间。


Group Descriptor Table

简称GDT,块组描述符,用于描述块组属性信息

  • 该块组内的可用数据块数量。。
  • 该块组内的可用inode数量。
  • 该块组的块位图和inode位图的位置
  • 该块组的inode表的位置。

Boot block通常包含了启动计算机或其他设备所需的初始指令和数据,这些指令和数据是设备启动过程中最先被加载和执行的部分。我们现在先不考虑他。

Super Block(超级块)
存放文件系统本身的结构信息,是文件系统的核心。
Super Block的信息被破坏,可以说整个文件系统结构就被破坏了
记录的信息主要有:

  • bolck 和 inode的总量,
  • 未使用的block和inode的数量
  • 一个block和inode的大小
  • 最近一次挂载的时间,
  • 最近一次写入数据的时间
  • 最近一次检验磁盘的时间等其他文件系统的相关信息。
  • 还有一些用于管理文件的函数

从功能上来讲,一个分区有一个Super Block就够了,那为什么实际是每隔几个Group就要设立一个Super Block呢?

因为一旦一个Super Block崩溃了,那么他管理的数据都要gg,所以我们不能把鸡蛋都放在一个篮子里。因此每隔几个group就设立一个super Block,正常情况下就一个Super Block负责管理该区,当他出了问题,OS就会把好的SuperBlock复制过去以修复问题,大大提高了文件系统的稳定性。请注意一个区中的不同位置的Super block的数据是完全一样的,

文件系统以分区为单位,不同的分区可以安装不同的文件系统

inode编号是以分区为单位分配的,而不是以组为单位,

即C盘中可能有个inode编号是100,D盘也有个inode编号100,但是不可能在C盘的两个inode编号都是100

每个组的inode在分配编号时只需要确定该组的起始inode编号即可(保存在GDT中),他的编号就等于该组的起始inode编号+i(在该组用的第i份空间),例如她所在组的起始inode编号是1000,它用的是第25份空间,那他的编号就是1025,Datablock也是同理。


inode对Data block的映射

在inode中还有一个元素

__le32	i_block[EXT2_N_BLOCKS];/* Pointers to blocks */

i_block就是一个int类型的数组,他的容量是EXT2_N_BLOCKS,这个宏是15,这个数组的前12个空间存放的就是数据块的编号,显然靠这12编号最多只能存放48个字节的数据

一级间接块索引表指针存储的数据块编号所对应的数据块不是直接存储文件数据的,而是在这个块中存储其他块的编号,一个编号是int类型占四个字节,一个块是4KB可以存1000个块号,通过这1000个块可以存储的数据时4MB

同理 一级间接块索引表指针指向的数据块存的是1000个一级间接块索引表指针,相当于可以存4GB。

三级间接块索引表指针指向的数据块存的是1000个二级间接块索引表指针,相当于可以存4TB。

加入我们没有这些间接索引,那单单12个块很容易就会不够,可如果你开太大比如1200甚至120000个块,那inode本身空间就太大了消耗了很多空间。

如此一来就在保证inode本身所占空间不太大且固定的基础上,基本可以满足各种大小的文件的存储,

或许有同学会问,为什么不直接放个int类型指针,需要几个块就malloc几个int空间来存放

因为mallco分配的是内存空间,我们这里讨论的都是磁盘空间!!

只能说,优雅,太优雅了(间谍过家家音)。


inode编号与文件名的映射

显然我们在通过指令或者代码中的函数操作文件时,使用的都是文件名,那么文件名和inode又有何关系呢?

首先明确,文件名没有存放在inode中。

如何理解目录?

事实上,目录本身也是个文件,

所以它也有自己的inode和data block,我们可以看出在底层磁盘,是不区分所谓的文件类型的,大家都是一个inode+几个block,

目录的内容是什么?

是文件名和inode编号的映射关系 ,当然这个映射是相互的,inode编号也可以映射文件名,这也是为什么一个目录下不会有同名文件。

所以我们我们要在磁盘中找到一个文件,只需要通过他的上级目录和它自己的文件名就可以得到他的inode编号,接着通过编号找到这个inode结构体,结构体里面有对应的数据块编号,如此就得到了文件的数据,即可加载到内存中。

这里我们就知道了,一个文件的文件名是存储在他的上级目录中的。这就是为什么当你没有读权限时无法看到目录中的内容,因为读不了目录文件,所以就无法获得inode与文件名的关系,所以你就访问不了目录里面的东西;没有写权限就是无法将inode与文件名的映射写到目录中

为什么不把文件名放到inode?

编号可以进行加减以实现一些优化,编号的大小比较速度也优于字符串,因此为了提高效率就只用inode了,那么没有用武之地的文件名就没有在inode的存放的必要了。事实上用户id,组id等信息也是以整型的形式存放的,而不是直接存储我们的字符串形式的用户名。而文件名即字符串这种东西是为了方便我们用户的观看和使用才被使用的。可以看出OS为了提高效率真的是煞费苦心。

但是你先别高兴,要从磁盘找到我们的文件,要先找到上级目录拿着它的inode映射去在磁盘找,可是,要找到上级目录得先找到上上级目录啊,诶死循环了。

当然不是啦,根目录是特殊处理的,他的inode和文件名是固定的,所以找到根目录不用再依靠别的文件。这就是为什么我们访问一个文件需要相对路径或者绝对路径,因为要打开一个文件,就要从自身开始层层递归到根目录才可以。


路径缓存struct dentry

他是一个存放于内存的结构

我们现在知道了要找到一个文件,就要从他的根目录开始把他的路径中的文件夹都打开(从磁盘加载到内存),才可以最终获得该文件的inode,进而在磁盘中找到它,但是这样就意味着我们为了找到inode要不断和磁盘做IO去打开文件,效率不忍直视,于是佬们发明了dentry

首先dentry是一种树状结构,它里面有父子节点的指针以便于构造树状结构,它里面存放了一个文件的文件名(字符串)和她对应的inode

他所构建的结构就像linux的tree命令一样

最开始这个数据结构就会默认把根节点设置为根目录,之后加入我们要把project1打开,就会打开根目录->home->用户->projcet,此时就会对于每个文件都会创建新的dentry把该文件的文件名和inode的映射关系存放进去,并且把这些节点按照上面的树状结构链接起来,如此一来下次我们再要打开project2,就可以在内存中通过dentry直接获取用户A文件的inode,不用从根目录开始层层推导,新的文件也会带来新的dentry,继续链接到该树状结构上。当然一些dentry路径要是一直不被用就会被OS去掉,以节省空间。

这样我们就把找inode的操作只依靠内存实现了,效率比磁盘高了太多。

至于为什么不直接预加载所有路径,因为占用的空间太大了,等你的内存什么时候扩展到512了再说吧。


文件增删查改

在学习了这些东西后,我们回头在看文件的增删查改。

查找一个文件

如果是对于相对路径,先拼接成成绝对路径,

先去dentry里看有没有对应的缓存,如果有直接拿到inode,如果没有就层层展开,从根目录开始映射文件名和inode编号,最后拿到目标文件的inode

让inode编号和每个组Group的GDT中记录的inode起始编号比较,直到找到该文件所在Group,然后在inode Bitmap中检查该inode编号是否合法(即bitmap的该比特位为1),合法后去inode Table找到inode结构体,通过结构体中的i_block找到文件的数据块,至此,文件的内容(数据块)和属性(inode)就算都找到了。

创建一个文件

OS在inode Bitmap中确定第i个比特位,它对应的值为0,表示inode Table对应位置的inode还未被使用。

使用包括inode编号i在内的信息创建一个inode结构体,把它放到inode table第i份(以128字节为单位)空间,接着计算他的大小需要占用几个数据块(向上取整),通过Block Bitmap给他在Data balock分配n个数据块空间存储内容数据,并将数据块的编号存放在inode的i_block数组中。

如何删除一个文件

先查找到文件的inode编号,接着找到inode结构体获取数据块编号,最后把对应的inode Bitmap的比特位和Block Bitmap比特位都置零即可。

这意味着当你删掉了它后,他的数据还在Data Block中保存,也有被恢复的可能,不过你要是接着又建立了一些文件,占用了原本文件的Data Block就真的恢复不了了。

修改一个文件

先通过查找找到该文件,然后把文件加载到内存,对其内容修改,最后将修改后的数据刷新回磁盘中的文件即可。

-------------------------------写了很久,创作不易,麻烦点赞关注支持一下--------------------------------------

​​​​​​​

相关推荐
HfCloud12 分钟前
【笔记】Linux下编译Python3.10.15为动态库同时正确处理OpenSSL3依赖
linux·运维·笔记·python
楚疏笃18 分钟前
openssl编译安装升级为新版本
linux·运维·服务器
麻瓜也要学魔法1 小时前
linux一键部署apache脚本
linux·运维·apache
Mar_mxs1 小时前
Linux环境下配置neo4j图数据库
linux·数据库·neo4j
施嘉伟2 小时前
PostgreSQL WAL日志膨胀处理
运维·数据库·postgresql
wish3662 小时前
Clean Docker Images and Container by Cron Job
运维·经验分享·docker·容器·devops
dessler2 小时前
云计算&虚拟化-kvm-无损扩容磁盘&分区
linux·运维·云计算
犯困的土子哥2 小时前
Linux:进程的概念
linux
As小神龙3 小时前
NFS服务器
运维·服务器