基于内核源码
fs/f2fs/、fs/erofs/、include/linux/f2fs_fs.h编写。 分析的内核版本:master 分支(合并至 nfsd-7.0-2 标签)。
- F2FS 概述与设计目标
- F2FS 磁盘布局
- F2FS 核心数据结构
- F2FS Node 与 Data 分离体系
- F2FS 日志结构写入与温度分类
- F2FS 垃圾回收(GC)机制
- F2FS 检查点(Checkpoint)机制
- F2FS 原子写与 Volatile 写
- F2FS 透明压缩
- EROFS 概述与只读设计理念
- EROFS 磁盘布局
- EROFS inode 格式
- EROFS 数据布局类型
- EROFS 压缩体系
- EROFS fscache 网络后端
- F2FS 与 EROFS 对比分析
F2FS(Flash-Friendly File System)由三星电子于 2012 年开发,针对基于 NAND Flash 的存储设备(eMMC、UFS、SSD)进行了深度优化。其核心设计思路来自于日志结构文件系统(Log-Structured File System, LFS),但针对 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 在日志结构的基础上引入了以下关键优化:
- 多头日志(Multi-head Logging):将数据按"冷热"分类写入不同日志,减少 GC 时的数据迁移量。
- 自适应日志(Adaptive Logging):在空间充裕时使用 LFS 顺序写,在空间紧张时回退到 SSR(Slack Space Recycle)模式进行随机覆盖写。
- Node Address Table(NAT):引入间接寻址层,解耦 inode 的物理位置与逻辑地址,使得文件系统迁移数据时不需要更新上层 inode。
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.h,struct 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 */F2FS 超级块位于磁盘偏移 1024 字节处(#define F2FS_SUPER_OFFSET 1024,include/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 设备预留)
每个 Segment 在 SIT 中对应一个 f2fs_sit_entry(include/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。
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 的上层结构。
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)
+-----------------------+
struct f2fs_sb_info(fs/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];
/* ... */
};/* 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 */
};/* 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 */
/* ... */
};/* 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;
};F2FS 的重要设计决策之一是将 Node 块和 Data 块分开管理,分别写入不同的日志流。
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 /* 节点偏移起始 */
};磁盘 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) /* 二级间接节点 *//* 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 ≈ 1018,NIDS_PER_BLOCK ≈ 1018。
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
/* 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) /* 压缩数据标记 */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 挂载选项控制)。
| 日志类型 | 写入的数据种类 | 更新频率 | 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 2621440F2FS 支持两种写入模式,通过 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 磨损。
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,
};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;
}F2FS 实现了三种 victim 选择策略:
选择有效块数量最少的 Segment(迁移代价最小):
/* fs/f2fs/gc.c: line 408 */
if (p->gc_mode == GC_GREEDY)
return get_valid_blocks(sbi, segno, true);综合考虑有效块比例和 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,即"越空越老"越优先。
基于 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_management(fs/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; /* 年龄阈值 */
};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;
}/* 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 */
};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)
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 字段,选取较新的有效版本
/* 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 被禁用 */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
当 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 彻底删除。
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 的快速恢复。
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 0x00000004Volatile 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) 才实际持久化。
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 */压缩以 Cluster 为单位,一个 Cluster 包含 2^compress_log_size 个页(MIN_COMPRESS_LOG_SIZE=2,MAX_COMPRESS_LOG_SIZE=8)。
Cluster(假设 log_size=2,即 4 页)
+-------+-------+-------+-------+
| Page0 | Page1 | Page2 | Page3 | <- 原始数据(4 * 4KB = 16KB)
+-------+-------+-------+-------+
压缩后
+----------+----------+
| CPage0 | CPage1 | <- 压缩后(例如只需 2 个页)
+----------+----------+
COMPRESS_ADDR 作为前两个 Block 的地址标记(表示 cluster 头)
/* 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))/* 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 */EROFS(Enhanced Read-Only File System)由华为于 2017 年开发,主要针对 Android 系统分区、容器镜像、嵌入式只读文件系统等场景。与 F2FS 完全相反,EROFS 的核心设计原则是静态布局、无写入、无日志。
- 零写放大:完全消除写操作,Flash 寿命理论上无限(对系统分区而言)。
- 布局简单:无需事务机制、无 journal/log、无 GC,实现复杂度极低。
- 高读性能:数据布局在 mkfs 时即可全局优化,例如将热文件连续排布、将小文件内联到 inode 中减少 I/O 次数。
- 数据去重:只读特性天然适合数据去重(deduplication),相同内容的文件只需保存一份。
- fscache 网络后端:EROFS 可作为网络文件系统客户端,通过 fscache 将远端镜像按需拉取并缓存,特别适合容器和 OTA 场景。
兼容特性(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 /* 元数据压缩 */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 |
+------------------+
/* 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);EROFS 通过 NID(Node ID)定位 inode,NID 本质上是 inode 相对 metadata area 起始处的字节偏移 >> islotbits(fs/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 大小)。
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 *//* 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 纳秒值,以节省空间。
/* 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);struct erofs_inode(fs/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;
};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;
}/* 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 */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
};文件数据连续存放于磁盘,inode 中的 startblk 指定起始 block:
inode.startblk ──> [ Block 0 | Block 1 | Block 2 | ... ]
文件数据顺序存放
适合中大型文件,I/O 路径简单,访问连续。
文件数据的末尾"尾部"(tail)内联到 inode 所在的 block 中,前面部分与 Flat Plain 相同:
metadata block 中的 inode:
+------------------+----------------------------+
| inode (64 bytes) | tail data (≤ block - 64B) |
+------------------+----------------------------+
前置数据(如果有):
inode.startblk ──> [ Block 0 | Block 1 | ... ]
适合小文件(整个文件可以内联)或大文件的最后一个不完整 block。
文件被划分为固定大小的 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 存放在不同物理设备上,实现跨设备文件系统。
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);
}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;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) | | | |
+----------+ +----+----+
EROFS 的压缩索引(z_erofs_lcluster_index,fs/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, /* 压缩簇头(第二类,双算法)*/
};对于 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;
}对于 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 ... */
}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);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 正确解压的前提。
EROFS on fscache(代号 erofs-ondemand)是 Linux 5.19 引入的特性,允许 EROFS 文件系统镜像存放在远端服务器(HTTP/HTTPS/P2P 等),内核通过 fscache/cachefiles 机制按需拉取数据并本地缓存。这个特性对容器场景极其重要:
- 容器启动时只拉取实际访问的文件内容,而不是整个镜像层,显著加快启动速度。
- 配合 overlay 文件系统,实现不可变基础层 + 可读写上层的容器文件系统。
struct erofs_fscache(fs/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 */挂载选项示例:
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 路由。
struct erofs_device_info(fs/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 字段指定数据位于哪个后端设备。
| 维度 | F2FS | EROFS |
|---|---|---|
| 读写模式 | 可读写 | 只读 |
| 日志机制 | 日志结构(LFS) | 无日志 |
| GC 机制 | 有(后台+前台 GC) | 无 |
| 检查点 | 双 CP,定期写入 | 无 |
| 原子写 | 支持(COW inode) | 不适用 |
| 压缩 | 透明压缩(簇粒度) | 透明压缩(pcluster 粒度) |
| 网络后端 | 不支持 | 支持(fscache/ondemand) |
| 典型应用场景 | 手机用户数据分区、SSD 存储 | Android 系统分区、容器镜像 |
| 实现复杂度 | 高(GC+CP+原子写+压缩) | 低(读路径+压缩解压) |
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 字节
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 混合
+--------------+
| 比较项 | F2FS | EROFS |
|---|---|---|
| 压缩粒度 | Cluster(2^N 个页面) | pcluster(可变大小) |
| 压缩算法 | LZO/LZ4/ZSTD/LZORLE | LZ4/LZMA/DEFLATE/ZSTD |
| 索引结构 | COMPRESS_ADDR 标记 | lcluster_index(紧凑/全量) |
| 解压触发 | 读取 page 时 | 读取 page 时 |
| 尾部内联 | 不支持 | 支持(ztailpacking) |
| 写路径压缩 | 支持(透明写入) | 不支持(只读,mkfs 时压缩) |
| 数据去重 | 不支持 | 支持(fragment) |
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 分析生成
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 区域
类似地,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() 将其转换为普通目录。
扩展属性(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 个)。
/* 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),由写入路径直接触发。
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;
}选中 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(保证映射关系持久化)
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。
优势一:启动速度提升
- 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 开销为零)
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 进行内部垃圾回收。
在容器技术(如 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
测试环境: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 优化小文件)
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 压缩数据可以直接缓存在页缓存中,后续访问命中页缓存时 无需再次解压,实现了"解压一次,缓存永久有效"的语义。
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 全部进入页缓存,后续访问零开销
EROFS 解压任务通过 z_erofs_decompress_queue(fs/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() 通知等待的读取者
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 的版本号,选择较新的那个。
/* 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);
}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 命令,性能最好但依赖硬件可靠性。
/* 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 过滤保留字段 */
};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
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 # 空闲时 GCsysfs 接口:
ls /sys/fs/f2fs/<device>/
# 包含:gc_min_sleep_time, gc_max_sleep_time, max_small_discards,
# dirty_nats_ratio, cp_interval 等可调参数挂载时调试选项:
# 检查超级块 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/# 查看文件系统统计(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 分析生成