Linux 存储与文件系统深度剖析(五):Ext4 文件系统源码分析

Linux 存储与文件系统深度剖析(五):Ext4 文件系统源码分析

Ext4 是 Linux 世界中使用最为广泛的文件系统之一。从 1992 年诞生的 Ext2 到今天仍在亿级服务器上运行的 Ext4,这个文件系统家族经历了三十余年的演进,积累了大量为生产环境验证的设计智慧。本文基于 Linux 6.4-rc1 内核源码(fs/ext4/fs/jbd2/),从磁盘格式到内核实现,逐层深入剖析 Ext4 的核心机制。


1. 历史演进:从 Ext2 到 Ext4

1.1 Ext2:奠定基础(1993)

Ext2(Second Extended Filesystem)由 Rémy Card 于 1993 年为 Linux 设计,确立了沿用至今的基本磁盘布局:块组(Block Group)划分、inode 表、块位图、inode 位图。Ext2 没有日志,崩溃后需要 e2fsck 做全盘一致性检查,在大容量磁盘上这可能耗时数小时。

1.2 Ext3:引入日志(2001)

Ext3 在 Ext2 的基础上叠加了 JBD(Journaling Block Device)日志层,提供三种日志模式:

模式 说明 性能 一致性
journal 数据+元数据都写日志 最慢 最强
ordered 只日志元数据,数据在元数据提交前落盘 中等 强(默认)
writeback 只日志元数据,数据顺序无保证 最快

Ext3 的磁盘格式与 Ext2 完全兼容,只需在超级块中设置日志特性位即可将 Ext2 升级为 Ext3。

1.3 Ext4:突破限制(2008)

Ext4 于 2008 年随 Linux 2.6.28 正式合并主线,主要突破:

  • Extent 树取代间接块映射,大文件性能大幅提升,最大单文件 16 TiB
  • 支持 48 位块地址,最大卷 1 EiB
  • 延迟分配(Delalloc) 显著减少碎片
  • 持久化预分配(Persistent Preallocation)
  • 在线碎片整理
  • HTree 目录索引(实际源自 Ext3,但 Ext4 普遍启用)
  • 日志校验和,JBD2 替代 JBD
  • 纳秒时间戳,扩展至 2446 年
  • 元数据校验和(crc32c)

2. 磁盘布局

2.1 整体结构

Ext4 将分区划分为若干块组(Block Group),每组大小默认 128 MiB(4K 块时 32768 块)。磁盘头部保留 1024 字节的 boot sector,其后是超级块,然后是块组描述符表,最后是各块组本身。

1
2
3
4
5
6
+----------+----------+------------+----------+----------+- - -
| Boot | Super | Group Desc | Group 0 | Group 1 |
| Sector | Block | Table | Data | Data |
| (1024B) | (1024B+) | (N blocks) | ... | ... |
+----------+----------+------------+----------+----------+- - -
0 1024 2048 ...

每个块组内部布局(以 4K 块为例):

1
2
3
4
5
+----------+----------+----------+----------+---------+
| Block | Inode | Inode | Data | Data |
| Bitmap | Bitmap | Table | Blocks | Blocks |
| (1 blk) | (1 blk) | (N blks) | ... | ... |
+----------+----------+----------+----------+---------+

2.2 超级块:struct ext4_super_block

超级块存于偏移 1024 字节处,是整个文件系统的”身份证”。以下是内核中的完整磁盘结构定义(fs/ext4/ext4.h,第 1230 行):

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
struct ext4_super_block {
/*00*/ __le32 s_inodes_count; /* Inodes count */
__le32 s_blocks_count_lo; /* Blocks count */
__le32 s_r_blocks_count_lo; /* Reserved blocks count */
__le32 s_free_blocks_count_lo; /* Free blocks count */
/*10*/ __le32 s_free_inodes_count; /* Free inodes count */
__le32 s_first_data_block; /* First Data Block */
__le32 s_log_block_size; /* Block size: 1024 << s_log_block_size */
__le32 s_log_cluster_size; /* Allocation cluster size */
/*20*/ __le32 s_blocks_per_group; /* # Blocks per group */
__le32 s_clusters_per_group; /* # Clusters per group */
__le32 s_inodes_per_group; /* # Inodes per group */
__le32 s_mtime; /* Mount time */
/*30*/ __le32 s_wtime; /* Write time */
__le16 s_mnt_count; /* Mount count */
__le16 s_max_mnt_count; /* Maximal mount count */
__le16 s_magic; /* Magic signature: 0xEF53 */
__le16 s_state; /* File system state */
__le16 s_errors; /* Behaviour when detecting errors */
__le16 s_minor_rev_level; /* minor revision level */
/*40*/ __le32 s_lastcheck; /* time of last check */
__le32 s_checkinterval; /* max. time between checks */
__le32 s_creator_os; /* OS */
__le32 s_rev_level; /* Revision level */
/*50*/ __le16 s_def_resuid; /* Default uid for reserved blocks */
__le16 s_def_resgid; /* Default gid for reserved blocks */
/* EXT4_DYNAMIC_REV superblocks only: */
__le32 s_first_ino; /* First non-reserved inode */
__le16 s_inode_size; /* size of inode structure */
__le16 s_block_group_nr; /* block group # of this superblock */
__le32 s_feature_compat; /* compatible feature set */
/*60*/ __le32 s_feature_incompat; /* incompatible feature set */
__le32 s_feature_ro_compat; /* readonly-compatible feature set */
/*68*/ __u8 s_uuid[16]; /* 128-bit uuid for volume */
/*78*/ char s_volume_name[EXT4_LABEL_MAX]; /* volume name */
/*88*/ char s_last_mounted[64]; /* directory where last mounted */
/*D0*/ __u8 s_journal_uuid[16]; /* uuid of journal superblock */
/*E0*/ __le32 s_journal_inum; /* inode number of journal file */
__le32 s_journal_dev; /* device number of journal file */
__le32 s_last_orphan; /* start of list of inodes to delete */
__le32 s_hash_seed[4]; /* HTREE hash seed */
__u8 s_def_hash_version; /* Default hash version to use */
__le16 s_desc_size; /* size of group descriptor */
/*100*/ __le32 s_default_mount_opts;
__le32 s_first_meta_bg; /* First metablock block group */
__le32 s_mkfs_time; /* When the filesystem was created */
__le32 s_jnl_blocks[17]; /* Backup of the journal inode */
/* 64bit support valid if EXT4_FEATURE_INCOMPAT_64BIT */
/*150*/ __le32 s_blocks_count_hi; /* Blocks count (high 32 bits) */
__le32 s_r_blocks_count_hi; /* Reserved blocks count */
__le32 s_free_blocks_count_hi; /* Free blocks count */
__le16 s_min_extra_isize; /* All inodes have at least # bytes */
__le16 s_want_extra_isize; /* New inodes should reserve # bytes */
__le32 s_flags; /* Miscellaneous flags */
__le16 s_raid_stride; /* RAID stride */
__le16 s_mmp_update_interval; /* # seconds to wait in MMP checking */
__le64 s_mmp_block; /* Block for multi-mount protection */
__le32 s_raid_stripe_width; /* blocks on all data disks (N*stride)*/
__u8 s_log_groups_per_flex; /* FLEX_BG group size */
__u8 s_checksum_type; /* metadata checksum algorithm used */
__le64 s_kbytes_written; /* nr of lifetime kilobytes written */
__le32 s_error_count; /* number of fs errors */
__le32 s_first_error_time; /* first time an error happened */
__le32 s_first_error_ino; /* inode involved in first error */
__le64 s_first_error_block; /* block involved of first error */
__u8 s_first_error_func[32]; /* function where the error happened */
__le32 s_first_error_line; /* line number where error happened */
/* ... */
__le32 s_checksum_seed; /* crc32c(uuid) if csum_seed set */
};

