F2FS 文件系统:从 Wandering Tree 到 UFS 上的逆袭

F2FS 文件系统:从 Wandering Tree 到 UFS 上的逆袭

基于 Linux 5.15 内核源码学习整理(Documentation/filesystems/f2fs.rstfs/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/)整理。

posted @ 2026-08-04 12:25  zenwater  阅读(1)  评论(0)    收藏  举报