3. F2FS 与 ext4 的元数据组织对比

在文件系统移植过程中,元数据组织方式的差异是内核适配的核心难点。ext4 作为传统日志型文件系统,其元数据设计围绕块设备特性优化;而 F2FS 针对 NAND Flash 的物理特性(擦除块、写放大、磨损均衡)重新设计了元数据布局。本章从 inode 结构、目录索引算法和块分配策略三个维度进行深度对比。

3.1 inode 结构对比

inode 是文件系统元数据的核心载体,存储文件属性、权限、时间戳以及数据块指针。ext4 与 F2FS 的 inode 结构在设计哲学上存在根本差异。

核心差异: ext4 的 inode 采用固定大小(256B),使用间接块指针链管理大文件;F2FS 的 inode 采用可变大小(默认 3.5KB),内嵌直接块指针数组,消除间接寻址。

3.1.1 ext4 inode 结构

ext4 inode 大小为 256 字节,包含 15 个块指针(i_block[15]),采用三级间接寻址架构:

struct ext4_inode {
    __le16  i_mode;         // 文件类型与权限
    __le16  i_uid;          // 用户ID(低16位)
    __le32  i_size_lo;      // 文件大小(低32位)
    __le32  i_atime;        // 访问时间
    __le32  i_ctime;        // 状态变更时间
    __le32  i_mtime;        // 修改时间
    __le32  i_dtime;        // 删除时间
    __le16  i_gid;          // 组ID(低16位)
    __le16  i_links_count;  // 硬链接计数
    __le32  i_blocks_lo;    // 块计数(低32位)
    __le32  i_flags;        // 文件标志
    __le32  i_block[EXT4_N_BLOCKS]; // 15个块指针
    __le32  i_generation;   // 文件版本(NFS使用)
    __le32  i_file_acl;     // 扩展属性块
    __le32  i_size_high;    // 文件大小(高32位)
    __le32  i_obso_faddr;   // 碎片化地址(已废弃)
    __le16  i_extra_isize;  // 扩展inode大小
    // ... 扩展属性区域
};

15 个块指针的布局策略:

  • i_block[0-11]:12 个直接块指针,直接指向数据块
  • i_block[12]:一级间接块指针,指向包含块指针的块
  • i_block[13]:二级间接块指针
  • i_block[14]:三级间接块指针
移植注意点: ext4 的间接块寻址在 64 位模式下会产生大量元数据 I/O。对于大文件(>4GB),三级间接块链需要读取 3 个元数据块才能定位数据块,这在 Flash 设备上会显著增加读放大。

3.1.2 F2FS inode 结构

F2FS 的 inode 结构设计为 node block,大小等于一个块(4KB),其中 inode 部分占用 3.5KB,剩余空间用于直接块指针:

struct f2fs_inode {
    __le16  i_mode;         // 文件类型与权限
    __le8   i_advise;       // 文件建议(冷热数据提示)
    __le8   i_inline;       // 内联标志(data/xattr)
    __le32  i_uid;          // 用户ID
    __le32  i_gid;          // 组ID
    __le32  i_links;        // 硬链接计数
    __le64  i_size;         // 文件大小(64位)
    __le64  i_blocks;       // 块计数(64位)
    __le64  i_atime;        // 访问时间
    __le64  i_ctime;        // 状态变更时间
    __le64  i_mtime;        // 修改时间
    __le32  i_generation;   // 文件版本
    __le32  i_current_depth; // 目录当前深度
    __le32  i_xattr_nid;    // 扩展属性节点ID
    __le32  i_flags;        // 文件标志
    __le32  i_pino;         // 父目录inode号
    __le32  i_namelen;      // 文件名长度
    __le8   i_name[F2FS_NAME_LEN]; // 文件名(内联)
    // 直接块指针区域
    __le32  i_addr[DEF_ADDRS_PER_INODE]; // 约923个直接块指针
    // NAT(节点地址表)信息
    struct f2fs_nat_entry i_nat;
};
关键差异: F2FS 的 inode 内嵌约 923 个直接块指针(DEF_ADDRS_PER_INODE = 923),这意味着小于 3.6MB(923 × 4KB)的文件完全不需要间接寻址。对于嵌入式设备中大量的小文件场景,这消除了所有间接块 I/O。

3.1.3 inode 结构对比表