几个关键字段解析:

  • **s_magic**:魔数 0xEF53,内核挂载时首先校验此值。
  • **s_log_block_size**:实际块大小 = 1024 << s_log_block_size,合法值 0/1/2/3 对应 1K/2K/4K/8K。
  • s_feature_compat/incompat/ro_compat:三级特性位。incompat 中存在内核不认识的位时,必须拒绝挂载ro_compat 中有未知位时只能只读挂载;compat 中有未知位可以正常挂载(向后兼容)。
  • **s_journal_inum**:日志文件对应的 inode 号,通常为 inode 8。
  • **s_last_orphan**:崩溃前未完成删除的 inode 链表头,挂载时由 ext4_orphan_cleanup() 处理。
  • **s_error_***:内核会将首次和最近一次错误的函数名、行号、涉及的块/inode 持久化到超级块,tune2fs -l 可以查看。

2.3 块组描述符:struct ext4_group_desc

每个块组的元数据地址由块组描述符表记录,定义于 fs/ext4/ext4.h,第 338 行:

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
struct ext4_group_desc
{
__le32 bg_block_bitmap_lo; /* Blocks bitmap block */
__le32 bg_inode_bitmap_lo; /* Inodes bitmap block */
__le32 bg_inode_table_lo; /* Inodes table block */
__le16 bg_free_blocks_count_lo; /* Free blocks count */
__le16 bg_free_inodes_count_lo; /* Free inodes count */
__le16 bg_used_dirs_count_lo; /* Directories count */
__le16 bg_flags; /* EXT4_BG_flags (INODE_UNINIT, etc) */
__le32 bg_exclude_bitmap_lo; /* Exclude bitmap for snapshots */
__le16 bg_block_bitmap_csum_lo; /* crc32c(s_uuid+grp_num+bbitmap) LE */
__le16 bg_inode_bitmap_csum_lo; /* crc32c(s_uuid+grp_num+ibitmap) LE */
__le16 bg_itable_unused_lo; /* Unused inodes count */
__le16 bg_checksum; /* crc16(sb_uuid+group+desc) */
/* 以下字段仅在启用 64BIT 特性时存在 */
__le32 bg_block_bitmap_hi; /* Blocks bitmap block MSB */
__le32 bg_inode_bitmap_hi; /* Inodes bitmap block MSB */
__le32 bg_inode_table_hi; /* Inodes table block MSB */
__le16 bg_free_blocks_count_hi; /* Free blocks count MSB */
__le16 bg_free_inodes_count_hi; /* Free inodes count MSB */
__le16 bg_used_dirs_count_hi; /* Directories count MSB */
__le16 bg_itable_unused_hi; /* Unused inodes count MSB */
__le32 bg_exclude_bitmap_hi; /* Exclude bitmap block MSB */
__le16 bg_block_bitmap_csum_hi; /* crc32c(s_uuid+grp_num+bbitmap) BE */
__le16 bg_inode_bitmap_csum_hi; /* crc32c(s_uuid+grp_num+ibitmap) BE */
__u32 bg_reserved;
};

bg_flags 字段中有几个重要标志位:

1
2
3
#define EXT4_BG_INODE_UNINIT  0x0001  /* Inode 表/位图未初始化(新建组优化)*/
#define EXT4_BG_BLOCK_UNINIT 0x0002 /* 块位图未初始化 */
#define EXT4_BG_INODE_ZEROED 0x0004 /* 磁盘上的 inode 表已初始化为零 */

INODE_UNINITBLOCK_UNINIT 是 Ext4 的延迟初始化(lazy init)特性:mke2fs 只初始化第一个块组,其余块组在内核后台线程中异步初始化,大容量磁盘格式化因此能在秒级完成。

