Skip to content

Latest commit

 

History

History
2386 lines (1891 loc) · 83.8 KB

File metadata and controls

2386 lines (1891 loc) · 83.8 KB

Linux 内核文件系统深度分析:F2FS 与 EROFS

基于内核源码 fs/f2fs/fs/erofs/include/linux/f2fs_fs.h 编写。 分析的内核版本:master 分支(合并至 nfsd-7.0-2 标签)。


目录

  1. F2FS 概述与设计目标
  2. F2FS 磁盘布局
  3. F2FS 核心数据结构
  4. F2FS Node 与 Data 分离体系
  5. F2FS 日志结构写入与温度分类
  6. F2FS 垃圾回收(GC)机制
  7. F2FS 检查点(Checkpoint)机制
  8. F2FS 原子写与 Volatile 写
  9. F2FS 透明压缩
  10. EROFS 概述与只读设计理念
  11. EROFS 磁盘布局
  12. EROFS inode 格式
  13. EROFS 数据布局类型
  14. EROFS 压缩体系
  15. EROFS fscache 网络后端
  16. F2FS 与 EROFS 对比分析

1. F2FS 概述与设计目标

F2FS(Flash-Friendly File System)由三星电子于 2012 年开发,针对基于 NAND Flash 的存储设备(eMMC、UFS、SSD)进行了深度优化。其核心设计思路来自于日志结构文件系统(Log-Structured File System, LFS),但针对 Flash 特性做了大量改进。

1.1 Flash 存储的特殊性

Flash 存储设备具有以下特性,传统文件系统难以高效应对:

  • 写放大(Write Amplification):Flash 的最小擦除单元(erase block)远大于最小写单元(page),覆盖写会触发 read-modify-write,放大实际写入量。
  • 顺序写友好:Flash 控制器的 FTL(Flash Translation Layer)对顺序写具有更好的性能,随机写会导致 FTL 内部碎片和 GC 开销。
  • 写寿命限制(P/E Cycles):每个 Flash cell 的写入次数有限,均匀磨损(wear leveling)至关重要。

F2FS 在日志结构的基础上引入了以下关键优化:

  1. 多头日志(Multi-head Logging):将数据按"冷热"分类写入不同日志,减少 GC 时的数据迁移量。
  2. 自适应日志(Adaptive Logging):在空间充裕时使用 LFS 顺序写,在空间紧张时回退到 SSR(Slack Space Recycle)模式进行随机覆盖写。
  3. Node Address Table(NAT):引入间接寻址层,解耦 inode 的物理位置与逻辑地址,使得文件系统迁移数据时不需要更新上层 inode。

2. F2FS 磁盘布局

2.1 整体区域划分

F2FS 将磁盘划分为以下几个固定区域:

+------------------+  offset 0
|   Boot Sector    |  (保留,供 bootloader 使用)
+------------------+  offset 1024 bytes
|   Superblock x2  |  (两份超级块互为备份)
+------------------+
|   Checkpoint x2  |  (双 CP 区域,交替写入)
+------------------+
|   Segment Info   |  (SIT: Segment Information Table)
|      Table       |
+------------------+
|   Node Address   |  (NAT: Node Address Table)
|      Table       |
+------------------+
|  Segment Summary |  (SSA: Segment Summary Area)
|      Area        |
+------------------+
|                  |
|    Main Area     |  (Node 块 + Data 块,日志结构写入)
|                  |
+------------------+

相关字段定义于 include/linux/f2fs_fs.hstruct f2fs_super_block(第 112 行)中通过以下字段描述各区域起始位置:

/* include/linux/f2fs_fs.h: struct f2fs_super_block (line 112) */
__le32 cp_blkaddr;       /* start block address of checkpoint */
__le32 sit_blkaddr;      /* start block address of SIT */
__le32 nat_blkaddr;      /* start block address of NAT */
__le32 ssa_blkaddr;      /* start block address of SSA */
__le32 main_blkaddr;     /* start block address of main area */

还有对应的 segment 计数字段:

__le32 segment_count_ckpt;  /* # of segments for checkpoint */
__le32 segment_count_sit;   /* # of segments for SIT */
__le32 segment_count_nat;   /* # of segments for NAT */
__le32 segment_count_ssa;   /* # of segments for SSA */
__le32 segment_count_main;  /* # of segments for main area */

2.2 Superblock 详解

F2FS 超级块位于磁盘偏移 1024 字节处(#define F2FS_SUPER_OFFSET 1024include/linux/f2fs_fs.h 第 14 行),大小固定。关键字段解析:

/* include/linux/f2fs_fs.h: struct f2fs_super_block (line 112) */
struct f2fs_super_block {
    __le32 magic;                    /* Magic Number */
    __le16 major_ver;                /* Major Version */
    __le16 minor_ver;                /* Minor Version */
    __le32 log_blocksize;            /* log2 block size in bytes */
    __le32 log_blocks_per_seg;       /* log2 # of blocks per segment */
    __le32 segs_per_sec;             /* # of segments per section */
    __le32 secs_per_zone;            /* # of sections per zone */
    __le64 block_count;              /* total # of user blocks */
    __le32 segment_count;            /* total # of segments */
    __le32 segment_count_main;       /* # of segments for main area */
    /* ... 各区域起始地址 ... */
    __le32 root_ino;                 /* root inode number */
    __le32 node_ino;                 /* node inode number */
    __le32 meta_ino;                 /* meta inode number */
    __u8   uuid[16];                 /* 128-bit uuid for volume */
    __le32 feature;                  /* defined features */
    __u8   s_stop_reason[MAX_STOP_REASON]; /* stop checkpoint reason */
    __u8   s_errors[MAX_F2FS_ERRORS];      /* reason of image corrupts */
    __le32 crc;                      /* checksum of superblock */
} __packed;

层次结构:Block < Segment < Section < Zone

  • Block:基本 I/O 单元,大小等于 PAGE_SIZE(通常 4KB 或 16KB)
  • Segment:512 个 Block(4KB 页时 = 2MB;16KB 页时 = 8MB)
  • Section:由 segs_per_sec 个 Segment 组成(SSD 通常配置为 1)
  • Zone:由 secs_per_zone 个 Section 组成(为 ZNS 设备预留)

2.3 SIT(Segment Information Table)

每个 Segment 在 SIT 中对应一个 f2fs_sit_entryinclude/linux/f2fs_fs.h 第 417 行):

/* include/linux/f2fs_fs.h: line 417 */
struct f2fs_sit_entry {
    __le16 vblocks;                      /* [15:10] alloc type; [9:0] valid block count */
    __u8   valid_map[SIT_VBLOCK_MAP_SIZE]; /* 64 bytes bitmap for valid blocks */
    __le64 mtime;                        /* segment age for cleaning */
} __packed;

其中 vblocks 字段编码了两个信息(注释位于第 406-415 行):

  • [9:0]:有效块数量(valid block count)
  • [15:10]:分配类型(CURSEG_XXXX_TYPE,即温度分类)

SIT_VBLOCK_MAP_SIZE = 64 字节即 512 位,对应一个 Segment 内的 512 个 Block。

2.4 NAT(Node Address Table)

NAT 是 F2FS 的核心数据结构,将 Node ID(NID)映射到物理 Block 地址:

/* include/linux/f2fs_fs.h: line 376 */
struct f2fs_nat_entry {
    __u8   version;     /* latest version of cached nat entry */
    __le32 ino;         /* inode number */
    __le32 block_addr;  /* block address */
} __packed;

NAT_ENTRY_PER_BLOCK = F2FS_BLKSIZE / sizeof(struct f2fs_nat_entry) 决定了每个 NAT Block 能容纳的条目数量。NAT 的存在使得 F2FS 在执行 GC 时搬移 Node 块后,只需更新 NAT 表项,而无需遍历修改所有引用该 Node 的上层结构。

2.5 SSA(Segment Summary Area)

SSA 为每个 Block 记录"反向映射"信息,即该 Block 属于哪个 inode 的哪个位置,用于 GC 时快速定位需要更新的上层结构:

/* include/linux/f2fs_fs.h: line 448 */
struct f2fs_summary {
    __le32 nid;       /* parent node id */
    union {
        __u8 reserved[3];
        struct {
            __u8   version;     /* node version number */
            __le16 ofs_in_node; /* block index in parent node */
        } __packed;
    };
} __packed;

SSA Block 的布局(注释位于 include/linux/f2fs_fs.h 第 539 行):

+-----------------------+ <--- Block Start
| struct f2fs_summary   |
| entries[0..N-1]       |   N = entries_in_sum (512 for 4KB blocks)
+-----------------------+
| struct f2fs_journal   |   (NAT/SIT 增量日志)
+-----------------------+
| struct summary_footer |   (type + checksum)
+-----------------------+

3. F2FS 核心数据结构

3.1 f2fs_sb_info:超级块运行时信息

struct f2fs_sb_infofs/f2fs/f2fs.h 第 1742 行)是 F2FS 最核心的运行时数据结构,承载了所有子系统的管理句柄:

/* fs/f2fs/f2fs.h: line 1742 */
struct f2fs_sb_info {
    struct super_block *sb;              /* pointer to VFS super block */
    struct f2fs_super_block *raw_super;  /* raw super block pointer */

    /* node 管理器 */
    struct f2fs_nm_info *nm_info;        /* node manager */
    struct inode *node_inode;            /* cache node blocks */

    /* segment 管理器 */
    struct f2fs_sm_info *sm_info;        /* segment manager */

    /* checkpoint */
    struct f2fs_checkpoint *ckpt;        /* raw checkpoint pointer */
    int cur_cp_pack;                     /* remain current cp pack */
    struct f2fs_rwsem cp_global_sem;     /* checkpoint procedure lock */
    struct f2fs_rwsem cp_rwsem;          /* blocking FS operations */

    /* GC 相关 */
    struct f2fs_rwsem gc_lock;
    struct f2fs_gc_kthread *gc_thread;   /* GC thread */
    struct atgc_management am;           /* atgc management */
    unsigned int gc_mode;                /* current GC state */

    /* 压缩 */
    struct f2fs_mount_info mount_opt;    /* mount options */

    /* 基本参数 */
    unsigned int blocks_per_seg;         /* blocks per segment */
    unsigned int segs_per_sec;           /* segments per section */

    /* 统计 */
    atomic_t nr_pages[NR_COUNT_TYPE];
    /* ... */
};

3.2 f2fs_nm_info:Node 管理器

/* fs/f2fs/f2fs.h: line 1063 */
struct f2fs_nm_info {
    block_t nat_blkaddr;        /* base disk address of NAT */
    nid_t max_nid;              /* maximum possible node ids */
    nid_t available_nids;       /* # of available node ids */

    /* NAT 缓存 */
    struct radix_tree_root nat_root;     /* root of the nat entry cache */
    struct radix_tree_root nat_set_root; /* root of the nat set cache */
    struct f2fs_rwsem nat_tree_lock;
    struct list_head nat_entries;        /* cached nat entry list (clean) */
    unsigned int nat_cnt[MAX_NAT_STATE]; /* the # of cached nat entries */

    /* 空闲 NID 管理 */
    struct radix_tree_root free_nid_root;
    struct list_head free_nid_list;
    unsigned int nid_cnt[MAX_NID_STATE];

    /* checkpoint 相关 */
    char *nat_bitmap;           /* NAT bitmap pointer */
};

3.3 f2fs_sm_info:Segment 管理器

/* fs/f2fs/f2fs.h: line 1183 */
struct f2fs_sm_info {
    struct sit_info *sit_info;           /* whole segment information */
    struct free_segmap_info *free_info;  /* free segment information */
    struct dirty_seglist_info *dirty_info; /* dirty segment information */
    struct curseg_info *curseg_array;    /* active segment information */

    struct f2fs_rwsem curseg_lock;       /* for preventing curseg change */

