Ext系列文件系统详解:从磁盘寻址到 inode、目录与软硬链接
当我们执行
touch、open或rm时,操作系统究竟修改了磁盘上的哪些内容?要回答这个问题,需要把磁盘寻址、分区、数据块、inode、目录、挂载与链接串成一条完整链路。本文以 Ext2 的经典布局为主线,建立 Ext 系列文件系统的整体认知。
一、从机械磁盘说起
机械硬盘由盘片、主轴、磁头和传动臂等部件组成。盘片表面被划分为磁道,磁道又被划分为扇区;不同盘面上半径相同的磁道可以抽象为柱面。
传统磁盘几何结构常用三个量描述:
- Cylinder:柱面编号;
- Head:磁头或盘面编号;
- Sector:磁道中的扇区编号。
这就是 CHS(Cylinder-Head-Sector)寻址。若已知每个柱面的磁头数为HPC,每条磁道的扇区数为SPT,一种经典换算关系是:
LBA = C × HPC × SPT + H × SPT + (S - 1) C = LBA ÷ (HPC × SPT) H = (LBA mod (HPC × SPT)) ÷ SPT S = (LBA mod SPT) + 1CHS 中的扇区编号通常从 1 开始,而 LBA 从 0 开始,因此公式中需要减 1 或加 1。
为什么后来使用 LBA
CHS 依赖具体磁盘几何参数,表达范围也受到字段位数限制。LBA(Logical Block Address,逻辑块地址)将磁盘看成线性的扇区数组:
物理视角:盘片 → 磁道 → 扇区 逻辑视角:[0][1][2][3] ... [N-1] ↑ LBA 线性地址操作系统只需要提供一个 LBA,磁盘控制器和固件负责完成内部定位。现代存储设备不应再按真实 CHS 几何结构理解,CHS 更适合作为认识磁盘寻址演进的历史模型。
课件以 512 字节作为扇区大小进行推导,这对经典磁盘模型很常见。但现代磁盘可能使用 512 字节逻辑扇区、4KiB 物理扇区或 4KiB 原生扇区,实际参数应通过系统工具查询。
二、扇区、块和分区有什么区别
扇区是设备寻址层面的单位,但文件系统通常不会每次只管理一个扇区。为了提高效率,文件系统会把连续扇区组合为块(block)。常见块大小是 4KiB,但具体大小在格式化时确定。
| 概念 | 所在层次 | 作用 |
|---|---|---|
| 扇区 | 块设备 | 设备寻址和读写单位 |
| 块 | 文件系统 | 分配文件内容的基本单位 |
| 分区 | 磁盘布局 | 将一块磁盘划分为多个逻辑区域 |
| 文件系统 | 分区或块设备之上 | 组织文件内容与元数据 |
如果逻辑扇区为 512 字节、文件系统块为 4KiB,那么一个块包含 8 个逻辑扇区。已知 LBA 时可粗略计算块号:
块号 = LBA ÷ 8 块起始 LBA = 块号 × 8分区只是划定一段可使用的块设备地址范围。要在其中按文件和目录组织数据,还需要格式化,即写入某种文件系统的管理结构。
三、inode:文件名之外的文件身份
Linux 文件可以抽象为:
文件 = 内容 + 元数据内容保存在数据块中,元数据则由 inode(index node,索引节点)保存。inode 通常记录:
- 文件类型和权限;
- 所有者 UID 与组 GID;
- 文件大小;
- 数据块数量;
- 硬链接计数;
- 访问、修改和状态变更时间;
- 指向文件数据块的索引信息。
可以使用以下命令观察 inode:
# 查看文件的 inode 编号ls-liexample.txt# 查看详细元数据statexample.txt需要特别注意两点:
- 文件名通常不保存在文件自身的 inode 中;
- Linux 中常见的
ctime是 inode 状态变更时间,不是通常意义上的文件创建时间。
inode 编号只需在所属文件系统内唯一。不同分区或不同文件系统中可能出现相同的 inode 编号,因此不能脱离设备或文件系统单独使用 inode 号标识全局文件。
四、Ext2 如何组织一个分区
Ext2 会把分区划分为多个块组(Block Group)。各块组采用相似布局,使文件系统不必依靠一张无限增长的全局管理表。
Ext2 分区 │ ├─ Block Group 0 │ ├─ Superblock │ ├─ Group Descriptor Table │ ├─ Block Bitmap │ ├─ Inode Bitmap │ ├─ Inode Table │ └─ Data Blocks │ ├─ Block Group 1 │ └─ 相似结构 │ └─ Block Group N └─ 相似结构1. Superblock:描述整个文件系统
超级块保存文件系统的全局信息,例如:
- block 与 inode 总数;
- 空闲 block 与 inode 数量;
- block 和 inode 大小;
- 每个块组的 block 与 inode 数量;
- 文件系统状态、特性标志和 UUID;
- 挂载次数和检查时间等信息。
超级块一旦损坏,文件系统可能无法正常识别。因此 Ext 文件系统会按相应规则在部分块组中保存备份,而不是简单理解为每个块组都必然保存完整副本。
2. GDT:块组说明书
GDT(Group Descriptor Table,块组描述符表)记录各块组的布局和统计信息,例如:
- Block Bitmap 位于哪里;
- Inode Bitmap 位于哪里;
- Inode Table 从哪里开始;
- 还有多少空闲数据块与 inode。
超级块描述整个文件系统,块组描述符则帮助系统定位每个块组内部的管理区域。
3. Block Bitmap:数据块使用情况
Block Bitmap 使用位图记录数据块是否已分配。一个 bit 对应一个数据块:
0:空闲 1:已使用位图可以快速寻找空闲块,也能在释放文件内容时把对应位重新清零。
4. Inode Bitmap 与 Inode Table
Inode Bitmap 记录哪些 inode 空闲,Inode Table 则连续保存当前块组中的 inode 实体。创建文件时,内核先从位图中寻找空闲 inode,再在 Inode Table 的对应位置写入元数据。
5. Data Blocks:保存实际内容
Data Blocks 保存文件内容。不同类型的文件对数据块的解释不同:
- 普通文件的数据块保存字节内容;
- 目录的数据块保存目录项;
- 符号链接较短时,目标路径还可能直接保存在 inode 可利用的空间中,具体行为与文件系统实现有关。
五、inode 如何找到文件内容
经典 Ext2 inode 中包含一组块指针,通常包括:
- 直接块指针;
- 一级间接块指针;
- 二级间接块指针;
- 三级间接块指针。
inode ├─ 直接指针 ─────────→ 数据块 ├─ 一级间接指针 ─────→ 指针块 → 数据块 ├─ 二级间接指针 ─────→ 指针块 → 指针块 → 数据块 └─ 三级间接指针 ─────→ 指针块 → 指针块 → 指针块 → 数据块小文件可以直接通过 inode 中的直接指针定位,访问层级少;大文件则通过多级间接索引扩展可寻址的数据块数量。这种设计兼顾了小文件效率和大文件容量。
Ext4 对这一机制进行了重要改进,常使用 extent 描述连续的数据块范围,减少大文件需要维护的离散块指针数量。因此,学习直接与间接块适合理解 Ext2 的经典模型,但不能把它当成所有现代 Ext4 文件的唯一组织方式。
六、创建一个文件时发生了什么
假设执行:
touchabc从文件系统角度,可以将过程概括为:
- 在 Inode Bitmap 中寻找空闲 inode;
- 在 Inode Table 中初始化 inode,写入类型、权限、时间等元数据;
- 如果文件产生内容,则从 Block Bitmap 中申请数据块;
- 把数据写入 Data Blocks,并在 inode 中记录映射;
- 在父目录的数据块中新增目录项,建立名称
abc到 inode 号的映射; - 更新相关位图、统计信息和时间戳。
这也解释了为什么文件名和文件内容不是直接绑定的:文件名属于目录项,inode 才保存文件自身的元数据和数据索引。
七、目录为什么也是文件
从文件系统角度看,目录同样拥有 inode 和数据块。区别在于,目录的数据块保存的是目录项,其核心信息可以抽象为:
文件名 → inode 编号下面的程序读取目录项并输出名称与 inode 编号:
#include<dirent.h>#include<stdio.h>#include<stdlib.h>intmain(intargc,char*argv[]){if(argc!=2){fprintf(stderr,"usage: %s DIRECTORY\n",argv[0]);returnEXIT_FAILURE;}DIR*dir=opendir(argv[1]);if(dir==NULL){perror("opendir");returnEXIT_FAILURE;}structdirent*entry;while((entry=readdir(dir))!=NULL){printf("inode=%llu name=%s\n",(unsignedlonglong)entry->d_ino,entry->d_name);}if(closedir(dir)!=0){perror("closedir");returnEXIT_FAILURE;}returnEXIT_SUCCESS;}opendir()和readdir()是 libc 提供的目录流接口,并非都应简单称为直接系统调用。它们会在底层借助操作系统提供的目录读取能力。
八、路径是如何解析的
当程序访问:
/home/alice/project/test.c内核不能把整串路径直接当成一个磁盘文件名,而要从根目录开始逐级查找:
根目录 / ↓ 查找 home 对应的目录项 /home ↓ 查找 alice /home/alice ↓ 查找 project /home/alice/project ↓ 查找 test.c 目标 inode相对路径则以进程的当前工作目录为起点。进程调用open()时提供的路径,加上进程维护的工作目录信息,使内核能够确定解析起点。
如果每次访问都从磁盘逐级读取目录,性能会很差。Linux VFS 会利用 dentry(目录项缓存)等内存结构缓存名称到 inode 的解析结果,并配合哈希、LRU 等机制加速查找和回收。
这里的struct dentry字段属于具体内核版本实现,可能随版本变化。更稳定的理解是:dentry 表示路径中的一个名称组件,帮助 VFS 缓存“父目录 + 名称”到目标对象的解析关系。
九、为什么分区必须挂载
每个文件系统内部都有自己的 inode 编号空间,而 Linux 向用户呈现的是一棵统一的目录树。挂载(mount)负责把一个文件系统的根连接到现有目录树中的某个挂载点。
Linux 根目录树 / ├─ home ├─ etc └─ mnt └─ mydisk ← 挂载另一个文件系统可以用镜像文件进行教学实验:
# 创建 64 MiB 镜像文件ddif=/dev/zeroof=disk.imgbs=1Mcount=64status=progress# 写入 Ext4 文件系统mkfs.ext4 disk.img# 创建挂载点sudomkdir-p/mnt/mydisk# 通过 loop 设备挂载镜像sudomount-oloop disk.img /mnt/mydisk# 查看挂载结果findmnt /mnt/mydisk# 使用完成后卸载sudoumount/mnt/mydisk上述命令需要管理员权限。卸载前要确保没有进程正在使用挂载点中的文件,且当前终端不位于该目录内。
路径解析到挂载点时,VFS 会切换到被挂载文件系统的根,再继续解析剩余路径。因此用户可以通过统一路径访问不同设备、分区和文件系统。
十、Ext2、Ext3 与 Ext4 的关系
| 文件系统 | 典型特点 |
|---|---|
| Ext2 | 经典块组、inode 和位图设计,不提供日志机制 |
| Ext3 | 在 Ext2 基础上加入日志,提高异常断电后的恢复能力 |
| Ext4 | 引入 extent、延迟分配、更大容量等增强,并继续使用块组与 inode 等核心思想 |
本文使用 Ext2 讲解,是因为它的经典布局更容易理解。Ext3 和 Ext4 并非只是把版本号递增,它们在可靠性、分配策略、索引方式和容量等方面都有明显增强。
十一、硬链接与软链接
1. 硬链接
硬链接是在目录中增加另一个名称,让它指向同一个 inode:
echo"hello">origin.txtlnorigin.txt backup.txtls-liorigin.txt backup.txt两个名称通常会显示相同的 inode 编号。删除其中一个名称,只是移除一个目录项并减少链接计数;当链接计数归零且没有进程继续打开该文件时,文件系统才具备回收相关 inode 与数据块的条件。
硬链接通常不能跨文件系统,因为 inode 编号只在所属文件系统内有意义。普通用户也不能随意为目录创建硬链接,以避免目录树出现难以管理的环路。
2. 软链接
软链接又称符号链接,本身是一个独立文件,保存目标路径:
ln-sorigin.txt shortcut.txtls-liorigin.txt shortcut.txt readlink shortcut.txt软链接拥有自己的 inode,可以跨文件系统,也可以指向目录;但如果目标被移动或删除,软链接可能变成悬空链接。
3. 对比
| 对比项 | 硬链接 | 软链接 |
|---|---|---|
| inode | 与目标相同 | 拥有独立 inode |
| 保存内容 | 目录项直接指向目标 inode | 保存目标路径 |
| 跨文件系统 | 通常不可以 | 可以 |
| 链接目录 | 通常受限制 | 可以 |
| 目标删除后 | 其他硬链接仍可访问数据 | 可能成为悬空链接 |
十二、文件的三个常见时间
stat常显示以下时间:
atime:文件内容最后访问时间;mtime:文件内容最后修改时间;ctime:inode 状态最后变更时间,例如权限、所有者或链接数变化。
部分文件系统还支持文件创建时间(birth time),但它与ctime不是同一个概念,是否可见取决于文件系统、内核和工具支持。
十三、总结
本文的核心链路可以归纳为:
磁盘扇区与 LBA ↓ 分区与文件系统块 ↓ Ext 块组管理结构 ↓ inode 保存元数据和数据索引 ↓ 目录项建立文件名与 inode 的映射 ↓ 路径解析与 dentry 缓存 ↓ 挂载把多个文件系统接入统一目录树理解这条链路后,就能解释许多 Linux 文件操作:创建文件是在分配 inode、数据块并添加目录项;删除文件是在移除目录项并更新链接计数;硬链接是多个名称指向同一 inode;软链接则是保存目标路径的独立文件。
