从磁盘硬件到 Ext 文件系统:一次彻底搞懂 Linux 磁盘级文件系统(学习笔记)

本文是我学习 Linux 磁盘与文件系统部分(7.12–7.14)的整理笔记。
路线:磁盘物理结构 → CHS / LBA 寻址 → 块(Block) → 分区分组 → inode → 位图与删除文件的真相
前置知识:进程视角下的文件(已打开的文件)。本文回答的是那个更本质的问题——文件在没被打开之前,到底是什么?


一、为什么要先学磁盘?(7.12)

之前我们聊的都是「已打开的文件」:语言级函数、系统调用、缓冲区、进程的文件描述符表……但文件总得有个「家」。文件在打开之前,就躺在磁盘上。所以要理解文件系统,必须先理解磁盘这个硬件。

对机械磁盘的第一印象:

  • 它是计算机中唯一的机械设备——CPU、内存都是电子器件,纳秒级响应;磁盘要转动、要磁头摆动,慢了几万倍不止;
  • 但它容量大、价格便宜、数据断电不丢,所以注定长期担任「存储主力」;
  • 磁盘装进服务器,服务器放进机柜,机柜再进机房——这就是「磁盘 → 服务器 → 机柜 → 机房」的层级。

一个重要的细节:磁头不接触盘面。 磁头悬浮在离盘面几十纳米的高度,靠盘面高速旋转带起的气流「托」住。一旦接触(划伤、高温),数据就可能丢失——这就是为什么磁盘不能摔、不能刮花、不能消磁。


二、磁盘是怎么存数据的?(7.13 上)

2.1 磁性存储的本质

磁盘上布满了数以亿计的微小磁铁(磁性材料),用南北极方向表示 0 和 1。磁头通过电流改变磁场方向,就完成了写入;读取时感应磁场方向变化,就完成了读出。

关键性质:

  • 磁性不会自发退磁 → 所以叫永久磁盘(断电不丢数据);
  • 想彻底销毁数据?高温消磁整个盘面即可。

2.2 磁盘的物理结构

一块磁盘自上而下有多个盘片,每个盘片有正反两个盘面(例如 3 个盘片 = 6 个盘面,配 6 个磁头)。每个盘面上:

  • 磁道(Track):一圈圈的同心圆;
  • 扇区(Sector):磁道被切分成的弧段,是磁盘 IO 的最小物理单元,一般 512 字节(或 4KB)。

所有磁头固定在同一组臂上,共进退——同一时刻,所有盘面上的磁头都指向各自盘面的同一号磁道。这些同半径的磁道在空间上叠成一个柱面(Cylinder)

一个有意思的工程取舍:内圈扇区面积小、外圈大,理论上外圈能存更多数据。但那需要更复杂的适配算法,市面上的磁盘干脆让每个扇区容量相等——简单,且效率可预期。

2.3 CHS 定位:三维数组的下标

要在磁盘上找到数据,本质是三步定位:

  1. 找到哪个柱面(Cylinder)——磁头臂摆动到目标磁道;
  2. 确定哪个磁头(Head)——即哪个盘面;
  3. 等盘片旋转,让目标扇区(Sector)转到磁头下。

所以磁盘在逻辑上就是一个三维数组:

Sector disk[C][H][S];  // 柱面 × 磁头 × 扇区

CHS 就是这个三维数组的下标。柱面 == 磁道(磁头共进退,摆动的本质就是选磁道);盘片旋转的本质是选扇区(转得越快,效率越高);最后选定磁头读数据。


三、从 CHS 到 LBA:把三维数组拉直(7.13 下)

三维数组太麻烦,寻址要给三个数字。我们换个视角:

  1. 把一个盘面的所有磁道拉直,盘面就成了一个线性结构;
  2. 把 6 个盘面的线性结构首尾相接,整个磁盘就成了一个一维数组

于是每个扇区都有了一个唯一的线性下标——这就是 LBA(Logical Block Address,逻辑块地址)

3.1 CHS vs LBA

CHSLBA
视角磁盘物理结构(三维数组)逻辑线性(一维数组)
寻址需要三个下标一个数字搞定
谁在用磁盘硬件内部文件系统 / 操作系统

两者可以通过固定的逻辑运算互相转换,而这个转换由磁盘内部的伺服系统自动完成

结论:磁盘是一个黑盒。操作系统只需要传入一个 LBA 数字,伺服系统负责把它翻译成 CHS 三步定位,读写任意扇区。

3.2 为什么硬件访问如此昂贵?(7.14 补充)

以机械硬盘为例:磁头要等盘面达到额定转速、产生的气流能托住磁头后才能进场工作。而从 0 加速到额定转速要 1~2 秒,寻道、旋转等待与电信号相比慢数万倍。结论:

调用这个硬件的代价极高,要尽可能少地调用它、每次调用多干点活。(底层磁盘因此支持一次连续读写多个扇区。)这也是后面「块」「预读」「内核缓冲区」一系列设计的原始动机——和「用户态切内核态代价高」是同一种思路。


四、文件系统的引入:块、分区、分组(7.13 下)

4.1 为什么不按 512B 访问,而按 4KB 访问?

磁盘最小物理单元是 512 字节扇区,但操作系统(以 Ext2 为例)与磁盘打交道的基本单位是 4KB 的「块(Block)」= 8 个扇区。原因:

  1. 效率:访问一次硬件代价太大,一次读 4KB 配合预读机制,摊薄成本;写入则配合刷新机制;
  2. 软硬件解耦:OS 全程只说「块号」,由 块号 → LBA → CHS 层层翻译,磁盘细节被彻底屏蔽(Attention 机制:细节蒙蔽);