    block_t seg0_blkaddr;   /* block address of 0'th segment */
    block_t main_blkaddr;   /* start block address of main area */
    block_t ssa_blkaddr;    /* start block address of SSA area */

    unsigned int segment_count;      /* total # of segments */
    unsigned int main_segments;      /* # of segments in main area */
    unsigned int reserved_segments;  /* # of reserved segments */
    unsigned int ovp_segments;       /* # of overprovision segments */
    /* ... */
};

3.4 f2fs_inode_info:内存 inode

/* fs/f2fs/f2fs.h: line 937 */
struct f2fs_inode_info {
    struct inode vfs_inode;         /* serve a vfs inode */
    unsigned long flags[BITS_TO_LONGS(FI_MAX)]; /* per-file flags */
    unsigned char i_advise;         /* file attribute hints */
    unsigned int i_current_depth;   /* only for directory depth */
    nid_t i_xattr_nid;             /* node id that contains xattrs */

    struct task_struct *atomic_write_task; /* store atomic write task */
    struct extent_tree *extent_tree[NR_EXTENT_CACHES]; /* extent cache */
    union {
        struct inode *cow_inode;    /* COW inode for atomic write */
        struct inode *atomic_inode; /* point to atomic_inode */
    };

    /* GC 保护 */
    struct f2fs_rwsem i_gc_rwsem[2];

    /* 压缩相关 */
    atomic_t i_compr_blocks;
    unsigned char i_compress_algorithm;
    unsigned char i_log_cluster_size;
    unsigned int i_cluster_size;

    /* 原子写相关 */
    unsigned int atomic_write_cnt;
    loff_t original_i_size;
};

4. F2FS Node 与 Data 分离体系

F2FS 的重要设计决策之一是将 Node 块Data 块分开管理,分别写入不同的日志流。

4.1 Node 块结构

Node 块是 F2FS 的元数据核心,一个 Node 块可以是三种类型之一(include/linux/f2fs_fs.h 第 361 行):

/* include/linux/f2fs_fs.h: line 361 */
struct f2fs_node {
    /* can be one of three types: inode, direct, and indirect types */
    union {
        struct f2fs_inode i;      /* inode node */
        struct direct_node dn;    /* direct node */
        struct indirect_node in;  /* indirect node */
    };
    struct node_footer footer;
} __packed;

node_footer 记录了 Node 的身份信息(第 250 行):

/* include/linux/f2fs_fs.h: line 250 */
struct node_footer {
    __le32 nid;          /* node id */
    __le32 ino;          /* inode number */
    __le32 flag;         /* include cold/fsync/dentry marks and offset */
    __le64 cp_ver;       /* checkpoint version */
    __le32 next_blkaddr; /* next node page block address */
} __packed;

其中 flag 字段的位域定义于第 352 行:

enum {
    COLD_BIT_SHIFT = 0,    /* cold data标记 */
    FSYNC_BIT_SHIFT,       /* fsync标记 */
    DENT_BIT_SHIFT,        /* dentry标记 */
    OFFSET_BIT_SHIFT       /* 节点偏移起始 */
};

4.2 f2fs_inode:磁盘 inode

磁盘 inode 结构(第 289 行)包含文件元数据及直接/间接 Block 地址指针:

/* include/linux/f2fs_fs.h: line 289 */
struct f2fs_inode {
    __le16 i_mode;          /* file mode */
    __u8   i_advise;        /* file hints */
    __u8   i_inline;        /* inline flags: INLINE_XATTR/DATA/DENTRY 等 */
    __le32 i_uid;
    __le32 i_gid;
    __le64 i_size;          /* file size in bytes */
    __le64 i_blocks;        /* file size in blocks */
    /* ... 时间戳字段 ... */

    struct f2fs_extent i_ext; /* caching a largest extent */

    union {
        struct {
            /* 扩展属性区域 (extra_attr 特性启用时) */
            __le16 i_extra_isize;
            __le32 i_projid;
            __le32 i_inode_checksum;
            __le64 i_crtime;
            __le64 i_compr_blocks;      /* # of compressed blocks */
            __u8   i_compress_algorithm;
            __u8   i_log_cluster_size;
            __le16 i_compress_flag;
        } __packed;
        __le32 i_addr[DEF_ADDRS_PER_INODE]; /* Pointers to data blocks */
    };
    __le32 i_nid[DEF_NIDS_PER_INODE]; /* direct(2), indirect(2), double_indirect(1) */
} __packed;

地址指针常量(第 258-278 行):

#define DEF_ADDRS_PER_INODE   /* 块内扣除 footer 和 nid 数组后的剩余 __le32 个数 */
#define DEF_NIDS_PER_INODE  5 /* Node IDs in an Inode */

/* 5 个 NID 的角色 */
#define NODE_DIR1_BLOCK     (DEF_ADDRS_PER_INODE + 1)  /* 直接节点 1 */
#define NODE_DIR2_BLOCK     (DEF_ADDRS_PER_INODE + 2)  /* 直接节点 2 */
#define NODE_IND1_BLOCK     (DEF_ADDRS_PER_INODE + 3)  /* 一级间接节点 */
#define NODE_IND2_BLOCK     (DEF_ADDRS_PER_INODE + 4)  /* 一级间接节点 */
#define NODE_DIND_BLOCK     (DEF_ADDRS_PER_INODE + 5)  /* 二级间接节点 */

4.3 直接节点与间接节点

/* include/linux/f2fs_fs.h: line 344 */
struct direct_node {
    __le32 addr[DEF_ADDRS_PER_BLOCK]; /* array of data block address */
} __packed;

struct indirect_node {
    __le32 nid[NIDS_PER_BLOCK]; /* array of node id */
} __packed;

DEF_ADDRS_PER_BLOCK = (F2FS_BLKSIZE - sizeof(struct node_footer)) / sizeof(__le32)NIDS_PER_BLOCK = (F2FS_BLKSIZE - sizeof(struct node_footer)) / sizeof(__le32)

对于 4KB 块大小,DEF_ADDRS_PER_BLOCK ≈ 1018NIDS_PER_BLOCK ≈ 1018

4.4 文件块映射树形结构

f2fs_inode (inode node)
├── i_addr[0..DEF_ADDRS_PER_INODE-1]    直接 Data Block 地址
├── i_nid[0] -> direct_node              直接节点 1 (1018 个 Data Block)
├── i_nid[1] -> direct_node              直接节点 2 (1018 个 Data Block)
├── i_nid[2] -> indirect_node            一级间接节点 (1018 个 direct_node NID)
│              └── direct_node[0..1017] -> 各含 1018 个 Data Block
├── i_nid[3] -> indirect_node            一级间接节点
└── i_nid[4] -> indirect_node            二级间接节点 (1018 个 indirect_node NID)
               └── indirect_node[0..1017] -> direct_node -> Data Block

4.5 特殊 Block 地址标记

/* include/linux/f2fs_fs.h: line 23 */
#define NULL_ADDR       ((block_t)0)    /* 未分配/无效 */
#define NEW_ADDR        ((block_t)-1)   /* 已分配但未写入磁盘(delalloc)*/
#define COMPRESS_ADDR   ((block_t)-2)   /* 压缩数据标记 */

5. F2FS 日志结构写入与温度分类

5.1 活跃日志(Active Logs)

F2FS 默认使用 6 个活跃日志,分为 Node 类和 Data 类,每类各有 3 种温度。温度划分的目的是将生命周期相似的数据归类存储,减少 GC 时的数据迁移量(fs/f2fs/f2fs.h 第 1146 行):

/* fs/f2fs/f2fs.h: line 1146 */
#define NR_CURSEG_DATA_TYPE  (3)  /* Hot/Warm/Cold Data */
#define NR_CURSEG_NODE_TYPE  (3)  /* Hot/Warm/Cold Node */
#define NR_CURSEG_PERSIST_TYPE (NR_CURSEG_DATA_TYPE + NR_CURSEG_NODE_TYPE) /* = 6 */

/* fs/f2fs/f2fs.h: line 1153 */
enum log_type {
    CURSEG_HOT_DATA  = 0,  /* directory entry blocks */
    CURSEG_WARM_DATA,      /* data blocks */
    CURSEG_COLD_DATA,      /* multimedia or GCed data blocks */
    CURSEG_HOT_NODE,       /* direct node blocks of directory files */
    CURSEG_WARM_NODE,      /* direct node blocks of normal files */
    CURSEG_COLD_NODE,      /* indirect node blocks */
    NR_PERSISTENT_LOG,
    CURSEG_COLD_DATA_PINNED = NR_PERSISTENT_LOG, /* pinned file */
    CURSEG_ALL_DATA_ATGC,  /* SSR allocator in hot/warm/cold data area */
};

上限配置:MAX_ACTIVE_LOGS = 16(磁盘格式支持最多 16 个,其中 Node 8 个、Data 8 个),当前实现使用 2/4/6 个(通过 active_logs 挂载选项控制)。

5.2 温度分类规则

日志类型 写入的数据种类 更新频率 GC 优先级
HOT_DATA 目录项(dentry) 最后 GC
WARM_DATA 普通文件数据 中等 GC
COLD_DATA 多媒体文件、GC 迁移的数据 优先 GC
HOT_NODE 目录文件的直接节点 最后 GC
WARM_NODE 普通文件的直接节点 中等 GC
COLD_NODE 间接节点 优先 GC

文件的温度由 i_advise 字段控制(fs/f2fs/f2fs.h 第 853 行):

/* fs/f2fs/f2fs.h: line 853 */
#define FADVISE_COLD_BIT    0x01  /* cold 文件 */
#define FADVISE_HOT_BIT     0x20  /* hot 文件 */

#define file_is_cold(inode) is_file(inode, FADVISE_COLD_BIT)
#define file_is_hot(inode)  is_file(inode, FADVISE_HOT_BIT)

热数据与温数据的年龄阈值定义(第 743-745 行):

/* fs/f2fs/f2fs.h: line 743 */
/* 定义数据块年龄小于 1GB(262144 blocks)为 hot data */
#define DEF_HOT_DATA_AGE_THRESHOLD   262144
/* 定义数据块年龄小于 10GB(2621440 blocks)但大于 1GB 为 warm data */
#define DEF_WARM_DATA_AGE_THRESHOLD  2621440

5.3 LFS 模式与 SSR 模式

F2FS 支持两种写入模式,通过 fs_mode 挂载选项控制:

/* fs/f2fs/f2fs.h: line 1521 */
enum {
    FS_MODE_ADAPTIVE,       /* use both lfs/ssr allocation */
    FS_MODE_LFS,            /* use lfs allocation only */
    FS_MODE_FRAGMENT_SEG,   /* segment fragmentation mode */
    FS_MODE_FRAGMENT_BLK,   /* block fragmentation mode */
};
  • LFS 模式(Log-Structured File System):严格顺序追加写,所有写操作都在当前活跃 Segment 的末尾进行,不回填旧数据位置。这对 Flash 写性能最优,但空间利用率较低。
  • SSR 模式(Slack Space Recycle):当空闲 Segment 不足时,F2FS 选择已有足够空洞(invalid blocks)的 Segment 进行随机覆盖写,提升空间利用率,但写入模式变为随机,可能增加 Flash 磨损。

6. F2FS 垃圾回收(GC)机制

6.1 GC 概述与触发条件

GC(Garbage Collection)的根本任务是回收含有大量无效块(invalid blocks)的"脏 Segment",将其中有效数据迁移后释放整个 Segment 供重用。

GC 线程函数 gc_thread_func()fs/f2fs/gc.c 第 31 行)说明了触发条件:

GC 触发条件(注释位于 gc.c line 89):
0. GC 当前未在执行
1. 有足够多的脏 Segment
2. I/O 子系统空闲(writeback pages 数量低)
3. I/O 子系统空闲(bdev request list 低)

GC 模式枚举(fs/f2fs/f2fs.h 第 1501 行):