2.4 inode 结构:struct ext4_inode

inode 是文件系统的核心抽象,存储文件的元数据和数据块地址。Ext4 的磁盘 inode 定义于 fs/ext4/ext4.h,第 714 行:

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
struct ext4_inode {
__le16 i_mode; /* File mode: 类型+权限位 */
__le16 i_uid; /* Low 16 bits of Owner Uid */
__le32 i_size_lo; /* Size in bytes (低32位) */
__le32 i_atime; /* Access time */
__le32 i_ctime; /* Inode Change time */
__le32 i_mtime; /* Modification time */
__le32 i_dtime; /* Deletion Time */
__le16 i_gid; /* Low 16 bits of Group Id */
__le16 i_links_count; /* Links count */
__le32 i_blocks_lo; /* Blocks count (以512字节扇区为单位) */
__le32 i_flags; /* File flags: EXTENTS_FL, INLINE_DATA_FL 等 */
union {
struct { __le32 l_i_version; } linux1;
/* ... */
} osd1;
__le32 i_block[EXT4_N_BLOCKS]; /* 数据块指针区域,共60字节 */
__le32 i_generation; /* File version (for NFS) */
__le32 i_file_acl_lo; /* File ACL */
__le32 i_size_high; /* Size in bytes (高32位) */
__le32 i_obso_faddr; /* Obsoleted fragment address */
union {
struct {
__le16 l_i_blocks_high;
__le16 l_i_file_acl_high;
__le16 l_i_uid_high;
__le16 l_i_gid_high;
__le16 l_i_checksum_lo; /* crc32c(uuid+inum+inode) LE */
__le16 l_i_reserved;
} linux2;
/* ... */
} osd2;
__le16 i_extra_isize; /* 扩展区域大小,用于存额外字段 */
__le16 i_checksum_hi; /* crc32c(uuid+inum+inode) BE */
__le32 i_ctime_extra; /* 纳秒+epoch扩展 ctime */
__le32 i_mtime_extra; /* 纳秒+epoch扩展 mtime */
__le32 i_atime_extra; /* 纳秒+epoch扩展 atime */
__le32 i_crtime; /* File Creation time */
__le32 i_crtime_extra; /* 纳秒+epoch扩展 crtime */
__le32 i_version_hi; /* 64位版本号高32位 */
__le32 i_projid; /* Project ID */
};

关键设计点:

  1. i_block[EXT4_N_BLOCKS]:共 15 个 __le32,占 60 字节。对于传统间接块映射(Ext2/3 遗留),前 12 个是直接块指针,第 13/14/15 个分别是一级/二级/三级间接块指针。对于启用 Extent 树的 Ext4 文件(EXT4_EXTENTS_FL),这 60 字节改为存储 extent 头和 extent 叶子节点,完全不同的语义。

  2. 纳秒时间戳i_ctime 等字段存 Unix 秒数(32位),i_ctime_extransec << 2 | epoch 编码:低2位为 epoch 扩展位,可将时间范围延伸至 2446 年;高30位为纳秒。

  3. **i_extra_isize**:Ext4 支持大 inode(256字节起),i_extra_isize 记录 EXT4_GOOD_OLD_INODE_SIZE(128字节)之后额外使用的字节数,存放扩展字段(crtime、版本号、校验和等)。


3. Extent 树机制

Ext4 最重要的性能改进之一是用 B+ 树形式的 Extent 树替代了 Ext2/3 的多级间接块指针。

3.1 核心数据结构

定义于 fs/ext4/ext4_extents.h

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
/*
* This is the extent on-disk structure.
* It's used at the bottom of the tree (leaf nodes).
*/
struct ext4_extent {
__le32 ee_block; /* first logical block extent covers */
__le16 ee_len; /* number of blocks covered by extent */
__le16 ee_start_hi; /* high 16 bits of physical block */
__le32 ee_start_lo; /* low 32 bits of physical block */
};

/*
* This is index on-disk structure.
* It's used at all the levels except the bottom (internal nodes).
*/
struct ext4_extent_idx {
__le32 ei_block; /* index covers logical blocks from 'block' */
__le32 ei_leaf_lo; /* pointer to the physical block of the next
* level. leaf or next index could be there */
__le16 ei_leaf_hi; /* high 16 bits of physical block */
__u16 ei_unused;
};

/*
* Each block (leaves and indexes), even inode-stored has header.
*/
struct ext4_extent_header {
__le16 eh_magic; /* probably will support different formats */
__le16 eh_entries; /* number of valid entries */
__le16 eh_max; /* capacity of store in entries */
__le16 eh_depth; /* has tree real underlying blocks?
* depth == 0 means the tree is a leaf */
__le32 eh_generation; /* generation of the tree */
};

#define EXT4_EXT_MAGIC cpu_to_le16(0xf30a)
#define EXT4_MAX_EXTENT_DEPTH 5

ext4_extent(叶子节点) 是一个”运行”(run)描述符:从逻辑块 ee_block 开始的 ee_len 个连续块,映射到物理块 (ee_start_hi << 32) | ee_start_lo。注意 ee_len 的最高位(MSB)有特殊含义:

  • ee_len <= 0x8000:已初始化的 extent
  • ee_len > 0x8000:未写(unwritten/preallocated)extent,实际长度为 ee_len & 0x7FFF

最大初始化 extent 覆盖 32768 块(EXT_INIT_MAX_LEN = 0x8000),即 4K 块时 128 MiB。

ext4_extent_idx(内部节点) 存储索引,ei_block 是该子树覆盖的起始逻辑块,ei_leaf_lo/hi 指向子节点所在物理块。

树的根始终存在 inode 的 i_block[0..14](60字节)中:前 12 字节是 ext4_extent_header,后 48 字节最多放 3 个 ext4_extent(叶子)或 ext4_extent_idx(内部节点)。

