F2FS 文件系统:从 Wandering Tree 到 UFS 上的逆袭
F2FS 文件系统:从 Wandering Tree 到 UFS 上的逆袭
基于 Linux 5.15 内核源码学习整理(
Documentation/filesystems/f2fs.rst、fs/f2fs/)
一、背景:F2FS 是什么
F2FS(Flash-Friendly File System)由三星(Jaegeuk Kim 主导)于 2012 年设计,2013 年合入 Linux 3.8 主线,专为 NAND 闪存介质(SSD、eMMC、UFS、SD 卡)设计。
核心动机:NAND 闪存与机械硬盘特性完全不同——读写不对称(写慢读快)、擦除块粒度大、异地更新(不能原地覆盖,必须先擦除再写)、寿命有限(PE 次数限制)。传统 ext4 等基于"原地更新"假设的文件系统直接用在闪存上性能差、磨损大。
设计上它继承了经典 Log-structured File System (LFS) 思想(Rosenblum & Ousterhout, 1992):所有修改顺序追加写入,形成日志式结构。
二、先厘清概念:Journaling vs LFS
中文里"日志"一词翻译了两个完全不同的英文概念,极易混淆:
| 英文 | 含义 | 代表 |
|---|---|---|
| Journaling(日志记录) | 元数据操作先写"日记本"再落盘,崩溃时重放 | ext4、XFS、NTFS |
| Log-structured(日志结构) | 整个文件系统就是一个顺序日志,所有写追加 | f2fs、NILFS2、JFFS2/UBIFS |
ext4 有 journal,但不是 LFS。 二者是互斥的写模型:ext4 数据原地覆盖,LFS 数据异地追加。
三、LFS 的两大难题
1. Wandering Tree(游走树)
LFS 采用异地更新:任何修改都写成新块,旧块作废。对一个多级索引的文件做一次数据更新:
数据 A 写到新块 A' → 需要改 直接指针块 中的指针
直接指针块 也得异地写新块 → 需要改 间接指针块 中的指针
间接指针块 也得异地写新块 → 需要改 inode 中的指针
inode 也得异地写 → 需要改 inode map
inode map 也得异地写 → 需要改 checkpoint
一次数据更新引发整棵索引树级联重写,像"游走"一样扩散——这就是 Wandering Tree。后果:写放大(1 个数据块更新引发 5~6 次写)、空间浪费、加速 GC 负担。
Wandering Tree 属于哪类文件系统? 它是 LFS 家族(异地更新) 专属:f2fs、NILFS2、JFFS2/UBIFS。COW 系文件系统(btrfs/ZFS)有同源但不同名的"COW 写放大"。ext4 这类原地更新文件系统天然免疫——因为数据块地址不变,索引根本不用改。
2. Cleaning Overhead(清理开销)
异地更新产生大量失效块散布全盘,需要 GC 回收。回收过程(选段、加载索引、交叉引用检查、搬移有效数据)可能造成长延迟。
四、F2FS 的解法:NAT 斩断 Wandering Tree
f2fs 的核心设计:
- Node:统一指 inode 和各级指针块;
- NAT(Node Address Table):全盘唯一的
nid(node号) → 物理地址映射表。
关键转变:node 块之间的父子指针不再指向物理地址,而是指向 nid(逻辑号),物理地址经 NAT 间接查询:
旧方案(原版 LFS): 父块 ──物理地址──▶ 子块 (子块一挪,父块必改)
f2fs 方案: 父块 ──── nid ────▶ NAT ──▶ 物理地址 (物理挪动,父块不动)
数据更新只需改一个 NAT 表项(O(1)),而非整棵索引树(O(深度))。代价是 NAT 本身要维护、要 checkpoint 落盘。
本地源码锚点(Linux 5.15,fs/f2fs/):
| 组件 | 位置 |
|---|---|
| 查询 node 地址(核心) | node.c:546 f2fs_get_node_info() |
| 读 node 页 | node.c:1488 f2fs_get_node_page() |
| NAT 落盘 | node.c:3003 __flush_nat_entry_set() |
| Node Manager | f2fs.h:921 struct f2fs_nm_info |
五、F2FS vs ext4
| 维度 | ext4(原地更新+日志) | f2fs(异地更新+checkpoint) |
|---|---|---|
| 数据写 | 随机覆盖,地址固定 | 顺序追加,地址漂移 |
| 闪存写放大 | 大(小块随机写 → FTL 读改写) | 小(大块顺序写 → 匹配 FTL) |
| 冷热数据 | 不区分 | 冷热分离多日志 |
| 一致性 | jbd2 日志(记操作) | checkpoint 快照 + NAT |
| GC | 无 | 有(后台化、策略化) |
| 擅长 IO | 随机写/读平衡(磁盘) | 顺序写(闪存) |
六、F2FS vs 经典 LFS(第三代 vs 前两代)
| 经典 LFS 缺陷 | 前代表现 | f2fs 对策 |
|---|---|---|
| Wandering tree | 索引级联重写 | NAT 间接层切断 |
| Cleaning overhead | 回收长延迟 | 多日志(默认 6 条)+ 冷热分离 |
| GC 卡顿 | 前台 GC 阻塞 | 后台 GC + gc_merge |
| segment 浪费 | 碎片化 | SSR + 自适应分配 |
| 介质假设 | JFFS2 面向裸 NAND | 面向带 FTL 的闪存 |
| 挂载/恢复 | 扫描全盘慢 | checkpoint 快照快恢复 |
七、为什么 UFS 上 F2FS 优于 ext4
UFS 硬件特性:带 FTL 的 NAND;顺序写带宽是随机写的 3~10 倍;擦除-再写导致物理写放大;PE 寿命有限。
- ext4:应用随机小写 → 原地覆盖 → FTL 读改写整块 → 写放大 3~10 倍 → 性能差、磨损快。
- f2fs:回写攒成顺序大块 → 匹配 UFS 顺序写通道;冷热分离;discard/TRIM 及时释放 → 随机写场景性能提升约 2~3 倍、磨损显著降低。
实证:Pixel、三星旗舰等新机 data 分区默认 f2fs(Android 官方建议);三星 2012 年论文实测随机写较 ext4 提升约 2 倍。
边界(诚实评估):大文件顺序读、服务端长时间随机读场景两者接近;f2fs 需定期 GC,极端碎片下前台 GC 仍可能感知卡顿(可用 gc_merge/disable_roll_forward 调优)。
八、Android 文件系统演进史
裸 NAND 时代 eMMC+FTL 时代 UFS 时代
YAFFS2 (LFS) → ext4 (原地更新) → f2fs (协作式)
2008~2011 2011~2016 2016~至今
| 时代 | 介质 | 文件系统 | 逻辑 |
|---|---|---|---|
| Android 1.0~2.2 | 裸 NAND(MTD,无 FTL) | YAFFS2(LFS) | 闪存管理自己做,LFS 是唯一合理选择 |
| Android 2.3~6 | eMMC(内置 FTL) | ext4 | FTL 把闪存伪装成块设备,LFS 价值被吸收,ext4 成熟稳定成最优解 |
| Android 7+ | eMMC/UFS | f2fs | FTL 兜不住随机小写(写放大 3~10x),f2fs 在 FS 层攒顺序写,与 FTL 分工 |
当时为何不选其他 LFS? JFFS2/UBIFS 面向裸 NAND+MTD(eMMC 有 FTL 无意义);NILFS2 是 HDD 向且不稳定;f2fs 2012 年才发布,Android 换 ext4 时(2011)它还不存在。
九、总结
F2FS 的本质:用"异地顺序写 + NAT/checkpoint"换取闪存友好的写性能,再用间接层消化掉 LFS 自己的副作用(Wandering Tree、GC 开销)。它在 UFS 上优于 ext4,是因为 UFS 的顺序写快、随机写惨、写放大伤寿命,而 f2fs 恰好踩在 UFS 的强项上,ext4 恰好命中它的死穴。
本文是 Linux 文件系统学习系列的一部分,基于 Linux 5.15 内核源码(本地 fs/f2fs/)整理。

浙公网安备 33010602011771号