/* fs/f2fs/f2fs.h: line 1501 */
enum {
    GC_NORMAL,       /* 正常 GC */
    GC_IDLE_CB,      /* 空闲时 Cost-Benefit 策略 */
    GC_IDLE_GREEDY,  /* 空闲时 Greedy 策略 */
    GC_IDLE_AT,      /* 空闲时 Age-Threshold 策略 */
    GC_URGENT_HIGH,  /* 紧急:高优先级 Greedy */
    GC_URGENT_LOW,   /* 紧急:低优先级 CB */
    GC_URGENT_MID,   /* 紧急:中优先级 CB */
    MAX_GC_MODE,
};

6.2 GC 策略选择

select_gc_type()fs/f2fs/gc.c 第 254 行)根据 GC 类型(前台/后台)和当前模式选择策略:

/* fs/f2fs/gc.c: line 254 */
static int select_gc_type(struct f2fs_sb_info *sbi, int gc_type)
{
    int gc_mode;

    if (gc_type == BG_GC) {
        if (sbi->am.atgc_enabled)
            gc_mode = GC_AT;   /* 后台 GC 优先用 Age-Threshold */
        else
            gc_mode = GC_CB;   /* 否则用 Cost-Benefit */
    } else {
        gc_mode = GC_GREEDY;   /* 前台 GC 用 Greedy(最快回收) */
    }
    /* 根据当前 gc_mode 覆盖 */
    switch (sbi->gc_mode) {
    case GC_URGENT_HIGH:   gc_mode = GC_GREEDY; break;
    case GC_IDLE_CB:       gc_mode = GC_CB;     break;
    case GC_IDLE_AT:       gc_mode = GC_AT;     break;
    /* ... */
    }
    return gc_mode;
}

6.3 Victim Segment 选择算法

F2FS 实现了三种 victim 选择策略:

Greedy 策略

选择有效块数量最少的 Segment(迁移代价最小):

/* fs/f2fs/gc.c: line 408 */
if (p->gc_mode == GC_GREEDY)
    return get_valid_blocks(sbi, segno, true);

Cost-Benefit(CB)策略

综合考虑有效块比例和 Segment 年龄,偏向选择"又老又空"的 Segment:

/* fs/f2fs/gc.c: line 367 */
static unsigned int get_cb_cost(struct f2fs_sb_info *sbi, unsigned int segno)
{
    unsigned long long mtime = f2fs_get_section_mtime(sbi, segno);
    unsigned int vblocks = get_valid_blocks(sbi, segno, true);
    unsigned char u;  /* 有效块比例 */
    unsigned char age;/* Segment 年龄(相对归一化) */

    u = BLKS_TO_SEGS(sbi, vblocks * 100);

    if (sit_i->max_mtime != sit_i->min_mtime)
        age = 100 - div64_u64(100 * (mtime - sit_i->min_mtime),
                sit_i->max_mtime - sit_i->min_mtime);

    /* cost = MAX - (100 * (100-u) * age) / (100+u) */
    return UINT_MAX - ((100 * (100 - u) * age) / (100 + u));
}

CB 策略的核心公式:选择代价(cost)最小的 Segment,即 (1-u) * age 最大的 Segment,即"越空越老"越优先。

AT-GC(Age-Threshold GC)策略

基于 age 和 valid block 比例的综合权重评分,用于 ATGC 模式(fs/f2fs/gc.c 第 552 行):

/* fs/f2fs/gc.c: line 593 */
/* age = 10000 * x% * age_weight */
age = div64_u64(accu * (max_mtime - ve->mtime), total_time) * age_weight;

/* u = 10000 * x% * (100 - age_weight) */
u = div64_u64(accu * (sec_blocks - vblocks), sec_blocks) * (100 - age_weight);

cost = UINT_MAX - (age + u);

ATGC 通过 struct atgc_managementfs/f2fs/f2fs.h 第 1422 行)管理 victim 红黑树:

/* fs/f2fs/f2fs.h: line 1422 */
struct atgc_management {
    bool atgc_enabled;
    struct rb_root_cached root;          /* victim rb-tree 根 */
    struct list_head victim_list;        /* 所有 victim 条目链表 */
    unsigned int victim_count;
    unsigned int candidate_ratio;        /* 候选比例 */
    unsigned int max_candidate_count;    /* 最大候选数 */
    unsigned int age_weight;             /* 年龄权重(vblock_weight = 100 - age_weight)*/
    unsigned long long age_threshold;    /* 年龄阈值 */
};

6.4 前台 GC 与后台 GC

gc_thread_func()fs/f2fs/gc.c 第 31 行)的主循环逻辑:

后台 GC (BG_GC):
  - 等待 wait_ms 超时或被唤醒
  - 检查 I/O 是否空闲
  - 使用 BG_GC + CB/AT 策略,轻量迁移
  - 迁移后标记 victim_secmap,供前台 GC 复用

前台 GC (FG_GC):
  - 由 f2fs_balance_fs() 在写入路径触发(空间不足)
  - 使用 FG_GC + Greedy 策略,快速回收
  - 优先使用后台 GC 预标记的 victim

关键代码(gc.c 第 144-165 行):

/* fs/f2fs/gc.c: line 155 */
gc_control.init_gc_type = sync_mode ? FG_GC : BG_GC;
gc_control.no_bg_gc = foreground;
gc_control.nr_free_secs = foreground ? 1 : 0;

if (f2fs_gc(sbi, &gc_control)) {
    /* 没有找到 victim,增加睡眠时间 */
    if (!foreground)
        wait_ms = gc_th->no_gc_sleep_time;
}

6.5 GC 控制结构

/* fs/f2fs/f2fs.h: line 1454 */
struct f2fs_gc_control {
    unsigned int victim_segno;      /* target victim segment number */
    int init_gc_type;               /* FG_GC or BG_GC */
    bool no_bg_gc;
    bool should_migrate_blocks;
    bool err_gc_skipped;
    bool one_time;                  /* require one time GC in one migration unit */
    unsigned int nr_free_secs;      /* # of free sections to do GC */
};

6.6 GC 流程总览

f2fs_gc()
  |
  +-- f2fs_get_victim()          选择 victim segment
  |     |
  |     +-- select_policy()      根据 gc_type 选择扫描策略
  |     +-- 遍历 dirty_segmap    找代价最小的 segment
  |     +-- get_gc_cost()        计算 GC 代价(Greedy/CB/AT)
  |
  +-- 对 victim 中每个有效 Block:
  |     |
  |     +-- 如果是 Data Block:
  |     |     gc_data_segment()
  |     |       +-- 找到对应 inode
  |     |       +-- 将数据迁移到新位置
  |     |       +-- 更新 NAT/SIT
  |     |
  |     +-- 如果是 Node Block:
  |           gc_node_segment()
  |             +-- 读取 node footer 获得 nid
  |             +-- 搬移 node 到新位置
  |             +-- 更新 NAT 条目(block_addr 字段)
  |
  +-- 释放 victim segment(加入 free list)

7. F2FS 检查点(Checkpoint)机制

7.1 双 CP 区域设计

F2FS 使用两个交替的 Checkpoint 区域实现原子 checkpoint 语义(include/linux/f2fs_fs.h 第 180 行):

/* include/linux/f2fs_fs.h: line 180 */
#define F2FS_CP_PACKS  2  /* # of checkpoint packs */

设计原理:

CP 区域 0 (Active)           CP 区域 1 (Backup)
+------------------+         +------------------+
| CP Header Block  |         | CP Header Block  |
+------------------+         +------------------+
| Orphan Blocks    |         | Orphan Blocks    |
+------------------+         +------------------+
| CP Data Block(s) |         | CP Data Block(s) |
+------------------+         +------------------+
| CP Footer Block  |         | CP Footer Block  |
+------------------+         +------------------+

写入时:始终写入非当前活跃的那个 CP 区域
完成后:用新 CP 的 version 覆盖 Header,使之成为最新有效 CP
挂载时:比较两个 CP 的 checkpoint_ver 字段,选取较新的有效版本

7.2 f2fs_checkpoint 结构

/* include/linux/f2fs_fs.h: line 182 */
struct f2fs_checkpoint {
    __le64 checkpoint_ver;          /* checkpoint block version number */
    __le64 user_block_count;        /* # of user blocks */
    __le64 valid_block_count;       /* # of valid blocks in main area */
    __le32 rsvd_segment_count;      /* # of reserved segments for gc */
    __le32 overprov_segment_count;  /* # of overprovision segments */
    __le32 free_segment_count;      /* # of free segments in main area */

    /* 当前活跃 Segment 的状态(8 个 Node Segment + 8 个 Data Segment)*/
    __le32 cur_node_segno[MAX_ACTIVE_NODE_LOGS]; /* = 8 */
    __le16 cur_node_blkoff[MAX_ACTIVE_NODE_LOGS];
    __le32 cur_data_segno[MAX_ACTIVE_DATA_LOGS]; /* = 8 */
    __le16 cur_data_blkoff[MAX_ACTIVE_DATA_LOGS];

    __le32 ckpt_flags;              /* CP_UMOUNT_FLAG / CP_ORPHAN_PRESENT_FLAG 等 */
    __le32 cp_pack_total_block_count;
    __le32 cp_pack_start_sum;       /* start block number of data summary */
    __le32 valid_node_count;        /* Total number of valid nodes */
    __le32 valid_inode_count;       /* Total number of valid inodes */
    __le32 next_free_nid;           /* Next free node number */
    __le32 sit_ver_bitmap_bytesize; /* Default value 64 */
    __le32 nat_ver_bitmap_bytesize; /* Default value 256 */
    __le32 checksum_offset;
    __le64 elapsed_time;            /* mounted time */
    unsigned char alloc_type[MAX_ACTIVE_LOGS]; /* 各 log 的分配类型 */

    /* SIT 和 NAT 版本位图(变长,跟随结构体末尾)*/
    unsigned char sit_nat_version_bitmap[];
} __packed;

CP flags 含义(第 164 行):

#define CP_UMOUNT_FLAG         0x00000001  /* 正常卸载 */
#define CP_ORPHAN_PRESENT_FLAG 0x00000002  /* 有孤儿 inode */
#define CP_COMPACT_SUM_FLAG    0x00000004  /* SSA 使用紧凑格式 */
#define CP_ERROR_FLAG          0x00000008  /* 发生了错误 */
#define CP_FSCK_FLAG           0x00000010  /* 需要 fsck */
#define CP_LARGE_NAT_BITMAP_FLAG 0x00000400 /* 使用大 NAT bitmap */
#define CP_DISABLED_FLAG       0x00001000  /* checkpoint 被禁用 */

7.3 Checkpoint 执行流程

CP 时序通过 cp_time 枚举记录(fs/f2fs/f2fs.h 第 319 行):

CP_TIME_START          开始 checkpoint
CP_TIME_LOCK           获取 cp_global_sem
CP_TIME_OP_LOCK        block_operation(阻止新的文件操作)
CP_TIME_MERGE_WRITE    flush DATA/NODE/META 到设备
CP_TIME_FLUSH_NAT      刷新 NAT 到磁盘
CP_TIME_FLUSH_SIT      刷新 SIT 到磁盘
CP_TIME_SYNC_META      同步 meta pages
CP_TIME_SYNC_CP_META   同步 CP meta pages
CP_TIME_WAIT_DIRTY_META 等待脏 meta 写完
CP_TIME_WAIT_CP_DATA    等待 CP data 写完
CP_TIME_FLUSH_DEVICE   flush 设备缓存
CP_TIME_WAIT_LAST_CP   等待上一个 CP pack 完成
CP_TIME_END            unblock_operation

7.4 孤儿 Inode 处理

当 inode 被删除但仍有进程持有文件描述符时,F2FS 将其记录为孤儿 inode,在 CP 时写入孤儿块(f2fs_orphan_block,第 225 行):

