目录
1.磁盘的结构
1.1磁盘的物理结构
1.2 磁盘的存储结构
1.3 磁盘的逻辑结构
2.文件系统
在上一篇文章基础IO中,我们主要是讲了被打开的文件与进程的关系,以及操作系统是如何管理这些被打开的文件的,但是磁盘有这么多文件,被打开的和没被打开的都需要操作系统进行管理,比如将他们放在合适的位置,按照合适的格式与布局,方便用户随时打开,所有的这些文件,包括被打开的和没有被打开的文件,都是需要操作系统的文件系统模块进行管理的。
既然文件是存在磁盘上的,那么首先我们需要了解磁盘。
1.磁盘的结构
1.1磁盘的物理结构
磁盘我们可能现在见的很少,因为现在我们的笔记本电脑上基本上用的都是固态硬盘ssd了,他的效率比磁盘高,但是相应的价格也更高,但是在我们的企业端,磁盘由于存储容量大价格低,依旧是存储的主流。
磁盘是我们计算机中唯一的一个机械结构,同时他还是外设,刷冲buff的加持,导致他的访问速度相对于cpu和内存而言就非常慢了。
对于磁盘的物理结构,我们可以在网上找几张图片来稍微了解一下
这是一摞磁盘的侧面结构图
这是磁盘的俯视图,磁盘我们可以看到磁盘是可以多片形成一摞来存储的,一摞有多个磁盘,我们可以称之为盘片,每个磁盘都有两面,每一面都是可以存储数据的。同时每一面都配有一个磁头,那么一个盘片就有两个磁头,一摞由n个盘片组成的磁盘阵列就有 2n个磁头 。盘片是有一个马达来进行转动的,所有的盘片都是连在这一个主轴马达上,所有所有的盘片的转动是统一的。 磁头也是一摞,数量是盘片的两倍,同时,所有的磁头也是连接在一个音速马达上的,磁头的摆动也是统一的,当然磁盘也有对应的硬件电路以及伺服系统。盘面和磁头之间是有一些举例,距离很小,这就导致了磁头必须很稳定,如果磁头有抖动,就有可能刮花盘片,可能导致数据丢失。
1.2 磁盘的存储结构
磁盘并不是我们想象中的那么光滑,微观上看磁盘是凹凸不平的,凸起的地方表示被磁化,表示二进制的1,凹下去的地方表示没有被磁化,代表二进制的0 。
磁盘的存储结构我们也可以找到一些图片来理解。
磁盘的存储结构是由磁道和扇区来划分的,一个盘面被划分为很多的同心圆,同心圆之间的区域就叫磁道,每个磁道之间是有一定间隔的,就比如上面的图中,并不是所有的圆环状结构都是磁道,磁道与磁道之间需要相隔一段距离,以免磁头在对某一磁道读写数据时不会对相邻磁道产生干扰。 同时,磁道也是分扇区的,可以理解为从圆心射出的一条一条的射线将每一个磁道分成了若干个扇区,当然,射线之间的角度是相同的。
磁盘的寻址的时候,也就是进行读写的时候,不是以字节为单位的,而是以扇区为基本单位的,一个扇区的大小是512个字节。但是从上图中我们可以看出越远离磁盘中心的山区的面积越大,每个扇区设计的比特位密度不一样,这就能够让每一块扇区的存储容量都一样大了。 如果把所有山区的比特位密度都设置为一样的,那么当然越往外的扇区的存储容量就越大,但是这时候就需要对磁头的读写进行更复杂的设计了,不如第一种方案来的轻松,同时还可能会出现其他的一些问题,所以现在一般还是采用的所有扇区的存储容量都是512字节。
磁盘被称为块设备的原因就是因为他的基本单位不是一字节,而是512字节。
当我们将数据存入磁盘特定位置或者要从磁盘中读取数据时,我们如何找到磁盘的相应位置呢?也就是磁盘的寻址的问题。
如果只是像上图的单面的磁盘,我们只需要确定磁道和扇区就能够找到唯一对应的一块扇区了。磁道和山区编号都是从0开始编号,我们可以定位上图中的绿色部分扇区为 5 磁道的某一扇区,具体是第几扇区我们可以为扇区划分一个起始的编号位置。
在机械设计上,我们首先知道磁片是在快速旋转的,而磁头则是在一定范围内左右摆动,左右摆动就是在确定磁道,而确定好磁道之后,磁头就不动了,磁片旋转就是在确定扇区,当扇区的起始位置被磁头读取到的时候,磁头就开始读取或者写入该扇区的数据。但是我们知道,磁片的旋转速度是非常快的,而磁头则需要一次性的就将该扇区的所有数据都读出来或者写进去,这就要求磁头要有一定的读取速度,同时也不能一味追求磁片的旋转速度,因为如果磁头的读取速度跟不上,磁片的旋转也就没有意义了,总不能每一次都从扇区的起始位置开始读,然后还没读完就转走了吧。
我们的磁盘一般是成摞使用的,那么成片的盘面和磁头的存储结构中我们还有一个概念叫做柱面
柱面就是一摞磁盘的同一条磁道组成的一个立体的类似于圆柱的结构,起始柱面我们就可以理解为磁道。
那么在这种多个磁片的寻址的时候,就需要确定三个东西, 一是所有磁头一起摆动确定在哪个柱面或者磁道,然后确定是在哪个磁头(也就是在确定是在那个盘面上),最后确定是在哪个扇区。
这种磁盘的寻址方法我们称之为 CHS 定位法 ,C(柱面 cylinder)H(磁头 head)S(扇区 sector)。
1.3 磁盘的逻辑结构
对于磁盘的这种磁道和扇区的存储结构,操作系统是不方便直接使用盘面磁道和扇区管理的。但是我们却可以将其抽象成为一个线性的数组,我们知道磁盘是扇区为基本单位的,那么我数据中的元素就是一个扇区也就是512byte,而磁道和盘面的划分则是用一定的下标范围来表示。
这样一来,我们在逻辑上可以将磁盘抽象成一个 sector arr[n]的数组,数组的每一个元素都是一个扇区,这样一来,操作系统对磁盘的管理,就转换成了对数组的管理,这就是先描述再组织,哪一部分是第一面,每一个磁道有几个扇区等,我们都可以使用下标的范围来表示。
在物理上我们使用CHS定位法来进行磁盘寻址,而在操作系统层面,则是使用数组的下标来定位一个扇区,这种单位的方法叫做LBA定位法,我们称这种地址为LBA地址。
只要知道了扇区的下标,就能通过一系列算法来将下标转换为 盘面 磁道和扇区的编号,比如我们可以通过取模和取余数操作来进行定位,将LBA地址转换为CHS地址,从而让磁头使用对应的CHS地址进行写入和读取
为什么操作系统要使用逻辑地址LBA而不直接使用CHS地址呢?
1.便于管理,管理一个一维数组的成本肯定是要比管理一堆三维的CHS地址要简单的。
2.不让OS代码与硬件强耦合。因为操作系统不一定只管理磁盘,也有可能会管理ssd等其他的存储设备,那么对于操作系统而言,管理这些存储设备都可以抽象为管理一个数组,操作系统只需要使用LBA地址就能够对硬件进行管理,底层调用驱动的时候驱动中会有将LBA地址转换为具体的硬件的地址的方法,这样就让操作系统与硬件解耦了,当底层硬件发生变化时,不会影响我们的操作系统,只需要修改数组下标和物理地址的映射关系。
2.文件系统
虽然磁盘访问的基本单位是512字节,但是对于操作系统而言依旧很小,因为我们一般的文件都是至少几kb或者几mb,如果每次操作系统只加载一个扇区也就是512byte的话,那么与磁盘的IO次数就会变多,而进程访问磁盘是很慢的,既有可能阻塞,同时还需要等待磁头定位数据等,这会导致我们的进程的效率十分低。
因为一次读取一个扇区读到的数据太少,所以操作系统的文件系统会定制的进行多个扇区的读取,也就是一次不是读取一个扇区512byte,而是一次读取 1kb、2kb,或者4kb(最常见)也就是八个扇区为基本读取单位。你哪怕只需要访问一个比特的数据,操作系统也会把其附近的4kb的数据都加载到内存中(数据所在的块),如果你需要写入,那么也是一次性将4kb的数据写入到磁盘的对应区域,整体读取和整体写入。这样做虽然有点浪费,但是在计算机领域有一个局部性原理,大概就是当你需要访问某一个数据时,它的周围的数据很大概率会被访问(提高缓存命中率),那么一次性将4kb的数据都加载到内存中来就能减少IO的次数,提高了效率,这本质上是一种数据预加载,以空间换时间的做法。
所以其实内存也是被划分为 4kb大小的空间的,比如4GB的内存就可以划分为1M个4kb大小的空间,没一块空间被称为页框,操作系统为了提高效率,必定会对内存进行区域划分来为磁盘数据的读写提供便利,我们前面也讲过内存与外设的IO的基本单位就是4kb.
磁盘中的文件尤其是可执行文件,是按照4kb大小划分好的块,比如将代码放在一个块中,将数据放在一个块中...,这些块被称为页帧,这样做也是为了方便将数据加载到内存中,以提高效率。
IO块的大小我们可以使用 stat -f / 命令来查看,因为不同的操作系统可能会有一些差异,但是绝大多数还是以4kb为基本单位的。
既然内存和外设之间进行IO的基本单位是4kb,那么把磁盘以扇区划分就不是很方便了,我们也可以把磁盘划分成为以4kb为基本单位的线性空间。 但是我们的磁盘的容量一般很大,加入我们的磁盘有500GN的容量,我们将其划分为4kb的空间的话,数量就太多了,也很不方便管理,所以实际在划分磁盘空间的时候首先是将磁盘划分为不同的区域,也就是分区,分区是从逻辑上对其进行分区而不是从物理上。
将整个磁盘划分为多个分区之后,比如上图中,那么晚每个分区就只需要管理好这一百多个GB就可以了,而以此类推,只要将一个分区管理好了,那么管理其他的分区使用的方法就可以直接复用了,也就是管理好一个分区了,也就管理好整个磁盘了。 就好比我们现在的笔记本,一般只有一块硬盘,但是出厂时系统就将其分为C盘和D盘,C盘和D盘其实只是分区而已,而不是真的有两块盘。
但是即使我们进行了分区,每个分区还是有100GB的空间需要管理,这还是很大,所以这时候每一个分区还需要进行分组,比如我们把100GB分成20个组,每个组就需要管理5GB的空间,当然这个5GB是我们用来举例的,实际上哥哥操作系统的文件系统可能都会有差异。
当将一个分区划分为5GB大小的分组之后,同样的道理,我们只需要能够管理好一个分组,那么其他的分组也都能够用相同的方法来管理好,也就能够管理好整个分区了,那么也就能够管理好整个磁盘了,这就是一种分治的思想。
实际上分组并不是从分区的开始就分组,而是在每一个分区的开头会有一小块区域,叫做Boot Block,这个区域叫做启动块,它的大小一般是1kb,用来存储磁盘分区信息和启动信息。Boot Block的主要作用是引导计算机启动,将操作系统载入内存
而每一个分组又是如何划分的呢?
在这之中,Super Block 叫做超级块,简称SB,他保存的是整个文件系统的信息。我们再进行磁盘分区之后,一般做的第一件事就是格式化,格式化的本质就是清空数据,重写文件系统,文件系统的信息就保存在Super Block中,他里面保存了这个分区一共有多少组、起始块号是多少、结束块号是多少、每一个分组从哪开始从哪结束、每一个分区已经被使用了多少、未被使用多少、使用率是多少,以及整个分区的健康状态还有文件系统相关的一系列方法,这些信息都被保存在超级块当中。这个超级块并不是每一个的分组都有,一般分区的第一个分组是有这个超级快的,而其他分组则不一定有,有一定比率的分组有超级块。 按照刚刚说的超级块这么重要且它是属于整个分区的文件系统,那他为什么不是保存在Boot Block这样的位置呢?我们将Super Block这样的重要信息放在多个分组中,这其实是一种对超级块的备份,因为如果我们只保存了一份文件系统,那么当这个唯一的文件系统出问题的话,我们的整个分区就寄了,而如果存在多个备份,那么当正在使用的文件系统出现问题,我们可以拷贝文件系统的备份来恢复文件系统。文件系统正常的情况下,所有的Super Block 中的内容是完全相同的,要更新所有的超级块都会同步更新。
除了Super Block ,其他五个区域才是每个分组都会存在。我们之前说过文件的属性和内容是分开存储的,保存文件属性的叫做inode块,不同文件系统的inode块的大小可能是不一样的,而在同一个文件系统中,inode块的大小是固定的,一般而言一个文件有一个inode,inode中包含了文件的几乎所有属性,为什么是几乎呢?因为有一个属性不在inode中存储,就是文件名。文件属性是存储在inode中,而文件内容则是存储在数据块 datablock 中,每一个数据块的大小也是在格式化文件是系统的时候就确定下来了,一般都是 4 kb ,而不同文件的内容以及其所有的空间大小不一样,那么他们占有的数据块的数量就不一样。
inode 就如同进程的 pcb 性质差不多,它能够在一个文件系统内标识一个唯一的文件,那么不同的inode之间为了区分彼此,就需要都一个自己的 id 编号,这就有点类似于pid,只不过pid在一个操作系统内只有一套,而inode 的编号则是在每一个文件系统中都有自己独立的一套编号,也就是每个分区之间都有一套自己的编号系统。
ls -i我们即将可以显示文件的 inode编号
按我们上面的划分,一个分组5G说大也不大,说小也不小,文件的大小也各不相同,而一个分组内能够创建的文件的个数是有限制的,毕竟空间有限,不可能无限创建文件。 inode table也就是inode表中保存了本分组所有的可用的 inode ,可用的包括已经被使用和没有被使用的 ,每一个inode一般是128个字节,同时,这个inode表在确定分组的时候就已经确定了,也就是这个分组所能够创建的文件的最大个数也就是本分组的inode数量在分组的时候就被确定下来的,所以inode表的大小在分组的时候也是确定的,也就是 inode数量 * 128 byte 。 当我们需要创建一个文件时,第一件事就是需要在inode table中找到一个没有被使用的indoe,把问价的所有属性填充到inode中,这时候问价的属性就被存储起来的了。而如果要像文件中写入数据时,则需要去找分组中未被使用的datablock进行写入。
但是怎么标识一个inode是否已经被使用呢?这就要用到分组中的 inode bitmap 了,这是一个inode的位图结构,本分组中所有的 inode 都在位图中对应了一个比特位,如何对应呢?我们前面说了inode是有编号的,而一个分组中的inode也是在分组的时候就分配好的,而同一分组的inode的编号是连续的,那么我们就能够在位图中用第 0 个比特位表示本分组的第一个inode ,以此类推,在位图结构和inode表之间就建立了一种映射关系。 而每一个inode如果已经被使用,那么它所对应的inode 位图中的比特位就会被置为 1 ,而如果这个文件被删除了,这个比特位又会被置为 0 。而我们在创建文件的时候,寻找未被使用的inode时,也不是直接去遍历inode表,而是在inode位图中找到第一个为 0 的比特位,这个比特位为 0 ,意味着他映射的 inode 没有被使用,然后把这个比特位置为 1 ,将文件的属性填充进对应的 inode 中。
而当一个文件的已有的数据用完了,需要新的数据块进行写入时,如何找到未被使用的数据块呢?这就要用到datablock的位图结构,也就是 block Bitmap ,这个位图结构的每个比特位映射着一个数据块是否已经被使用,在一个分组内,数据块也是连续编号的,所以可以映射到 位图结构中,同样,该位为 1 则表示对应的数据块已经被使用了 ,而为 0 则表示对应的数据块还没有被使用。要新增数据块的时候也是在位图中去寻找第一个不为0的比特位,找到之后置为1 ,同时找到对应的数据块进行写入。
GDT(Group Descriptor Table) ,快组描述表,这里面包含了对应分组的宏观的属性信息,比如这个分组的inode一共有多少个,被使用了多少和没有被使用的有多少,以及datablock有多少个,被使用和没有被使用的数量等一些信息。
当我们在查找一个文件时,统一使用的是 inode编号,inode是跨组的,在同一个分区内,不同组的inode是不一样的,但是不能跨分区,不同分组使用的是各自的一套文件系统。所以我们在一个分区内去查一个文件时,首先是拿着他的inode在 inode位图中找到对应的比特位,如果该比特位为0,则说明该文件根本不存在,如果比特位为1 ,再去对应的inode中拿到文件的属性。但是我们的inode中只存储了文件的属性,文件的内容怎么查找呢?或者说,我们怎么去拿到文件所占有的数据块呢? 这一点不用担心,在一个inode中保存了文件的出文件名之外的所有属性,那么文件的内容所对应的存储的数据块肯定也是保存在inode中的。在inode中,有一个数组,数组中存放的是数据块的编号,这个数组一共存储 15 个元素,也就是能够存储 15 个数据块的编号,也就能够找到 15 个数据块,那是不是意味着只能存储 15 个数组块的内容也就是 60 kb的内容呢?当然不是,我们经常见到少则几十兆,大则几个G的文件,这说明文件的大小肯定不止 60 kb的,我们可以用下面的图来理解这个数组指向的数组块的意义。
这十五个数据块编号中,前 12 个编号是指向的数据块是直接用于存储文件的内容的,这十二个数据块能够存储的数据的大小就是 48 kb 了。
而其他三个编号指向的数据块则不是直接存储数据,比如 blocks[12]指向的数据块,里面存的不是文件内容,而是存储的其他的数据块的编号,这些编号所指向的数据块才是直接用于存储数据的,我们可以想象一下,我们假设数据块编号就是一个 int 类型的数,那么 4kb的空间能够指向多少个数据块? 我们称这为一级索引,他的数据块中存的是存放数据的数据块的编号。
而blocks[13] 指向的数据块则是一个二级索引,这个数据块里面存储的是其他的数据块的编号,而他所指向的这些数据块存储的还不是内容,还是一个只想其他数据块的编号,这些编号对应的数据块才是存储数据的,
block[14]则是一个三级索引,如图所示
可想而知一个文件能够存储的数据还是很大的,所以我们并不需要担心inode能否管理足够的数据块来存储足够的内容。
创建文件和删除文件的过程其实很简单,创建文件首先需要在 inode位图中找到一个为0的比特位,将其置为 1 之后找到其映射的 inode ,然后将文件属性填进 inode 中,如果需要写入内容,则需要在 block bitmap位图中找到一个为 0 的比特位,将其置为1 ,然后将其所映射的数据块编号写入到inode的数组中,在往该数据块中写入内容。创建完文件之后,inode 的编号是要返回的,为什么要返回inode编号呢?这一点我们一会在目录的数据块中讲到。
删除文件则更简单,只需要拿着对应的inode编号,首先找到对应的 inode块,将inode中的数据块编号数组所对应的所有数据块在其位图中将比特位置为 0 ,这个步骤就相当于将文件内容清除了,然后再将inode编号所对应的inode位图中的比特位置为 0 ,这就把文件属性也清楚了,至此,文件属性和内容就删除了,但是文件名还没有删除,这一点我们下面会讲到,文件名删除之后整个文件就被删除了,所以文件删除其实采用的是惰性删除的方式,将其inode和数据块变成未被使用的,然后就可以等待被覆盖了。 所以我们平常使用电脑时,会发现下载的速度很慢,而删除的速度相对而言就十分快了。
同时,这是不是表明了我们删除文件之后是有概率能恢复的呢?由于他是惰性删除,在数据没有被覆盖之前,我们是不是可以将文件恢复过来呢?我们只需要先想办法获得我们删除文件的 inode 编号(或者在删除文件之前留过备份),然后拿着这个编号首先再inode 位图中将其对应的比特位置为 1 ,先将 inode 恢复,然后再使用inode之中的数据块编号,先将数据快编号对应的数据块位图的比特位置为1 ,一个一个恢复数据块,最后就能恢复文件了。 就比如我们的windows系统,将文件删除之后我们可以在回收站恢复文件,就算将回收站的文件也删除了,也还是有办法恢复文件,它会将文件留存一段时间。
但是在我们将文件误删之后,我们最好的做法还是什么也不要做,特别是不要进行写入或者创建文件的行为,防止你的inode和数据块被覆盖写入了,不要进行过多的磁盘IO,要是被覆盖了,我们就恢复不了了。
但是我们平时对文件进行操作的时候用过文件的 inode 吗?好像没有吧?我们使用的都是文件名来进行操作,这就说明在某个地方一定存在着 文件名 和 inode编号 之间的映射关系,而这个地方我们也不难猜到,就是目录。 任何一个文件都一定处在一个目录下,而目录也是一个文件,也有它的属性和内容,以及它的文件/目录名,也就是有自己的 inode 块以及数据块,inode块中存的自然是目录的各种属性,但是他的数据块中存什么内容呢?我们直接说结论, 目录的数据块中存的就是这个目录下的所有文件的文件名和inode的映射关系。 这就是我们平时能够平常使用使用文件名进行文件操作的原因,我们使用文件名的时候,首先就会在对应的目录的数据块中去寻找对应的映射关系,找出对应的 inode编号进而完成对文件的操作。
在同一个目录下不可能存在两个相同的文件名,同时在一个分组内也不会存在相同的 inode 编号,所以在一个目录的数据块中,文件名和inode编号其实是互为映射关系的,通过文件名可以找到对应的inode,通过inode也能知道他的文件名,当然前提是文件名的路径我们是确定的。 同时,这也是为什么我们使用文件的时候,要不就得标识绝对路径来表示文件名,要不就是默认在当前路径也就是当前目录下搜索文件,因为不同目录下的文件名是可以相同的,也就不具备唯一性,而无法构建唯一的映射了。
了解了目录的数据块之后,我们也就能够更深入的理解目录的权限了。为什么我们在一个目录下创建文件需要目录的写权限? 因为创建文件的本质就是申请inode填充属性,还有一个重要的部分就是要构建文件名与 inode 编号的映射关系, 而映射关系保存在目录的数据块中,要添加映射关系就是要对目录的数据块进行写入,所以必须要有目录的写权限才能够在目录下创建文件。那么为什么罗列目录的文件需要读权限呢? 罗列目录的所有文件当然需要知道目录的文件有哪些,自然也就需要在目录的数据块中查找,因为文件名只被保存在他所在目录的数据块中,同时要罗列文件的属性也需要在映射关系中找到文件的inode来打印文件的属性。
同时为什么公共目录有时候需要粘滞位,就是因为目录的写权限无法取出,如果没有写权限了,那么无法创建文件的公共目录也就没有意义了,但是有了写权限又意味着能够对文件进行删除操作,所以必须要一种特殊的权限来应对这种情况,这就是粘滞位的作用。