路径查找使用辅助结构 ext4_ext_path

1
2
3
4
5
6
7
8
9
struct ext4_ext_path {
ext4_fsblk_t p_block; /* physical block of this level */
__u16 p_depth; /* depth of this level */
__u16 p_maxdepth;
struct ext4_extent *p_ext; /* pointer to extent (leaf) */
struct ext4_extent_idx *p_idx; /* pointer to index (internal) */
struct ext4_extent_header *p_hdr; /* header of this block */
struct buffer_head *p_bh; /* buffer head for this block */
};

3.2 ext4_find_extent:树遍历实现

核心查找函数 ext4_find_extent()fs/ext4/extents.c,第 883 行)从根向叶子走一遍 B+ 树,每层调用二分搜索:

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
71
72
73
74
75
struct ext4_ext_path *
ext4_find_extent(struct inode *inode, ext4_lblk_t block,
struct ext4_ext_path **orig_path, int flags)
{
struct ext4_extent_header *eh;
struct buffer_head *bh;
struct ext4_ext_path *path = orig_path ? *orig_path : NULL;
short int depth, i, ppos = 0;
int ret;
gfp_t gfp_flags = GFP_NOFS;

if (flags & EXT4_EX_NOFAIL)
gfp_flags |= __GFP_NOFAIL;

eh = ext_inode_hdr(inode); /* 从 inode.i_block 取树根 header */
depth = ext_depth(inode); /* 当前树深度 */
if (depth < 0 || depth > EXT4_MAX_EXTENT_DEPTH) {
EXT4_ERROR_INODE(inode, "inode has invalid extent depth: %d", depth);
ret = -EFSCORRUPTED;
goto err;
}

if (!path) {
/* account possible depth increase */
path = kcalloc(depth + 2, sizeof(struct ext4_ext_path), gfp_flags);
if (unlikely(!path))
return ERR_PTR(-ENOMEM);
path[0].p_maxdepth = depth + 1;
}
path[0].p_hdr = eh;
path[0].p_bh = NULL;

i = depth;
/* 对深度为0的树(全在inode内)可以直接缓存 extent */
if (!(flags & EXT4_EX_NOCACHE) && depth == 0)
ext4_cache_extents(inode, eh);

/* 从根向叶子遍历内部节点 */
while (i) {
/* 在当前层的 idx 数组中二分查找 */
ext4_ext_binsearch_idx(inode, path + ppos, block);
path[ppos].p_block = ext4_idx_pblock(path[ppos].p_idx);
path[ppos].p_depth = i;
path[ppos].p_ext = NULL;

/* 读取子节点对应的磁盘块 */
bh = read_extent_tree_block(inode, path[ppos].p_idx, --i, flags);
if (IS_ERR(bh)) {
ret = PTR_ERR(bh);
goto err;
}

eh = ext_block_hdr(bh);
ppos++;
path[ppos].p_bh = bh;
path[ppos].p_hdr = eh;
}

path[ppos].p_depth = i;
path[ppos].p_ext = NULL;
path[ppos].p_idx = NULL;

/* 在叶子节点做二分搜索找到对应 extent */
ext4_ext_binsearch(inode, path + ppos, block);
if (path[ppos].p_ext)
path[ppos].p_block = ext4_ext_pblock(path[ppos].p_ext);

ext4_ext_show_path(inode, path);
return path;

err:
ext4_free_ext_path(path);
if (orig_path) *orig_path = NULL;
return ERR_PTR(ret);
}

3.3 叶子层二分搜索:ext4_ext_binsearch

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
static void
ext4_ext_binsearch(struct inode *inode,
struct ext4_ext_path *path, ext4_lblk_t block)
{
struct ext4_extent_header *eh = path->p_hdr;
struct ext4_extent *r, *l, *m;

if (eh->eh_entries == 0) {
/* 空叶子:在 split/add 路径中会遇到 */
return;
}

l = EXT_FIRST_EXTENT(eh) + 1; /* 从第二个 extent 开始搜 */
r = EXT_LAST_EXTENT(eh);

while (l <= r) {
m = l + (r - l) / 2;
if (block < le32_to_cpu(m->ee_block))
r = m - 1;
else
l = m + 1;
}

/* path->p_ext 指向不大于 block 的最后一个 extent */
path->p_ext = l - 1;
}

函数返回后,path->p_ext 指向起始逻辑块不超过目标 block 的那个 extent。调用者还需检查 block < ee_block + ee_len 来确认 block 确实落在该 extent 范围内,否则说明是个”洞”(hole)。


4. 块组管理

4.1 块分配器:mballoc

Ext4 使用多块分配器mballocfs/ext4/mballoc.c)一次申请多个连续块,减少碎片。分配请求用 struct ext4_allocation_request 描述:

1
2
3
4
5
6
7
8
9
10
11
struct ext4_allocation_request {
struct inode *inode; /* 目标 inode */
unsigned int len; /* 希望分配的块数 */
ext4_lblk_t logical; /* 目标逻辑块号 */
ext4_lblk_t lleft; /* 最近已分配的左侧逻辑块 */
ext4_lblk_t lright; /* 最近已分配的右侧逻辑块 */
ext4_fsblk_t goal; /* 物理块 hint(优先尝试目标) */
ext4_fsblk_t pleft; /* 左侧逻辑块对应的物理块 */
ext4_fsblk_t pright; /* 右侧逻辑块对应的物理块 */
unsigned int flags; /* EXT4_MB_HINT_* 标志 */
};

flags 字段的核心标志:

1
2
3
4
5
6
#define EXT4_MB_HINT_MERGE       0x0001  /* 优先尝试合并到相邻 extent */
#define EXT4_MB_HINT_METADATA 0x0004 /* 分配元数据块 */
#define EXT4_MB_HINT_DATA 0x0020 /* 分配数据块 */
#define EXT4_MB_HINT_NOPREALLOC 0x0040 /* 不预分配 */
#define EXT4_MB_DELALLOC_RESERVED 0x0400 /* 延迟分配预留块 */
#define EXT4_MB_STREAM_ALLOC 0x0800 /* 流式分配(顺序写优化)*/

mballoc 的核心策略是”伙伴系统”(buddy system):每个块组维护一个伙伴位图,记录不同大小的空闲连续块位置。分配时按 cr(criteria,标准)从 0 到 3 逐步放宽条件:

  • cr=0:只分配大碎片(≥ 请求大小),延迟分配优先路径
  • cr=1:平均碎片大小匹配,红黑树查找
  • cr=2:顺序扫描块组
  • cr=3:任意可用块

4.2 灵活块组(Flex_BG)

flex_bg 特性启用时,相邻的若干块组(默认 16 个)合并为一个”超级块组”(flex group)。超级块组内的块位图和 inode 位图集中存放在第一个块组,数据块则紧随其后连续分布,大幅减少了寻道次数。


5. JBD2 日志机制与崩溃一致性

5.1 JBD2 架构

Ext4 使用 JBD2(Journaling Block Device 2)提供日志能力,JBD2 是独立内核子系统,也可被其他文件系统(如 OCFS2)使用。核心实体:

journal_tinclude/linux/jbd2.h,第 770 行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct journal_s
{
unsigned long j_flags; /* General journaling state flags */
int j_errno; /* Outstanding error on journal */
struct mutex j_abort_mutex; /* Lock for aborting procedure */
struct buffer_head *j_sb_buffer; /* Superblock buffer */
journal_superblock_t *j_superblock; /* Superblock structure */
rwlock_t j_state_lock; /* Protect scalar fields */
int j_barrier_count; /* Processes waiting for barrier lock */
struct mutex j_barrier; /* The barrier lock itself */
transaction_t *j_running_transaction; /* Current running transaction */
transaction_t *j_committing_transaction; /* Being committed */
transaction_t *j_checkpoint_transactions;/* Checkpointed transactions */
/* ... */
unsigned long j_commit_interval; /* commit interval in jiffies */
struct task_struct *j_task; /* kjournald2 thread */
int j_max_transaction_buffers; /* max buffers per transaction */
/* ... */
};

transaction_tinclude/linux/jbd2.h,第 560 行)

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
struct transaction_s
{
journal_t *t_journal; /* 所属日志 */
tid_t t_tid; /* 事务序号 */
enum {
T_RUNNING, /* 正在接收修改 */
T_LOCKED, /* 准备提交,不再接收 */
T_SWITCH, /* 切换到新事务 */
T_FLUSH, /* 刷新数据 */
T_COMMIT, /* 提交阶段 */
T_COMMIT_DFLUSH, /* 提交数据刷盘 */
T_COMMIT_JFLUSH, /* 提交日志刷盘 */
T_COMMIT_CALLBACK, /* 执行提交后回调 */
T_FINISHED /* 已完成 */
} t_state;
unsigned long t_log_start; /* 事务在日志中的起始位置 */
int t_nr_buffers; /* t_buffers 链表中的 buffer 数 */
struct journal_head *t_reserved_list; /* 已预留但未修改的 buffer */
struct journal_head *t_buffers; /* 已修改的元数据 buffer */
struct journal_head *t_forget; /* 可在 checkpoint 后释放的 buffer */
struct journal_head *t_checkpoint_list; /* 等待 checkpoint 的 buffer */
struct journal_head *t_shadow_list; /* IO 过程中的 shadow buffer */
struct list_head t_inode_list; /* 关联 inode 列表 */
spinlock_t t_handle_lock; /* 保护 handle 相关信息 */
/* ... */
};

5.2 事务生命周期

JBD2 事务遵循严格的状态机,一次完整的写操作流程如下:

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
应用程序写入


ext4_journal_start() ──→ 获取 handle_t,绑定到 running transaction


jbd2_journal_get_write_access(handle, bh)
│ 将 buffer 的原始内容(或修改后)拷贝到日志 shadow

修改 buffer_head 内容


jbd2_journal_dirty_metadata(handle, bh)
│ 标记 buffer 为"脏元数据",加入 t_buffers 链表

jbd2_journal_stop(handle)
│ 减少 handle 引用计数,若为0则可触发事务提交

kjournald2 线程

├── 写 Journal Descriptor Block(描述本次事务涉及的所有块)
├── 写 Journal Data(所有脏的元数据块内容)
├── 写 Journal Commit Block(表示事务完整写入日志)


checkpoint(异步)

└── 将日志中的块真正写回文件系统原始位置,释放日志空间

jbd2_journal_start() 函数定义于 fs/jbd2/transaction.c

1
2
3
4
handle_t *jbd2_journal_start(journal_t *journal, int nblocks)
{
return jbd2__journal_start(journal, nblocks, 0, 0, GFP_NOFS, 0, 0);
}

参数 nblocks 是本次操作预期修改的日志块数量上限,JBD2 会为此在日志中预留空间。

5.3 三种数据模式的实现

data=ordered(默认)模式下,fs/ext4/ext4_jbd2.h 中的宏 EXT4_ORDERED_DATA_MODE 控制以下逻辑:提交事务的元数据之前,必须确保文件的数据页先于元数据落盘——这通过将 inode 加入 t_inode_list 并在提交时强制 writeback 实现,防止日志重放后看到指向未初始化块的元数据。


6. 读写实现:从 VFS 到磁盘

6.1 读路径:ext4_file_read_iter