/* include/linux/f2fs_fs.h: line 225 */
struct f2fs_orphan_block {
    __le32 ino[F2FS_ORPHANS_PER_BLOCK]; /* inode numbers */
    __le32 reserved;
    __le16 blk_addr;      /* block index in current CP */
    __le16 blk_count;     /* Number of orphan inode blocks in CP */
    __le32 entry_count;   /* Total number of orphan nodes in current CP */
    __le32 check_sum;     /* CRC32 */
} __packed;

下次挂载时若发现 CP_ORPHAN_PRESENT_FLAG,则回放孤儿块,将这些 inode 彻底删除。

7.5 CP 与 fsync 的关系

F2FS 通过 node_footer.flag 中的 FSYNC_BIT 支持轻量级 fsync——只需将相关 Node 块顺序写到当前活跃的 Warm Node 段,并设置 FSYNC_BIT,无需触发完整 CP。

Roll-forward 恢复时,F2FS 扫描 Main Area 中含有 FSYNC_BIT 的 Node 块,重建上次 CP 后的修改,实现类似 ext4 journal commit 的快速恢复。


8. F2FS 原子写与 Volatile 写

8.1 原子写(Atomic Write)

F2FS 提供 ioctl(F2FS_IOC_START_ATOMIC_WRITE) 接口,允许应用程序以原子方式更新文件内容。

实现原理是引入 COW(Copy-on-Write)inode(fs/f2fs/f2fs.h 第 977-983 行):

/* fs/f2fs/f2fs.h: line 977 */
struct task_struct *atomic_write_task;  /* store atomic write task */
/* ... */
union {
    struct inode *cow_inode;    /* copy-on-write inode for atomic write */
    struct inode *atomic_inode; /* point to atomic_inode */
};

原子写流程:

1. ioctl(START_ATOMIC_WRITE)
   - 设置 FI_ATOMIC_FILE 标志
   - 创建 cow_inode,作为原始文件的 shadow

2. 所有写操作重定向到 cow_inode

3. ioctl(COMMIT_ATOMIC_WRITE)
   - 将 cow_inode 的内容原子地替换到原始 inode
   - 通过 CP 保证持久性
   - 清除 FI_ATOMIC_FILE 标志

4. ioctl(ABORT_ATOMIC_WRITE)
   - 丢弃 cow_inode,原文件不受影响

相关统计字段(fs/f2fs/f2fs.h 第 1994 行):

/* fs/f2fs/f2fs.h: line 1994 */
atomic64_t current_atomic_write; /* 当前进行中的原子写数量 */
s64 peak_atomic_write;           /* 峰值数量 */
u64 committed_atomic_block;      /* 已提交的原子写 block 数 */
u64 revoked_atomic_block;        /* 已回滚的原子写 block 数 */

F2FS 特性标志(include/linux/f2fs_fs.h 第 265 行):

#define F2FS_FEATURE_ATOMIC_WRITE  0x00000004

8.2 Volatile 写

Volatile Write 是比 Atomic Write 更轻量的接口,通过 ioctl(F2FS_IOC_START_VOLATILE_WRITE) 启动。与 Atomic Write 不同,Volatile Write 不保证持久性,适用于数据库 WAL 等只需要 crash consistency 而不需要 durability 的场景。Volatile Write 模式下 fsync 会被忽略,直到显式调用 ioctl(F2FS_IOC_COMMIT_ATOMIC_WRITE) 才实际持久化。


9. F2FS 透明压缩

9.1 压缩算法支持

F2FS 4.15 内核起支持透明压缩,compress_algorithm 字段定义了支持的算法(fs/f2fs/f2fs.h 第 1624 行):

/* fs/f2fs/f2fs.h: line 1624 */
enum compress_algorithm_type {
    COMPRESS_LZO,    /* LZO 压缩 */
    COMPRESS_LZ4,    /* LZ4 压缩(默认)*/
    COMPRESS_ZSTD,   /* ZSTD 压缩 */
    COMPRESS_LZORLE, /* LZO RLE 变体 */
    COMPRESS_MAX,
};

挂载选项中的压缩配置(fs/f2fs/f2fs.h 第 250 行):

/* fs/f2fs/f2fs.h: line 250 */
unsigned char compress_algorithm;   /* algorithm type */
unsigned char compress_log_size;    /* cluster log size */
unsigned char compress_level;       /* compress level */
bool compress_chksum;               /* compressed data chksum */
unsigned char compress_ext_cnt;     /* extension count */
int compress_mode;                  /* FS 模式或 USER 模式 */

磁盘 inode 中存储压缩信息(include/linux/f2fs_fs.h 第 329 行):

/* include/linux/f2fs_fs.h: line 329 */
__le64 i_compr_blocks;             /* # of compressed blocks */
__u8   i_compress_algorithm;       /* compress algorithm */
__u8   i_log_cluster_size;         /* log of cluster size */
__le16 i_compress_flag;            /* bit 0: chksum; [8,15]: compress level */

9.2 Cluster 压缩单元

压缩以 Cluster 为单位,一个 Cluster 包含 2^compress_log_size 个页(MIN_COMPRESS_LOG_SIZE=2MAX_COMPRESS_LOG_SIZE=8)。

Cluster(假设 log_size=2,即 4 页)
+-------+-------+-------+-------+
| Page0 | Page1 | Page2 | Page3 |  <- 原始数据(4 * 4KB = 16KB)
+-------+-------+-------+-------+
       压缩后
+----------+----------+
| CPage0   | CPage1   |  <- 压缩后(例如只需 2 个页)
+----------+----------+
COMPRESS_ADDR 作为前两个 Block 的地址标记(表示 cluster 头)

9.3 压缩上下文

/* fs/f2fs/f2fs.h: line 1657 */
struct compress_ctx {
    struct inode *inode;
    pgoff_t cluster_idx;       /* cluster index number */
    unsigned int cluster_size; /* page count in cluster */
    struct page **rpages;      /* pages store raw data in cluster */
    unsigned int nr_rpages;
    struct page **cpages;      /* pages store compressed data */
    unsigned int nr_cpages;
    void *rbuf;                /* virtual mapped address on rpages */
    struct compress_data *cbuf; /* virtual mapped address on cpages */
    size_t rlen;               /* valid data length in rbuf */
    size_t clen;               /* valid data length in cbuf */
};

压缩数据头(第 1641 行):

/* fs/f2fs/f2fs.h: line 1641 */
struct compress_data {
    __le32 clen;      /* compressed data size */
    __le32 chksum;    /* compressed data checksum */
    __le32 reserved[COMPRESS_DATA_RESERVED_SIZE]; /* 4 个保留字段 */
    u8 cdata[];       /* compressed data */
};
#define COMPRESS_HEADER_SIZE  (sizeof(struct compress_data))

9.4 压缩文件标志

/* fs/f2fs/f2fs.h: line 280-287 (include/linux/f2fs_fs.h) */
#define F2FS_COMPRESS_RELEASED  0x80  /* file released compressed blocks */

在内存 inode flags 中(第 922 行):

FI_COMPRESSED_FILE,    /* indicate file's data can be compressed */
FI_COMPRESS_CORRUPT,   /* indicate compressed cluster is corrupted */
FI_COMPRESS_RELEASED,  /* compressed blocks were released */

10. EROFS 概述与只读设计理念

EROFS(Enhanced Read-Only File System)由华为于 2017 年开发,主要针对 Android 系统分区、容器镜像、嵌入式只读文件系统等场景。与 F2FS 完全相反,EROFS 的核心设计原则是静态布局、无写入、无日志

10.1 只读设计的优势

  1. 零写放大:完全消除写操作,Flash 寿命理论上无限(对系统分区而言)。
  2. 布局简单:无需事务机制、无 journal/log、无 GC,实现复杂度极低。
  3. 高读性能:数据布局在 mkfs 时即可全局优化,例如将热文件连续排布、将小文件内联到 inode 中减少 I/O 次数。
  4. 数据去重:只读特性天然适合数据去重(deduplication),相同内容的文件只需保存一份。
  5. fscache 网络后端:EROFS 可作为网络文件系统客户端,通过 fscache 将远端镜像按需拉取并缓存,特别适合容器和 OTA 场景。

10.2 EROFS 特性标志

兼容特性(fs/erofs/erofs_fs.h 第 15 行):

/* fs/erofs/erofs_fs.h: line 15 */
#define EROFS_FEATURE_COMPAT_SB_CHKSUM              0x00000001  /* 超级块校验和 */
#define EROFS_FEATURE_COMPAT_MTIME                  0x00000002  /* 全局 mtime */
#define EROFS_FEATURE_COMPAT_XATTR_FILTER           0x00000004  /* xattr 过滤器 */
#define EROFS_FEATURE_COMPAT_SHARED_EA_IN_METABOX   0x00000008
#define EROFS_FEATURE_COMPAT_ISHARE_XATTRS          0x00000020  /* inode 共享 xattr */

不兼容特性(第 26 行):

/* fs/erofs/erofs_fs.h: line 26 */
#define EROFS_FEATURE_INCOMPAT_LZ4_0PADDING    0x00000001  /* LZ4 前缀填充 */
#define EROFS_FEATURE_INCOMPAT_COMPR_CFGS      0x00000002  /* 压缩配置块 */
#define EROFS_FEATURE_INCOMPAT_BIG_PCLUSTER    0x00000002  /* 大压缩簇 */
#define EROFS_FEATURE_INCOMPAT_CHUNKED_FILE    0x00000004  /* chunk 索引 */
#define EROFS_FEATURE_INCOMPAT_DEVICE_TABLE    0x00000008  /* 多设备表 */
#define EROFS_FEATURE_INCOMPAT_ZTAILPACKING    0x00000010  /* 尾部内联压缩 */
#define EROFS_FEATURE_INCOMPAT_FRAGMENTS       0x00000020  /* 小文件碎片合并 */
#define EROFS_FEATURE_INCOMPAT_DEDUPE          0x00000020  /* 去重 */
#define EROFS_FEATURE_INCOMPAT_XATTR_PREFIXES  0x00000040  /* xattr 前缀 */
#define EROFS_FEATURE_INCOMPAT_48BIT           0x00000080  /* 48 位寻址 */
#define EROFS_FEATURE_INCOMPAT_METABOX         0x00000100  /* 元数据压缩 */

11. EROFS 磁盘布局

EROFS 的磁盘布局极为简单,没有动态区域:

+------------------+  offset 0
|   Boot Sector    |  (保留 1024 字节)
+------------------+  offset 1024 bytes (EROFS_SUPER_OFFSET)
|   Superblock     |  (144 字节固定大小,后接可选扩展槽)
+------------------+
|   Optional       |  (Extra device table / xattr prefixes 等)
|   Extensions     |
+------------------+
|                  |
|  Metadata Area   |  (inode table + xattr data)
|  (from meta_blk) |  meta_blkaddr 指定起始 block
|                  |
+------------------+
|                  |
|   Data Area      |  (文件内容,与 inode 通过 startblk 关联)
|                  |
+------------------+
|   Shared xattr   |  (xattr_blkaddr 指定起始 block)
|      Area        |
+------------------+

11.1 EROFS 超级块

/* fs/erofs/erofs_fs.h: line 54 */
struct erofs_super_block {
    __le32 magic;           /* file system magic number */
    __le32 checksum;        /* crc32c to avoid unexpected on-disk overlap */
    __le32 feature_compat;
    __u8   blkszbits;       /* filesystem block size in bit shift */
    __u8   sb_extslots;     /* superblock size = 128 + sb_extslots * 16 */
    union {
        __le16 rootnid_2b;  /* nid of root directory */
        __le16 blocks_hi;   /* (48BIT on) blocks count MSB */
    } __packed rb;
    __le64 inos;            /* total valid ino # */
    __le64 epoch;           /* base seconds used for compact inodes */
    __le32 fixed_nsec;      /* fixed nanoseconds for compact inodes */
    __le32 blocks_lo;       /* blocks count LSB */
    __le32 meta_blkaddr;    /* start block address of metadata area */
    __le32 xattr_blkaddr;   /* start block address of shared xattr area */
    __u8   uuid[16];
    __u8   volume_name[16];
    __le32 feature_incompat;
    union {
        __le16 available_compr_algs; /* bitmap for available compression algorithms */
        __le16 lz4_max_distance;
    } __packed u1;
    __le16 extra_devices;   /* # of devices besides the primary device */
    __le16 devt_slotoff;
    /* ... */
    __le64 packed_nid;      /* nid of the special packed inode */
    __le64 rootnid_8b;      /* (48BIT on) nid of root directory */
    __le64 metabox_nid;     /* (METABOX on) nid of the metabox inode */
};

