第六篇:在线扩容与 FSMap — 不停机扩展与块所有者查询¶
源码:fs/ext4/resize.c (2192 lines) · fs/ext4/fsmap.c (792 lines) 头文件:fs/ext4/ext4.h · fs/ext4/fsmap.h
系列目录:ext4 内核源码深度解析
1. 概述¶
ext4 是少数支持在线扩容的文件系统之一——无需卸载、无需停机,就能扩展文件系统容量。这项能力的背后是精巧的元数据处理:新增块组描述符、更新超级块、调整伙伴位图,一切都在 journal 保护下完成。
与扩容相辅相成的是 FSMap(FS_IOC_GETFSMAP)ioctl —— 一种块级取证工具,能报告每个物理块的所有者(超级块、GDT、inode table、日志、数据块或空闲块)。它建立在 mballoc 的查询基础设施之上,为磁盘取证、碎片分析和管理工具提供基础数据。
本篇将逐层剖析这两个子系统的完整实现:
| 函数 | 文件:行号 | 功能 |
|---|---|---|
ext4_resize_begin |
resize.c:46 | 扩容前检查和加锁 |
ext4_flex_group_add |
resize.c:1535 | 添加新 flex group |
ext4_group_extend |
resize.c:1834 | 扩展最后一个块组 |
ext4_setup_new_descs |
resize.c:1338 | 初始化新组描述符 |
ext4_update_super |
resize.c:1418 | 更新超级块统计 |
ext4_getfsmap |
fsmap.c:703 | FSMap 主入口 |
ext4_getfsmap_helper |
fsmap.c:85 | 格式化输出记录 |
ext4_getfsmap_datadev |
fsmap.c:528 | 数据设备块遍历 |
2. 扩容前检查:ext4_resize_begin¶
ext4_resize_begin(resize.c:46)是所有扩容操作的入口栅检查,它在真正开始操作之前验证所有前置条件:
检查清单:
ext4_resize_begin(sb)
│
├─[1] CAP_SYS_RESOURCE 权限检查 (line 51)
│ 无权限 → -EPERM
│
├─[2] resize_inode 一致性检查 (line 58-62)
│ s_reserved_gdt_blocks>0 但 !FEATURE_RESIZE_INODE
│ → -EFSCORRUPTED
│
├─[3] 必须使用主超级块 (非备份) (line 69-74)
│ s_sbh->b_blocknr != s_first_data_block
│ → -EPERM
│
├─[4] 文件系统无错误 (line 80-84)
│ s_mount_state & EXT4_ERROR_FS
│ → -EPERM (错误状态下扩容风险太高)
│
├─[5] sparse_super2 不支持在线扩容 (line 86-89)
│ → -EOPNOTSUPP
│
└─[6] test_and_set_bit_lock(EXT4_FLAGS_RESIZING) (line 91-93)
防止并发扩容 → -EBUSY
EXT4_FLAGS_RESIZING 锁是原子操作的位锁,确保一次只有一个进程执行扩容。ext4_resize_end(resize.c:98)在操作完成后清除此标志。
// resize.c:98-105
int ext4_resize_end(struct super_block *sb, bool update_backups)
{
clear_bit_unlock(EXT4_FLAGS_RESIZING, &sbi->s_ext4_flags);
smp_mb__after_atomic(); // 内存屏障
if (update_backups)
return ext4_update_overhead(sb, true);
return 0;
}
3. 两种扩容模式¶
ext4 支持两种在线扩容模式:
┌──────────────────────────────────────────────────────────┐
│ 在线扩容模式 │
├──────────────────┬───────────────────────────────────────┤
│ flex_group_add │ 添加完整的新 flex group(s) │
│ (resize.c:1535) │ 增加组数,增加块数,增加 inode 数 │
│ │ mkfs 后从未被填充的连续空间 │
├──────────────────┼───────────────────────────────────────┤
│ group_extend │ 扩展最后一个块组的大小 │
│ (resize.c:1834) │ 不增加组数,只增加最后一个组的块数 │
│ │ 用于设备扩容后末尾残留空间 │
└──────────────────┴───────────────────────────────────────┘
3.1 ext4_flex_group_add — 添加新 Flex Group¶
ext4_flex_group_add(resize.c:1535)是最常用的扩容方式,每次添加一个完整的 flex group(多个 block group 的捆绑)。
// resize.c:1535
static int ext4_flex_group_add(struct super_block *sb,
struct inode *resize_inode,
struct ext4_new_flex_group_data *flex_gd)
输入结构:ext4_new_flex_group_data 包含:
- groups[] — 每个新块组的 ext4_new_group_data
- count — 新组的数量
- bg_flags[] — 每个组的标志(如 EXT4_BG_INODE_ZEROED 等)
完整流程:
ext4_flex_group_add(sb, resize_inode, flex_gd)
│
├─[1] setup_new_flex_group_blocks(sb, flex_gd) (line 1555)
│ 确保新块组的块在设备上存在且可写入
│ 也涉及块位图的初始化
│
├─[2] 计算 journal credit: (line 1565)
│ credit = 3 // sb, resize inode, dindirect
│ + 1 + DIV_ROUND_UP(count, DESC_PER_BLOCK) // GDT
│ + reserved_gdb // Reserved GDT blocks
│
├─[3] handle = ext4_journal_start_sb(sb, HT_RESIZE, credit) (line 1569)
│
├─[4] ext4_journal_get_write_access(sb, s_sbh) (line 1576)
│ 获取超级块的写访问
│
├─[5] ext4_add_new_descs(handle, sb, group, (line 1583)
│ resize_inode, count)
│ 在 GDT 中为新组添加描述符条目
│ 处理 resize_inode 备份(非 meta_bg 模式)
│
├─[6] ext4_setup_new_descs(handle, sb, flex_gd) (line 1588)
│ 初始化新组描述符的详细内容
│
├─[7] ext4_update_super(sb, flex_gd) (line 1592)
│ 更新超级块的块计数和 inode 计数
│
├─[8] ext4_handle_dirty_metadata(s_sbh) (line 1594)
│
├─[9] ext4_journal_stop(handle) (line 1597)
│
└─[10] update_backups() (line 1610)
将超级块和 GDT 更新传播到所有备份位置
3.2 ext4_group_extend — 扩展最后一个块组¶
ext4_group_extend(resize.c:1834)处理设备扩容后最后一个块组的膨胀:
// resize.c:1834
int ext4_group_extend(struct super_block *sb,
struct ext4_super_block *es,
ext4_fsblk_t n_blocks_count)
为什么需要这个函数?
当底层块设备(如 LVM LV 或虚拟磁盘)增大时,最后一个块组往往不满。ext4_group_extend 直接将空闲块追加到最后一个块组,无需创建新的 block group 描述符。
流程:
ext4_group_extend(sb, es, n_blocks_count)
│
├─[1] 验证:
│ n_blocks_count > o_blocks_count (不可缩小) (line 1860-1863)
│ n_blocks_count 不溢出设备地址空间 (line 1853-1857)
│
├─[2] 计算最后一个块组的剩余空间:
│ ext4_get_group_no_and_offset(o_blocks_count, &group, &last) (line 1866)
│ last == 0? → 组刚好满,改用 flex_group_add (line 1868)
│ add = BLOCKS_PER_GROUP - last (line 1873)
│ add = min(add, n_blocks_count - o_blocks_count) (line 1880)
│
├─[3] 检查设备容量:
│ ext4_sb_bread(o_blocks_count + add - 1) (line 1888)
│ 读取最后一个块验证设备容量 → IS_ERR → -ENOSPC
│
└─[4] ext4_group_extend_no_check(sb, o_blocks_count, add) (line 1895)
实际的块扩展操作:
├─更新 block bitmap (新块标记为空闲)
├─更新 buddy cache
├─更新组描述符中的 free_blocks_count
└─更新超级块
4. 组描述符初始化:ext4_setup_new_descs¶
ext4_setup_new_descs(resize.c:1338)负责初始化新增块组的组描述符(group descriptor):
// resize.c:1338
static int ext4_setup_new_descs(handle_t *handle,
struct super_block *sb,
struct ext4_new_flex_group_data *flex_gd)
流程:
ext4_setup_new_descs(handle, sb, flex_gd)
│
for (i = 0; i < flex_gd->count; i++):
│
├─ 定位 GDT 块: (line 1353-1361)
│ gdb_off = group % DESC_PER_BLOCK
│ gdb_num = group / DESC_PER_BLOCK
│ gdb_bh = sbi->s_group_desc[gdb_num]
│
├─ 定位组描述符: (line 1361-1362)
│ gdp = gdb_bh->b_data + gdb_off * DESC_SIZE(sb)
│
├─ memset(gdp, 0, DESC_SIZE) (line 1364)
│
├─ 设置关键字段:
│ ext4_block_bitmap_set(sb, gdp, block_bitmap) (line 1365)
│ ext4_inode_bitmap_set(sb, gdp, inode_bitmap) (line 1366)
│ ext4_inode_table_set(sb, gdp, inode_table) (line 1373)
│ ext4_free_group_clusters_set(sb, gdp, free_clusters) (line 1374)
│ ext4_free_inodes_set(sb, gdp, INODES_PER_GROUP) (line 1376)
│ ext4_itable_unused_set(sb, gdp, INODES_PER_GROUP) (line 1378)
│ gdp->bg_flags = *bg_flags (line 1380)
│
├─ ext4_group_desc_csum_set(sb, group, gdp) (line 1381)
│ 设置组描述符校验和(metadata checksum)
│
├─ ext4_handle_dirty_metadata(gdb_bh) (line 1383)
│
└─ ext4_mb_add_groupinfo(sb, group, gdp) (line 1393)
将新组注册到 mballoc 的 per-group 信息数组中
关键点:ext4_mb_add_groupinfo(第1393行)在这里被调用,将新块组添加到多块分配器的数据结构中。这确保新块组立即可用于块分配。
5. 超级块更新:ext4_update_super¶
ext4_update_super(resize.c:1418)在添加新块组后更新超级块的全局计数:
// resize.c:1418
static void ext4_update_super(struct super_block *sb,
struct ext4_new_flex_group_data *flex_gd)
内存屏障协议:
这是一个写内存屏障,确保在更新 s_groups_count 之前,所有从属数据(s_blocks_count, s_inodes_count 等)已经对所有 CPU 可见。
协议的另半部分在读者端:
更新内容:
ext4_update_super(sb, flex_gd)
│
├─ 遍历 flex_gd->groups: (line 1440)
│ blocks_count += group_data[i].blocks_count
│ free_blocks += group_data[i].free_clusters_count
│
├─ 更新超级块 disk 字段: (line 1450)
│ ext4_blocks_count_set(es, es->s_blocks_count + blocks_count)
│ ext4_free_blocks_count_set(es, es->s_free_blocks_count + free_blocks)
│ le32_add_cpu(&es->s_inodes_count, INODES_PER_GROUP * count)
│ le32_add_cpu(&es->s_free_inodes_count, INODES_PER_GROUP * count)
│
├─ smp_wmb() (line 1477)
│ 内存屏障:所有数据写入完成后才更新组数
│
├─ sbi->s_groups_count += flex_gd->count (line 1480)
│ 更新 s_blockfile_groups(块文件最大组数) (line 1481)
│
├─ ext4_r_blocks_count_set — 预留块按比例增加 (line 1486)
│
└─ percpu_counter_add — 更新 per-CPU 空闲计数器 (line 1490-1493)
s_freeclusters_counter, s_freeinodes_counter
6. Meta_bg 格式与 resize_inode¶
6.1 Meta Block Groups (meta_bg)¶
传统 ext2/ext3 文件系统中,所有块组描述符(GDT)都在第一个块组中。对于超大文件系统(>2^32 blocks),GDT 本身可能超过一个块组。
meta_bg 解决了这个问题:块组描述符不再局限于第一组,而是散布在各个块组中,每个"meta block group"(通常是 64 个组)有自己的 GDT 块。
传统布局 (non-meta_bg):
Group 0: [superblock, GDT(for ALL groups), reserved GDT, block bitmap, ...]
Group 1: [data...]
Group 2: [data...]
meta_bg 布局 (groups_per_meta=64):
Group 0: [superblock, GDT(for groups 0-63), ...]
Group 1: [GDT(for groups 1-63 的备份), data...]
Group 64: [GDT(for groups 64-127), ...]
Group 65: [GDT(for groups 65-127 的备份), data...]
meta_bg 让扩容可以轻松添加 GDT 块,不再受限于第一组的空间限制。
6.2 resize_inode¶
对于非 meta_bg 的文件系统,ext4 使用resize_inode 来管理 GDT 的扩展。这是一个特殊的 inode,存储着指向 GDT 备份块的间接块指针。
当添加新组时,ext4_add_new_descs(resize.c:约1000行)会:
1. 在 GDT 中添加新组描述符
2. 更新 resize_inode 的间接块指针(如果需要新的 GDT 备份块)
3. 处理 reserved GDT block 的分配
随着文件系统增长到 meta_bg 阈值(通常是 128 个组),可以调用 ext4_convert_meta_bg(resize.c:1909)将文件系统转换为 meta_bg 格式,之后就不再需要 resize_inode 了。
7. FSMap — FS_IOC_GETFSMAP¶
FS_IOC_GETFSMAP 是一个 ioctl(FS_IOC_GETFSMAP),提供底层的"哪个块属于谁"的查询能力。它最初在 XFS 中实现,ext4 在 Linux 4.12 中跟进。
7.1 查询状态结构¶
// fsmap.c:42-54
struct ext4_getfsmap_info {
struct ext4_fsmap_head *gfi_head; // 用户提供的查询头
ext4_fsmap_format_t gfi_formatter; // 格式化回调
void *gfi_format_arg; // 格式化参数
ext4_fsblk_t gfi_next_fsblk; // 上次返回的最后一个块+1
u32 gfi_dev; // 设备 ID
ext4_group_t gfi_agno; // 当前组号
struct ext4_fsmap gfi_low; // 下限查询键
struct ext4_fsmap gfi_high; // 上限查询键
struct ext4_fsmap gfi_lastfree; // 上一组尾部的空闲区域
struct list_head gfi_meta_list; // 固定元数据链表
bool gfi_last; // 最后一个 extent?
};
关键字段说明:
| 字段 | 作用 |
|---|---|
gfi_next_fsblk |
跟踪连续扫描进度,用于检测块之间的"间隙" |
gfi_low / gfi_high |
用户指定的扫描范围,{physical, owner} 二维键 |
gfi_lastfree |
跨块组边界的空闲区域跟踪 |
gfi_meta_list |
本组内的固定元数据(有序链表),用于后续与空闲块的交叉输出 |
7.2 块所有者常量¶
// fsmap.h:46-54
#define EXT4_FMR_OWN_FREE FMR_OWN_FREE // 0: 空闲空间
#define EXT4_FMR_OWN_UNKNOWN FMR_OWN_UNKNOWN // 0: 未知所有者
#define EXT4_FMR_OWN_FS FMR_OWNER('X', 1) // 静态 fs 元数据 (超级块)
#define EXT4_FMR_OWN_LOG FMR_OWNER('X', 2) // 日志
#define EXT4_FMR_OWN_INODES FMR_OWNER('X', 5) // inode 表
#define EXT4_FMR_OWN_GDT FMR_OWNER('f', 1) // 组描述符表
#define EXT4_FMR_OWN_RESV_GDT FMR_OWNER('f', 2) // 保留 GDT 块
#define EXT4_FMR_OWN_BLKBM FMR_OWNER('f', 3) // 块位图
#define EXT4_FMR_OWN_INOBM FMR_OWNER('f', 4) // inode 位图
FMR_OWNER 宏将两个字符编码为 64 位所有者 ID:
这允许工具通过可读字符区分不同类型。
7.3 ext4_getfsmap — 主入口¶
// fsmap.c:703
int ext4_getfsmap(struct super_block *sb,
struct ext4_fsmap_head *head,
ext4_fsmap_format_t formatter, void *arg)
流程:
ext4_getfsmap(sb, head, formatter, arg)
│
├─[1] 验证输入: (line 712-716)
│ fmh_iflags 只允许 FMH_IF_VALID
│ fmh_keys[0] 和 fmh_keys[1] 必须是有效设备
│
├─[2] 设置设备处理器: (line 720-731)
│ handlers[0] = {data_dev, ext4_getfsmap_datadev}
│ if (journal device): handlers[1] = {journal_dev, ext4_getfsmap_logdev}
│ sort(handlers, ...) 按设备 ID 排序
│
├─[3] 处理 continuation key: (line 744-747)
│ dkeys[0] = head->fmh_keys[0]
│ dkeys[0].fmr_physical += dkeys[0].fmr_length // 从上次结束处继续
│ dkeys[0].fmr_owner = 0
│ dkeys[0].fmr_length = 0
│ dkeys[1] = {0xFF, 0xFF, ..., 0xFF}
│
├─[4] 验证 bounds: (line 750)
│ ext4_getfsmap_check_keys(dkeys, &head->fmh_keys[1])
│
├─[5] 初始化 info 结构: (line 753-757)
│ info.gfi_next_fsblk = keys[0].fmr_physical + keys[0].fmr_length
│ info.gfi_formatter = formatter
│ info.gfi_head = head
│
└─[6] 遍历每个设备处理器: (line 760+)
for (i = 0; i < EXT4_GETFSMAP_DEVS; i++)
handlers[i].gfd_fn(sb, dkeys, &info)
└── ext4_getfsmap_datadev(sb, dkeys, &info)
└── 迭代块组 + 固定元数据 + mballoc 查询
7.4 ext4_getfsmap_datadev — 数据设备遍历¶
// fsmap.c:528
static int ext4_getfsmap_datadev(struct super_block *sb,
struct ext4_fsmap *keys,
struct ext4_getfsmap_info *info)
这是数据设备上最复杂的逻辑,它需要交错输出固定元数据和空闲/已用区域:
交叉输出算法:
ext4_getfsmap_datadev()
│
├─[1] 计算组范围: (line 544-559)
│ start_ag = 起始组号, end_ag = 结束组号
│
├─[2] 搜集固定元数据: (line 573)
│ ext4_getfsmap_find_fixed_metadata(sb, &meta_list)
│ 对每个组生成: 超级块, GDT, 保留 GDT, 块位图, inode 位图, inode table
│ 存储在有序链表 meta_list 中
│
├─[3] 遍历每个组 (start_ag → end_ag): (line 578)
│ │
│ ├─ ext4_mb_query_range(sb,
│ │ ext4_getfsmap_meta_helper, ← 处理固定元数据
│ │ ext4_getfsmap_datadev_helper, ← 处理空闲/已用区域
│ │ info)
│ │
│ │ mballoc 内部: 位图/buddy 遍历
│ │ → 对每个 extent 调用 datadev_helper
│ │
│ └─ ext4_getfsmap_helper(sb, info, &info->gfi_lastfree)
│ 输出组尾部的空闲区域
│
├─[4] 输出最后的空闲区域: (line 620)
│
└─[5] 发送终止记录 (dummy EXT4_FMR_OWN_UNKNOWN): (line 636)
ext4_getfsmap_helper(sb, info, &termination_rec)
ext4_mb_query_range 是 mballoc 提供的块位图查询接口,它通过 buddy cache 高效遍历每个组的所有 extent(空闲区域和已分配区域),并调用回调函数。
7.5 ext4_getfsmap_helper — 格式化输出¶
// fsmap.c:85
static int ext4_getfsmap_helper(struct super_block *sb,
struct ext4_getfsmap_info *info,
struct ext4_fsmap *rec)
这是 FSMap 的核心输出函数,负责:
-
过滤:丢弃在查询下限之前的记录(line 103)
-
计数模式:当
fmh_count == 0时仅计数而不返回实际数据 -
间隙检测:发现物理块序列中的空洞
// fsmap.c:134 if (rec_fsblk > info->gfi_next_fsblk) { // 发现间隙!输出为 EXT4_FMR_OWN_UNKNOWN fmr.fmr_physical = info->gfi_next_fsblk; fmr.fmr_owner = EXT4_FMR_OWN_UNKNOWN; fmr.fmr_length = rec_fsblk - info->gfi_next_fsblk; fmr.fmr_flags = FMR_OF_SPECIAL_OWNER; error = info->gfi_formatter(&fmr, info->gfi_format_arg); } -
格式化输出:调用用户提供的
formatter回调将记录复制到用户空间缓冲区 -
边界检查:当
fmh_entries >= fmh_count时返回EXT4_QUERY_RANGE_ABORT,避免缓冲区溢出
7.6 固定元数据的有序合并¶
ext4_getfsmap_find_fixed_metadata(fsmap.c:470)为每个块组生成一系列固定元数据记录:
每个块组的固定元数据 (按物理位置顺序):
┌──────────────────┐
│ SUPERBLOCK │ ← EXT4_FMR_OWN_FS (仅 group 0 或 sparse_super 组)
│ GDT │ ← EXT4_FMR_OWN_GDT
│ RESV_GDT │ ← EXT4_FMR_OWN_RESV_GDT
│ BLOCK BITMAP │ ← EXT4_FMR_OWN_BLKBM
│ INODE BITMAP │ ← EXT4_FMR_OWN_INOBM
│ INODE TABLE │ ← EXT4_FMR_OWN_INODES
└──────────────────┘
这些记录被插入到一个链表中,然后排序和合并。merge 阶段(ext4_getfsmap_merge_fixed_metadata,fsmap.c:435)将相邻的同类元数据记录合并为更大的 extent。
8. FSMap 完整查询流程¶
用户空间: ioctl(fd, FS_IOC_GETFSMAP, &head)
│
▼
ext4_ioc_getfsmap()
│
▼
ext4_getfsmap(sb, head, formatter, arg) // fsmap.c:703
│
├─ 设备 1: ext4_getfsmap_datadev(sb, keys, info) // fsmap.c:528
│ │
│ ├─ ext4_getfsmap_find_fixed_metadata()
│ │ ┌────────────────────────────────────┐
│ │ │ meta_list (已排序): │
│ │ │ block 0-0: SUPERBLOCK (OWN_FS) │
│ │ │ block 1-5: GDT (OWN_GDT) │
│ │ │ block 6-10: RESV_GDT (OWN_RESV) │
│ │ │ block 11-11: BLOCK BITMAP (BLKBM) │
│ │ │ block 12-12: INODE BITMAP (INOBM) │
│ │ │ block 13-100: INODE TABLE (INODES) │
│ │ └────────────────────────────────────┘
│ │
│ └─ for each group:
│ ext4_mb_query_range()
│ ├─ meta_helper: 逐条输出 meta_list 项
│ └─ datadev_helper:
│ 每次 mballoc 报告一个空闲 extent:
│ ext4_getfsmap_helper(info, rec)
│ {
│ 检测间隙 → OWN_UNKNOWN
│ 映射 owner → OWN_FREE
│ formatter(&fmr, arg)
│ }
│
└─ 设备 2 (可选): ext4_getfsmap_logdev(sb, keys, info)
└── 日志设备上的 OWN_LOG 记录
输出示例(概念性):
PHYSICAL OFFSET LENGTH OWNER
0 1 EXT4_FMR_OWN_FS (超级块)
1 5 EXT4_FMR_OWN_GDT (组描述符)
6 5 EXT4_FMR_OWN_RESV_GDT (保留 GDT)
11 1 EXT4_FMR_OWN_BLKBM (块位图)
12 1 EXT4_FMR_OWN_INOBM (inode 位图)
13 88 EXT4_FMR_OWN_INODES (inode 表)
101 500 EXT4_FMR_OWN_FREE (空闲数据块)
601 200 EXT4_FMR_OWN_UNKNOWN (已分配数据块)
...
9. 扩容安全性设计¶
9.1 Journal 保护¶
所有扩容操作都在 journal 事务保护下完成。如果扩容过程中系统崩溃: 1. Journal replay 会回滚或完成未完成的事务 2. 超级块的更新是原子性的(通过 JBD2 revoke 机制) 3. 不会出现"部分扩容"导致的文件系统损坏
9.2 内存屏障协议¶
写入者(扩容): 读者(分配器):
s_blocks_count = new_value s_groups_count = READ_ONCE(...)
s_inodes_count = new_value smp_rmb()
smp_wmb() blocks_count = s_blocks_count ← 安全
smp_wmb() ...
s_groups_count = new_value
这确保了分配器只有在看到新的 s_groups_count 后才会读取新组的元数据——此时元数据已经初始化完成。
9.3 Per-CPU 计数器¶
扩容通过 percpu_counter_add 更新空闲块计数器(resize.c:1490-1493),这是无锁的、per-CPU 的原子累加操作。相比直接修改全局计数器,这避免了扩容时的全局锁竞争。
10. fscrypt 集成总览¶
ext4 的 fscrypt 是 per-inode 的加密框架,密钥通过 fscrypt_prepare_new_inode(ialloc.c:995)从父目录继承。EXT4_INODE_FSCRYPT_MASK 标记加密 inode。文件名以密文存储于磁盘,ext4_fname_prepare/ext4_fname_diskcmp 处理明文↔密文转换。块数据加密发生在 bio 层而非 ext4 内部。关键集成点:inode 创建(ialloc.c:995)、目录加密上下文验证(namei.c:1792)、加密目录项查找(namei.cext4_fname_prepare)。
11. 扩容完整流程¶
resize2fs ioctl → ext4_resize_fs(sb, n_blocks_count)
│
├─ ext4_resize_begin(sb) // resize.c:46
│ └── CAP_SYS_RESOURCE, 无错误, test_and_set_bit(RESIZING)
├─ 计算 add = DIV_ROUND_UP(n_blocks, BLOCKS_PER_GROUP) - s_groups_count
├─ for each new flex group:
│ ├─ ext4_setup_new_descs(handle, sb, flex_gd) // resize.c:1338
│ ├─ ext4_update_super(sb, flex_gd) // resize.c:1418
│ │ └── s_blocks_count+=..., smp_wmb(), s_groups_count+=flex_size
│ └─ ext4_handle_dirty_metadata(s_sbh)
└─ ext4_resize_end(sb) // resize.c:98
└── clear_bit(RESIZING), update_backups
12. 总结与性能考量¶
扩容性能:使用 flex_group_add 模式,瓶颈在块位图初始化。EXT4_FLAGS_RESIZING 位锁保证串行化,正常 I/O 在扩容过程中不受影响。
FSMap 应用:磁盘取证(块→文件映射)、碎片分析(空闲 extent 分布)、空间审计(保留 GDT/日志等隐藏空间)、调试(验证分配器输出)。
限制:ext4_group_extend 只能扩展最后一组,sparse_super2 不支持在线扩容,64-bit 支持最大 ~1 EiB,FSMap 需要 CAP_SYS_ADMIN。
系列结语¶
六篇文章纵览了 ext4 内核源码的核心子系统:
第一篇:超级块与块组 — 磁盘布局: 从宏观层面理解 ext4 的"建筑蓝图"
第二篇:Extent 树 — 逻辑到物理映射: 文件数据块的精确寻址
第三篇:JBD2 日志 — 崩溃一致性: 断电不丢数据的底层保障
第四篇:块与 inode 分配: 多块分配器与 Orlov 算法的空间管理
第五篇:目录与 HTree 索引: 从 O(n) 到 O(log n) 的查找优化
第六篇:在线扩容与 FSMap: 运行时弹性扩展与块取证
从超级块的结构体字段到 mballoc 的 buddy cache,从 ext4_lookup 的 HTree 二分查找到 ext4_resize_begin 的内存屏障协议——这些代码共同构成了 Linux 最广泛使用的文件系统的完整图景。
ext4 的设计哲学是渐进式演化:在 ext2/ext3 的骨架上,通过特性标志(features bitmap)逐步加入 extent、flex_bg、meta_bg、HTree、journal checksum、metadata_csum 等能力,保持向后兼容的同时拥抱现代存储需求。这 6 篇文章试图揭示的正是这种演化背后的代码实现。
内核源码探索无止境,每个函数调用背后都可能藏着设计者深思熟虑的权衡。希望这系列文章能成为你深入 ext4 源码的导航图。
延伸阅读¶
- Linux Kernel Documentation: filesystems/ext4/
- Ted Ts'o, "The ext4 file system" (USENIX ;login:, 2009)
- Mingming Cao et al., "Ext4 block and inode allocator improvements" (OLS 2008)
Documentation/filesystems/ext4/*.rstin the kernel tree- ext4 邮件列表: linux-ext4@vger.kernel.org