文章目录
- 一、前言
- 二、认识硬件----磁盘
- 1.基本介绍
- 2.磁盘的存储构成
- 3.磁盘的逻辑结构
- 4.回归到硬件
- 三、文件系统
- 1.划分
- 2.Block group
- (1)Data blocks
- (2)inode Table
- (3)Block Bitmap
- (4)inode Bitmap
- (5)Group Descriptor Table(GDT)
- (6)Super Block
- 3.总结
- 4.一些其他问题
- 5.如何理解“目录”
一、前言
我们在前面已经知道了一个被打开的文件,在操作系统中是以进程来创建一个struct file结构体来描述这个文件。这个结构体里面有文件的各种信息。
那么没有被打开的文件呢?
如果一个文件没有被打开,那么是在磁盘中存储的。
我们会比较关心以下几个问题
- 路径问题
- 存储问题
- 获取的问题(属性+文件内容)
- 效率
文件 = 文件内容 + 属性 -->所以我们知道,磁盘上存储文件 = 存文件的内容 + 存文件的属性
而文件的内容是使用数据块来存储
文件属性是用inode
所以Linux的文件在磁盘中存储,是将属性和内容分开存储的!!
就比如下图中,test.c的文件属性和内容是分开来存储的
二、认识硬件----磁盘
1.基本介绍
在十几年前,我们的电脑用的是磁盘,也就是机械盘。
后来随着技术的进步,我们用的都是固态硬盘了(SSD)
不过由于磁盘比较便宜,所以现在企业用的还是磁盘。
磁盘是我们电脑唯一的机械设备,也是一个外设,所以注定了磁盘的效率一定低下
如下是一个光盘的图片
这种光盘它只有一面是光的,另外一面是介绍信息的
如下是一些磁盘的照片
在这些磁盘中,它的盘面两面都是光的,也有好几张盘面
如下所示
当我们关机的时候,这个磁头会停靠在下面的这个磁头停靠点。当磁盘加电以后,盘面高速旋转。同时磁头也在来回左右摆动。
同时我们也要知道,磁头是一面一个,一个盘面有两个磁头,上下各一个,也就是说,对于前面的蓝色图片中,它应该有六个磁头
不过虽然他们一面一个,但是这些磁头是整体移动。
还需要注意的是磁头和盘面不接触!
不过对于这种磁盘,不经摔,有可能摔了之后,磁头刮花了盘面,导致数据丢失了。不过现在我们的电脑都是使用了固态硬盘,一般不会出现这种问题。所以磁盘退出个人电脑是历史的必然
而且一旦磁盘有了灰尘,由于高速运转,也可能导致磁盘损坏,所以磁盘还必须得在无尘环境。
这个磁盘它的表面看上去是非常光滑的,实际是凹凸不平的。所以数据就是直接在盘面上存储的,也就是说会通过这个硬件电路主板等一些设备,会将数据导入到磁盘内部。所以我们的各种数据其实都是在盘面上以二进制进行存储的。
而我们的计算机只认识二进制,所以盘面上存储的都是二进制。我们的这个磁盘叫做永久性存储介质。内存是掉电易失性介质
我们举一个例子:吸铁石有两种极性。假设N设置为1,S设置为0,而且我们可以通过一些电气特性改变吸铁石的磁性。即改变南北极
也就是说,我们可以将这个盘面看作一个由无数个小吸铁石组成的。当磁头移动的时候,它可以通过电磁特性,将南北极逆置,这就相当于写入了一个1。如此一来,磁头可以利用它的电气特性去写入人为定义好的0,1数据的。
如果未来我们想要销毁一个磁盘的数据,我们可以去直接用火烧,使其消磁,不过这样成本太高了,所以现在我们就是让磁盘的生产厂商从软件层面一件式写0或者写1去擦除掉数据。只要符合一定的标准即可。
2.磁盘的存储构成
如上图所示
磁盘中会以马达为中心,向外辐射,就会有很多半径不同的同心圆
同心圆是有宽度的,就如同上图磁盘上的黄色一样。
如下所示的图中,它只会在上面的绿色的同心圆上存储,它具有宽度,我们下面的这个图比较夸张,中间白色的部分是不存储数据的。
它的数据就是在下图中所示的红色圈中的扇形区域中
这样的同心圆,我们称之为磁道,而磁道会被划分为一段一段的,我们称之为扇区
即每一个磁盘都有很多个磁道构成,而每个磁道又有很多个扇区组成。
磁盘被访问的最基本单元是扇区 ---- 512字节/4KB(4KB不常见)
这也意味着,未来哪怕我们只想要改变一个数据,我们都要将这512字节全部改掉。
也就是说,我们可以把磁盘看作由无数个扇区构成的存储介质
所以要把数据存储到磁盘,第一个解决的问题是定位一个扇区:哪一面(定位用哪个磁头),哪一个磁道,哪一个扇区。
像在下图中,我们这一堆盘片的同一个半径下就形成了柱面,类似于磁道
而我们前面说过,磁盘中,盘面会快速移动,磁头会左右移动,这个过程本质就是定位磁道和柱面的过程,磁头的左右自动就是在确定在哪个磁道上
一旦我们确定好了磁道和柱面,磁头就不在左右移动了。
而一旦磁头不动了,只剩下盘面在高速运转,这个过程就是定位扇区的过程。
所以磁盘本身的效率就取决于磁头的左右移动,以及盘片的转速。所以如果磁盘上的数据是无序的乱放着的,势必会导致效率低下,因为访问的次数变多了。这是因为运动越少,效率越搞,运动越多,效率越低
所以我们软件设计上,设计者,一定要有意识的将相关数据放在一起。这样可以减少机械运动。
前面的这种磁道(Cylinder),磁头(Header),扇区(Sector)这种方式我们称之为CHS寻址方式,这要我们有这三个参数,就可以找到磁盘中的某个扇区了
3.磁盘的逻辑结构
如下是磁带的图片
这个磁带和磁盘一样都是一种存储介质。
对于这个磁带,他物理上卷起来,就有点类似于磁盘的同心圆一样。
这个磁带他物理上是一个圆形的,不过我们可以将他扯出来,即延展开,逻辑上,我们看他是线性的
也就是说,我们可以将这个抽象为一个线性结构
.
而磁盘也一样,它在物理上是一个圆形的,但是在我们看来,逻辑上也可以是一个线性的
比如磁盘有六面,所以我们可以把他看作是这样的
而这个每一面都有很多的同心圆,每个同心圆都是一个磁道
而每个磁道上都有很多的扇区
所以最终这个磁盘就会被抽象为,以扇区为基本单位的数组
所以它的任意一个扇区都有下标
可是问题是磁盘只知道CHS
而此时我们已经有下标了,所以我们可以用下标逆向的推出对应的CHS了(注意:里面和外面的扇区数量是一样的,因为我们可以通过一些密度的方式去让大小虽然不一样,但是容量是一样的)
比如每个盘面2w个扇区
每个盘面有50个
每个磁道有400个扇区
假如当前我们是28888号扇区
我们先用28888/20000 = 1,代表如果第一个是0号盘面,那么此时处于1号盘面
然后28888%20000=8888,代表是这个盘面中的第8888号扇区
8888 / 400 = 22 ,代表是第22号磁道
8888 % 400 = 88, 代表是88号扇区
所以C磁道为22,H磁头为1,S扇区为88。所以这样物理地址即CHS地址就出来了。
而前面的28888是逻辑扇区地址,即LBA地址。这样的话他们就可以自己去进行转化了。
以上就是对于磁盘理解的建模过程
4.回归到硬件
在我们的计算机中,不仅仅CPU有寄存器,其他设备(外设也有),磁盘也有。
磁盘以下几个寄存器比较常用:控制寄存器,数据寄存器,地址寄存器,状态寄存器
如下所示,控制寄存器中可以存储r/w,以便控制I/O方向,数据寄存器可以从内存中读取数据等等,地址寄存器用的就是LBA地址,进行转化后找到在哪里进行读写
从而将最终向磁盘中写入数据,最终状态显示出结果,操作系统就可以根据状态寄存器里面的结果来进一步决定
三、文件系统
1.划分
假设我们现在我们现在的磁盘有800GB,那么操作系统如何对这个磁盘空间进行管理呢?
我们现在知道了操作系统可以使用LBA来访问扇区
那么我们怎么知道哪里存的是文件的属性,哪里存的是文件的内容呢?
我们知道我们现在有800GB,我们知道有这么大,是非常难以管理的,所以我们要对其进行划分
所以既然800GB不好管理,那么200GB就好管理了。以上的划分工作我们也称之为分区
而我们要分区这个是非常简单的,如下所示,只需要定义好起始和结束即可
struct partion
{
int start;
int end;
}
struct partion part[5];
所以我们只需要将着200GB的空间给管理好,其他的空间都是一样的,都可以这样去管理
但是这200GB也不是很好管理,因为它也很大,在这个分区中,它的头部有一个Boot Block
这个Boot Block它通常会记录操作系统刚启动时候的信息,起始分区在哪里,结束分区在哪里。这个是与开机相关的东西。一般这个在第一个盘面的0号磁道的0号扇区处放着,在一些其他的分区头部有时候也会有。在其他分区的头部也会有的原因是防止磁盘这里的数据丢了,导致操作系统挂了,这样在一些其他分区的头部也有一份,以便这个数据丢失以后,可以直接去拷贝一份,利用修复工具去修复。
不过这个Boot Block并不是一个重点,因为它是与开机相关的,在我们的职业生涯中,服务器基本不会关机的。
除了第一个东西以外,剩下的东西是由无数个Block group组成的。即划分为了很多个Block group
假设每一个Block group有10GB,那么只要我们将每一个Block group给管理好了,那么所有的10GB都可以管理好,进而200GB就可以管理好,进而整个磁盘就可以管理好了
所以我们现在的问题就进一步转化了为对这个10GB的研究
上面的这种思想就是分治的思想
2.Block group
在如下所示的分区中
第一个Super Block(SB)描述的是文件系统的信息
第二个Group Descriptor Table(GDT)描述的是分组的情况
剩下的四个字段都是存储数据和属性的内容了
(1)Data blocks
首先是Data blocks它是存文件内容的的区域,比如它有15000个扇区,它会将这么多扇区以块的形式呈现出来。即磁盘认为访问的最小单位是扇区,但是文件不一定这么认为,它可以被设置为1024,2048,4096。常见的是4KB大小,它被称为文件系统块大小!
就比如说,我们要创建一个新文件,哪怕我们只在这个文件中写入一个字节的内容,在文件系统中,都要在某一个分区中,找到某一个块组,在这个块里面,申请一个块,这个块必须是4KB,也就是要把连续的八个扇区拿出来。
所以说在磁盘上访问的最小单位是512字节,但是在操作系统上他是以4KB为单位进行块级别的去访问的,从我们的磁盘当中,把磁盘调入到内存,以4KB方式进行调入,当他写到磁盘时,也以4KB方式来进行写入。
我们可能会觉得这个比较浪费,但是其实还好,因为任何一个文件,只有最后一个块才会产生浪费
一般而言,每一个块只有自己的数据
同时这个Data blocks是我们整个Block group块中占据最大的那一部分
其次就是inodeTable比较大,剩下的都比较短
(2)inode Table
第二个是inode Table
inode是单个文件的所有的属性,一般大小是128字节
也就是说所有文件的属性都在inode里面存储,所有的文件内容都在Data block里面存储。
一般而言,一个文件,一个inode
所以我们这里会有很多个inode
当我们在打开文件的时候,首先读取到的肯定是inode
而这里有很多个inode,所以每个inode有唯一的编号
一般而言每个文件可能会占用很多个块,所以当它有多个块的时候,占了哪些块也算是文件的属性,所以这个inode和数据块一定会建立某种对应的关系
所以这里,也再次印证了我们之前所说的Linux的文件在磁盘中存储,是将属性和内容分开存储的!!
我们可以将inode想象成一个结构体,它里面存储着文件的属性
struct inode { //文件的所有属性 };
我们需要知道的一个事实是:在Linux中,文件的属性,不包含文件的名称!!!
如果我们要查一个文件的inode,我们可以用ls -li就可以看到,这个i代表的就是inode
所以在Linux里,标识文件其实用的是inode编号,而不是文件名那么现在我们已经有了inode了,如何找到对应的数据块呢?
其实在我们的inode结构体里面,是这样的
#define NUM 15 struct inode { inode number; 文件类型 权限 引用计数 拥有者 所属组 ACM时间 int block[NUM]; //NUM随操作系统的不同而不同 };
有了上面的这个block数组以后,我们就可以将每个文件所用到的数据块的编号这个数字一次填到这个数组里。 然后就建立了属性和文件的对应关系
根据数组里面的块号就可以直接将文件的内容读出来了
不过我们可能会好奇,一个块只有4KB,而我们只有15个块,难道一个文件最大只能由60KB吗?当然不是的
如下图所示中,其实如果是15个大小的数组的话,那么前12个就是直接索引,第13和第14个是间接索引(两级索引)
即如下图所示,他会认为这里所指向的内容并不是文件的内容,而是继续存储文件的块号。利用这个块号继续索引其他块
而第十五个块号存储的是三级索引,也就是说,它指向的块号,是用来存储二级索引的
最终经过inode的映射就可以找到对应的文件内容
(3)Block Bitmap
假设我们这个Data bolcks里面由20000个块
我们知道申请一个块和创建一个块是比较常见的动作
所以我们怎么知道那些块被使用,哪些块没有被使用呢?
如果我们由20000个块,所以我们就有20000个比特位
所以比特位的位置和块号映射起来,比特位的内容表示,该块有没有被使用!
所以删一个文件的时候,用不用把块(文件内容)清空呢?
其实是不用了,我们只需要把对应位图的编号内容置0即可。
(4)inode Bitmap
同理,像Block Bitmap一样,我们要知道哪一个inode有没有被占用
所以用这个inode Bitmap可以将比特为的位置和inode编号映射起来,比特位的内容是inode是否是有效的!
所以我们删除一个文件只需要去考虑Block Bitmap和inode Bitmap即可,与后面的内容和属性没有任何关系!
(5)Group Descriptor Table(GDT)
GDT描述的是整个分组的基本使用情况
如Block Bitmap,inode Bitmap等使用情况
整个分组的使用都由GDT来统一管理
(6)Super Block
Super Block它表示的是文件系统的基本信息
它称之为超级块
它有个基本特点,它描述的是整个分区的文件系统的基本情况,虽然这个Super Block是在这个组里面的。但是它表示的是分区的基本情况
比如下图中,Super Block衡量的是这200GB的基本情况
Super Block代表的是文件系统的基本信息:里面包含的是整个分区的基本使用情况。
比如一共有多少个组,每个组的大小,每个组的inode数量,每个组的block数量,每个组的起始inode,文件系统的类型与名称等那么每个组都有Super Block吗?
其实不是的,Super Block不会在也不必在每个组里存在。
不过Super Block会在系统里存在多份。
这是为了防止该组的Block Group挂掉了,各个组的边界都消失了,最终导致这200GB的分区都挂掉了。
那么操作系统怎么知道哪个块有Super Block呢,其实是有一个魔数的东西
3.总结
每一个分区在被使用之前,都必须提前先将部分文件系统的属性信息提前设置进对应的分区中,方便我们后序使用这个分区或者分组
上面这个过程其实也是格式化
这个过程也有点类似于,先描述好属性的信息,然后再组织好文件。
我们可以看到某个文件的属性信息
4.一些其他问题
首先我们要知道
在Linux系统中,一个文件,一个inode,每一个inode都有自己的inode编号(inode的设置,是以分区为单位的,不能跨分区)
inode表示文件的所有属性,但是文件名并不属于inode内的属性
那么
新建一个文件,系统要做什么??
删除一个文件,系统要做什么??
查找一个文件,系统要做什么??
修改一个文件,系统要做什么??
首先,当我们新建一个文件的时候,一定会在某一个路径下创建,它是可以确认在哪一个分区里面的。
一旦确认在哪个分区了,在整个分区上去分配inode
而整个分区上,比如说第0组是110000,第二组是1000120000
比如说,我们用Block group 0 ,于是就用它获取一个inode编号。
只需要查GDT,先发现它的使用率很低,还有很多的inode,然后去扫面inode Bitmap去位图中扫描对应的编号。找到最小的那个即可。
比如说是10005个inode,我们就可以很方便的确认是第二个组里面
然后我们将对应的位图置为1即可
最后在对应的inode Table里面将对应的文件属性一填写。
至此这个文件就新建成功了
当我们想要去文件写入的时候,先确认写入数据量的大小,然后去遍历这个Block Bitmap去找到没有使用的块,找到块号之后,直接将块号填写到对应的属性之中
然后再次跳转过去,将文件内容写入进去
至此文件就写入成功了
如果要删除某一个文件
我们是一定知道它的目录的,也就知道他是处于哪一个分区的
我们也一定知道它的inode的。
我们根据inode的范围确认它是哪一个分组的
然后就可以减去前面已经有的inode的值
然后我们去找到对应的inode里面的数据块表,然后根据这里面的块号,去修改对应的Block Bitmap,将对应的内容由1置为0即可。这样的话,对应的数据内容就删除掉了
然后就可以直接再inode Bitmap中去索引了,然后我们就只需要将这个比特位由1置为0即可
所以我们的删除只是删除对应位图中的数据
所以下载就会很慢,但是删除却很快
所以说删除其实等于允许被覆盖
所以查找一个文件也很简单
我们只需要去找到对应的位图结构中去看该inode是否有效
有效则进一步inode Table里找到对应的inode,把对应的属性全拿出来。
然后通过里面的数组找到对应的数据块,将里面的数据拼接好,交给我们
这样就把文件打开了
就好比下面的例子中
我们查看test.txt的内容,就是先根据它当前的路径确定再哪个分区,然后根据inode去找到对应的Block group,最后找到对应的inode,然后从inode里面的该数组中,找到对应的数据块内容,最后将数据块的内容加载到内容中。
如下面的例子
当我们去查看某个文件的信息的时候,也是一样的,先确定分区,然后找到对应的Block group,最后从inode Table中,找到对应的inode结构体,然后打印对应的内容。
如果我们要修改文件
比如修改属性,那么也要通过inode从而找到对应的inode属性数据块
找到之后直接进行更新即可
如果是要修改文件内容
比如是新增,那么也要找到inode,然后找到对应的数据块,如果不够,就再申请一个数据块,然后往数据里面一填写即可
如果要删除,直接去将inode里面数组的数据块下标所对应的Block Bitmap给清空即可
5.如何理解“目录”
以上所有的问题都得到了解答,现在所有的问题都指向了一个问题。
我们知道inode里面是没有文件名的,那么我们要获取一个文件名的时候,是先要文件名还是先要inode。即我们怎么知道一个文件的inode???
我们使用者从来没有关心过inode,用的永远都是文件名!!!
所以文件名和inode一定会产生某种关系
我们需要理解“目录”
那么目录是文件吗?
目录肯定是文件,它也有自己独立的inode,目录也要有自己的属性。
那么目录有内容吗?
我们知道文件 = 内容 + 属性
那么目录一定有内容
那么目录要不要数据块呢?
当然目录也要有数据块!!
那么目录数据块里面放什么呢?
它里面存放的其实是
该目录下,文件的文件名和对应文件的inode映射关系!!
所以当我们使用ll指令的时候
我们需要先找到该目录文件的inode结构体
所以我们就能从数据块中找到对应的文件名和inode对应的关系
然后就能找到对应的文件了,最后对接上前面的部分了
所以我们就知道了
- 为什么同一个目录下不能有同名文件(因为目录里面的数据块是一共KV结构,K不能重复)
- 目录下,没有w,我们无法创建文件(因为无法将对应文件的文件名和inode映射关系写入数据块中)
- 目录下,没有r,我们无法查看文件(因为没有r,我们无法访问数据块,拿不到文件名和inode的映射关系)
- 目录下,没有x,我们无法进入这个目录(当我们要进入一个目录的时候,本质就是把这个目录的inode找到以后,然后将我们当前的环境变量做出更新,可是现在,我们可以加一个判断,不让将这个目录拼接到环境变量中,不让更新环境变量)
可是目录是文件,也有inode编号,我们怎么知道目录的inode呢?
我们知道,我们当前的目录的inode本质也是上一级目录的文件。所以我们可以去访问上一级目录去找到它的inode。
最终我们会一路向上找,最终找到根目录。
根目录对应的文件名是确定的。
所以访问任何文件都需要通过路径来进行访问。
不过这样我们可能会觉得很慢?因为要一路递归下去。
这样确实很慢,所以Linux会将我们曾经访问过的,常见的目录都放在一个dentry缓存中,需要用的时候直接从缓存中拿,而不用直接读盘了。