定义于 fs/ext4/file.c,第 130 行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static ssize_t ext4_file_read_iter(struct kiocb *iocb, struct iov_iter *to)
{
struct inode *inode = file_inode(iocb->ki_filp);

/* 若文件系统已强制关闭(forced shutdown),直接返回 EIO */
if (unlikely(ext4_forced_shutdown(EXT4_SB(inode->i_sb))))
return -EIO;

/* 零长度读无需更新 atime */
if (!iov_iter_count(to))
return 0;

#ifdef CONFIG_FS_DAX
/* 若是 DAX(直接访问 PMEM)模式,走 DAX 路径 */
if (IS_DAX(inode))
return ext4_dax_read_iter(iocb, to);
#endif
/* Direct I/O:绕过 page cache */
if (iocb->ki_flags & IOCB_DIRECT)
return ext4_dio_read_iter(iocb, to);

/* 默认:buffered I/O,经由 page cache */
return generic_file_read_iter(iocb, to);
}

generic_file_read_iter 在 VFS 层实现,最终通过 mapping->a_ops->read_folio() 触发 Ext4 的 ext4_read_folio(),后者通过 ext4_map_blocks() 将逻辑块号翻译为物理块号,再提交 bio 到块层。

6.2 写路径:ext4_file_write_iter

定义于 fs/ext4/file.c,第 696 行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static ssize_t
ext4_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
struct inode *inode = file_inode(iocb->ki_filp);

if (unlikely(ext4_forced_shutdown(EXT4_SB(inode->i_sb))))
return -EIO;

#ifdef CONFIG_FS_DAX
if (IS_DAX(inode))
return ext4_dax_write_iter(iocb, from);
#endif
/* Direct I/O 写 */
if (iocb->ki_flags & IOCB_DIRECT)
return ext4_dio_write_iter(iocb, from);
else
/* Buffered 写(包含延迟分配路径) */
return ext4_buffered_write_iter(iocb, from);
}

ext4_buffered_write_iter 调用 generic_perform_write,后者对每个页面调用 address_space_operations

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
static ssize_t ext4_buffered_write_iter(struct kiocb *iocb,
struct iov_iter *from)
{
ssize_t ret;
struct inode *inode = file_inode(iocb->ki_filp);

if (iocb->ki_flags & IOCB_NOWAIT)
return -EOPNOTSUPP;

inode_lock(inode); /* 持有 inode 写锁 */
ret = ext4_write_checks(iocb, from); /* 检查大小、quota 等 */
if (ret <= 0)
goto out;

current->backing_dev_info = inode_to_bdi(inode);
ret = generic_perform_write(iocb, from); /* 核心写入循环 */
current->backing_dev_info = NULL;

out:
inode_unlock(inode);
if (likely(ret > 0)) {
iocb->ki_pos += ret;
ret = generic_write_sync(iocb, ret); /* 若 O_SYNC 则等待落盘 */
}
return ret;
}

generic_perform_write 的每次迭代都调用:

  1. ext4_da_write_begin():准备页面,标记延迟分配
  2. 将用户数据拷贝进页面
  3. ext4_da_write_end():完成写入,更新 i_disksize

6.3 逻辑块到物理块映射:ext4_map_blocks

ext4_map_blocks()fs/ext4/inode.c,第 478 行)是读写路径的核心枢纽:

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
int ext4_map_blocks(handle_t *handle, struct inode *inode,
struct ext4_map_blocks *map, int flags)
{
struct extent_status es;
int retval;

map->m_flags = 0;

/* 首先查询内存中的 extent status 缓存(避免磁盘读) */
if (!(EXT4_SB(inode->i_sb)->s_mount_state & EXT4_FC_REPLAY) &&
ext4_es_lookup_extent(inode, map->m_lblk, NULL, &es)) {
if (ext4_es_is_written(&es) || ext4_es_is_unwritten(&es)) {
/* 已写或未写的真实 extent,直接返回物理地址 */
map->m_pblk = ext4_es_pblock(&es) +
map->m_lblk - es.es_lblk;
map->m_flags |= ext4_es_is_written(&es) ?
EXT4_MAP_MAPPED : EXT4_MAP_UNWRITTEN;
retval = es.es_len - (map->m_lblk - es.es_lblk);
if (retval > map->m_len)
retval = map->m_len;
map->m_len = retval;
} else if (ext4_es_is_delayed(&es) || ext4_es_is_hole(&es)) {
/* 延迟分配或空洞:返回0,上层按需分配 */
map->m_pblk = 0;
retval = 0;
}
/* ... */
goto found;
}

/* cache miss:持 i_data_sem 读锁查询 extent 树 */
/* 若 flags 含 EXT4_GET_BLOCKS_CREATE,则可能触发实际块分配 */
/* ... */
}

ext4_map_blocks 三步查询策略:

  1. Extent Status Tree(内存缓存):高速 RB 树,存储已查询过的 extent 状态(written/unwritten/delayed/hole)。
  2. Extent 树(磁盘 B+ 树):调用 ext4_ext_map_blocks()ext4_find_extent() 查找。
  3. 块分配:若需要创建新块,调用 mballoc 分配物理块,再通过 ext4_ext_insert_extent() 插入 extent 树。

7. 延迟分配(Delayed Allocation)

延迟分配是 Ext4 性能优化的核心特性,通过挂载选项 delalloc(默认开启)启用。

7.1 原理

传统文件系统在 write() 时立即分配物理块。Ext4 延迟分配将块分配推迟到 writeback(脏页回写)时,好处在于:

  1. 写入更多数据后,分配器对局部性有更好的判断,可以分配连续的大块
  2. 若文件被删除前从未发生回写(如临时文件),完全避免了磁盘分配

7.2 实现:ext4_da_write_beginext4_da_get_block_prep

