Linux:磁盘和Ext系列文件系统
摘要本文系统梳理了磁盘与文件系统的核心知识。前半部分从磁盘的物理结构、存储结构、逻辑结构出发讲解了磁道、柱面、扇区等基本概念并对比了 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 磁盘的逻辑结构磁盘盘面的磁道是同心圆我们可将其类比卷绕的磁带。将环形磁道的数据展开“拉直”得到线性序列那么磁盘在逻辑上等价于一维数组每个扇区拥有对应的一维编号这个地址就是LBALogical Block Address逻辑块地址。这里抽象一个逻辑概念柱面将所有盘片上同一半径的磁道合起来构成一个圆柱这个逻辑上的圆柱被称为柱面。因为传动臂上的磁头是共进退的移动一次所有磁头同时切换到同一柱面。在逻辑上来看所谓磁盘便是由无数个柱面构成。磁盘的真实情况如下把单个环形磁道 “拉直”磁道上连续的扇区就形成线性序列构成一维数组将同一个柱面内所有盘面的磁道展开多个磁头对应的磁道就新增一个维度组成二维数组再把不同半径、不同柱面的磁道整合进来就形成了完整的三维数组。磁盘在逻辑上会把这套由柱面、磁头、扇区组成的三维寻址体系映射为一维的 LBA 线性地址以此简化磁盘的访问过程。1.4 CHS LBACHS 是由柱面、磁头、扇区共同构成的三维寻址方式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 Bitmapinode 位图标记本组内 inode 的占用 / 空闲状态5 inode Tableinode 表存放大量 inode 节点每个 inode 记录一个文件的元数据权限、大小、数据块指针等6 Data Blocks数据块真正存放文件内容的区域。GDT 是一张全局总表表内包含分区内所有块组的描述符单条描述符记录对应一个块组记录该块组的块位图、inode 位图、inode 表所在的块编号以及本组空闲块、空闲 inode 的统计信息。块组的设计一方面可以将文件的 inode 与对应数据尽量放在同一组内减少磁盘寻道开销另一方面每组独立维护位图元数据分配 inode 与数据块时只需要检索当前块组避免全局扫描同时超级块、GDT 的选择性多副本备份在节约磁盘空间的前提下降低局部损坏造成整个文件系统失效的风险。2.2.3 文件名和目录在 Linux 的 inode 体系中文件名并不保存在 inode 里面。很多人会误以为 inode 存储文件名但 inode 只负责存放文件本身的元数据例如文件大小、权限、时间戳、数据块指针等信息并不记录文件名。Linux 有一句核心思想一切皆文件。文件可以拆成两部分文件属性 文件内容目录本身也属于一种特殊的文件。 那目录这个特殊文件里面存放的内容是什么目录文件在磁盘上保存一条条目录条目记录文件名 和 对应 inode 编号的映射关系。内核把磁盘上读到的目录条目在内存中封装成dentrydirectory entry目录项。一个 dentry 代表一个文件 / 目录的名字与 inode 的关联是内核内存中的对象。dcache目录项缓存本质就是 dentry 的缓存池用来缓存已经解析过的 dentry。当我们传入一条文件路径如/home/test/file.txt内核会执行路径解析从根目录/开始逐级查找每一级名称。优先在 dcache 中查找对应的 dentry如果缓存命中直接拿到 inode缓存未命中才去读取磁盘上的目录文件读出目录条目在内存新建 dentry 并放入 dcache再继续解析下一级。不断重复这个查找过程直到解析到路径最后的文件名得到目标文件的 inode最后读取 inode 元数据和数据块访问文件真实内容。每一次逐级目录查找都会产生磁盘 IOdcache 缓存 dentry 的目的就是减少重复解析路径带来的磁盘开销。后续再次访问同一路径时内核优先查询内存里的 dentry 缓存不需要重复读取磁盘上的目录文件大幅加速路径解析。这也就解释了为什么访问文件时需要依赖当前工作目录。相对路径的解析会以当前工作目录对应的 dentry 作为解析起点本质是进程拿到当前目录对应的 dentry/inode查找目录内文件名 - inode 映射才能把文件名翻译成 inode 号进而访问目标文件。如果无法读取当前目录文件或者缓存失效且磁盘目录不可读就不能完成文件名到 inode 号的解析无法访问目标文件。