编译时大小检查(第 447 行):

BUILD_BUG_ON(sizeof(struct erofs_super_block) != 144);

11.2 NID 寻址

EROFS 通过 NID(Node ID)定位 inode,NID 本质上是 inode 相对 metadata area 起始处的字节偏移 >> islotbitsfs/erofs/internal.h 第 333 行):

/* fs/erofs/internal.h: line 333 */
static inline erofs_off_t erofs_iloc(struct inode *inode)
{
    struct erofs_sb_info *sbi = EROFS_I_SB(inode);
    erofs_nid_t nid_lo = EROFS_I(inode)->nid & EROFS_DIRENT_NID_MASK;

    if (erofs_inode_in_metabox(inode))
        return nid_lo << sbi->islotbits;  /* metabox 中的 inode */
    return erofs_pos(inode->i_sb, sbi->meta_blkaddr) +
        (nid_lo << sbi->islotbits);
}

其中 islotbits 对应 inode 槽位大小的 bit shift,通常为 5(即每个槽位 32 字节,对应 compact inode 大小)。


12. EROFS inode 格式

EROFS 定义了两种 on-disk inode 格式,通过 i_format 字段的 bit 0 区分(fs/erofs/erofs_fs.h 第 138 行):

/* fs/erofs/erofs_fs.h: line 138 */
#define EROFS_INODE_LAYOUT_COMPACT   0  /* 32-byte on-disk inode */
#define EROFS_INODE_LAYOUT_EXTENDED  1  /* 64-byte on-disk inode */

12.1 紧凑 inode(compact,32 字节)

/* fs/erofs/erofs_fs.h: line 161 */
struct erofs_inode_compact {
    __le16 i_format;        /* inode format hints (version + datalayout) */
    __le16 i_xattr_icount;
    __le16 i_mode;
    union erofs_inode_i_nb i_nb;  /* nlink 或 startblk_hi */
    __le32 i_size;          /* 文件大小(32位,最大 4GB)*/
    __le32 i_mtime;         /* 相对于 epoch 的秒数 */
    union erofs_inode_i_u i_u;    /* startblk_lo / blocks_lo / rdev / chunk info */
    __le32 i_ino;           /* for 32-bit stat compatibility */
    __le16 i_uid;
    __le16 i_gid;
    __le32 i_reserved;
};
BUILD_BUG_ON(sizeof(struct erofs_inode_compact) != 32);

紧凑 inode 的时间戳采用基于 epoch 的相对时间(以秒为单位),并共享超级块中的 fixed_nsec 纳秒值,以节省空间。

12.2 扩展 inode(extended,64 字节)

/* fs/erofs/erofs_fs.h: line 177 */
struct erofs_inode_extended {
    __le16 i_format;
    __le16 i_xattr_icount;
    __le16 i_mode;
    union erofs_inode_i_nb i_nb;
    __le64 i_size;          /* 文件大小(64位,支持超大文件)*/
    union erofs_inode_i_u i_u;
    __le32 i_ino;
    __le32 i_uid;           /* 32位 uid/gid(vs compact 的 16 位)*/
    __le32 i_gid;
    __le64 i_mtime;         /* 完整 64 位时间戳 */
    __le32 i_mtime_nsec;    /* 纳秒精度 */
    __le32 i_nlink;         /* 32 位 nlink */
    __u8   i_reserved2[16];
};
BUILD_BUG_ON(sizeof(struct erofs_inode_extended) != 64);

12.3 内存 inode 结构

struct erofs_inodefs/erofs/internal.h 第 275 行):

/* fs/erofs/internal.h: line 275 */
struct erofs_inode {
    erofs_nid_t nid;

    unsigned long flags;          /* atomic flags */
    unsigned char datalayout;     /* 数据布局类型 */
    unsigned char inode_isize;    /* on-disk inode 大小(32 或 64)*/
    bool dot_omitted;
    unsigned int xattr_isize;

    union {
        erofs_blk_t startblk;     /* flat/inline 布局的起始 block */
        struct {
            unsigned short chunkformat;
            unsigned char  chunkbits;  /* chunk 布局 */
        };
        struct {               /* 压缩布局 */
            unsigned short z_advise;
            unsigned char  z_algorithmtype[2];
            unsigned char  z_lclusterbits;
            union {
                u64    z_tailextent_headlcn;
                u64    z_extents;
            };
            erofs_off_t    z_fragmentoff;
            unsigned short z_idata_size;
        };
    };
    struct inode vfs_inode;
};

12.4 inode 读取流程

erofs_read_inode()fs/erofs/inode.c 第 36 行)区分两种格式:

/* fs/erofs/inode.c: line 77 */
switch (erofs_inode_version(ifmt)) {
case EROFS_INODE_LAYOUT_EXTENDED:
    vi->inode_isize = sizeof(struct erofs_inode_extended);
    /* 处理跨 block 边界的情况 */
    if (ofs + vi->inode_isize <= sb->s_blocksize) {
        ofs += vi->inode_isize;
        die = (struct erofs_inode_extended *)dic;
    } else {
        /* 跨 block:读两个 block 并拼接 */
        memcpy(&copied, dic, gotten);
        ptr = erofs_read_metabuf(..., blkaddr + 1, ...);
        memcpy((u8 *)&copied + gotten, ptr, ...);
        die = &copied;
    }
    inode->i_size = le64_to_cpu(die->i_size);
    break;

case EROFS_INODE_LAYOUT_COMPACT:
    vi->inode_isize = sizeof(struct erofs_inode_compact);
    inode->i_size = le32_to_cpu(dic->i_size);
    /* mtime = epoch + i_mtime(秒偏移)*/
    inode_set_mtime(inode, sbi->epoch + le32_to_cpu(dic->i_mtime),
                    sbi->fixed_nsec);
    break;
}

12.5 i_format 字段布局

/* fs/erofs/erofs_fs.h: line 120 */
#define EROFS_I_VERSION_BIT     0  /* bit 0: 0=compact, 1=extended */
#define EROFS_I_DATALAYOUT_BIT  1  /* bits 1-3: data layout (3 bits) */
#define EROFS_I_NLINK_1_BIT     4  /* bit 4: compact non-dir nlink=1 */
#define EROFS_I_DOT_OMITTED_BIT 4  /* bit 4: directory omits '.' dirent */

13. EROFS 数据布局类型

EROFS 支持 5 种数据布局(fs/erofs/erofs_fs.h 第 104 行):

/* fs/erofs/erofs_fs.h: line 104 */
enum {
    EROFS_INODE_FLAT_PLAIN      = 0,  /* 无压缩 flat,无尾部内联 */
    EROFS_INODE_COMPRESSED_FULL = 1,  /* 压缩,非紧凑索引 */
    EROFS_INODE_FLAT_INLINE     = 2,  /* 无压缩 flat,有尾部内联 */
    EROFS_INODE_COMPRESSED_COMPACT = 3, /* 压缩,紧凑索引 */
    EROFS_INODE_CHUNK_BASED     = 4,  /* chunk 索引 */
    EROFS_INODE_DATALAYOUT_MAX
};

13.1 Flat Plain 布局

文件数据连续存放于磁盘,inode 中的 startblk 指定起始 block:

inode.startblk ──> [ Block 0 | Block 1 | Block 2 | ... ]
                     文件数据顺序存放

适合中大型文件,I/O 路径简单,访问连续。

13.2 Flat Inline 布局

文件数据的末尾"尾部"(tail)内联到 inode 所在的 block 中,前面部分与 Flat Plain 相同:

metadata block 中的 inode:
+------------------+----------------------------+
| inode (64 bytes) | tail data (≤ block - 64B)  |
+------------------+----------------------------+

前置数据(如果有):
inode.startblk ──> [ Block 0 | Block 1 | ... ]

适合小文件(整个文件可以内联)或大文件的最后一个不完整 block。

13.3 Chunk-Based 布局

文件被划分为固定大小的 chunk,每个 chunk 通过 erofs_inode_chunk_index 独立寻址,支持多设备:

/* fs/erofs/erofs_fs.h: line 271 */
struct erofs_inode_chunk_index {
    __le16 startblk_hi;   /* starting block number MSB */
    __le16 device_id;     /* back-end storage id */
    __le32 startblk_lo;   /* starting block number LSB */
};
BUILD_BUG_ON(sizeof(struct erofs_inode_chunk_index) != 8);

chunk 大小由 chunkbits 决定:chunksize = blocksize << (chunkformat & EROFS_CHUNK_FORMAT_BLKBITS_MASK)

这种布局允许不同 chunk 存放在不同物理设备上,实现跨设备文件系统。

13.4 数据布局的填充逻辑

erofs_read_inode() 中对布局的处理(fs/erofs/inode.c 第 153-209 行):

/* fs/erofs/inode.c: line 198 */
if (vi->datalayout == EROFS_INODE_CHUNK_BASED) {
    vi->chunkformat = le16_to_cpu(copied.i_u.c.format);
    vi->chunkbits = sb->s_blocksize_bits +
        (vi->chunkformat & EROFS_CHUNK_FORMAT_BLKBITS_MASK);
}

14. EROFS 压缩体系

14.1 压缩算法支持

EROFS 支持四种压缩算法(fs/erofs/erofs_fs.h 第 303 行):

/* fs/erofs/erofs_fs.h: line 303 */
enum {
    Z_EROFS_COMPRESSION_LZ4      = 0,
    Z_EROFS_COMPRESSION_LZMA     = 1,
    Z_EROFS_COMPRESSION_DEFLATE  = 2,
    Z_EROFS_COMPRESSION_ZSTD     = 3,
    Z_EROFS_COMPRESSION_MAX
};

各算法配置结构(超级块扩展区域):

/* fs/erofs/erofs_fs.h: line 313 */
struct z_erofs_lz4_cfgs {
    __le16 max_distance;        /* 最大滑动窗口距离 */
    __le16 max_pclusterblks;    /* 最大物理压缩簇块数 */
    u8 reserved[10];
} __packed;

struct z_erofs_lzma_cfgs {
    __le32 dict_size;   /* LZMA 字典大小 */
    __le16 format;
    u8 reserved[8];
} __packed;

14.2 逻辑簇(lcluster)与物理簇(pcluster)

EROFS 压缩的核心概念是两级簇结构:

  • 逻辑簇(lcluster):大小为 blocksize << z_lclusterbits,是解压的最小逻辑单元。
  • 物理簇(pcluster):一个或多个逻辑簇压缩后的数据,存放于磁盘连续区域。
文件逻辑视图(每格为 1 个 lcluster):
+------+------+------+------+------+------+
| lc 0 | lc 1 | lc 2 | lc 3 | lc 4 | lc 5 |
+------+------+------+------+------+------+

压缩后磁盘视图:
pcluster 0          pcluster 1
+----------+        +----+----+
| lc0+lc1  |        | lc2|lc3 |   lc4+lc5 (合并成一个更大的 pcluster)
| (HEAD1)  |        |    |    |
+----------+        +----+----+

14.3 压缩索引结构

EROFS 的压缩索引(z_erofs_lcluster_indexfs/erofs/erofs_fs.h 第 403 行):