对比维度 ext4 F2FS
inode 大小 256B(固定) 3.5KB(固定,占一个块)
直接块指针数 12 923
间接寻址层级 三级(12+1+2+3) 两级(直接 + 间接节点块)
最大文件大小 16TB(4K块) 3.94TB(4K块)
时间戳精度 秒级(扩展后纳秒) 纳秒级(原生)
文件名存储 目录项中 inode 内联(短文件名)
扩展属性 独立块或 inode 内 独立节点块或 inode 内联

3.1.4 inode 结构对比 SVG

ext4 vs F2FS inode 结构对比 ext4 inode (256B) 元数据区域 (权限/时间戳/标志) i_block[0-11] 直接块指针 (12个) i_block[12] 一级间接 → 块指针块 i_block[13] 二级间接 → 间接块链 i_block[14] 三级间接 → 深层间接链 → 小文件(<48KB): 直接访问,无额外I/O → 大文件(>4GB): 需读取3个元数据块 F2FS inode (3.5KB) 元数据区域 (权限/时间戳/标志/文件名) i_addr[0..922] 直接块指针 923个直接指针 × 4KB = 3.6MB 小文件零间接寻址 大文件仅需一级间接节点块 NAT 条目(节点地址表映射) 移植关键点 1. ext4 的 inode 大小固定为 256B,F2FS 占用整个块(4KB),导致 inode 表空间占用增加 2. F2FS 的 923 个直接指针消除了小文件的间接寻址,但 inode 写入代价更高 3. 移植时需要重新实现 inode 读写路径,特别是 i_addr 数组的解析逻辑 4. F2FS 支持 inode 内联数据(inline_data),小文件数据可直接存储在 inode 中

3.2 目录索引:Hash Tree vs HTree

目录索引决定了在目录中查找文件的速度。ext4 使用 HTree(哈希树),F2FS 使用 Hash Tree(基于哈希的二分查找树)。两者都使用哈希值加速查找,但树结构和分裂策略不同。

3.2.1 ext4 HTree 索引

ext4 的 HTree 是一种 B-tree 变体,使用目录项的哈希值作为键值:

  • 根节点:存储在目录 inode 的 i_block[0] 中,包含哈希种子和树深度
  • 内部节点:存储哈希范围(hash_start, hash_end)和指向子节点的块号
  • 叶节点:传统的目录项块,包含文件名和 inode 号
  • 分裂策略:当叶节点满时,分裂为两个节点,更新父节点的哈希范围
// ext4 HTree 根节点结构
struct dx_root {
    __le32  dot;              // '.' 条目
    __le32  dotdot;           // '..' 条目
    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_entry {
    __le32  hash;    // 哈希值(低32位)
    __le32  block;   // 子节点块号
};
HTree 性能特征: 查找时间复杂度 O(log n),但树深度受限于 indirect_levels(最大 3 层)。对于包含数百万文件的目录,HTree 的深度可能达到 3 层,需要 3 次元数据读取。

3.2.2 F2FS Hash Tree 索引

F2FS 的目录索引采用 基于哈希的二分查找树,设计更简洁:

  • 根节点:目录 inode 中的 i_current_depth 字段记录当前深度
  • 节点结构:每个目录块包含一个哈希槽位数组,槽位按哈希值排序
  • 查找算法:二分查找 + 哈希值比较,找到匹配的槽位后读取目录项
  • 分裂策略:当目录块满时,深度加 1,将条目重新分布到 2^depth 个块中
// F2FS 目录项块结构
struct f2fs_dentry_block {
    struct f2fs_dentry_bitmap {
        __le32  bitmap[8];    // 256位位图,标记槽位使用状态
    } dentry_bitmap;
    struct f2fs_dentry_reserved {
        __le32  reserved[3];  // 保留字段
    } reserved;
    struct f2fs_dentry_slot {
        __le32  hash_code;    // 完整哈希值(32位)
        __le32  ino;          // inode号
        __le16  name_len;     // 文件名长度
        __u8    file_type;    // 文件类型
        __u8    name[F2FS_SLOT_LEN]; // 文件名(8字节对齐)
    } dentry[NR_DENTRY_IN_BLOCK]; // 约 64 个槽位
};

3.2.3 目录索引对比表

对比维度 ext4 HTree F2FS Hash Tree
数据结构 B-tree 变体 哈希二分查找树
键值 哈希值(低32位) 完整哈希值(32位)
最大深度 3 层(固定) 动态(受限于 i_current_depth)
分裂粒度 块级分裂 全局深度增加
查找复杂度 O(log n) O(log n) + 二分查找
哈希冲突处理 线性探测 二分查找 + 线性探测
目录项大小 可变(8B + 文件名) 固定(16B + 8B对齐)
每块条目数 约 128-512(取决于文件名长度) 64(固定)

3.2.4 目录索引结构 SVG

目录索引结构对比:ext4 HTree vs F2FS Hash Tree ext4 HTree 根节点 (dx_root) 内部节点 (dx_entry[]) 叶节点 (目录项块) 深度固定最大3层 哈希值低32位作为键 块级分裂,更新父节点 F2FS Hash Tree inode (i_current_depth) 深度0: 1个目录块 深度1: 2个目录块 深度2: 4个目录块 深度动态增长,无上限 完整32位哈希值 全局深度增加,重新分布条目 移植适配要点 1. ext4 使用 dx_root 结构存储树元数据,F2FS 使用 inode 中的 i_current_depth 2. 目录遍历系统调用(getdents)需要根据不同的索引结构实现迭代逻辑

3.3 块分配策略差异

块分配策略直接影响文件系统的碎片化程度、写入性能和 Flash 寿命。ext4 的分配器针对机械硬盘优化,F2FS 的分配器针对 NAND Flash 的擦除块特性设计。

3.3.1 ext4 块分配策略

ext4 使用 多块分配器(mballoc),核心策略包括:

  • 块组(Block Group):将磁盘划分为 128MB 的块组,每个块组有独立的块位图
  • 预分配(Preallocation):通过 inode 预分配位图预留连续块
  • 延迟分配(Delayed Allocation):在数据写入内存后、刷盘前决定物理块位置
  • 碎片整理:支持在线 defrag,但需要额外系统调用
// ext4 块分配器核心结构
struct ext4_allocation_context {
    struct inode *ac_inode;           // 目标inode
    struct ext4_sb_info *ac_sb;       // 超级块信息
    struct ext4_allocation_request {
        ext4_lblk_t logical;          // 逻辑块号
        ext4_fsblk_t goal;            // 目标物理块号
        ext4_lblk_t len;              // 请求长度
        int flags;                    // 分配标志
    } ac_req;
    struct ext4_free_extent {
        ext4_fsblk_t fe_start;        // 起始物理块
        ext4_lblk_t fe_len;           // 连续块长度
    } ac_f_ex;                        // 最终分配结果
    int ac_status;                    // 分配状态
};

3.3.2 F2FS 块分配策略

F2FS 的块分配器围绕 段(Segment) 和 区段(Section) 设计:

  • 段(Segment):2MB 的连续块集合,是擦除块的基本单位
  • 区段(Section):多个段组成区段,用于数据隔离
  • 冷热数据分离:根据文件类型(数据/目录/元数据)分配到不同的段
  • 日志结构化写入:所有写入都是追加写入,通过清理(GC)回收空间
  • 自适应日志(Adaptive Logging):根据设备利用率动态切换追加/原地更新
// F2FS 段分配器核心结构
struct f2fs_sit_entry {
    __le16  valid_blocks;    // 段中有效块数
    __le16  cur_valid_map;   // 有效块位图
    __le32  usage;           // 使用计数
};

struct f2fs_segment_entry {
    unsigned int segno;      // 段号
    unsigned int zone;       // 所属区段
    unsigned int type;       // 段类型(热/温/冷/元数据)
    struct list_head list;   // 空闲段链表
};

3.3.3 块分配策略对比表

对比维度 ext4 F2FS
分配单位 块(4KB) 段(2MB)
空间管理 块组(128MB) 段 + 区段(2MB/区段)
写入模式 原地更新 追加写入(日志结构)
碎片控制 延迟分配 + 预分配 冷热分离 + 清理GC
空间回收 在线 defrag(可选) 后台 GC(强制)
元数据更新 日志(journal) 检查点(checkpoint)
写放大因子 低(1.0-1.2) 中(1.5-3.0,取决于GC)
Flash 磨损均衡 无原生支持 段级磨损均衡

3.3.4 块分配策略 SVG

块分配策略对比:ext4 vs F2FS ext4 分配流程 1. 延迟分配(内存中) 2. 查找块组位图 3. 预分配连续块 4. 原地写入数据块 5. 更新日志(journal) F2FS 分配流程 ← 上一章 📖 返回目录 下一章 →