延迟分配写入由 ext4_da_write_begin() 启动(fs/ext4/inode.c,第 2875 行):

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
static int ext4_da_write_begin(struct file *file, struct address_space *mapping,
loff_t pos, unsigned len,
struct page **pagep, void **fsdata)
{
int ret, retries = 0;
struct folio *folio;
pgoff_t index;
struct inode *inode = mapping->host;

if (unlikely(ext4_forced_shutdown(EXT4_SB(inode->i_sb))))
return -EIO;

index = pos >> PAGE_SHIFT;

/* 若文件系统已满或正在进行 fsverity,退回到非延迟分配模式 */
if (ext4_nonda_switch(inode->i_sb) || ext4_verity_in_progress(inode)) {
*fsdata = (void *)FALL_BACK_TO_NONDELALLOC;
return ext4_write_begin(file, mapping, pos, len, pagep, fsdata);
}
*fsdata = (void *)0;

/* 处理 inline data(小文件数据存 inode 内)*/
if (ext4_test_inode_state(inode, EXT4_STATE_MAY_INLINE_DATA)) {
ret = ext4_da_write_inline_data_begin(mapping, inode, pos, len,
pagep, fsdata);
if (ret < 0) return ret;
if (ret == 1) return 0;
}

retry:
folio = __filemap_get_folio(mapping, index, FGP_WRITEBEGIN,
mapping_gfp_mask(mapping));
if (IS_ERR(folio))
return PTR_ERR(folio);

folio_wait_stable(folio);

/* ext4_da_get_block_prep:标记块为 delayed,不实际分配 */
ret = __block_write_begin(&folio->page, pos, len, ext4_da_get_block_prep);
if (ret < 0) {
folio_unlock(folio);
folio_put(folio);
if (pos + len > inode->i_size)
ext4_truncate_failed_write(inode);
if (ret == -ENOSPC &&
ext4_should_retry_alloc(inode->i_sb, &retries))
goto retry;
return ret;
}
*pagep = &folio->page;
return ret;
}

ext4_da_get_block_prep() 是关键:它调用 ext4_da_map_blocks(),如果块还未分配,只在 extent status tree 中标记一个 delayed 状态,不分配任何物理块,并将 buffer 标记为 BH_Delay

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
int ext4_da_get_block_prep(struct inode *inode, sector_t iblock,
struct buffer_head *bh, int create)
{
struct ext4_map_blocks map;
int ret = 0;

BUG_ON(create == 0);
BUG_ON(bh->b_size != inode->i_sb->s_blocksize);

map.m_lblk = iblock;
map.m_len = 1;

/* 查询现有映射:若已分配则直接返回;若是空洞则创建 delayed 记录 */
ret = ext4_da_map_blocks(inode, iblock, &map, bh);
if (ret <= 0)
return ret;

map_bh(bh, inode->i_sb, map.m_pblk);
ext4_update_bh_state(bh, map.m_flags);

if (buffer_unwritten(bh)) {
set_buffer_new(bh);
set_buffer_mapped(bh);
}
return 0;
}

真正的块分配发生在 writeback 路径的 ext4_writepages()mpage_prepare_extent_to_map()ext4_map_blocks() 中,此时一次为多个连续的 delayed 块分配物理空间。


8. 目录索引:HTree(dx_root)

8.1 线性目录 vs HTree

Ext2/3 中目录是简单的线性链表,每次查找要从头遍历所有目录项,在大目录(数千文件)中性能极差。Ext4 的 HTree(Hash Tree)将目录实现为基于文件名哈希的 B+ 树,查找复杂度降为 O(log n)。

