摘要:本文系统梳理了磁盘与文件系统的核心知识。前半部分从磁盘的物理结构、存储结构、逻辑结构出发,讲解了磁道、柱面、扇区等基本概念,并对比了 CHS 三维寻址与 LBA 一维线性地址的转换关系。后半部分以 ext2 文件系统为例,介绍了块、分区、inode 等基础概念,深入剖析了块组内部构成以及文件名与目录的映射机制,帮助读者建立从磁盘硬件到文件系统的完整认知。
一、磁盘介绍
1.1 磁盘的物理结构
物理结构指硬盘拆开后能看到的机械与电子部件,是真实存在的硬件。
1.盘片(Platter):圆形铝合金 / 玻璃基片,表面镀磁性涂层,用来存储数据;一块硬盘可有 1~ 多张盘片,上下两面(盘面)都能记录。
2.主轴电机(Spindle Motor):带动所有盘片高速旋转(5400/7200 rpm 等)。
3.磁头(Read/Write Head):每个盘面一个,悬浮在盘面上方纳米级间隙,写时改变磁场、读时感应磁场。
4.磁头臂(Actuator Arm):承载所有磁头,整体同步摆动。
5.音圈电机(VCM):驱动磁头臂径向移动,完成寻道。
6.控制电路板(PCB):主控芯片、缓存、接口电路,负责与主机通信和控制读写。
7.密封盘腔:无尘腔体,保护盘片不被划伤。
1.2 磁盘的存储结构
指数据在盘片上按什么规则排列,是逻辑划分,不是看得见的零件。
1.磁道(Track):盘面上的同心圆,数据沿磁道存放;从外向内编号,0 号在最外圈。
2.柱面(Cylinder):所有盘片上同一半径的磁道合起来构成一个圆柱(逻辑概念);磁头臂移动一次,所有磁头同时切换到同一柱面。
3.扇区(Sector):磁道被切成的弧段,是磁盘最小物理读写单位;传统 512 字节,现代 4K。每个扇区存储容量严格相等,不会出现半径大的扇区容量大,半径小的扇区容量小的情况。
容量公式:硬盘容量 = 磁头数 × 柱面数 × 每柱面扇区数 × 每扇区字节数(即 CHS 三要素)。
那么,我们该如何定位扇区?
1.先定位磁头(header)编号,找到扇区在哪个盘面上。
2.再确定磁头要访问到哪个磁道(cylinder)上。
3.最后确定在当前磁道上,是哪个扇区(sector)
上面的是数据定位方法之一:CHS地址定位。
1.3 磁盘的逻辑结构
磁盘盘面的磁道是同心圆,我们可将其类比卷绕的磁带。将环形磁道的数据展开“拉直”,得到线性序列,那么磁盘在逻辑上等价于一维数组,每个扇区拥有对应的一维编号,这个地址就是LBA(Logical Block Address)逻辑块地址。
这里抽象一个逻辑概念:柱面
将所有盘片上同一半径的磁道合起来构成一个圆柱,这个逻辑上的圆柱被称为柱面。因为传动臂上的磁头是共进退的,移动一次,所有磁头同时切换到同一柱面。在逻辑上来看,所谓磁盘便是由无数个柱面构成。
磁盘的真实情况如下:
把单个环形磁道 “拉直”,磁道上连续的扇区就形成线性序列,构成一维数组;
将同一个柱面内所有盘面的磁道展开,多个磁头对应的磁道就新增一个维度,组成二维数组;
再把不同半径、不同柱面的磁道整合进来,就形成了完整的三维数组。
磁盘在逻辑上会把这套由柱面、磁头、扇区组成的三维寻址体系,映射为一维的 LBA 线性地址,以此简化磁盘的访问过程。
1.4 CHS & LBA
CHS 是由柱面、磁头、扇区共同构成的三维寻址方式,LBA 则是扁平化的一维线性地址,这两套地址之间自然可以相互转换。
操作系统在处理磁盘读写的 IO 请求时,并不会使用复杂的 CHS 三维地址,而是直接向磁盘传递 LBA 一维地址。操作系统只需要关心连续的扇区编号,完全不用感知磁盘底层盘片、磁道、柱面这些硬件布局,这样很好做到了软硬件的解耦。
当磁盘控制器收到操作系统下发的 LBA 编号之后,会在硬件内部自动完成地址换算,把一维的 LBA 线性地址翻译成底层对应的 CHS 三维地址,定位到目标扇区,最后完成数据的读写操作。这种转换机制屏蔽了磁盘内部复杂的物理结构,极大简化了操作系统对磁盘的访问逻辑。
二、 文件系统
2.1 概念引入
2.1.1 块(block)
当操作系统进行数据读取时,并不是一个个扇区的进行读取数据,这样读取的效率太低了,而是一次性读取多个扇区,即一次读取一个“块”。
磁盘的的每个分区被分为一个个的“块”,一个“块”的大小是由格式化的时候确定的,并且不可更改,块的大小一般为4KB,即八个扇区构成一个块。块是文件读取的最小单位。
2.1.2 分区
磁盘整体容量通常十分庞大,如果直接对整块磁盘的全部空间统一管理,元数据维护、空间分配都会带来巨大开销,因此我们会对磁盘进行分区,将整块磁盘切分为多个独立区域。站在 Windows 的视角,分区就是我们熟悉的 C 盘、D 盘、E 盘这类盘符,C/D/E 对应的就是磁盘分区。(很多人会误以为分区等价于格式化,实际上分区仅仅是在磁盘分区表中标记出一段连续扇区的范围,格式化是在分区内部创建文件系统,二者不能混为一谈)。
对于奉行 “一切皆文件” 理念的 Linux 系统,磁盘与分区同样遵循这套划分逻辑。在Linux 下分区划分的最小单位是扇区。早期硬盘采用 CHS 寻址方式,分区边界必须对齐到柱面,因此当时分区的最小划分单位是柱面;现代磁盘普遍采用 LBA 逻辑扇区寻址,不再受柱面边界约束,分区可以从任意扇区起始,分区的最小单位也就变成了扇区。分区只是磁盘层面的空间划分,仅依靠 MBR 或 GPT 分区表记录每个分区在磁盘上的起始 LBA 扇区号与占用扇区总数,分区本身只是裸的扇区集合,没有文件系统结构。
仅仅划分分区还不够,分区只是磁盘上一块独立的空间区间,想要存放文件,还需要在分区上构建文件系统,以 ext2 为例,它并不会把整个分区当作一个整体直接管理。ext2 会将一整个分区内部的所有逻辑块,进一步切分成若干块组(block group)。
2.1.3 inode
当我们ls -l时,除了看见文件名外,还能看见文件的各种元信息(属性),由上面可知,文件数据都存放在“块”中,因此一定找到一个地方进行存放元信息(属性信息)。这种存储元信息的区域称为inode。每个文件都有对应的inode。
2.2 ext2文件系统
这里我们选择 ext2 文件系统作为学习切入点。各类现代文件系统底层的设计思路大同小异,都是依靠元数据记录文件位置与属性、划分管理单元来降低磁盘管理开销。吃透 ext2 的基础架构,就能建立起通用的文件系统底层认知,后续再去理解 ext3、ext4 等衍生文件系统,或是其他类型文件系统时,学习门槛会低很多。
磁盘经过分区后,每个分区可以单独格式化为 Ext2 文件系统。MBR 位于磁盘最前端,负责保存分区表,把磁盘空间切分为多个独立分区;每个分区内部包含引导扇区(Boot Sector)与 ext2 文件系统本体。启动块(Boot Sector)的大小是确定的(由PC标准规定),用来存放分区信息和启动信息。
2.2.1 块组
如果仅仅划分分区肯定是还不够的,分区只是磁盘上一块独立的空间区间,想要存放文件,还需要在分区上构建文件系统,ext2并不会把整个分区当作一个整体直接管理。ext2 会将一整个分区内部的所有逻辑块,进一步切分成若干块组(block group)。
2.2.2 块组内部构成
块组内包含块位图(Block Bitmap)、inode 位图(inode Bitmap)、inode 表(inode Table)与数据块(Data Blocks);而超级块(Super Block)与块组描述符表( GDT) 是文件系统全局元数据,二者是成对备份的。现代 ext2 默认开启稀疏超级块 sparse_super 特性,不会在每一个块组存放超级块和 GDT 副本,只在 0、1 以及组号为 3、5、7 幂次的块组存放完整备份;早期 ext2 没有稀疏超级块时,才会在全部块组都存放超级块 + GDT。
1 Super Block(超级块):记录整个文件系统的全局信息,包含块大小、inode 总数、空闲块数量等核心参数;
2 GDT(块组描述符表):保存所有块组的描述信息;
3 Block Bitmap(块位图):标记本组内数据块的占用 / 空闲状态;
4 inode Bitmap(inode 位图):标记本组内 inode 的占用 / 空闲状态;
5 inode Table(inode 表):存放大量 inode 节点,每个 inode 记录一个文件的元数据(权限、大小、数据块指针等);
6 Data Blocks(数据块):真正存放文件内容的区域。
GDT 是一张全局总表,表内包含分区内所有块组的描述符,单条描述符记录对应一个块组,记录该块组的块位图、inode 位图、inode 表所在的块编号,以及本组空闲块、空闲 inode 的统计信息。块组的设计,一方面可以将文件的 inode 与对应数据尽量放在同一组内,减少磁盘寻道开销;另一方面每组独立维护位图元数据,分配 inode 与数据块时只需要检索当前块组,避免全局扫描,同时超级块、GDT 的选择性多副本备份,在节约磁盘空间的前提下,降低局部损坏造成整个文件系统失效的风险。
2.2.3 文件名和目录
在 Linux 的 inode 体系中,文件名并不保存在 inode 里面。很多人会误以为 inode 存储文件名,但 inode 只负责存放文件本身的元数据,例如文件大小、权限、时间戳、数据块指针等信息,并不记录文件名。
Linux 有一句核心思想:一切皆文件。文件可以拆成两部分:文件属性 + 文件内容,目录本身也属于一种特殊的文件。 那目录这个特殊文件里面存放的内容是什么?目录文件在磁盘上保存一条条目录条目,记录文件名 和 对应 inode 编号的映射关系。
内核把磁盘上读到的目录条目,在内存中封装成dentry(directory entry,目录项)。一个 dentry 代表一个文件 / 目录的名字与 inode 的关联,是内核内存中的对象。dcache(目录项缓存)本质就是 dentry 的缓存池,用来缓存已经解析过的 dentry。
当我们传入一条文件路径(如/home/test/file.txt),内核会执行路径解析:从根目录/开始,逐级查找每一级名称。优先在 dcache 中查找对应的 dentry;如果缓存命中,直接拿到 inode;缓存未命中,才去读取磁盘上的目录文件,读出目录条目,在内存新建 dentry 并放入 dcache,再继续解析下一级。不断重复这个查找过程,直到解析到路径最后的文件名,得到目标文件的 inode,最后读取 inode 元数据和数据块,访问文件真实内容。
每一次逐级目录查找都会产生磁盘 IO,dcache 缓存 dentry 的目的就是减少重复解析路径带来的磁盘开销。后续再次访问同一路径时,内核优先查询内存里的 dentry 缓存,不需要重复读取磁盘上的目录文件,大幅加速路径解析。
这也就解释了为什么访问文件时需要依赖当前工作目录。相对路径的解析会以当前工作目录对应的 dentry 作为解析起点,本质是进程拿到当前目录对应的 dentry/inode,查找目录内文件名 - inode 映射,才能把文件名翻译成 inode 号,进而访问目标文件。如果无法读取当前目录文件,或者缓存失效且磁盘目录不可读,就不能完成文件名到 inode 号的解析,无法访问目标文件。