/* fs/erofs/erofs_fs.h: line 403 */
struct z_erofs_lcluster_index {
    __le16 di_advise;
    __le16 di_clusterofs;   /* where to decompress in the head lcluster */
    union {
        __le32 blkaddr;     /* for HEAD lclusters: pcluster 起始 block */
        __le16 delta[2];    /* for NONHEAD lclusters: [0] 到 HEAD 距离,[1] 到下个 HEAD 距离 */
    } di_u;
};
BUILD_BUG_ON(sizeof(struct z_erofs_lcluster_index) != 8);

lcluster 的类型(第 387 行):

/* fs/erofs/erofs_fs.h: line 387 */
enum {
    Z_EROFS_LCLUSTER_TYPE_PLAIN   = 0,  /* 未压缩 */
    Z_EROFS_LCLUSTER_TYPE_HEAD1   = 1,  /* 压缩簇头(第一类)*/
    Z_EROFS_LCLUSTER_TYPE_NONHEAD = 2,  /* 压缩簇中间块 */
    Z_EROFS_LCLUSTER_TYPE_HEAD2   = 3,  /* 压缩簇头(第二类,双算法)*/
};

14.4 紧凑索引(Compact Index)

对于 EROFS_INODE_COMPRESSED_COMPACT 布局,EROFS 使用紧凑索引减少元数据开销(fs/erofs/zmap.c 第 96 行):

/* fs/erofs/zmap.c: line 96 */
static int z_erofs_load_compact_lcluster(struct z_erofs_maprecorder *m,
                                         unsigned long lcn, bool lookahead)
{
    /* 紧凑索引有两种编码密度:
     * 4B 编码:每个 pack 包含 2 个 lcluster(lclusterbits <= 14)
     * 2B 编码:每个 pack 包含 16 个 lcluster(lclusterbits <= 12)
     */
    if (1 << amortizedshift == 4 && lclusterbits <= 14)
        vcnt = 2;
    else if (1 << amortizedshift == 2 && lclusterbits <= 12)
        vcnt = 16;
}

紧凑索引的每个 pack 末尾存储一个 __le32 校验字段,其余位用于存储 lcluster 的 lo(block 偏移)和 type(类型)字段,通过位打包减少空间占用:

/* fs/erofs/zmap.c: line 63 */
static unsigned int decode_compactedbits(unsigned int lobits,
                                         u8 *in, unsigned int pos, u8 *type)
{
    const unsigned int v = get_unaligned_le32(in + pos / 8) >> (pos & 7);
    const unsigned int lo = v & ((1 << lobits) - 1);
    *type = (v >> lobits) & 3;  /* 2 bit 类型 */
    return lo;
}

14.5 全量索引(Full Index)

对于 EROFS_INODE_COMPRESSED_FULL 布局,使用 8 字节的 z_erofs_lcluster_index 为每个 lcluster 保存完整信息,读取更直接,但占用更多空间。解析代码(fs/erofs/zmap.c 第 23 行):

/* fs/erofs/zmap.c: line 23 */
static int z_erofs_load_full_lcluster(struct z_erofs_maprecorder *m,
                                      unsigned long lcn)
{
    const erofs_off_t pos = Z_EROFS_FULL_INDEX_START(erofs_iloc(inode) +
            vi->inode_isize + vi->xattr_isize) +
            lcn * sizeof(struct z_erofs_lcluster_index);
    struct z_erofs_lcluster_index *di;

    di = erofs_read_metabuf(&m->map->buf, inode->i_sb, pos, m->in_mbox);
    /* ... 解析 di_advise, di_clusterofs, di_u ... */
}

14.6 尾部内联压缩(Tail Packing)

EROFS_FEATURE_INCOMPAT_ZTAILPACKING 特性允许压缩文件的最后一个不完整 pcluster 的数据直接内联到 inode 所在的 metadata block 中,避免额外的 I/O:

inode block:
+------------------+----------+------------------+
| erofs_inode_ext  | xattrs   | z_erofs_map_hdr  |
| (64 bytes)       |          | + compact/full   |
|                  |          | index            |
+------------------+----------+------------------+------------------+
                                                 | tail inline data |
                                                 | (z_idata_size B) |
                                                 +------------------+

z_erofs_map_header 的位置(fs/erofs/erofs_fs.h 第 360 行):

/* fs/erofs/erofs_fs.h: line 360 */
struct z_erofs_map_header {
    union {
        __le32 h_fragmentoff;   /* fragment data offset in packed inode */
        struct {
            __le16 h_reserved1;
            __le16 h_idata_size; /* encoded size of tailpacking data */
        };
    };
    __le16 h_advise;
    union {
        struct {
            __u8 h_algorithmtype; /* bit 0-3: HEAD1; bit 4-7: HEAD2 */
            __u8 h_clusterbits;   /* bit 0-3: lcluster bits - blkszbits */
        } __packed;
        __le16 h_extents_hi;
    } __packed;
};
BUILD_BUG_ON(sizeof(struct z_erofs_map_header) != 8);

14.7 滑动窗口解压

EROFS 的 LZ4 压缩使用滑动窗口,max_distance_pages 表示解压时需要保留多少之前的页面数据(fs/erofs/internal.h 第 74 行):

/* fs/erofs/internal.h: line 74 */
struct erofs_sb_lz4_info {
    u16 max_distance_pages; /* # of pages needed for EROFS lz4 rolling decompression */
    u16 max_pclusterblks;   /* maximum possible blocks for pclusters in the filesystem */
};

解压时内核维护一个滑动窗口,保证回看(backreference)所需的历史数据可用,这是 LZ4 正确解压的前提。


15. EROFS fscache 网络后端

15.1 设计背景

EROFS on fscache(代号 erofs-ondemand)是 Linux 5.19 引入的特性,允许 EROFS 文件系统镜像存放在远端服务器(HTTP/HTTPS/P2P 等),内核通过 fscache/cachefiles 机制按需拉取数据并本地缓存。这个特性对容器场景极其重要:

  • 容器启动时只拉取实际访问的文件内容,而不是整个镜像层,显著加快启动速度。
  • 配合 overlay 文件系统,实现不可变基础层 + 可读写上层的容器文件系统。

15.2 核心数据结构

struct erofs_fscachefs/erofs/internal.h 第 88 行):

/* fs/erofs/internal.h: line 88 */
struct erofs_fscache {
    struct fscache_cookie *cookie;  /* fscache cookie,对应一个缓存对象 */
    struct inode *inode;            /* anonymous inode for the blob */

    /* 共享域模式(多个挂载点共享缓存)*/
    struct erofs_domain *domain;
    struct list_head node;
    refcount_t ref;
    char *name;
};

struct erofs_domain(第 81 行)实现多挂载点共享同一 fscache volume:

/* fs/erofs/internal.h: line 81 */
struct erofs_domain {
    refcount_t ref;
    struct list_head list;
    struct fscache_volume *volume;  /* fscache volume */
    char *domain_id;
};

超级块信息中的 fscache 字段(第 163 行):

/* fs/erofs/internal.h: line 163 */
struct fscache_volume *volume;  /* fscache volume */
struct erofs_domain *domain;    /* 共享域 */
char *fsid;                     /* 文件系统唯一标识 */
char *domain_id;                /* 共享域 ID */

15.3 工作模式

挂载选项示例:
  mount -t erofs -o fsid=<image-id>,domain_id=<domain> \
        none /mnt/container

工作流程:
  1. 内核读取 EROFS metadata(通过 fscache 从远端拉取 metadata blob)
  2. 应用程序读取文件 /mnt/container/usr/lib/xxx.so
  3. EROFS 计算文件的 block 地址
  4. 检查 fscache 本地缓存是否有对应 block
  5a. 命中:直接从本地缓存读取,零网络 I/O
  5b. 未命中:通过 fscache 后端向远端请求数据,写入本地缓存后返回

判断是否处于 fscache 模式(第 192 行):

/* fs/erofs/internal.h: line 192 */
static inline bool erofs_is_fscache_mode(struct super_block *sb)
{
    return IS_ENABLED(CONFIG_EROFS_FS_ONDEMAND) &&
            !erofs_is_fileio_mode(EROFS_SB(sb)) && !sb->s_bdev;
}

sb->s_bdev 为 NULL(无块设备)时,EROFS 处于 fscache 模式——没有本地块设备,所有 I/O 通过 fscache 路由。

15.4 多设备支持

struct erofs_device_infofs/erofs/internal.h 第 42 行):

/* fs/erofs/internal.h: line 42 */
struct erofs_device_info {
    char *path;
    struct erofs_fscache *fscache;  /* fscache 后端 */
    struct file *file;              /* fileio 后端 */
    struct dax_device *dax_dev;     /* DAX 后端 */
    u64 fsoff, dax_part_off;

    erofs_blk_t blocks;
    erofs_blk_t uniaddr;
};

EROFS 可以同时支持多个后端设备,每个 chunk 索引中的 device_id 字段指定数据位于哪个后端设备。


16. F2FS 与 EROFS 对比分析

16.1 设计哲学对比

维度 F2FS EROFS
读写模式 可读写 只读
日志机制 日志结构(LFS) 无日志
GC 机制 有(后台+前台 GC)
检查点 双 CP,定期写入
原子写 支持(COW inode) 不适用
压缩 透明压缩(簇粒度) 透明压缩(pcluster 粒度)
网络后端 不支持 支持(fscache/ondemand)
典型应用场景 手机用户数据分区、SSD 存储 Android 系统分区、容器镜像
实现复杂度 高(GC+CP+原子写+压缩) 低(读路径+压缩解压)

16.2 inode 结构对比

F2FS on-disk inode (f2fs_inode)          EROFS on-disk inode
+---------------------------+           Compact (32B):
| i_mode (2B)               |           +--------------------+
| i_inline (1B)             |           | i_format (2B)      |
| i_uid/gid/links (12B)     |           | i_mode (2B)        |
| i_size (8B)               |           | i_size (4B)        |
| i_blocks (8B)             |           | i_mtime (4B)       |
| timestamps (24B)          |           | i_uid/gid (4B)     |
| i_generation (4B)         |           +--------------------+
| i_xattr_nid (4B)          |
| i_flags (4B)              |           Extended (64B):
| i_pino (4B)               |           +--------------------+
| i_name (255B)             |           | i_format (2B)      |
| i_ext (extent, 12B)       |           | i_mode (2B)        |
| extra attrs (variable)    |           | i_size (8B) [64bit]|
| i_addr[] (data ptrs)      |           | i_uid/gid (8B)     |
| i_nid[5] (node ptrs)      |           | i_mtime (8B) [ns]  |
| node_footer (20B)         |           | i_nlink (4B)       |
+---------------------------+           +--------------------+
大小:约 4KB(整个 block)              大小:32 或 64 字节

16.3 磁盘布局对比

F2FS 磁盘布局                            EROFS 磁盘布局
+--------------+                        +--------------+
|  Superblock  | 动态,含 feature 位     |  Superblock  | 固定 144B
+--------------+                        +--------------+
|  Checkpoint  | 双区域,周期写入         |  Metadata    | 一次性写入
|  Area x2     |                        |  Area        | (inodes + xattrs)
+--------------+                        +--------------+
|  SIT         | 每 Segment 74B          |  Data Area   | 文件数据
+--------------+                        +--------------+
|  NAT         | 每 Node 9B             |  Shared      | 共享 xattr 区域
+--------------+                        |  xattr Area  |
|  SSA         | 每 Block 7B            +--------------+
+--------------+
|  Main Area   | Node + Data 混合
+--------------+

16.4 压缩实现对比

比较项 F2FS EROFS
压缩粒度 Cluster(2^N 个页面) pcluster(可变大小)
压缩算法 LZO/LZ4/ZSTD/LZORLE LZ4/LZMA/DEFLATE/ZSTD
索引结构 COMPRESS_ADDR 标记 lcluster_index(紧凑/全量)
解压触发 读取 page 时 读取 page 时
尾部内联 不支持 支持(ztailpacking)
写路径压缩 支持(透明写入) 不支持(只读,mkfs 时压缩)
数据去重 不支持 支持(fragment)

