ext4文件系统磁盘布局:从概念到实验,从磁盘到内存

5 阅读16分钟

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_header12 字节魔数(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>
查看文件 inodedebugfs -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)
  1. 每个块组都有对应的 struct ext4_group_desc 结构体,存放在内核内存中(通常是通过 s_group_desc 数组索引)。

  2. 这些结构体在内存中是分散存储的(通过指针数组连接),不要求物理连续。

  3. 结构体中的字段值(如 bg_block_bitmap)是从磁盘上的组描述符读取并缓存下来的。对于 Group 2,字段值就是 1311393729 等。

  4. 内存中的 Group 信息是“逻辑组织”,内核通过数组索引(如 s_group_desc[2])来访问 Group 2 的信息,而不是通过磁盘物理块号。

  5. 无论 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 的连续区域里。