Ext2文件系统
目录
1. 了解磁盘
1.1 磁盘物理结构
1.2 磁盘存储结构
1.3 磁盘逻辑结构
2. 引入文件系统
2.1 “块”概念
2.2 “分区”概念
2.3 “inode”概念
3. 文件系统
3.1 宏观认识
3.2 Block Group
4. 路径
4.1 路径解析
4.2 路径缓存
5. 软硬链接
5.1 硬链接
5.2 软链接
6. 挂载分区
6.1 格式化
6.2 挂载
1. 了解磁盘
磁盘 是计算机中用于长期存储数据的非易失性存储设备。即使断电,存储在磁盘上的数据也不会丢失。
1.1 磁盘物理结构
磁盘盘片 是存储数据的载体,是相当于是“写”数据的“纸张”。注意:一个盘片有两个盘面
盘片通常由金属或玻璃制成,表面极其光滑。盘片上涂有一层薄薄的磁性材料,这层材料由无数个微小的磁粒组成。数据就是通过改变这些磁粒的极性(南极或北极)来存储的。一个硬盘通常由多张盘片叠放在一起,共同固定在一个主轴电机上。
磁头 是负责读取和写入数据的执行器,是“写”数据的“笔”。
每个盘片的上下两面都对应一个磁头。它们被安装在同一个磁头臂上,像一把梳子的齿一样。磁头本身非常微小,通过一个精密的音圈电机 控制其来回移动。
写入数据:当需要写入数据时,磁头会产生一个磁场,改变其正下方盘片磁性材料的极性。用“北极”代表二进制的1,用“南极”代表0,从而将电信号转化为磁信号记录在盘片上。
读取数据:当需要读取数据时,磁头会感应其下方盘片磁性区域的磁场极性变化,将这些磁信号转换回电信号(0和1)。
注意:磁头在工作时并不接触盘片!它们悬浮在盘片上方一个极小的高度上(比头发丝的直径还要小几百倍)。
当硬盘通电开始旋转时,盘片旋转产生的气流会将磁头托起,使其“飞行”在盘面上方。当硬盘断电停止时,磁头会降落到盘片上一个特定的、不存储数据的区域(称为启停区 或着陆区)。这个设计是为了避免磁头与盘片接触,划伤珍贵的磁性涂层,导致数据丢失(这就是所谓的“划盘”)。
磁道 一个盘面上以主轴为中心的同心圆环。
扇区 磁盘读写的最小物理单位。它是由磁道进一步划分而成的弧段。
传统上,每个扇区固定大小为 512字节(后面我们还是以这个数据为标准)。现代“高级格式”硬盘,每个扇区大小为 4096字节(4KB)。操作系统从磁盘读取数据,即使只需要1个字节,也必须至少读写整个扇区。
柱面 在所有盘片上,相同半径的磁道在垂直方向上构成的抽象圆柱面。
柱面是优化磁盘性能的关键概念。
因为所有磁头都固定在同一个磁头臂上,它们会同时移动到同一个柱面。当需要读写大量连续数据时,如果数据都存储在同一个柱面上,硬盘就可以在第一个盘面读写完后,无需移动磁头臂,直接切换到第二个盘面的磁头继续读写,然后再切到第三个...这样大大减少了磁头寻道的时间,极大地提高了读写速度。
1.2 磁盘存储结构
如果我们要存储数据到扇区,要如何确定一个扇区的位置呢?
我们可以先确定柱面(cylinder)位置,再确定磁头(header)位置,这样就可以确定具体的磁道位置了,最后再锁定扇区(sector)的位置,这就是CHS寻址法。
磁头的容量 = 一个盘面的磁道(柱面)数*一个磁道的扇区数*磁头数*一个扇区大小(512字节)
这种CHS模式支持的硬盘容量有限,因为系统用8bit来存储磁头地址,用10bit来存储柱面地址,用6bit来存储扇区地址,而⼀个扇区共有512Byte,这样使用CHS寻址一块硬盘最大容量为256*1024*63*512B=8064MB(1MB=1048576B)(若按1MB=1000000B来算就是8.4GB)。所以我们引入了下面的存储结构。
1.3 磁盘逻辑结构
我们把一个磁道拉直,看成线性结构,把一个扇区看成一个存储单元,如果一个磁道有N个扇区,那么我们可以把磁道抽象成下图的样子:
因为前面我们已经提到了:磁头是共进退的,在存储数据时,前一个磁头所指向的磁道数据写完了之后不是往同一个盘面的下一个磁道写,而是往下一个盘面的同一柱面的磁道写数据,所以我们展开时也应该以柱面为单位:
那么整个磁盘其实就相当于是个三维数组,根据C语言中关于数组的知识,我们其实可以知道不管是二维数组还是三维数组,其实本质上都是一维数组,那么这里的三维数组我们同样可以当做一维数组:
那么每个扇区都有了唯一的下标,这个我们就叫LBA(Logical Block Address)地址,就是把磁盘在逻辑上抽象成一维数组的下标。我们可以把LBA想象成一个一维数组的索引,而CHS是一个三维坐标(C, H, S)。
LBA地址和CHS地址是可以相互转换的,这一点应该也不难想到,我们上面的图中其实就很清楚了,具体的转换方法如下:
我们规定变量:
Hpc: 每个柱面的磁头数(Heads per Cylinder)
Spt: 每个磁道的扇区数(Sectors per Track)
已知LBA求C、H、S:
柱面号 C = LBA / (Hpc * Spt)
磁头号 H = (LBA % (Hpc * Spt)) / Spt
扇区号 S = (LBA % Spt) + 1 // 扇区号S通常从1开始计数,而不是0
已知C、H、S求LBA:
LBA = (C * Hpc + H) * Spt + (S - 1)
2. 引入文件系统
2.1 “块”概念
磁盘的物理读写单位是扇区(512字节),但以扇区为单位进行管理会产生巨大的元数据开销(比如记录每个扇区属于哪个文件)。因此,为了减少数据开销,文件系统将多个连续的扇区组合成一个“块”来进行管理。最常见的块大小是4KB(8个扇区组成)。“块”是文件存取的最小单位。
我们现在已经把磁盘当成一个以扇区为单元的一维数组了,那么我们也可以由LBA地址算出每个块的地址了。
块号 = LBA / 8
LBA = 块号*8+ n (块内的第n块扇区)
2.2 “分区”概念
分区是在一个物理硬盘驱动器上创建的独立的、逻辑上隔离的连续区域。每个分区都可以被格式化为特定的文件系统,并被操作系统视为一个独立的逻辑驱动器(如在Windows中的C:盘、D:盘)。
2.3 “inode”概念
inode是Unix/Linux文件系统中的一种数据结构,它用来描述一个文件或目录的元数据(目录也是文件),并指向存储文件内容的实际数据块。inode本身并不包含文件名。
3. 文件系统
我们要在硬件上存储文件就要先把硬盘格式化为某种格式的文件系统,才能存储文件。创建文件系统的目的就是组织和管理硬盘中的文件。
3.1 宏观认识
在Linux下,最常见的文件系统就是ext2系列的文件系统,我们下面就以最早的ext2作为参考:
Disk: 代表整个物理硬盘驱动器。
MBR: 位于磁盘最开始的扇区,是计算机启动时最先读取的地方。它包含两部分重要内容:
引导程序: 一小段可执行代码,用于启动操作系统。
分区表: 记录了整个硬盘如何被划分为多少个分区。
Partition: 是从磁盘划分出的一个独立连续区域,可以格式化为特定的文件系统。
Boot Sector: 位于每个分区的开始。它包含用于加载该分区上操作系统的代码。对于文件系统分区,紧接着引导扇区的就是文件系统的元数据。
EXT2 File System: 整个分区都被 EXT2 文件系统的结构所管理。
Block Group: 为了提高效率和可靠性,EXT2 文件系统将整个分区进一步划分为多个更小的、结构相同的块组。每个块组都像一个“微型的文件系统”,拥有自己的一套元数据和数据块。这样可以将 inode 表和数据块分散靠近,减少磁头寻道时间。
3.2 Block Group
下面就是文件系统最核心的部分了:
Super Block: 文件系统的“总说明书”。它记录了整个文件系统的全局信息,如 inode 总数、块总数、块大小、文件系统状态等。按理来说,感觉这些数据是不是应该储存在上一层的文件系统里面,之所以储存在这里是为了防止损坏,多个块组中都有 Super Block 的备份,所以不是每个块组里面都有Super Block。
GDT: 块组描述符表。它描述了该块组自身的详细信息,如块位图的位置、inode 位图的位置、inode 表的位置、空闲块和 inode 的数量等。
Block Bitmap: 数据块使用情况地图。它是一个位图,每一位代表该块组中的一个数据块。如果位是 1,表示该数据块(Data Block)已使用;是 0,则表示空闲。
inode Bitmap: inode 使用情况地图。与块位图类似,它记录了该块组中 inode 表中哪些 inode 已被使用。
inode Table: inode 的“花名册”。它是一个由 inode 结构体组成的数组。每个文件或目录都唯一对应一个 inode,里面记录了该文件的元数据。
Data Blocks: 数据块(Data Block)数组。这里存放着文件的实际内容。如果这个文件时目录文件的话,那么就存储的是目录的结构信息(即文件名到 inode 号的映射关系)。
4. 路径
当我们要访问当前工作目录时,我们需要找到它的inode,所以需要访问上级目录,但上级目录被访问时也有相同的问题,那我们该如何解决这个问题呢?
4.1 路径解析
上述问题类似递归,我们一直往上查找,直到找到根目录作为出口,因为根目录的inode在文件系统挂载时就已经确定了。
所以我们在路径解析时,本质上是:从根目录开始,逐级打开目录文件,根据每一级的目录名查找其对应的 inode,通过这种链式查找机制最终定位到目标文件。
4.2 dentry结构
struct dentry {atomic_t d_count; // 引用计数struct inode *d_inode; // 关联的inodestruct hlist_node d_hash; // 哈希表链接 - 用于快速查找struct dentry *d_parent; // 父目录dentry - 构建树形结构struct qstr d_name; // 文件名union {struct list_head d_child; // 父目录的子项目链表struct rcu_head d_rcu;} d_u;struct list_head d_subdirs; // 本目录的子项目struct list_head d_lru; // LRU淘汰链表// ... 其他字段
};
4.2 路径缓存
有时候编译项目时,我们可能会访问几百个文件,如果我们每次访问文件都要做一次完整的路径解析,而路径解析需要读取磁盘上的目录内容,那就意味着要做很多次磁盘访问,磁盘I/O操作比内存操作慢数万倍!那样简直太浪费时间了。
所以我们出现了路径缓存这个方案来解决:路径缓存是Linux内核的一种性能优化机制。
路径缓存是通过内核中的dentry结构体在内存中构建一个完整的目录树来实现的。当第一次访问文件时,系统会从磁盘解析完整路径并创建对应的dentry节点,这些节点通过父子指针形成树形结构,同时被放入哈希表方便快速查找。后续再次访问相同路径时,直接在这个内存树中查找即可立即定位文件,无需重复磁盘I/O。所有dentry节点还通过LRU链表管理,内存不足时自动淘汰最近最少使用的节点,这样就实现了文件访问的加速和内存的高效利用。
5. 软硬链接
建立软硬链接都要使用ln命令,具体操作如下:
5.1 硬链接
▶ 如何建立硬链接?
ln [源文件] [硬链接文件]
建立test.c文件的硬链接:
查看硬链接和源文件的inode号:
第一项就是文件的inode号,可以看到硬链接和源文件的inode号是一样的,说明他们指向的是同一个inode。所以硬链接的存在可以让多个不同的文件名指向磁盘上的同一份文件数据。
硬链接其实就是新建一个文件与源文件的inode建立映射关系,如果源文件删除不会对硬链接有什么影响,而且可以通过硬链接恢复源文件内容,所以硬链接有备份的作用。
▶ 我们前面说过目录也是文件,那么是不是我们也可以给目录文件建立硬链接呢?
按理来说其实是可以的,但是操作系统不允许这种操作,因为这样会导致目录环路问题出现。我们很多工具都是需要遍历目录树的(比如find命令),如果出现目录环路,这样的命令就无法结束。所以操作系统直接从根源上解决问题:不允许用户给目录建立硬链接。
但是操作系统中的 " . "," .. "两个文件其实就是给当前目录和父目录建立硬链接
-
.
= 指向当前目录的硬链接 -
..
= 指向父目录的硬链接
▶ 那么这里怎么不考虑目录环路问题了呢?
因为这是操作系统自己建立的啊,而且文件名都是固定的!操作系统肯定会做好特殊情况的处理的,当我们使用find这类命令时,就会跳过这两个文件的,从而避免出现循环的情况。
5.2 软链接
▶ 如何建立软连接?
ln -s [源文件或目录] [软连接文件]
软链接是一个特殊的文件,其内容包含另一个文件的路径名。类似于 Windows 的快捷方式。
建立一个test.c文件:
编译链接为可执行文件:
建立软链接:
执行test和test-soft:
执行结果相同。
查看文件的inode号:
我们可以看到软链接文件的inode是独立的,所以软链接是独立文件,不用考虑循环问题。
6. 挂载分区
6.1 格式化
格式化,在计算机科学中更精确地称为 “创建文件系统” ,是指在存储设备(如硬盘分区、U盘、固态硬盘)上,按照一种特定的、预定义的数据结构,写入并初始化一套元数据和管理结构的过程。完成此过程的存储设备,即被称为 “已格式化的” 设备。
格式化的本质:建立管理框架
您可以将一个存储设备想象成一块完全空白的土地:
未格式化的设备:就像一块只有经纬度坐标(扇区地址)的空白土地。你知道哪里是哪里,但没有任何划分,无法有效地管理上面的建筑(文件)。
已格式化的设备:就像在这块土地上划定了道路、街区号、门牌号,并建立了一个市政厅(文件系统元数据)来记录哪个地址(数据块)上有什么建筑(文件)、谁拥有它、有多大等。这套完整的规则和基础设施,在Linux中就是一个文件系统。
因此,“已格式化”本质上等同于“已经建立了一个文件系统”。
6.2 挂载
一个已格式化的分区,只是一个准备好了数据结构的“数据容器”。它必须通过“挂载”操作,被关联到现有目录树的一个“挂载点”上,才能被用户和应用程序访问。
▶那么什么是挂载?如何实现挂载?
我们来通过一个小实验来理解一下:
我们来制作一个大磁盘块当做一个分区
dd命令:用于转换和拷贝文件,适用于底层数据操作。
if=/dev/zero:if(input file),从/dev/zero文件中读取数据。
/dev/zero:一个特殊的Linux设备文件,当你读取它时,它会无限地提供空字符(ASCII值为0的字符,即\0)。
of=./disk.img:of(output file),在当前目录下创建一个名为 disk.img 的文件,定义了数据将被写入的目标文件。
bs=1M表示每个块的大小为1M(即1024K,1048576Bytes)。
count=5:表示复制5个块。
这条命令使用 dd工具,创建了一个5M充满空字符的文件disk.img,我们把这个文件当做一个分区。
格式化写入文件系统:
创建一个空目录,再使用 df -h 命令查看可用的分区,现在我们建立的分区还不能使用,所以我们的分区没有列出来
将分区挂载到指定目录:
我们现在可以看到我们自己建立的分区也可以使用了。
卸载我们建立的分区:
相信聪明的你通过这个小小的实践过程已经能够深刻的理解挂载过程了!