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 { __le32 s_inodes_count; __le32 s_blocks_count_lo; __le32 s_r_blocks_count_lo; __le32 s_free_blocks_count_lo; __le32 s_free_inodes_count; __le32 s_first_data_block; __le32 s_log_block_size; __le32 s_log_cluster_size; __le32 s_blocks_per_group; __le32 s_clusters_per_group; __le32 s_inodes_per_group; __le32 s_mtime; __le32 s_wtime; __le16 s_mnt_count; __le16 s_max_mnt_count; __le16 s_magic; __le16 s_state; __le16 s_errors; __le16 s_minor_rev_level; __le32 s_lastcheck; __le32 s_checkinterval; __le32 s_creator_os; __le32 s_rev_level; __le16 s_def_resuid; __le16 s_def_resgid; __le32 s_first_ino; __le16 s_inode_size; __le16 s_block_group_nr; __le32 s_feature_compat; __le32 s_feature_incompat; __le32 s_feature_ro_compat; __u8 s_uuid[16 ]; char s_volume_name[EXT4_LABEL_MAX]; char s_last_mounted[64 ]; __u8 s_journal_uuid[16 ]; __le32 s_journal_inum; __le32 s_journal_dev; __le32 s_last_orphan; __le32 s_hash_seed[4 ]; __u8 s_def_hash_version; __le16 s_desc_size; __le32 s_default_mount_opts; __le32 s_first_meta_bg; __le32 s_mkfs_time; __le32 s_jnl_blocks[17 ]; __le32 s_blocks_count_hi; __le32 s_r_blocks_count_hi; __le32 s_free_blocks_count_hi; __le16 s_min_extra_isize; __le16 s_want_extra_isize; __le32 s_flags; __le16 s_raid_stride; __le16 s_mmp_update_interval; __le64 s_mmp_block; __le32 s_raid_stripe_width; __u8 s_log_groups_per_flex; __u8 s_checksum_type; __le64 s_kbytes_written; __le32 s_error_count; __le32 s_first_error_time; __le32 s_first_error_ino; __le64 s_first_error_block; __u8 s_first_error_func[32 ]; __le32 s_first_error_line; __le32 s_checksum_seed; };
几个关键字段解析:
**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; __le32 bg_inode_bitmap_lo; __le32 bg_inode_table_lo; __le16 bg_free_blocks_count_lo; __le16 bg_free_inodes_count_lo; __le16 bg_used_dirs_count_lo; __le16 bg_flags; __le32 bg_exclude_bitmap_lo; __le16 bg_block_bitmap_csum_lo; __le16 bg_inode_bitmap_csum_lo; __le16 bg_itable_unused_lo; __le16 bg_checksum; __le32 bg_block_bitmap_hi; __le32 bg_inode_bitmap_hi; __le32 bg_inode_table_hi; __le16 bg_free_blocks_count_hi; __le16 bg_free_inodes_count_hi; __le16 bg_used_dirs_count_hi; __le16 bg_itable_unused_hi; __le32 bg_exclude_bitmap_hi; __le16 bg_block_bitmap_csum_hi; __le16 bg_inode_bitmap_csum_hi; __u32 bg_reserved; };
bg_flags 字段中有几个重要标志位:
1 2 3 #define EXT4_BG_INODE_UNINIT 0x0001 #define EXT4_BG_BLOCK_UNINIT 0x0002 #define EXT4_BG_INODE_ZEROED 0x0004
INODE_UNINIT 和 BLOCK_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; __le16 i_uid; __le32 i_size_lo; __le32 i_atime; __le32 i_ctime; __le32 i_mtime; __le32 i_dtime; __le16 i_gid; __le16 i_links_count; __le32 i_blocks_lo; __le32 i_flags; union { struct { __le32 l_i_version; } linux1; } osd1; __le32 i_block[EXT4_N_BLOCKS]; __le32 i_generation; __le32 i_file_acl_lo; __le32 i_size_high; __le32 i_obso_faddr; 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; __le16 l_i_reserved; } linux2; } osd2; __le16 i_extra_isize; __le16 i_checksum_hi; __le32 i_ctime_extra; __le32 i_mtime_extra; __le32 i_atime_extra; __le32 i_crtime; __le32 i_crtime_extra; __le32 i_version_hi; __le32 i_projid; };
关键设计点:
i_block[EXT4_N_BLOCKS]:共 15 个 __le32,占 60 字节。对于传统间接块映射(Ext2/3 遗留),前 12 个是直接块指针,第 13/14/15 个分别是一级/二级/三级间接块指针。 对于启用 Extent 树的 Ext4 文件(EXT4_EXTENTS_FL),这 60 字节改为存储 extent 头和 extent 叶子节点 ,完全不同的语义。
纳秒时间戳 :i_ctime 等字段存 Unix 秒数(32位),i_ctime_extra 用 nsec << 2 | epoch 编码:低2位为 epoch 扩展位,可将时间范围延伸至 2446 年;高30位为纳秒。
**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 struct ext4_extent { __le32 ee_block; __le16 ee_len; __le16 ee_start_hi; __le32 ee_start_lo; }; struct ext4_extent_idx { __le32 ei_block; __le32 ei_leaf_lo; __le16 ei_leaf_hi; __u16 ei_unused; }; struct ext4_extent_header { __le16 eh_magic; __le16 eh_entries; __le16 eh_max; __le16 eh_depth; __le32 eh_generation; }; #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; __u16 p_depth; __u16 p_maxdepth; struct ext4_extent *p_ext ; struct ext4_extent_idx *p_idx ; struct ext4_extent_header *p_hdr ; struct buffer_head *p_bh ; };
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); 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) { 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; if (!(flags & EXT4_EX_NOCACHE) && depth == 0 ) ext4_cache_extents(inode, eh); while (i) { 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 ; 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 ) { return ; } l = EXT_FIRST_EXTENT(eh) + 1 ; 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 = l - 1 ; }
函数返回后,path->p_ext 指向起始逻辑块不超过目标 block 的那个 extent。调用者还需检查 block < ee_block + ee_len 来确认 block 确实落在该 extent 范围内,否则说明是个”洞”(hole)。
4. 块组管理 4.1 块分配器:mballoc Ext4 使用多块分配器 (mballoc,fs/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 ; unsigned int len; ext4_lblk_t logical; ext4_lblk_t lleft; ext4_lblk_t lright; ext4_fsblk_t goal; ext4_fsblk_t pleft; ext4_fsblk_t pright; unsigned int flags; };
flags 字段的核心标志:
1 2 3 4 5 6 #define EXT4_MB_HINT_MERGE 0x0001 #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_t(include/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; int j_errno; struct mutex j_abort_mutex ; struct buffer_head *j_sb_buffer ; journal_superblock_t *j_superblock; rwlock_t j_state_lock; int j_barrier_count; struct mutex j_barrier ; transaction_t *j_running_transaction; transaction_t *j_committing_transaction; transaction_t *j_checkpoint_transactions; unsigned long j_commit_interval; struct task_struct *j_task ; int j_max_transaction_buffers; };
transaction_t(include/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; struct journal_head *t_reserved_list ; struct journal_head *t_buffers ; struct journal_head *t_forget ; struct journal_head *t_checkpoint_list ; struct journal_head *t_shadow_list ; struct list_head t_inode_list ; spinlock_t t_handle_lock; };
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); if (unlikely(ext4_forced_shutdown(EXT4_SB(inode->i_sb)))) return -EIO; if (!iov_iter_count(to)) return 0 ; #ifdef CONFIG_FS_DAX if (IS_DAX(inode)) return ext4_dax_read_iter(iocb, to); #endif if (iocb->ki_flags & IOCB_DIRECT) return ext4_dio_read_iter(iocb, to); 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 if (iocb->ki_flags & IOCB_DIRECT) return ext4_dio_write_iter(iocb, from); else 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); ret = ext4_write_checks(iocb, from); 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); } return ret; }
generic_perform_write 的每次迭代都调用:
ext4_da_write_begin():准备页面,标记延迟分配
将用户数据拷贝进页面
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 ; 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)) { 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)) { map ->m_pblk = 0 ; retval = 0 ; } goto found; } }
ext4_map_blocks 三步查询策略:
Extent Status Tree(内存缓存) :高速 RB 树,存储已查询过的 extent 状态(written/unwritten/delayed/hole)。
Extent 树(磁盘 B+ 树) :调用 ext4_ext_map_blocks() 走 ext4_find_extent() 查找。
块分配 :若需要创建新块,调用 mballoc 分配物理块,再通过 ext4_ext_insert_extent() 插入 extent 树。
7. 延迟分配(Delayed Allocation) 延迟分配是 Ext4 性能优化的核心特性,通过挂载选项 delalloc(默认开启)启用。
7.1 原理 传统文件系统在 write() 时立即分配物理块。Ext4 延迟分配将块分配推迟到 writeback(脏页回写)时,好处在于:
写入更多数据后,分配器对局部性有更好的判断,可以分配连续的大块
若文件被删除前从未发生回写(如临时文件),完全避免了磁盘分配
7.2 实现:ext4_da_write_begin 和 ext4_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; 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 ; 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); 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 ; 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 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; u8 info_length; u8 indirect_levels; u8 unused_flags; } info; struct dx_entry entries []; }; struct dx_node { struct fake_dirent fake ; struct dx_entry entries []; }; struct dx_tail { u32 dt_reserved; __le32 dt_checksum; };
查找时流程:计算文件名哈希 → 在 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 mount -o defaults,discard,nobarrier,delalloc,data=ordered /dev/nvme0n1 /data 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 e2fsck -p /dev/sda1 e2fsck -n /dev/sda1
e2fsck 按 5 个阶段检查:块/inode 位图、inode 结构、目录连通性、目录引用计数、块引用计数。超级块 s_error_count、s_first_error_func、s_first_error_line 等字段可以帮助定位历史错误来源。
10.2 debugfs:低级调试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 debugfs /dev/sda1 debugfs -R "stat <inode_number>" /dev/sda1 debugfs -R "extents <inode_number>" /dev/sda1 debugfs -R "stats" /dev/sda1 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 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 年演进中积累大量工程智慧的成熟文件系统。其核心设计决策:
Extent 树 以 60 字节的 inode 内嵌空间为根,5 层 B+ 树可寻址 1 EiB 空间,查找效率高,碎片少。
JBD2 日志 提供元数据原子性,三种数据模式灵活适配不同一致性需求。
延迟分配 通过推迟物理块分配到 writeback 时机,实现更大范围的连续分配,显著减少随机写碎片。
HTree 目录索引 将目录查找从 O(n) 降为 O(log n),数万文件的目录查找毫秒级完成。
元数据校验和 结合 JBD2 日志校验,从磁盘静默错误到日志重放错误均有保护。
对于大多数通用场景,Ext4 的默认配置(data=ordered、delalloc、barrier)是合理的起点。在高性能场景可开启 discard、journal_async_commit;数据库等需要精确控制 IO 语义的场景则应关闭 delalloc,配合 Direct I/O 使用。
参考源码 (Linux 6.4-rc1):
fs/ext4/ext4.h — 核心数据结构定义(ext4_super_block、ext4_group_desc、ext4_inode)
fs/ext4/ext4_extents.h — Extent 树结构体
fs/ext4/extents.c — Extent 树实现(ext4_find_extent、ext4_ext_binsearch)
fs/ext4/inode.c — inode 操作、ext4_map_blocks、延迟分配
fs/ext4/file.c — ext4_file_read_iter、ext4_file_write_iter
fs/ext4/namei.c — 目录操作、HTree(dx_root、dx_entry)
fs/ext4/super.c — 超级块挂载、挂载选项解析
fs/ext4/mballoc.c — 多块分配器
fs/jbd2/journal.c — JBD2 日志核心
fs/jbd2/transaction.c — 事务管理(jbd2_journal_start)
include/linux/jbd2.h — journal_t、transaction_t 定义