16.5 GC 机制对比

F2FS 的 GC 是 LFS 文件系统的共性代价:

F2FS GC 代价分析:
  写放大因子 = (有效数据量 + GC 搬移量) / 应用写入量

  最差情况(段内有效率 50%):
  GC 每次需迁移 512 * 50% = 256 个有效 block
  写放大约为 2x

F2FS 降低 GC 代价的策略:
  1. 冷热分离:延长 Cold Data 段的存活时间,减少频繁 GC
  2. ATGC:优先 GC 最老最空的段,最大化每次 GC 的收益
  3. 过量配置(overprovision):预留额外空间用于 GC 缓冲
  4. SSR 模式:空间紧张时绕过 GC,直接复用旧段的空洞

EROFS 无 GC 代价:
  只读特性从根本上消除了 GC 问题,但代价是牺牲了写能力

总结

F2FS 和 EROFS 代表了 Linux 内核中两种截然不同的文件系统设计理念:

F2FS 通过日志结构写入、多头日志温度分类、精细化 GC 策略和双 CP 原子性机制,在 Flash 存储上实现了高性能的读写语义,同时通过原子写、透明压缩等特性满足移动设备的多样化需求。其复杂性主要体现在 GC 与 CP 机制的协调配合上。

EROFS 则以极简主义的只读设计换取了零 GC 开销、确定性读性能和低实现复杂度。通过精心设计的 inode 格式(compact/extended 两档)、多种数据布局(flat/inline/chunk/compressed)以及基于 pcluster 的压缩机制,EROFS 在系统分区、容器镜像等只读场景中提供了优秀的存储密度和读取性能。其 fscache 网络后端更将文件系统延伸到了按需加载的云原生场景。

两者的设计思想并不对立,而是在不同的应用场景中各司其职——在现代 Android 设备中,EROFS 用于系统分区(只读、高密度),F2FS 用于用户数据分区(可读写、高性能),共同构建了完整的存储体系。


参考源码文件

文件路径 说明
include/linux/f2fs_fs.h F2FS 磁盘格式定义(超级块、checkpoint、inode、NAT、SIT、SSA)
fs/f2fs/f2fs.h F2FS 运行时数据结构(sb_info、nm_info、sm_info、inode_info)
fs/f2fs/gc.c GC 实现(gc_thread_func、f2fs_get_victim、CB/Greedy/ATGC 算法)
fs/f2fs/checkpoint.c Checkpoint 实现(锁机制、meta folio 管理、优先级调整)
fs/erofs/erofs_fs.h EROFS 磁盘格式定义(超级块、inode compact/extended、压缩索引)
fs/erofs/internal.h EROFS 运行时数据结构(sb_info、erofs_inode、fscache)
fs/erofs/inode.c EROFS inode 读取(erofs_read_inode、紧凑/扩展格式区分)
fs/erofs/zmap.c EROFS 压缩映射(compact/full index 解析、z_erofs_maprecorder)

由 Claude Code 分析生成


17. F2FS inline data 与 inline xattr 优化

17.1 inline data

F2FS 支持将小文件的数据直接内嵌到 inode 块中,避免额外的 data block 分配, 显著减少小文件的存储碎片和访问延迟。

inline data 布局

F2FS inode 块(4KB):
+-------------------------+    偏移
| f2fs_inode 结构体        |    0
| (含 i_inline 标志位)   |
+-------------------------+    ...
|  inline data 区域        |    MAX_INLINE_DATA(inode)
|  = 4KB - sizeof(f2fs_inode)|  最多约 3458 字节可用
+-------------------------+

判断是否使用 inline data(fs/f2fs/f2fs.h):

/* 文件是否使用 inline data */
static inline bool f2fs_has_inline_data(struct inode *inode)
{
    return is_inode_flag_set(inode, FI_INLINE_DATA);
}

/* inline data 大小 = inode 块剩余空间 */
static inline int MAX_INLINE_DATA(struct inode *inode)
{
    return sizeof(__le32) * (CUR_ADDRS_PER_INODE(inode) -
                              DEF_ADDRS_PER_INODE +
                              INLINE_ADDRS(inode)) -
           INLINE_EXTRA_END_SIZE;
}

inline data 生命周期

文件创建(小于阈值):
  -> f2fs_write_begin()
     -> f2fs_write_inline_data() 直接写入 inode 块

文件增长超出 inline 容量:
  -> f2fs_convert_inline_inode()
     -> 将 inline data 搬移到独立 data block
     -> 清除 FI_INLINE_DATA 标志
     -> 进入正常的 data block 路径

删除文件:
  -> truncate_inline_inode() 直接清零 inline 区域

17.2 inline dentry(inline 目录)

类似地,F2FS 对小目录也支持 inline dentry,将目录项直接存储在 inode 块中:

/* include/linux/f2fs_fs.h */
struct f2fs_inline_dentry {
    __u8     dentry_bitmap[SIZE_OF_DENTRY_BITMAP]; /* 位图(哪些 slot 有效)*/
    __u8     reserved[SIZE_OF_RESERVED];
    struct f2fs_dir_entry dentry[NR_INLINE_DENTRY]; /* 目录项数组 */
    __u8     filename[NR_INLINE_DENTRY][F2FS_SLOT_LEN]; /* 文件名 */
} __packed __aligned(4);

/* NR_INLINE_DENTRY 约为 50 个目录项(取决于块大小)*/

当目录条目超出 inline 容量时,f2fs_convert_inline_dir() 将其转换为普通目录。

17.3 inline xattr

扩展属性(xattr)也支持内嵌存储,通过 i_xattr_nid 字段指向 xattr 块, 但当 xattr 量很少时,可以直接存储在 inode 块的尾部:

inode 块布局(含 inline xattr):
+----------------------+
| f2fs_inode 结构体     |
+----------------------+  <- inode 块顶端
| inline xattr 数据     |  <- 从 inode 块尾部倒序存储
| entry3               |
| entry2               |
| entry1               |
+----------------------+

通过 F2FS_INLINE_XATTR_ADDRS 宏定义 inline xattr 保留的 addrs 数量(默认 4 个)。


18. F2FS GC 详细机制

18.1 GC 触发条件

/* fs/f2fs/gc.c */
/* GC 触发条件检查 */
static bool has_enough_free_secs(struct f2fs_sb_info *sbi,
                                   int freed, int needed)
{
    /* 可用 section 数 = free_sections + freed */
    int node_secs  = get_blocktype_secs(sbi, F2FS_DIRTY_NODES) / 2;
    int dent_secs  = get_blocktype_secs(sbi, F2FS_DIRTY_DENTS) / 2;
    int imeta_secs = get_blocktype_secs(sbi, F2FS_DIRTY_IMETA) / 2;

    /* 当空闲 section 不足写入所有脏数据时,需要 GC */
    return free_sections(sbi) + freed > needed + node_secs
           + dent_secs + imeta_secs + reserved_sections(sbi);
}

GC 线程(gc_thread_func()fs/f2fs/gc.c)在后台周期性运行:

  • min_sleep_time(默认 30ms):空间充足时的最小休眠时间
  • max_sleep_time(默认 60000ms):空间充足时的最大休眠时间
  • no_gc_sleep_time(默认 60000ms):无需 GC 时的休眠时间

当空闲空间低于阈值时,系统切换到前台 GC(Foreground GC),由写入路径直接触发。

18.2 GC 段选择策略

Greedy 策略(前台 GC 默认):

遍历所有 dirty segment:
  选择有效块数最少的 segment("最空"的段)
  -> 最大化每次 GC 的空间回收量

Cost-Benefit 策略(后台 GC 默认):

代价函数 = 有效块占比 / (当前时间 - 上次修改时间)

- 优先选择:有效块少、且长时间未修改的段
  (说明这些块是 Cold Data,不经常改变,GC 后不易再次变脏)
- 避免频繁 GC 刚写入的热数据段

ATGC(Age-based Targeted GC)策略(Linux 5.16+):

/* fs/f2fs/gc.c */
/* ATGC:按 segment 年龄(mtime)优先 GC 最老的段 */
static unsigned int get_atgc_cost(struct f2fs_sb_info *sbi,
                                   struct victim_sel_policy *p,
                                   unsigned int start)
{
    /* 代价 = 有效块数 * 时间系数 */
    unsigned int age = jiffies - GET_SEG_FROM_SB(sbi, segno)->mtime;
    return p->ofs_unit * (100 - sbi->atgc_info.age_weight) / 100
           + (max_age - min(age, max_age)) * sbi->atgc_info.age_weight / 100;
}

18.3 GC 搬移流程

选中 victim segment(假设为 segment X):

1. f2fs_get_victim():在 SIT 中标记 segment X 为 "candidate"

2. 遍历 segment X 的所有有效 block(通过 SSA summary 获取 block owner):
   - Node block:
     通过 NAT 查找 node 的当前物理地址
     如果当前地址 == segment X 中的地址(未被并发更新):
       将 node block 读出,写入新 segment Y
       更新 NAT 表(原 node number -> 新物理地址)
   - Data block:
     通过 SSA 获取 (ino, offset)
     通过 NAT 找到对应 inode
     检查 inode 的 data 映射是否仍指向该物理地址
     如是,将 data block 读出写到新 segment Y
     更新 inode 的 data block 地址

3. segment X 的有效块数变为 0 后,调用 f2fs_invalidate_blocks() 标记可擦除

4. 触发 checkpoint(保证映射关系持久化)

19. F2FS 与 EROFS 在 Android 中的应用

19.1 Android 存储分区演进

Android 版本    系统分区 FS    用户数据分区 FS    说明
-----------     -----------    ---------------    ----
2.x ~ 4.x      ext2/yaffs2    yaffs2             早期 MTD 时代
5.x ~ 9.x      ext4           ext4               eMMC/UFS 时代
10 ~ 11         ext4           F2FS(可选)       F2FS 逐步推广
12+             EROFS          F2FS               EROFS 系统分区

Android 12 开始,Google 强制要求系统分区(/system, /vendor, /product) 使用 EROFS(只读),用户数据分区(/data)使用 F2FS。

19.2 EROFS 在 Android 中的优势

优势一:启动速度提升
  - EROFS 无需 GC、无日志回放,挂载时间近乎 O(1)
  - 压缩后的系统镜像减少 Flash 读取量(小系统镜像 -> 更少 I/O)
  - 实测 Android 冷启动时间减少约 15-20%(Google Pixel 数据)

优势二:存储空间节省
  - EROFS 压缩比约 2:1(相比 ext4 节省约 40-60% 空间)
  - 系统更新(OTA)时差量更小

优势三:安全性提升
  - 只读文件系统天然防止系统分区被恶意写入
  - 配合 dm-verity(块设备完整性校验)提供强安全保证

优势四:内存效率
  - EROFS pcluster 压缩使用高效缓存:读入时解压,解压后的页可被页缓存复用
  - 对只读数据无需写回(dirty page tracking 开销为零)

19.3 F2FS 在 Android 数据分区的调优

Android 数据分区的 F2FS 典型挂载选项:

/dev/block/by-name/userdata /data f2fs
    noatime,nosuid,nodev,discard,
    reserve_root=32768,    # 为 root 保留 128MB
    resgid=1065,           # AID_RESERVED_DISK GID
    fsync_mode=nobarrier,  # 关闭 barrier(UFS 有内置保护)
    inline_xattr_size=4,   # inline xattr 保留 4 个 slots
    compress_algorithm=lz4, # 压缩算法
    compress_extension=*,   # 压缩所有扩展名
    lazytime                # 延迟 atime 更新

discard 选项触发 TRIM 命令,通知 eMMC/UFS 控制器哪些块已释放, 帮助 FTL 进行内部垃圾回收。

19.4 容器场景:EROFS over overlay

在容器技术(如 Kata Containers、Dragonball)中,EROFS 配合 fscache 实现:

