ext4 深入 — 从超级块到 Extent Tree / Ext4 in Depth from Superblocks to Extent Trees
📅 创建时间:2026-06-17 🏷️ 标签:#ext4 #文件系统 #inode #extent #journal #深入 📚 前置知识:[[04-filesystem-concepts]], [[05-filesystem-formats]]
📋 本章目标
- 深入理解 ext4 的超级块——文件系统元数据的锚点
- 掌握 Block Group 布局——为什么把磁盘分成多个组
- 深入 inode 结构——extent tree 的节点在磁盘上长什么样
- 理解 extent tree 的搜索、插入、分裂算法
- 掌握 journal 三种模式的安全性与性能 trade-off
- 理解 ext2 → ext3 → ext4 演进史——每个版本解决了什么具体问题
- 使用
dumpe2fs/debugfs实操查看文件系统的内部数据
第1部分:超级块与 Block Group — ext4 的磁盘布局
1.1 整体布局
┌─────────────────────────────────────────────────────────────┐
│ ext4 文件系统的磁盘布局 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 整个文件系统被切成多个 Block Group: │
│ │
│ ┌──────────┬──────────┬──────────┬─────┬──────────┐ │
│ │ Group 0 │ Group 1 │ Group 2 │ ... │ Group N │ │
│ └──────────┴──────────┴──────────┴─────┴──────────┘ │
│ │
│ 每个 Block Group 内部结构 (以 Group 0 为例): │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Superblock (1 块) ← 只有 Group 0,1,3,5,7,9... 有 │ │
│ │ GDT (多块) ← 组描述符表, 每个组一条 │ │
│ │ Block Bitmap (1 块) ← 本组内哪些块是空闲的 │ │
│ │ Inode Bitmap (1 块) ← 本组内哪些 inode 是空闲的 │ │
│ │ Inode Table (多块) ← 本组的 inode 数组 │ │
│ │ │ │
│ │ ... 数据块 ... │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 为什么多个 Group 需要重复存超级块和 GDT? │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 如果 Group 0 的超级块损坏 → 从 Group 1 恢复! │ │
│ │ 只在部分 Group 存 (稀疏超块) → 省空间 │ │
│ │ GDT 也是每个有超块的 Group 存一份备份 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 为什么要切成 Block Group? │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 局部性 —— 文件的数据块和 inode 尽量放在同一个 Group │ │
│ │ → 减少磁盘寻道 │ │
│ │ 2. 并行性 —— 不同 Group 可以独立操作 │ │
│ │ 3. 可靠性 —— 一个 Group 损坏不波及其他 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
1.2 超级块 (Superblock) — dumpe2fs 实战
bash
$ sudo dumpe2fs -h /dev/nvme0n1p3 | head -30
Filesystem volume name: <none>
Last mounted on: /
Filesystem UUID: a1b2c3d4-...
Filesystem magic number: 0xEF53 # ext 家族的魔数
Filesystem revision #: 1 (dynamic)
Filesystem features: has_journal ext_attr dir_index
filetype extent flex_bg sparse_super
large_file huge_file uninit_bg
dir_nlink extra_isize 64bit
metadata_csum
Block size: 4096 # 4KB 块
Inode size: 256 # 每个 inode 256 字节
Inode count: 12206080 # 约 12M 个 inode
Block count: 122096384 # 约 465GB / 4K = 122M 块
Reserved block count: 6104819 # root 预留 (~5%)
Free blocks: 94839219 # 还剩多少块空闲
Free inodes: 12190713 # 还剩多少 inode 空闲1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1.3 GDT — 组描述符表
┌─────────────────────────────────────────────────────────────┐
│ GDT (Group Descriptor Table) │
├─────────────────────────────────────────────────────────────┤
│ │
│ GDT 是一个数组,每个 Block Group 一条描述符: │
│ │
│ struct ext4_group_desc { │
│ __le32 bg_block_bitmap_lo; // Block Bitmap 位置 │
│ __le32 bg_inode_bitmap_lo; // Inode Bitmap 位置 │
│ __le32 bg_inode_table_lo; // Inode Table 位置 │
│ __le16 bg_free_blocks_count_lo; // 本组空闲块数 │
│ __le16 bg_free_inodes_count_lo; // 本组空闲 inode 数 │
│ __le16 bg_used_dirs_count_lo; // 本组目录数 │
│ __le16 bg_flags; // 标志 │
│ __le32 bg_exclude_bitmap_lo; // 快照排除位图 │
│ __le16 bg_block_bitmap_csum_lo; // Block Bitmap 校验 │
│ __le16 bg_inode_bitmap_csum_lo; // Inode Bitmap 校验 │
│ __le16 bg_itable_unused_lo; // Inode Table 未用部分 │
│ __le16 bg_checksum; // 组描述符自身的校验 │
│ // 64 位扩展... │
│ }; │
│ │
│ $ sudo dumpe2fs /dev/nvme0n1p3 | grep "Group 0:" -A 12 │
│ Group 0: (Blocks 0-32767) │
│ Primary superblock at 0, Group descriptors at 1-xxx │
│ Block bitmap at xxx │
│ Inode bitmap at yyy │
│ Inode table at zzz-www │
│ Free blocks: ..., Free inodes: ... │
│ │
└─────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
第2部分:inode — 从 ext2 间接块到 ext4 extent
2.1 ext2/3 的间接块回顾
┌─────────────────────────────────────────────────────────────┐
│ ext2/3 inode 的数据块指针 │
├─────────────────────────────────────────────────────────────┤
│ │
│ inode 里的 i_block[15] 数组: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [0] → 数据块 0 │ │
│ │ [1] → 数据块 1 │ │
│ │ ... │ │
│ │ [11] → 数据块 11 ← 直接指针, 共 12 个 │ │
│ │ │ │
│ │ [12] → 间接块 │ │
│ │ └→ [0] 数据块 12 │ │
│ │ [1] 数据块 13 │ │
│ │ ... (4K/4B = 1024 个指针) │ │
│ │ │ │
│ │ [13] → 二级间接块 │ │
│ │ └→ [0] 间接块 → [0..1023] 数据块 │ │
│ │ [1] 间接块 → [0..1023] 数据块 │ │
│ │ ... (1024 × 1024 ≈ 1M 个数据块) │ │
│ │ │ │
│ │ [14] → 三级间接块 │ │
│ │ └→ ... (1024³ ≈ 1B 个数据块) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 缺点: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 大文件需要大量元数据块 (间接块) → 碎片 │ │
│ │ 2. 随机访问要多次读间接块 → 慢 │ │
│ │ 3. 追加写入不断分配新间接块 → 元数据碎片 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2.2 ext4 的 Extent Tree — 革命性替代
┌─────────────────────────────────────────────────────────────┐
│ ext4 Extent Tree 结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 一个 extent 节点 = 存一段连续地址区间: │
│ │
│ struct ext4_extent { │
│ __le32 ee_block; // 文件内的起始块号 (逻辑) │
│ __le16 ee_len; // 这个 extent 覆盖多少块 │
│ __le16 ee_start_hi; // 物理块号高 16 位 │
│ __le32 ee_start_lo; // 物理块号低 32 位 │
│ }; │
│ │
│ 例: 一段描述 "文件偏移 0~255,物理存储 10000~10255" │
│ → ee_block=0, ee_len=256, ee_start=10000 │
│ │
│ Inode 的 i_block[60] 复用为 extent tree 的根: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ extent_header (12B): │ │
│ │ magic: 0xF30A │ │
│ │ entries: 2 ← 当前节点有 2 个条目 │ │
│ │ depth: 0 ← 叶子节点 (depth=0 = 叶子) │ │
│ │ │ │
│ │ extent[0]: │ │
│ │ [逻辑0 ~ 逻辑255] → [物理 10000 ~ 10255] │ │
│ │ 256 块连续! 一条 covers 1MB │ │
│ │ │ │
│ │ extent[1]: │ │
│ │ [逻辑256 ~ 逻辑511] → [物理 20000 ~ 20255] │ │
│ │ 又一个 1MB │ │
│ │ │ │
│ │ 2 个 extent = 描述了 512 块 = 2MB │ │
│ │ 如果这个文件是连续存储的 → 只需 1 个 extent │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 如果 extent 太多一个节点装不下 → 树变深: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Index Node │ │
│ │ (depth=1, 内部节点) │ │
│ │ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ idx[0] │ │ idx[1] │ │ │
│ │ │ [逻辑0~ │ │ [逻辑1024~ │ │ │
│ │ │ 1023] │ │ 2047] │ │ │
│ │ └────┬─────┘ └──────┬───────┘ │ │
│ │ ↓ ↓ │ │
│ │ ┌─────────┐ ┌──────────┐ │ │
│ │ │Leaf Node│ │Leaf Node │ │ │
│ │ │4 extent │ │3 extent │ │ │
│ │ │entries │ │entries │ │ │
│ │ └─────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 对比: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1GB 连续文件: │ │
│ │ ext2/3: 需要 51,200+ 个间接块 │ │
│ │ ext4: 只需 1 个 extent 条目 — 描述整个 1GB 区间! │ │
│ │ │ │
│ │ 10GB 碎片文件 (随机写): │ │
│ │ ext2/3: 百万级的间接块 → 灾难 │ │
│ │ ext4: 一棵多层的 extent tree → 可控 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
第3部分:Journal — 三种模式深度对比
3.1 三种 Journal 模式
┌─────────────────────────────────────────────────────────────┐
│ Journal 三种模式 — 安全 vs 性能 │
├─────────────────────────────────────────────────────────────┤
│ │
│ journal (data=journal): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 先写操作: │ │
│ │ 1. 元数据 + 文件数据 一起写 Journal │ │
│ │ 2. 标记事务为"正在提交" │ │
│ │ 3. 元数据 + 文件数据 写入磁盘 (checkpoint) │ │
│ │ 4. 标记事务为"已完成" │ │
│ │ │ │
│ │ 断电恢复: 所有数据都完整 │ │
│ │ │ │
│ │ 代价: 数据写了两次 (Journal + 最终位置) │ │
│ │ 写入吞吐量减半 │ │
│ │ 实际使用极少, 几乎没人用 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ordered (data=ordered, ext4 默认): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 先写数据, 再写元数据: │ │
│ │ 1. 文件数据直接写入最终磁盘位置 (不经过 Journal) │ │
│ │ 2. 等数据写完 → 元数据写 Journal │ │
│ │ 3. 元数据写入磁盘 │ │
│ │ │ │
│ │ 断电恢复: │ │
│ │ 数据已落盘 + 元数据从 Journal 恢复 → 文件完整 │ │
│ │ 数据未落盘 + 元数据恢复 → 文件大小正确但 │ │
│ │ 内容是旧的(新增部分丢失)│ │
│ │ 不会出现"垃圾数据" │ │
│ │ │ │
│ │ 性能: 好。数据只写一次, 元数据的 Journal 是顺序写 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ writeback (data=writeback): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 元数据写 Journal, 数据随意: │ │
│ │ 1. 元数据写 Journal │ │
│ │ 2. 文件数据写磁盘 (与步骤 1 可能同时或乱序) │ │
│ │ │ │
│ │ 断电恢复: │ │
│ │ 元数据恢复 → 文件标识为"存在, 大小=100KB" │ │
│ │ 数据可能还没写 → 读到的 100KB 是磁盘上原有的旧数据! │ │
│ │ → 安全风险! │ │
│ │ │ │
│ │ 性能: 最快 (数据写入不等待元数据) │ │
│ │ 安全: 最差 │ │
│ │ 适用: 只在乎速度不在乎安全 或 有上层安全机制 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 查看当前模式: │
│ $ cat /proc/mounts | grep " / " │
│ ... rw,relatime,data=ordered ... │
│ │
│ 修改 (fstab): │
│ UUID=xxx / ext4 defaults,data=writeback 0 1 │
│ │
└─────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
第4部分:ext2 → ext3 → ext4 演进史
┌─────────────────────────────────────────────────────────────┐
│ ext 家族的演进路径 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ext2 (1993): "用间接指针存文件" │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 第一个成熟的 Linux 文件系统 │ │
│ │ • 无 Journal → 断电必须 fsck → TB 级盘要跑数小时 │ │
│ │ • 文件名最大 255 字节 │ │
│ │ • 文件系统最大 2TB (后来扩大到 32TB) │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ ext3 (2001): "加上 Journal" │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 唯一增加: Journal │ │
│ │ • 完全兼容 ext2 → 可以无痛升级 │ │
│ │ • 三种 Journal 模式 │ │
│ │ • 断电恢复: 秒级 (Journal Replay) │ │
│ │ 对比 ext2: 小时级 (全盘 fsck) │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ ext4 (2008): "性能 + 扩展性" │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 重大改进: │ │
│ │ │ │
│ │ 1. Extent Tree → 取代间接块 │ │
│ │ 大文件性能飞跃 │ │
│ │ │ │
│ │ 2. 延迟分配 (Delayed Allocation) │ │
│ │ 减少碎片, 提高写入性能 │ │
│ │ │ │
│ │ 3. 文件系统最大容量: 1EB │ │
│ │ 文件最大: 16TB │ │
│ │ │ │
│ │ 4. 在线碎片整理 (e4defrag) │ │
│ │ │ │
│ │ 5. 纳秒级时间戳 │ │
│ │ │ │
│ │ 6. 日志校验和 (Journal checksum) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 兼容性路径: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ext2 是 ext3 的子集 — ext3 驱动可以挂载 ext2 │ │
│ │ ext3 是 ext4 的子集 — ext4 驱动可以挂载 ext3 │ │
│ │ │ │
│ │ 一块 2003 年格式化的 ext3 盘插入今天的 Linux: │ │
│ │ → ext4 驱动识别它 → 用兼容模式挂载 → 正常工作 │ │
│ │ 这是 ext 系列能活 30 年的核心原因 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
第5部分:debugfs — 直接读 inode
# 查看某个文件的 inode
$ ls -i hello.txt
42 hello.txt
# 用 debugfs 查看 inode 的原始数据
$ sudo debugfs /dev/nvme0n1p3
debugfs> stat <42>
Inode: 42 Type: regular Mode: 0644 Flags: 0x80000
Links: 1 Blockcount: 8
ctime: 0x... atime: 0x... mtime: 0x...
Size of extra inode fields: 32
EXTENTS:
(0): 12345678 ← extent: 起始物理块 12345678, 长度 1 块
(END)
# 可以用 debugfs 直接读取数据块内容
debugfs> dump_extents <42>
# 输出: 文件的逻辑→物理块映射
debugfs> quit1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
核心总结
总结1:ext4 快速参考卡
块大小: 4KB (默认)
inode 大小: 256B (默认)
最大 FS: 1EB
最大文件: 16TB
核心创新: Extent Tree + 延迟分配
默认安全: data=ordered1
2
3
4
5
6
2
3
4
5
6
总结2:四种主要数据结构
Superblock → 块大小、inode 数、UUID、魔数 0xEF53
GDT → 每个 Block Group 的元数据位置
Inode → 权限/时间戳/大小/Extent Tree 根
Extent → {逻辑起始块, 长度, 物理起始块}1
2
3
4
2
3
4
总结3:选 journal 模式的简单规则
不确定 → ordered (默认, 兼顾安全与性能)
追求极致性能 → writeback (但需理解安全风险)
极致安全 → journal (但性能极差, 一般没必要)1
2
3
2
3
章节测试
测试1:extent vs 间接块
一个 1GB、完全连续存储的文件。ext2/3 和 ext4 各自需要多少元数据块来存储其数据块指针?
测试2:ordered 模式
data=ordered 模式在断电后文件内容可能是什么状态?
测试3:Block Group 的价值
为什么 ext4 把磁盘切成多个 Block Group?
测试4:演进路线
ext2 → ext3 → ext4 各自的核心新增特性是什么?
参考答案
测试1答案
- ext2/3:1GB 连续文件 = 262,144 个 4KB 数据块。12 个直接指针覆盖 48KB,还需要 ~51,200 个间接块来存剩余的指针。总共约 51,200 个元数据块。
- ext4:1GB 连续文件 = 1 个 extent 条目(逻辑起始 0,长度 262,144,物理起始 ONE_VALUE)。这个 extent 直接存在 inode 的 i_block 里,不需要额外的元数据块。1 条 extent 搞定。
测试2答案
data=ordered 确保数据先于元数据写入。断电后:
- 如果数据已落盘 + 元数据也通过 Journal 恢复了 → 文件完整
- 如果数据还未落盘 → 元数据也不会被标记为已提交 → 文件不存在(看不到新文件)或文件大小不增加(追加写入的部分丢失)
- 不会出现文件存在但内容是磁盘旧有垃圾数据的情况(这是 writeback 才有的风险)
测试3答案
三个原因:
- 局部性——文件的数据块和 inode 尽量在同一 Group,减少磁盘寻道
- 并行性——不同 Group 可以独立操作(多线程并发),提升性能
- 可靠性——一个 Group 的元数据损坏不会波及整个文件系统
测试4答案
- ext2(1993):第一个成熟的 Linux 文件系统,间接块数据指针,无 journal
- ext3(2001):唯一新增——Journal(日志),断电后秒级恢复
- ext4(2008):Extent Tree 取代间接块 + 延迟分配 + 1EB 最大容量 + 纳秒时间戳 + 在线碎片整理
相关笔记
- [[04-filesystem-concepts]] - inode 和 journal 的基础概念
- [[05-filesystem-formats]] - ext4 与其他文件系统的横向对比
- [[13-btrfs-deep-dive]] - 另一种设计哲学的产物:COW 对比 Journal
下一步学习
- [ ] 阅读 13 - btrfs 深入
学习状态:🟡 开始学习