目录
- 1. 认识磁盘
- 1.1 结构组成
- 1.2 抽象磁盘结构
- 1.3 磁盘内的寄存器
- 2. Linux ext2 文件系统
- 2.1 Data blocks && inode Table
- 2.2 Block Bitmap && inode Bitmap
- 2.3 Group Descriptor Table && Super Block
1. 认识磁盘
虽然我们现在个人计算机基本都使用的是 SSD 固态硬盘,但在大型企业的后端服务器中,因为单价性价比更高,所以磁盘依旧保持一个不可替代的角色。
1.1 结构组成
磁盘是计算机硬件中唯一的机械设备,其主要构成有盘片,磁头,磁头臂,主轴马达等部件。
- 一个盘片有两面盘面,每个盘片都是由无数个同心圆组成。
- 每个盘面都有一个对应的磁头
- 所有的磁头都连在同一个磁头臂上的,因此所有磁头只能 “共进退”
- 所有的磁头和盘面不接触
每个盘片被划分为一个个磁道,每个磁道又划分为一个个扇区,最内侧磁道上的扇区面积最小,因此数据密度最大。而磁盘被访问的最基本单元是扇区,主流扇区大小有 512 byte 或者 4KB。我们可以把磁盘看做由无数个扇区构成的存储介质。
而要把数据存到磁盘,第一个解决的问题是定位一个扇区,先找到哪一个盘面(即定位用哪个磁头),再找磁道,最后定位扇区,这种寻址方式称为 CHS(Cylinder Header Sector)寻址方式
1.2 抽象磁盘结构
早些年,存储介质不仅有磁盘,还有光盘、磁带等。其中磁带就是一圈一圈缠绕起来的,与磁盘相似,可以看作由无数个同心圆构成。磁带放在磁带机工作,磁带就是被拉出来读取的,然后再缠绕回另一端。
所以如果我们愿意,我们可以自己把磁带拉出来读取。而磁带跟磁盘是一样的,都可以看作无数个同心圆。这样一来,磁盘在逻辑结构上就可以抽象为线性的。
这样一来,在磁盘扇区中,任何一个扇区都有下标,假设每个盘面有2w个扇区,每个盘面有50个磁道,那么每个磁道就有400个扇区,而某一个扇区编号为28888,那么就可以通过计算得到其所在磁面、磁道、扇区。
28888 / 20000 = 1, 位于 1 号磁面(从 0 号开始计算)
28888 % 20000 = 8888,位于该磁面的第 8888 号扇区
8888 / 400 = 22,位于该磁面的第 22 号磁道
8888 % 400 = 88,位于该磁道的第 88 号扇区
所以该扇区位于1号磁头,22号磁道的88号扇区,其中的扇区编号为 28888,称为逻辑扇区地址(LBA地址),LBA地址是可以与CHS 通过计算互相转换的。换言之,当磁盘想要定位查找某个文件,只需要左右摆动磁头,确定数据所在磁道,同时高速旋转的盘面,可以定位扇区。
1.3 磁盘内的寄存器
不仅仅CPU有"寄存器",其他设备,例如磁盘也有寄存器,用于
- 控制寄存器:用于控制磁盘读写
- 数据寄存器,存储数据
- 地址寄存器,记录LBA地址
- 状态寄存器,记录数据读写的状态情况
2. Linux ext2 文件系统
一个文件被打开,那么每一个文件会有一个文件描述符,操作系统为了方便管理打开的文件,采用 “先描述,后组织” 的形式,以 struct file 来描述打开的文件,并且与进程关联起来,设置文件描述符表,存储指向文件对象的指针。
那么对于没打开的文件,则在磁盘中存储,并且需要解决如下几个问题
- 文件路径问题
- 存储问题
- 获取问题(文件 = 文件属性 + 文件内容)
- 效率问题
我们都知道,进程、打开的文件都需要被操作系统管理起来,那么对于没打开的文件也自然要被管理。管理没打开的文件,本质就是在管理磁盘的数据。假设一个磁盘 500 GB,这么大的数据管理起来势必非常棘手,于是操作系统采用分治思想,先把磁盘做分区,再继续把每一个分区划分为 n 个块组(Block Group)。假设一个块组 10GB,这样一来,操作系统就从需要管理 500 GB 的数据转变为管理 10 GB 的数据,对于其它的块组、分区采用相同的管理方式即可。
换言之,想要了解操作系统是如何管理磁盘文件的,只需要了解 Block Group 的设计理念即可,Block Group 就是文件系统。
2.1 Data blocks && inode Table
Data blocks: 存放文件内容的区域。假设其大小为 1w 个扇区组成,以块的形式呈现。磁盘认为访存的基本单位是扇区,但是文件的块大小不一定按照扇区的大小来设计,文件系统的块大小一般为 4kb。换言之,假如我今天往一个文件写入的数据只有 1 byte,但是操作系统还是会给我连续开辟 8 个扇区的空间,用于存放这个文件的内容,通过文件系统去访问该文件的时候,一次也是连续访问 4KB 大小。
inode Table: 单个文件的所有属性,主流的大小为 128 byte,一般都是一个文件分配一个 inode,每个 inode 具有唯一性。 而 inode Table 这个名字就注定了这是一个表,里面存储了很多 inode,
所以今天我创建一个新文件,那么只需要在 inode Table 中开辟一块空间,给该文件分配一个 inode,把文件的所有属性填充在 inode 中即可;如果该文件还没有写入数据,Data blocks 块可以暂时不用分配。
[outlier@localhost inode]$ ls -li
total 0
#inode编号
34165052 -rw-rw-r-- 1 outlier outlier 0 Aug 10 22:03 log.txt
34150681 -rw-rw-r-- 1 outlier outlier 0 Aug 10 22:03 test.txt
每个文件的大小都是不一的,可能有一个文件很大,需要好几个数据块存储,那么我们就必须要知道,文件的内容存储在哪些数据块中,所以就注定了 inode 中必定需要有字段与 Data Block 建立关联,这样才能找到某个文件对于的数据块。
// inode 描述结构体
#define NUM 15
struct inode
{
inode id;
文件类型;权限;引用计数;拥有者;所属组;ACM 时间 ....
int blocks[NUM]; // 记录存储的数据块
}
假设 blocks 数组 15 元素,一个块大小 4KB,这样也才 60 KB,一个稍微大一点的文件就存储不下了。所以这个数组肯定不是直接记录数据块的块号那么简单。
假设这个索引数组有前12个元素是直接记录存储文件内容的块号,第12、13号下标采用两级索引,里面分别存储 200、2
01块号,但是 200、201号数据块不会用于存储文件内容,而是在这两个块中,存储其它数据块的块号,文件内容就存在与其中的数据块中。如果稍微计算一下,两个块 4 * 2 = 8 KB,8 * 1024 / int = 2048,即可以记录 2048 个块号,每一个块 4KB,一共可以存储大小为 8 MB 大小文件内容了。
如果再不够用,那不是还剩下14号下标吗,我采用三级索引,指向的一个数据块,同样不存储数据,这个块可以记录 1024 个块号,我让这记录的 1024 个块,再索引一次,一共可以记录 1024 * 1024 个块号,加起来的大小就是 1024 * 1024 * 4 kb = 4096 MB = 4GB。现在够用了吗??如果还不够,我可以减少直接索引的个数,继续增加两级索引,三级索引。
2.2 Block Bitmap && inode Bitmap
对于数据块,当我们新建一个文件,就给我们分配数据块,删除文件,释放数据块。但是,操作系统如何得知哪些数据块有被使用,哪些没有被使用呢?
Block Bitmap: 比特位的位置和块号进行映射,比特位的内容即表示该块有没有被使用(假如第800个比特位置1,代表编号为800的数据块被使用,置0则代表该数据块无文件使用)。
inode Bitmap: 比特位的位置和 inode 编号进行映射,比特位为1,代表映射对应的 inode 有效(有文件占用)。
有了 Block Bitmap 和 inode Bitmap,当我们删除一个文件时,就不再需要真的清空文件内容了(即不需要对数据块中的数据做清空),只需要根据文件的 inode 编号,查看 inode Bitmap,若位图为1,转而 inode Table 中的读取该 inode 的属性, 其中 blocks 记录了该文件所有的数据块编号,将块的编号映射到 Block Bitmap 对应的比特位,将比特位置 0,最后再把 inode Bitmap 的比特位置 0 即可。这也是为什么我们拷贝数据很慢,但删除文件,就算是大几十个GB,也就几秒钟的事情。
换言之,从理论技术出发,只要我们知道被删除文件的 inode 编号,我们是可以通过 inode Bitmap 把位图置 1,再根据 inode Table 读取文件属性,blocks 读取该文件所有的数据块编号,映射到 Block Bitmap,把这些比特位全部置 1,这样,一个被删除的文件就可以被恢复了。
2.3 Group Descriptor Table && Super Block
整个磁盘从分区到分组,最后细化成每个 Block Group。但是每个组内,该如何得知这个组内有多少个 Block?有多少个 inode?被占用的 inode、数据块有多少?没有被占用的有多少?整个 Block Group 的容量是多少?Block Group 的容量占用比是多少?下一个 inode 编号如何分配,…… ,与 Block Group 相关的所有属性,全部都记录在 Group Descriptor Table 中,简称为 GDT。
Group Descriptor Table(GDT): 描述整个分组 Block Group 的基本使用情况。
与 Block Bitmap && inode Bitmap 不同的是,Block Bitmap && inode Bitmap 只是关注某个数据块 && 某个 inode 的使用情况,并无法得知 Block Bitmap && inode Bitmap 的整体使用情况(使用率,总容量等等)。
Super Block: 文件系统的基本信息,里面包含的是整个分区的基本使用情况(分区容量,分区划分了多少分组,每个组的边界是什么,每个组的大小容量,每个组的 inode 数量,每个组的 Data block 数量,每个组的起始 inode编号,文件系统的类型与名称等等)。
需要注意的是,整个分区不止一份 Super Block 数据,但不是每个分组都有 Super Block 这样的字段。如果整个分区只保留一份 Super Block,那么当 Super Block 的数据出 bug 了,分区内的各个组可能都会被影响的(无法明确每个分组的边界,无法明确每个组的起始 inode……等等)。分区内存储多份 Super Block,可以在必要时对出问题的 Super Block 数据进行恢复修正,但是如果每个组都存在一份 Super Block,当分区内的使用情况需要更新时,成本就比较高。
格式化: 每一个分区在被使用之前,都必须先将部分文件系统的属性信息提前设置到对应的分区中,方便我们后续使用这个分区或者分组(加载 Group Descriptor Table && Super Block 的各种属性信息,清空 Block Bitmap && inode Bitmap,除了系统文件,将其它比特位都置为 0)。
总结:诸如 Group Descriptor Table && Super Block && Block Bitmap && inode Bitmap 这些字段数据都是为了管理文件数据 Data blocks && inode Table 而设置的属性信息!
下篇文章 【文件系统】软硬链接 会介绍与 inode 有关的软硬链接问题,有了文件系统的铺垫,理解软硬链接就不再是难事了。
如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!
感谢各位观看!