于是在 OS 眼中,磁盘不再是 C/H/S,而是:

一块 800GB 的磁盘 ÷ 4KB = 一维的块号数组(0 ~ 1亿+ 个块)

块号在功能上就类似于指针——一个数字,指向一块 4KB 的存储。

4.2 先分区,再分组

800GB 太大,直接管理不现实。OS 的思路依旧是「先描述、再组织」:

struct disk { /* ... */ };  // 先描述磁盘
  • 分区:按块号范围把磁盘切成几个大区(C/D/E 盘的由来);
  • 分组:每个分区还是太大,再按块号切成一个个块组(Block Group)

真正被管理的粒度就是块组。而**「格式化」的本质,就是往分区里写入全新的文件系统管理信息。**

4.3 文件 = 内容 + 属性,分开存储

Linux 下:文件 = 文件内容(Data Blocks)+ 文件属性(inode),二者分开存储

inode:文件的身份证
  • 每个文件的属性存在一个 struct inode{} 里:权限、所属组、大小、ACM 时间等;
  • inode 大小固定,一般 128 字节——固定大小就是为了方便数组化管理;
  • 一个文件一个 inode
  • 有意思的点:文件名不在 inode 里! 文件名只是给用户看的,内核全靠 inode 编号。
inode Table:属性的数组

inode 也和其他数据一样,以 4KB 为单位存放,不搞特殊:

一个 4KB 的 inode Table 数据块 = 4096 ÷ 128 = 32 个 inode

所以 inode Table 可以看作一个数组:拿到下标 N,就 inode[N] 拿到该文件的属性——这个下标就是常说的 inode 编号

i_block[]:内容和属性的桥梁

内容和属性分开存,那它们怎么关联?答案是 inode 里的一个数组:

le32 i_block[N];  // 记录该文件占用的各个 4KB data block 的块号

只要找到 inode,就能顺着 i_block[] 找到文件的全部数据块。例如文件占 3 个块,则 i_block[0..2] 有效。


五、位图、Super Block 与「删除文件」的真相(7.14)

5.1 一个块组里的五大区域

区域作用
Super Block整个分区的「总说明书」,记录分区整体参数
GDT(块组描述符表)管理当前这一个块组的资源
Block Bitmap位图,标记块组内每个 data block 是否被占用
Inode Bitmap位图,标记每个 inode 是否被使用
Inode Table存放本组所有文件的 inode 档案
Data Blocks真正存文件内容的地方

为什么要两份位图?因为 inode Table 和 Data Blocks 都是数组,必须有 O(1) 的方式判断「某个下标有没有数据」——位图正是干这个的,就像 O(1) 进程调度算法里用位图快速判断进程数组中的 PCB 是否存在一样。

Super Block 为什么能在分区里存多份、且每个块组附近都有备份?——某一份损坏了,可以拿别的副本恢复,这就是 Ext 文件系统的容错设计。

5.2 删除文件:为什么那么快,数据其实没删?

理解了位图,「删除文件为什么是秒删」就一目了然。删除流程:

  1. 拿 inode 编号 → inode[N] 找到该文件的 inode;
  2. 顺着 i_block[] 找到所有数据块的块号 → 把 Block Bitmap 中对应位由 1 置 0
  3. Inode Bitmap 中对应位由 1 置 0
  4. 完事。

注意:Inode Table 里的属性、Data Blocks 里的内容一个字节都没动! 它们只是「不再被位图承认」。下一次写入新文件时,这些「自由」的块会被覆盖。这就是为什么:

  • 删文件快(只改几个位);
  • 误删后数据还有机会被恢复(旧数据短期内仍在磁盘上)——同理,敏感数据要粉碎必须多次覆写。

5.3 内存视角:struct page 与内核缓冲区

换到内存的视角,上面这些结构一一对应:

  • Data Blocks 在内存中的对应物就是 struct page——本质就是一个 4KB 的数组 char a[4096]
  • 一个 4KB 的块(8 个扇区) ↔ 一个 page,而这个 page 正是内核级缓冲区
  • 内核对 page 的管理用基数树(radix tree):通过下标快速定位 page、向 page 写入数据;
  • 文件有个变量记录着「已读到/写到多少字节」(对应 inode 的 i_size),每 30 秒有后台线程(pdflush 类机制)把脏页刷回磁盘

i_size 记录文件的总字节数,i_size ÷ 4096 就能算出文件占了多少个块——属性的完备性再次体现:只凭一个 inode,就能完整还原一个文件。


六、总结:一条完整的抽象链

磁铁(0/1) → 扇区(512B) → 磁道/柱面/磁头 → 三维数组 CHS
       → 拉直成一维 → LBA 地址(磁盘黑盒自翻译)
       → 8 扇区打包成 4KB 块 → OS 视角:块号一维数组
       → 分区 → 块组 → SuperBlock/GDT/双位图/InodeTable/DataBlocks
       → 文件 = inode(属性) + DataBlocks(内容),靠 i_block[] 关联

几个贯穿全文的核心思想:

  1. 黑盒 + 抽象:每往上一层,下一层的细节就被屏蔽一次(CHS→LBA→块号);
  2. 空间换管理:位图用 1 bit 换来 O(1) 的占用查询;
  3. 少碰慢设备:块、预读、内核缓冲区、30 秒刷盘,全是为了摊薄昂贵的磁盘访问;
  4. 先描述,再组织:struct 描述磁盘/分区/块组/inode,再用数组、位图组织起来。

后续笔记(7.15–7.18)会继续:Ext2 对大文件的支持(间接块)、软硬链接、目录的本质。敬请期待。


本文基于 2026-07-12 ~ 07-14 的学习笔记整理,如有理解偏差欢迎评论区指正。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