ext4文件系统磁盘布局:从概念到实验,从磁盘到内存
概述
ext4 是 Linux 系统上使用最广泛的本机文件系统,也是 ext2/ext3 的继承者。它在保持向后兼容的同时,引入了对大容量存储、高性能和可靠性至关重要的新特性,是许多 Linux 发行版的默认文件系统。
ext4 相比传统文件系统(如 ext2/ext3)的主要优势:
| 优势 | 说明 |
|---|---|
| 更大容量 | 支持最大 1EB 的文件系统(需 64bit 特性)和 16TB 的单个文件 |
| Extent 区段 | 用 extent 树替代间接块映射,大幅减少大文件的元数据开销和碎片 |
| flex_bg 弹性块组 | 将元数据集中存放,减少磁盘寻道时间,加速元数据操作 |
| 延迟分配 | 推迟数据块分配决策,能做出更优的空间布局,提升性能并减少碎片 |
| 日志校验和 | 为日志(jbd2)添加校验和,增强系统崩溃后的数据一致性保障 |
| 在线调整 | 支持文件系统在线扩容,无需卸载(umount) |
本文主要是聚焦于理解 ext4 最核心的磁盘布局概念,并回答以下问题:
- ext4 的磁盘上到底有什么?块、块组、flex_bg 是如何组织的?
- 数据块位图、inode 位图、inode 表、extent 各自承担什么职责?
- 创建一个文件后,内核是如何找到对应的数据块的?
- flex_bg 模式下,块组的元数据到底存放在哪里?
- 磁盘上的布局,到了内存中变成了什么样子?
一、核心概念
块 (Block)
- 文件系统最小的寻址单位,大小可为 1KB、2KB、4KB、64KB
- 默认 4KB,与系统内存页大小匹配
- 所有文件数据、元数据都以块为单位存储
块组 (Block Group)
- 多个块组成一个块组,每个块组独立管理自己的数据和元数据
- 默认块组大小:128MB(4KB 块 × 32768 个块)
- 文件系统容量 = 块组数 × 块组大小
弹性块组 (flex_bg)
- 将多个物理块组绑定成一个逻辑块组
- 元数据集中:所有位图和 inode 表存放到第一个物理块组
- 数据分散:实际文件数据仍分布在各物理块组
- 目的:减少寻道时间,提升元数据操作性能
Ext4 磁盘布局
- ext4 磁盘布局 (flex_bg)
┌─────────────────────────────────────────────────────────────────┐
│ ext4 磁盘布局 (flex_bg) │
├───────────────────┬───────────────────┬───────────────────┬─────┤
│ Group 0 │ Group 1 │ Group 2 │ ... │
│ (Blocks 0-32767) │ (Blocks 32768- │ (Blocks 65536- │ │
│ │ 65535) │ 98303) │ │
├───────────────────┼───────────────────┼───────────────────┼─────┤
│ ┌─────────────┐ │ │ │ │
│ │ 超级块 │ │ │ │ │
│ │ 组描述符 │ │ │ │ │
│ │ 保留 GDT │ │ │ │ │
│ ├─────────────┤ │ │ │ │
│ │ Group0-7 │ ◀─┼── 指向 Group0 │ │ │
│ │ 块位图 │ │ │ │ │
│ ├─────────────┤ │ │ │ │
│ │ Group0-7 │ ◀─┼── 指向 Group0 │ │ │
│ │ inode 位图 │ │ │ │ │
│ ├─────────────┤ │ │ │ │
│ │ Group0-7 │ ◀─┼── 指向 Group0 │ │ │
│ │ inode 表 │ │ │ │ │
│ ├─────────────┤ │ │ │ │
│ │ Group0 数据 │ │ Group1 数据 │ Group2 数据 │ │
│ └─────────────┘ └───────────────────┴───────────────────┴─────┘
- flex_bg 模式下各块组的物理组成
| 块组 | 超级块 | 组描述符 | 保留 GDT | 数据块位图 | inode 位图 | inode 表 | 数据块 |
|---|---|---|---|---|---|---|---|
| Group 0 | ✅ 主副本 | ✅ 主副本 | ✅ | ✅ 存放 所有块组 的位图 | ✅ 存放 所有块组 的位图 | ✅ 存放 所有块组 的表 | ✅ Group 0 自己的数据 |
| Group 1 | ✅ 备份 | ✅ 备份 | ✅ | ❌ (指向 Group 0) | ❌ (指向 Group 0) | ❌ (指向 Group 0) | ✅ Group 1 的数据 |
| Group 2 | ❌ | ❌ | ❌ | ❌ (指向 Group 0) | ❌ (指向 Group 0) | ❌ (指向 Group 0) | ✅ Group 2 的数据 |
| Group 3 | ✅ 备份 | ✅ 备份 | ✅ | ❌ (指向 Group 0) | ❌ (指向 Group 0) | ❌ (指向 Group 0) | ✅ Group 3 的数据 |
| Group 4 | ❌ | ❌ | ❌ | ❌ (指向 Group 0) | ❌ (指向 Group 0) | ❌ (指向 Group 0) | ✅ Group 4 的数据 |
| Group 5 | ✅ 备份 | ✅ 备份 | ✅ | ❌ (指向 Group 0) | ❌ (指向 Group 0) | ❌ (指向 Group 0) | ✅ Group 5 的数据 |
| Group 6 | ❌ | ❌ | ❌ | ❌ (指向 Group 0) | ❌ (指向 Group 0) | ❌ (指向 Group 0) | ✅ Group 6 的数据 |
| Group 7 | ✅ 备份 | ✅ 备份 | ✅ | ❌ (指向 Group 0) | ❌ (指向 Group 0) | ❌ (指向 Group 0) | ✅ Group 7 的数据 |
| Group N... | 按 sparse_super 规则 | 按 sparse_super 规则 | 按 sparse_super 规则 | ❌ (指向 Group 0) | ❌ (指向 Group 0) | ❌ (指向 Group 0) | ✅ 各自的数据 |
- flex_bg模式下Group 0物理块布局
Group 0 物理块布局 (块大小 4KB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌──────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│ 块0 │ 块1 │ ... │ 块14-21 │ 块30-37 │ 块46- │ 块... │ │
│ SB │ GDT │ │ 数据块 │ inode │ 1645 │ inode表 │ 数据块 │
│ │ │ │ 位图 │ 位图 │ │ 继续 │ │
│ │ │ │ (各组) │ (各组) │ │ │ │
└──────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
↑ ↑
│ │
标记数据块 标记 inode
使用情况 使用情况
│
▼
┌─────────────────────────────┐
│ inode 表块 (46-1645) │
│ ┌─────────────────────────┐ │
│ │ inode 1 (256B) │ │
│ │ ├── i_mode = 0644 │ │
│ │ ├── i_size = 15 │ │
│ │ └── i_block[] = extent │ │
│ ├─────────────────────────┤ │
│ │ inode 2 │ │
│ │ ... │ │
│ └─────────────────────────┘ │
└─────────────────────────────┘
│
│ extent 指向
▼
┌─────────────────────────────┐
│ 数据块 (如块 2646) │
│ ┌─────────────────────────┐ │
│ │ "Hello ext4 lab" │ │
│ └─────────────────────────┘ │
└─────────────────────────────┘
二、核心概念详解
核心概念的职责分工
| 概念 | 职责 | 类比 |
|---|---|---|
| 数据块位图 | 标记哪个物理块被使用了(只记录已用/空闲,不记录属于谁) | 停车场的车位占用图(哪个车位有车,但不知道是谁的车) |
| inode 位图 | 标记哪个 inode 编号被使用了(只记录已用/空闲,不记录文件属性) | 停车场的停车卡发放记录(哪个卡号已发出,但不知道车主信息) |
| inode 表 | 存储每个 inode 的详细信息(权限、大小、时间、extent 指针等) | 停车场的车辆登记本(车牌号、车型、车主信息、停车卡号) |
| Extent | 记录文件数据实际存放在哪些物理块(逻辑块号 → 物理块号的映射) | 车辆登记本上记录的具体停车位置(停在 A区 12 号车位) |
| 数据块 | 实际存储文件内容 | 车位上停放的汽车本身 |
数据块位图详解
什么是数据块位图?
数据块位图 (Block Bitmap) 是用 1 个比特 (bit) 来表示一个数据块状态的数据结构:
| 比特值 | 含义 |
|---|---|
| 0 | 对应的数据块是空闲的,可以使用 |
| 1 | 对应的数据块是已使用的,已被文件数据或元数据占用 |
为什么需要数据块位图?
- 快速找到空闲数据块来存储文件内容
- 避免扫描整个数据块区域(可能几百万个块)
- 一个 4KB 的块 = 32768 个比特,可管理 32768 个数据块(即 128MB 空间)
inode 位图详解
什么是inode位图?
inode位图 (Bitmap) 是用1个比特 (bit) 来表示一个inode状态的数据结构:
| 比特值 | 含义 |
|---|---|
| 0 | 对应的 inode 是空闲的,可以使用 |
| 1 | 对应的 inode 是已使用的,已被文件占用 |
为什么需要 inode 位图?
- 快速找到空闲 inode 来创建新文件
- 避免扫描整个 inode 表(可能几千个块)
- 一个 4KB 的块 = 32768 个比特,可管理 32768 个 inode
数据块位图 vs inode 位图
| 对比项 | 数据块位图 | inode 位图 |
|---|---|---|
| 标记对象 | 数据块 | inode |
| 1 个比特代表 | 1 个数据块(4KB) | 1 个 inode(256 字节) |
| 每块组数量 | 32768 个数据块 | 32768 个 inode |
| 管理空间 | 128MB 数据 | 8MB inode 空间 |
| flex_bg 物理位置 | 集中在 Group 0 | 集中在 Group 0 |
inode表详解
存储内容
inode 表是多个连续的块,每个 inode 占用固定大小(默认 256 字节),存储:
| 字段 | 作用 |
|---|---|
i_mode | 文件类型和权限 |
i_size | 文件大小(字节) |
i_uid / i_gid | 文件所有者/所属组 |
i_atime / i_mtime / i_ctime | 访问/修改/状态变更时间 |
i_blocks | 文件占用的块数(512 字节为单位) |
i_block[15] | Extent 树的存储位置(60 字节) |
与 inode 位图的关系:一个“找空位 → 填信息”的协作流程
inode 位图和 inode 表 的关系,类似于“房间号登记表”和“房间内的住户信息”。
- inode 位图:记录哪些 inode 编号(房间号)是空闲的(0)还是已占用的(1)。它是一个快速查找工具,帮你找到第一个空闲的 inode 编号。
- inode 表:存储每个 inode 编号对应的详细信息(就像房间里的住户信息)。
它们的工作流程如下:
创建新文件时:
│
▼
1. 查询 inode 位图,找到第一个标记为 0(空闲)的 inode 编号
(例如 inode 位图显示:inode 1:1, 2:1, 3:0 → inode 3 空闲)
│
▼
2. 分配该 inode 编号(将位图中对应位从 0 改为 1)
│
▼
3. 在 inode 表中,找到 inode 3 的位置
(位置计算公式:inode 表起始块号 + 3 × 256 字节)
│
▼
4. 将新文件的属性(权限、大小、时间、extent 指针等)填入 inode 3 的存储区域
│
▼
5. 文件创建完成,inode 3 现在代表这个文件
图示:inode 位图与 inode 表的协作
inode 位图 (Group 0, 块 30) inode 表 (Group 0, 块 46-1645)
┌─────────────────────────┐ ┌─────────────────────────────┐
│ 比特位(第 1-4 个 inode) │ │ 块 46(inode 表起始) │
│ ┌─────────────────────┐ │ │ ┌─────────────────────────┐ │
│ │ inode 1: 1 (已用) │ │ │ │ 偏移 0: inode 1 │ │
│ │ inode 2: 1 (已用) │ │ │ │ 偏移 256: inode 2 │ │
│ │ inode 3: 0 (空闲) ←──┼─ │────── 分配 ──────→ │ 偏移 512: inode 3 ←───────┼─│ 填入文件属性
│ │ inode 4: 1 (已用) │ │ │ │ 偏移 768: inode 4 │ │
│ └─────────────────────┘ │ │ └─────────────────────────┘ │
└─────────────────────────┘ └─────────────────────────────┘
Extent(区段)详解
存储位置
Extent 信息存储在 inode 的 i_block[15] 数组中,不是单独的区域。
Extent 数据结构
| 组件 | 大小 | 内容 |
|---|---|---|
| extent_header | 12 字节 | 魔数(0xF30A)、条目数、树深度 |
| extent 条目 | 12 字节/个 | 逻辑块号、物理块号、长度 |
Extent 示例
inode 中的 i_block[15] (60 字节)
┌─────────────────────────────────────────────────────────────┐
│ extent_header │
│ ├── eh_magic = 0xF30A │
│ ├── eh_entries = 1 │
│ └── eh_depth = 0 │
├─────────────────────────────────────────────────────────────┤
│ extent 条目 │
│ ├── ee_block = 0 (文件内逻辑块号) │
│ ├── ee_len = 1 (连续块数量) │
│ └── ee_start = 24576 (物理块号) │
└─────────────────────────────────────────────────────────────┘
完整数据流
用户请求: cat hello.txt
│
▼
目录项: hello.txt → inode 编号 = 3
│
▼
inode 位图: 检查 inode 3 状态 → 1 (已使用)
│
▼
inode 表: 读取 inode 3 (位于块 46 + 2*256 偏移)
│
├── i_size = 15
├── i_mode = 0644
└── i_block[] = extent 树
│
▼
Extent: (0):24576 → 物理块 24576
│
▼
数据块位图: 确认块 24576 标记为 1 (已使用) [可选检查]
│
▼
数据块: 读取物理块 24576
│
▼
返回: "Hello ext4 lab"
相关概念的层次关系
┌─────────────────────────────────────────────────────────────┐
│ 目录项 (Directory Entry) │
│ "文件名 → inode 编号" 的映射 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 数据块位图 (Block Bitmap) │
│ "哪个数据块被使用了" 的标记表 │
│ 回答:物理块 N 是空闲还是已用? │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ inode 位图 (Inode Bitmap) │
│ "哪个 inode 编号被使用了" 的标记表 │
│ 回答:inode N 是空闲还是已用? │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ inode 表 (Inode Table) │
│ 存储每个 inode 的完整信息(大小、权限、时间等) │
│ 其中最重要的:数据块在哪里? │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Extent (区段) │
│ 存储在 inode 的 i_block[] 中,回答: │
│ "文件的第 N 个逻辑块,对应磁盘的第 M 个物理块" │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 数据块 (Data Blocks) │
│ 实际的文件内容 │
└─────────────────────────────────────────────────────────────┘
总结
- 数据块位图和 inode 位图分别是“数据块”和“inode”的空闲/已用标记表;
- inode 表是每个文件的“身份证”;
- Extent是身份证上记录的“住址”;
- 数据块是实际存放文件内容的“房子”。
上述五者分工明确,共同构成了 ext4 文件系统的核心管理机制。
三、实验验证
实验环境准备
# 创建一个 1GB 的 ext4 镜像(足够看到多个块组)
dd if=/dev/zero of=ext4_lab.img bs=1M count=1024
# 格式化为 ext4
mkfs.ext4 ext4_lab.img
# 创建挂载点
mkdir -p mnt
实验一:查看文件系统全局信息
# 查看超级块信息
dumpe2fs -h ext4_lab.img
# 重点关注以下字段:
# - Block size: 块大小(通常 4096)
# - Inode size: inode 大小(通常 256)
# - Block group: 块组数量
# - Filesystem features: 文件系统特性(64bit, flex_bg, extent 等)
观察要点:
- 确认
flex_bg特性已开启 - 确认
extent特性已开启 - 记录块组数量(1GB 镜像约 8 个块组)
实验二:查看块组布局
# 查看所有块组的详细信息
dumpe2fs ext4_lab.img | grep -E "Group [0-9]+:|Block bitmap|Inode bitmap|Inode table"
关键观察(flex_bg 开启时):
Group 0: (Blocks 0-32767)
Block bitmap at 14 (bg #0 + 14)
Inode bitmap at 30 (bg #0 + 30)
Inode table at 46-1645 (bg #0 + 46)
Group 1: (Blocks 32768-65535)
Block bitmap at 15 (bg #0 + 15) ← 注意:仍然指向 Group 0
Inode bitmap at 31 (bg #0 + 31)
Inode table at 1646-3245 (bg #0 + 1646)
结论:Group 1 及之后的所有块组,其位图和 inode 表都物理存储在 Group 0 中。这就是 flex_bg 的直观体现。
实验三:查看超级块的原始十六进制数据
# 查看超级块(位于块 0,偏移量 0)
hexdump -C -s 0 -n 1024 ext4_lab.img | head -40
# 找到魔数 0xEF53(小端序显示为 53 EF)
# 通常位于偏移量 0x438 附近
观察要点:
- 前 1024 字节是全
0(为 x86 引导程序预留) - 从偏移量 1024 开始才是超级块内容
- 找到
53 EF魔数
实验四:创建文件并追踪 inode
# 挂载镜像
sudo mount -o loop ext4_lab.img mnt/
# 创建测试文件
echo "Hello ext4 lab" | sudo tee mnt/hello.txt
dd if=/dev/urandom of=mnt/bigfile.txt bs=1M count=20
# 卸载
sudo umount mnt/
# 查看 hello.txt 的 inode 信息
debugfs -R "stat /hello.txt" ext4_lab.img
# 查看 bigfile.txt 的 inode 信息
debugfs -R "stat /bigfile.txt" ext4_lab.img
观察要点(小文件):
Inode: 12 Type: regular Size: 15
EXTENTS:
(0):2646
观察要点(大文件):
Inode: 13 Type: regular Size: 20971520
EXTENTS:
(0-5119):2646-7765
解读:
- 小文件:只有 1 个 extent,占用 1 个物理块
- 大文件:1 个 extent 就描述了 5120 个连续物理块(20MB)
实验五:用 hexdump 验证数据位置
# 从 debugfs 输出中找到物理块号(假设是 2646)
# 块大小 = 4096,偏移量 = 2646 × 4096
hexdump -C -s $((2646 * 4096)) -n 64 ext4_lab.img
预期输出:
06000000 48 65 6c 6c 6f 20 65 78 74 34 20 6c 61 62 0a 00 |Hello ext4 lab..|
结论:你亲手验证了 文件名 → inode → extent → 物理块 → 数据 的完整链路。
实验六:验证 flex_bg 布局(需要 >128MB 镜像)
如果你创建了 1GB 镜像,用以下命令确认:
# 查看 Group 0 和 Group 1 的元数据位置
dumpe2fs ext4_lab.img | grep -E "Group [01]|Block bitmap at|Inode bitmap at|Inode table at"
预期:Group 1 的位图、inode 位图、inode 表块号都在 Group 0 的范围内(如 15、31、1646 等)。
关键命令速查
| 目的 | 命令 |
|---|---|
| 查看超级块 | dumpe2fs -h <img> |
| 查看块组详情 | dumpe2fs <img> |
| 查看文件 inode | debugfs -R "stat /path" <img> |
| 查看原始十六进制 | hexdump -C -s <偏移量> -n <长度> <img> |
| 挂载镜像 | sudo mount -o loop <img> <dir> |
| 创建镜像 | dd if=/dev/zero of=<img> bs=1M count=<大小> |
| 格式化 | mkfs.ext4 <img> |
四、内存中的数据布局
磁盘上的 Group vs 内存中的 Group
| 层面 | Group 的含义 | 物理位置 | 内容 |
|---|---|---|---|
| 磁盘(物理) | 一段连续的物理块范围 | 磁盘上固定的位置 | 超级块、组描述符、位图、inode表、数据块 |
| 内存(逻辑) | 一个管理单元的数据结构 | 内核内存(任意位置) | 组描述符缓存、块位图缓存、inode缓存等 |
关键区别:内存中的“Group 信息”是分散存储的,而不是像磁盘那样在一个连续的物理区域内。
内存中的具体数据结构
当挂载一个 ext4 文件系统时,内核会为它分配一个 struct super_block 结构体,其中包含一个指向 struct ext4_sb_info(sbi)的指针。sbi 中存储了所有块组的运行时信息。
核心数据结构
// 简化版的 ext4 超级块信息(内核内存)
struct ext4_sb_info {
// 其他字段...
// 组描述符数组(内存缓存)
struct ext4_group_desc **s_group_desc;
// 块组数量
unsigned int s_groups_count;
// 块大小(如 4096)
unsigned int s_blocksize;
// 其他运行时状态...
};
每个 struct ext4_group_desc 结构体对应一个块组,它缓存了磁盘上组描述符的内容:
// 简化版的组描述符(内核内存)
struct ext4_group_desc {
__le32 bg_block_bitmap; // 块位图所在的物理块号(如 131)
__le32 bg_inode_bitmap; // inode 位图所在的物理块号(如 139)
__le32 bg_inode_table; // inode 表起始物理块号(如 1169)
__le16 bg_free_blocks_count; // 空闲块计数
__le16 bg_free_inodes_count; // 空闲 inode 计数
// 其他字段...
};
内存布局示意图
内存中的 ext4_sb_info (sbi)
│
├── s_group_desc[0] ──→ struct ext4_group_desc (Group 0 的缓存)
│ ├── bg_block_bitmap = 14
│ ├── bg_inode_bitmap = 30
│ ├── bg_inode_table = 46
│ └── bg_free_blocks_count = 28521
│
├── s_group_desc[1] ──→ struct ext4_group_desc (Group 1 的缓存)
│ ├── bg_block_bitmap = 15 ← 注意:物理上指向 Group 0
│ ├── bg_inode_bitmap = 31
│ ├── bg_inode_table = 1646
│ └── bg_free_blocks_count = 32639
│
├── s_group_desc[2] ──→ struct ext4_group_desc (Group 2 的缓存)
│ ├── bg_block_bitmap = 131 ← 物理上指向 Group 0
│ ├── bg_inode_bitmap = 139
│ ├── bg_inode_table = 3729
│ └── bg_free_blocks_count = 32768
│
└── ... (Group 3 到 Group N)
-
每个块组都有对应的
struct ext4_group_desc结构体,存放在内核内存中(通常是通过s_group_desc数组索引)。 -
这些结构体在内存中是分散存储的(通过指针数组连接),不要求物理连续。
-
结构体中的字段值(如
bg_block_bitmap)是从磁盘上的组描述符读取并缓存下来的。对于 Group 2,字段值就是131、139、3729等。 -
内存中的 Group 信息是“逻辑组织”,内核通过数组索引(如
s_group_desc[2])来访问 Group 2 的信息,而不是通过磁盘物理块号。 -
无论
flex_bg如何重定向,内存中的每个块组都保留自己的struct ext4_group_desc。Group 2 的struct ext4_group_desc始终存在,并且其中的bg_block_bitmap字段存储着实际的物理块号(131)。
数据流:从磁盘到内存
磁盘 (Group 0, 块 1) 内存
┌─────────────────────┐ ┌─────────────────────────────┐
│ 组描述符表(原始字节) │ │ struct ext4_group_desc[2] │
│ ... │ ──挂载时读取─→ │ (解析后的结构体) │
│ Group 2 的描述符: │ │ bg_block_bitmap = 131 │
│ 块位图 = 0x83 │ │ bg_inode_bitmap = 139 │
│ inode位图 = 0x8B │ │ bg_inode_table = 1169 │
│ inode表 = 0x491 │ └─────────────────────────────┘
│ ... │ │
└─────────────────────┘ │ 运行时使用
↓
需要访问 Group 2 的文件时:
内核查看内存中的
bg_block_bitmap = 131
总结
内存中按 Group 0、Group 1、Group 2... 这样的“逻辑分组”来组织数据(通过数组索引),但这些数据物理上分散存储在内核内存中,而不是像磁盘那样集中在 Group 0 的连续区域里。