8.2 核心数据结构(fs/ext4/namei.c

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
/* HTree 的一个索引项:哈希值 → 数据块号 */
struct dx_entry
{
__le32 hash; /* 文件名哈希值 */
__le32 block; /* 指向的目录数据块号 */
};

/* 目录树根节点(位于目录的第一个数据块) */
struct dx_root
{
struct fake_dirent dot; /* "." 目录项(占位) */
char dot_name[4];
struct fake_dirent dotdot; /* ".." 目录项(占位) */
char dotdot_name[4];
struct dx_root_info
{
__le32 reserved_zero;
u8 hash_version; /* 哈希函数版本:half-MD4/tea/siphash 等 */
u8 info_length; /* = 8,本结构体大小 */
u8 indirect_levels; /* 树深度(0 = 单层索引)*/
u8 unused_flags;
} info;
struct dx_entry entries[]; /* 索引项数组(紧随其后)*/
};

/* 内部节点(非根的索引块) */
struct dx_node
{
struct fake_dirent fake;
struct dx_entry entries[];
};

/* 每个 htree 块末尾的尾部(存 checksum) */
struct dx_tail {
u32 dt_reserved;
__le32 dt_checksum; /* crc32c(uuid+inum+dirblock) */
};

查找时流程:计算文件名哈希 → 在 dx_root.entries[] 中二分查找对应数据块 → 读取该数据块,线性扫描目录项匹配文件名。

哈希版本由超级块 s_def_hash_version 指定,默认为 half-MD4(Half-MD4,快速且分布均匀)。


9. 性能调优挂载选项

Ext4 提供丰富的挂载选项,以下是关键参数(来自 fs/ext4/super.c):

9.1 数据写入模式

选项 含义
data=journal 数据和元数据都写日志,最安全,最慢
data=ordered 默认;元数据写日志,数据在元数据前落盘
data=writeback 元数据写日志,数据写序不保证

9.2 日志相关

选项 含义
journal_async_commit 异步提交日志,减少写延迟
journal_checksum 启用日志块校验和(默认开)
commit=N 日志提交间隔(秒),默认 5 秒
noload 挂载时不加载日志(只读模式用)

9.3 分配与预分配

选项 含义
delalloc 延迟块分配(默认开)
nodelalloc 禁用延迟分配(数据库场景)
noauto_da_alloc 关闭 close 时的强制 delalloc 落盘
max_batch_time=N mballoc 批量分配最大等待时间(微秒)
min_batch_time=N mballoc 批量分配最小等待时间(微秒)

9.4 IO 特性

选项 含义
barrier / nobarrier 写屏障控制(默认开);SSD 可以安全关闭
discard / nodiscard trim/discard 支持(SSD 推荐开)
dioread_nolock Direct I/O 读时不持 inode 锁,提升并发读性能

9.5 错误处理

选项 含义
errors=continue 遇错继续(不推荐)
errors=remount-ro 遇错重挂载为只读(默认)
errors=panic 遇错 kernel panic

9.6 推荐生产配置示例

1
2
3
4
5
6
7
8
# 普通服务器(高可靠性)
mount -o defaults,data=ordered,barrier=1,journal_async_commit /dev/sda1 /data

# SSD 优化
mount -o defaults,discard,nobarrier,delalloc,data=ordered /dev/nvme0n1 /data

# 数据库(需 Direct I/O,避免延迟分配带来的不确定性)
mount -o defaults,nodelalloc,data=writeback,nobarrier /dev/sdb1 /dbdata

10. 常见问题排查

10.1 e2fsck:文件系统一致性检查

1
2
3
4
5
6
7
8
# 强制全面检查(卸载后运行)
e2fsck -f -v /dev/sda1

# 自动修复(生产环境慎用,建议先 -n 查看)
e2fsck -p /dev/sda1

# 只检查不修复
e2fsck -n /dev/sda1

e2fsck 按 5 个阶段检查:块/inode 位图、inode 结构、目录连通性、目录引用计数、块引用计数。超级块 s_error_counts_first_error_funcs_first_error_line 等字段可以帮助定位历史错误来源。

10.2 debugfs:低级调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 交互式进入(只读)
debugfs /dev/sda1

# 查看 inode 信息
debugfs -R "stat <inode_number>" /dev/sda1

# 查看 extent 树
debugfs -R "extents <inode_number>" /dev/sda1

# 查看超级块
debugfs -R "stats" /dev/sda1

# dump 指定文件内容
debugfs -R "dump <inode_number> /tmp/recovered" /dev/sda1

常用 debugfs 命令:

命令 用途
stat <inum> 显示 inode 详细信息
extents <inum> 显示 extent 树结构
htree <dir_inum> 显示目录 HTree 结构
logdump dump 日志内容(需 -f 挂载)
lsdel 列出已删除的 inode
undelete <inum> <name> 恢复已删除文件(有限支持)

10.3 tune2fs:调整文件系统参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看所有参数(包含错误历史)
tune2fs -l /dev/sda1

# 修改挂载间隔和最大挂载次数
tune2fs -i 0 -c 0 /dev/sda1

# 设置保留块比例(默认 5%,大盘可降低)
tune2fs -m 1 /dev/sda1

# 启用/禁用特性
tune2fs -O extents,uninit_bg,dir_index /dev/sda1

# 查看日志大小
tune2fs -l /dev/sda1 | grep -i journal

10.4 常见告警与处理

告警:EXT4-fs error (device sda1): ext4_find_extent:880: inode has invalid extent depth

原因:extent 树头部损坏,eh_depth 超过 EXT4_MAX_EXTENT_DEPTH(5)

处理:e2fsck -f /dev/sda1,若无法修复则需从备份恢复。

告警:EXT4-fs warning: maximal mount count reached

原因:挂载次数达到 s_max_mnt_count(默认 -1 表示不检查,但旧版本会有此限制)。

处理:tune2fs -c 0 /dev/sda1 取消限制,或 e2fsck -f 清零计数。

告警:EXT4-fs (sda1): delayed block allocation failed for inode … No space left

原因:延迟分配时发现空间不足,ext4_nonda_switch() 触发非延迟模式但仍分配失败。

处理:清理磁盘空间;检查 df -i 是否 inode 耗尽;检查 reserved block 占比(tune2fs -m)。


11. 总结

Ext4 是一个在 30 年演进中积累大量工程智慧的成熟文件系统。其核心设计决策:

  1. Extent 树 以 60 字节的 inode 内嵌空间为根,5 层 B+ 树可寻址 1 EiB 空间,查找效率高,碎片少。
  2. JBD2 日志 提供元数据原子性,三种数据模式灵活适配不同一致性需求。
  3. 延迟分配 通过推迟物理块分配到 writeback 时机,实现更大范围的连续分配,显著减少随机写碎片。
  4. HTree 目录索引 将目录查找从 O(n) 降为 O(log n),数万文件的目录查找毫秒级完成。
  5. 元数据校验和 结合 JBD2 日志校验,从磁盘静默错误到日志重放错误均有保护。

对于大多数通用场景,Ext4 的默认配置(data=ordereddelallocbarrier)是合理的起点。在高性能场景可开启 discardjournal_async_commit;数据库等需要精确控制 IO 语义的场景则应关闭 delalloc,配合 Direct I/O 使用。


参考源码(Linux 6.4-rc1):

  • fs/ext4/ext4.h — 核心数据结构定义(ext4_super_blockext4_group_descext4_inode
  • fs/ext4/ext4_extents.h — Extent 树结构体
  • fs/ext4/extents.c — Extent 树实现(ext4_find_extentext4_ext_binsearch
  • fs/ext4/inode.c — inode 操作、ext4_map_blocks、延迟分配
  • fs/ext4/file.cext4_file_read_iterext4_file_write_iter
  • fs/ext4/namei.c — 目录操作、HTree(dx_rootdx_entry
  • fs/ext4/super.c — 超级块挂载、挂载选项解析
  • fs/ext4/mballoc.c — 多块分配器
  • fs/jbd2/journal.c — JBD2 日志核心
  • fs/jbd2/transaction.c — 事务管理(jbd2_journal_start
  • include/linux/jbd2.hjournal_ttransaction_t 定义