容器运行时                OCI 镜像仓库
    |                        |
    | 挂载 EROFS(on-demand)  |
    v                        |
+----------+                 |
| EROFS FS |<--- fscache ---->| 按需拉取 layer 数据
+----------+                 |
    |
    | OverlayFS
    v
+----------+
| 容器视图  |  system layer (EROFS 只读) + work layer (tmpfs 可写)
+----------+

优势:

  • 容器启动无需等待完整镜像下载(按需拉取,首次访问时才下载)
  • 多容器共享同一 layer 的 fscache(节省缓存空间)
  • 与 containerd/nydus 集成,支持 nydus image format

19.5 性能基准对比数据

测试环境:Samsung Exynos SoC,UFS 3.1,Android 12

顺序读(系统库文件):
  ext4 (无压缩):350 MB/s
  EROFS (lz4):   280 MB/s(解压开销,但 I/O 量减少 50%)
  EROFS (lz4) 热缓存:接近内存带宽(页缓存命中)

随机读(小文件,4KB):
  ext4:45K IOPS
  EROFS:52K IOPS(+15%,因为元数据更紧凑,cache 利用率更高)

挂载时间:
  ext4:~120ms
  EROFS:~8ms(-93%)

F2FS vs ext4(用户数据分区,混合读写):
  顺序写:F2FS +25%(日志结构顺序写优势)
  随机写:F2FS +40%(避免写放大)
  随机读:相近
  文件创建:F2FS +30%(inline data 优化小文件)

20. EROFS 固定输出压缩(Fixed-sized Output Compression)

20.1 压缩模式设计原理

EROFS 的压缩方案被称为"固定输出压缩"(fixed-sized output compression), 与传统变长输出压缩(如 zlib/lz4 streaming)的区别在于:

传统变长压缩:
  Input:  [page0 | page1 | page2 | page3]  (4 pages = 16KB)
  Output: [compressed data, variable size]  (例如 7.3KB)
  问题:压缩块与页缓存的 4KB 边界不对齐,随机访问困难

EROFS 固定输出压缩:
  Input:  [16KB 数据]
  Output: 按 block 对齐:[block0 | block1]  (2 blocks = 8KB)
  保证:输出总是 block 对齐,可以直接作为页缓存的 page

这使得 EROFS 压缩数据可以直接缓存在页缓存中,后续访问命中页缓存时 无需再次解压,实现了"解压一次,缓存永久有效"的语义。

20.2 pcluster(压缩簇)的页缓存映射

pcluster(物理压缩单元)在磁盘上:
  Compressed data: [D0][D1]  (2 个压缩 block)

解压后在页缓存中:
  Page cache:
  +-------+-------+-------+-------+
  |  P0   |  P1   |  P2   |  P3   |  (4 pages,对应解压后 16KB)
  +-------+-------+-------+-------+
  page_offset: 0    4KB   8KB   12KB

关键:P0..P3 均映射到 (D0, D1) 解压后的内容
当任意一个 P? 被访问时,整个 pcluster 被解压
一旦解压,P0..P3 全部进入页缓存,后续访问零开销

20.3 z_erofs_decompress_queue

EROFS 解压任务通过 z_erofs_decompress_queuefs/erofs/zdata.c)批处理:

/* fs/erofs/zdata.c */
struct z_erofs_decompress_queue {
    struct super_block *sb;
    struct z_erofs_pcluster *head;    /* 等待解压的 pcluster 链表 */
    z_erofs_next_pcluster_t  owned_head;
    bool                     eio;    /* 有 I/O 错误 */
};

解压流程:

1. 页面读取触发 erofs_readahead() / erofs_readpage()
2. z_erofs_map_blocks_iter() 建立 pcluster -> page 映射
3. z_erofs_submit_queue() 提交 I/O(读取压缩 block)
4. I/O 完成后 z_erofs_decompress_pcluster() 执行解压
5. 解压结果直接写入页缓存对应的 page frame
6. unlock_page() 通知等待的读取者

21. F2FS 检查点(Checkpoint)与一致性保证

21.1 双 Checkpoint 区域设计

F2FS 维护两个 Checkpoint 区域(CP1 和 CP2),交替写入:

CP 区域布局(每个 CP 区域):
+-----------------------------+
| f2fs_checkpoint 头部         |  包含:version, elapsed_time, alloc_type...
+-----------------------------+
| orphan inode 列表            |  未完成的 unlink 操作
+-----------------------------+
| active segment 信息          |  当前写入的 segment 状态
+-----------------------------+
| nat_bits 位图                |  NAT 条目变化的快速索引
+-----------------------------+
| checksum(最后4字节)         |  整个 CP 的 CRC32
+-----------------------------+

CP 版本号:每次 checkpoint 时 checkpoint_ver 递增(通过 ++sbi->last_cp_ver), 挂载时比较 CP1 和 CP2 的版本号,选择较新的那个。

21.2 Checkpoint 写入流程

/* fs/f2fs/checkpoint.c */
int f2fs_write_checkpoint(struct f2fs_sb_info *sbi, struct cp_control *cpc)
{
    /* 1. 停止所有写入(阻塞 f2fs_lock_all)*/
    f2fs_lock_all(sbi);

    /* 2. 刷出 node/data/meta 页缓存 */
    f2fs_flush_nat_entries(sbi, cpc);
    f2fs_flush_sit_entries(sbi, cpc);

    /* 3. 更新 CP 头部(version、alloc_type 等)*/
    update_ckpt_flags(sbi, cpc);

    /* 4. 写入 CP 页(先写数据,再写头部,保证原子性)*/
    commit_checkpoint(sbi, ckpt, start_blk);

    /* 5. 写 CP 的第二个副本 */
    commit_checkpoint(sbi, ckpt, start_blk + sbi->blocks_per_seg);

    /* 6. 更新 CP 指针(指向刚写入的 CP)*/
    f2fs_update_time(sbi, CP_TIME);

    /* 7. 释放锁 */
    f2fs_unlock_all(sbi);
}

21.3 fsync 与 CP 的关系

fsync() 系统调用需要保证数据持久化。F2FS 的 fsync 分为两种模式:

fsync_mode=posix(默认)

fsync() 流程:
1. f2fs_do_sync_file()
2. 检查是否需要触发 checkpoint:
   - 若文件 inode 或其数据已在上次 CP 后修改 -> 触发 CP
   - 否则只需 flush I/O(journal 足够)
3. 等待 I/O 完成

fsync_mode=strict(更严格,类 ext4 行为)

每次 fsync 都触发完整 checkpoint,代价是性能下降(约 2-3x)。

fsync_mode=nobarrier(Android 常用)

信任底层存储(UFS)的掉电保护,不发送 FLUSH 命令,性能最好但依赖硬件可靠性。


22. EROFS 超级块与挂载流程

22.1 EROFS 超级块字段详解

/* fs/erofs/erofs_fs.h */
struct erofs_super_block {
    __le32 magic;              /* 魔数:EROFS_SUPER_MAGIC_V1 = 0xE0F5E1E2 */
    __le32 checksum;           /* CRC32 校验和(可选)*/
    __le32 feature_compat;     /* 兼容特性位图 */
    __u8   blkszbits;          /* 块大小 = 1 << blkszbits(通常 12 = 4KB)*/
    __u8   sb_extslots;        /* 超级块扩展 slot 数 */
    __le16 root_nid;           /* 根目录 nid */
    __le64 inos;               /* 文件系统中的 inode 总数 */
    __le64 build_time;         /* 构建时间(Unix timestamp)*/
    __le32 build_time_nsec;    /* 纳秒部分 */
    __le32 blocks;             /* 数据块总数 */
    __le32 meta_blkaddr;       /* metadata 区起始块地址 */
    __le32 xattr_blkaddr;      /* 共享 xattr 区起始块地址 */
    __u8   uuid[16];           /* 文件系统 UUID */
    __u8   volume_name[16];    /* 卷名 */
    __le32 feature_incompat;   /* 不兼容特性位图 */
    union {
        __le16 available_compr_algs; /* 可用压缩算法位图 */
        __le16 lz4_max_distance;     /* LZ4 最大后向引用距离 */
    };
    __le16 extra_devices;      /* 额外设备数(多设备支持)*/
    __le16 devt_slotoff;       /* 设备槽位偏移 */
    __u8   dirblkbits;         /* 目录块大小(1 << dirblkbits)*/
    __u8   xattr_prefix_count; /* xattr 前缀数量 */
    __le32 xattr_prefix_start; /* xattr 前缀起始地址 */
    __le64 packed_nid;         /* 打包文件的 nid(压缩索引用)*/
    __u8   xattr_filter_reserved; /* xattr 过滤保留字段 */
};

22.2 EROFS 挂载流程

erofs_fill_super()  [fs/erofs/super.c]
    |
    +-- erofs_read_superblock()
    |     读取并验证超级块(magic、checksum)
    |     设置 sb->s_blocksize = 1 << blkszbits
    |
    +-- erofs_init_fs_context()
    |     解析挂载选项(compress、cache_strategy 等)
    |
    +-- z_erofs_init_zip_subsystem()(若启用压缩)
    |     初始化 workqueue(z_erofs_workqueue)
    |     初始化 pcluster slab
    |
    +-- erofs_init_devices()
    |     处理多设备(extra_devices > 0)
    |
    +-- erofs_iget(sb, ROOT_NID(sbi))
          读取根目录 inode
          erofs_fill_inode()  解析 compact/extended inode

23. F2FS 与 EROFS 源码调试技巧

23.1 F2FS 调试接口

debugfs 接口(需要 CONFIG_F2FS_STAT_FS):

mount -t debugfs none /sys/kernel/debug

# 查看 F2FS 统计信息
cat /sys/kernel/debug/f2fs/status
# 输出包含:
#   - Dirty segments/sections 数量
#   - GC 次数(foreground/background)
#   - NAT hit/miss 统计
#   - dirty pages 数量
#   - free sections/segments

# 触发 GC
echo 1 > /sys/fs/f2fs/sda1/gc_urgent_sleep_time  # 减少 GC 间隔
echo 1 > /sys/fs/f2fs/sda1/gc_idle              # 空闲时 GC

sysfs 接口

ls /sys/fs/f2fs/<device>/
# 包含:gc_min_sleep_time, gc_max_sleep_time, max_small_discards,
#        dirty_nats_ratio, cp_interval 等可调参数

23.2 EROFS 调试接口

挂载时调试选项

# 检查超级块 checksum
mount -t erofs -o checksum /dev/sda1 /mnt/erofs

# 显示详细挂载信息(dmesg)
mount -t erofs -o dax=always /dev/pmem0 /mnt/erofs-dax

# fscache 模式的调试
cat /sys/kernel/debug/fscache/stats
cat /sys/kernel/debug/fscache/cookies

构建 EROFS 镜像(mkfs.erofs 工具):

# 基本用法
mkfs.erofs output.erofs /path/to/source/

# 启用 LZ4 压缩
mkfs.erofs -zlz4hc,9 output.erofs /path/to/source/

# 启用 ZSTD 压缩(更高压缩比)
mkfs.erofs -zzstd output.erofs /path/to/source/

# 调试:打印布局信息
mkfs.erofs --dry-run -zlz4hc output.erofs /path/to/source/

23.3 通用文件系统调试工具

# 查看文件系统统计(statfs)
stat -f /mnt/f2fs
# 输出:Block size, Total blocks, Free blocks, Available blocks...

# inode 信息
stat /mnt/f2fs/file.txt
# F2FS 特有:检查 inline data
debugfs.f2fs /dev/sda1
> stat <ino>  # 显示 inode 详细信息(flags 包含 inline 标志)

# 压缩统计(F2FS)
ioctl(F2FS_IOC_GET_COMPRESS_OPTION) 获取压缩配置
ioctl(F2FS_IOC_DECOMPRESS_FILE) 解压指定文件

由 Claude Code 分析生成