f2fs:buffered overwrite 快路径跳过 inode folio 查询

f2fs 写路径 · prepare_write_begin · 读 extent 缓存先行 · 缓存命中免付节点页查询开销

💡 一句话总结

在「已有数据块的逻辑→物理映射已被 f2fs 内存中的读 extent 缓存记录」的覆盖写(buffered overwrite)场景下,f2fs 每次写都先取 inode folio(dnode 节点页)再查缓存,多付一次节点 folio 查询(页缓存 xarray 查找 + 锁 + inode 校验和 + sanity 检查)的固定开销;本补丁把「查读 extent 缓存」提前到取 inode folio 之前,缓存命中即直接返回块地址、整体跳过节点 folio 查询。作者在 QEMU/KASAN x86_64 VM 中测得 64 字节覆盖写中位耗时 1724.93 → 1560.24 ns/write(约 -9.6%)、256 字节覆盖写 1713.38 → 1577.85 ns/write(约 -7.9%),20k 次写中 f2fs_get_inode_folio() 调用从 20004 降到 4(作者自报,未独立验证)。

📋 补丁基本信息

项目内容
补丁类型优化(performance,热路径减开销)
性能类别热路径(fast path 减指令 / 减节点页查询开销)
状态状态(Merged)· 合入版本:Linux 7.2(git describe --contains 确认首个包含 commit 的 tag 为 v7.2-rc1)
当前版本主线单版本(commit 直接合入 f2fs 树)· 主线 commit 链接
版本演进仅主线单版本,暂无多版本演进。commit message 无 Link:/Message-ID trailer,本环境 lore 查询超时,未检索到对应公开 lore 补丁系列(如实标注,见"方案演进")
作者机构Wenjie Qi(小米 Xiaomi,Signed-off-by qiwenjie@xiaomi.com)· 合入人 Jaegeuk Kim(f2fs 维护者)
提交日期2026-05-29(2026-06-23 经 f2fs-for-7.2-rc1 合入主线)
改动范围fs/f2fs/data.c · +5/-0 行 · 1 文件
核心函数prepare_write_begin() / f2fs_lookup_read_extent_cache_block() / f2fs_get_inode_folio()
原始链接git.kernel.org commit · Kernel F2FS 文档

📊 速览卡片

核心机制
缓存先行
优化目标
减热路径开销
适用场景
覆盖写热缓存
特性等级
★★★★
实测提升
-9.6%

特性等级依据:有实测数字(覆盖写中位耗时 -8%~-10%,节点 folio 查询调用近零)→ 幅度分高;纯内核 5 行改动、无配置/硬件门槛、on-disk 格式不变 → 落地与兼容分高;但收益仅作用于"块映射已被读 extent 缓存记录"的覆盖写场景(需文件曾读/写过),非全负载普适 → 综合 ★★★★(解读(AI 分析))。

🎯 解决什么问题

背景 / 原始动机(commit message 原文)
prepare_write_begin() 首先获取 inode folio 并构建 dnode,之后才检查读 extent 缓存。对一个非 inline、非压缩文件的普通覆盖写,extent 缓存命中时已经给出了数据块地址,后续路径不需要分配或更新任何节点状态。因此在该窄场景下,取 inode folio 是纯浪费——作者把读 extent 缓存的检查提前到取 inode folio 之前,避免覆盖写快路径上的一次节点 folio 查询(commit message 原文:"This avoids a node-folio lookup in the buffered overwrite fast path when the mapping is already cached")。
系统层面:覆盖写快路径上的"冗余节点 folio 查询"
f2fs 的 buffered 写路径:f2fs_write_begin() → prepare_write_begin()(决定数据块地址 blk_addr)。旧代码在函数内先调用 f2fs_get_inode_folio()(= __get_node_folio(),读取 inode 自身的 dnode 节点页)构建 dnode_of_data,再查读 extent 缓存。

关键事实:当 extent 缓存命中时,旧代码用的也是缓存里的块地址(*blk_addr = ei.blk + index - ei.fofs),dnode 里查出来的地址根本没被用到——也就是说,取 inode folio 的整套开销都是冗余的。这套开销包括:f2fs_grab_cache_folio() 在节点地址空间的 xarray 查找、read_node_folio() 的读页流程、folio_lock()、f2fs_inode_chksum_verify()(对 4KB inode 块算校验和,CPU 成本可观)、f2fs_sanity_check_node_footer(),最后还要 f2fs_put_dnode() 释放——每次写都付一次。

为什么能这样优化:读 extent 缓存保存的是"逻辑块 → 物理块"的映射;命中即得物理块地址,且意味着该块已分配(空洞不会进缓存),不需要任何节点状态变更,因此可以直接返回。
场景层面:反复覆盖写"已读/已写过的文件区域"
执行路径:应用对已有文件的某个偏移发起 buffered 写(write/pwrite)→ 页缓存缺页 → f2fs_write_begin → prepare_write_begin 取块地址。
为什么该场景遇到缺陷:f2fs 的读 extent 缓存由读路径(F2FS_GET_BLOCK_PRECACHE → f2fs_update_read_extent_cache_range())和写路径(f2fs_update_data_blkaddr() → f2fs_update_read_extent_cache())共同填充。所以"文件块曾被读过或写过 → 映射已进内存缓存树 → 后续覆盖写命中缓存"是常态。旧代码对每一次覆盖写都付节点 folio 查询开销,即使块地址早已在缓存里。
典型负载:数据库小记录原地更新(B+树节点/槽位重写)、日志与预写日志(WAL/journal)热区域重写、fio 随机写、文件热区域反复修改、读后改写(read-modify-write)型工作负载。硬件配置放大:该开销是每写一次的固定 CPU 成本,写的频率越高、块越小,占比越明显(benchmark 中小块覆盖写收益更显著)。
受影响负载:小写(sub-page)随机/顺序覆盖写、WAL 与数据库原地更新 · 为什么此特性解决此场景:把"查缓存"从"取节点页之后"挪到"之前"→ 命中即返回,冗余节点 folio 查询整体跳过

🧩 核心机制

核心逻辑点只有一个:把"查读 extent 缓存"从 inode folio 查询之后提前到函数入口——对缓存命中的覆盖写,一次性取得物理块地址并立即返回,跳过整个节点 folio 查询路径。

从系统层面看
prepare_write_begin() 函数顶部新增一行 4 条件早退守卫:
!inline && !compressed && (pos & PAGE_MASK) < i_size && extent_cache_hit → return 0。

三个排除条件分别保护什么:inline 数据文件(写可能触发 f2fs_convert_inline_folio() 内联转换)、压缩文件(需压缩准备 f2fs_prepare_compress_overwrite())、页起点 ≥ EOF 的写(需 f2fs_reserve_block() 预留新块)——这三类仍需走旧路径拿"特殊处理",故明确排除。

为什么 (pos & PAGE_MASK) < i_size_read(inode) 是安全边界:f2fs 按 4KB 页分配数据块,含任何 < i_size 偏移的页,其数据块必已分配。因此对该页,extent 缓存命中返回的物理地址一定有效,不需要预留块。跨 EOF 的页起点 ≥ i_size 时被排除,避免在未分配区域走快路径。

正确性要点:早退路径只设置 *blk_addr,不设置 *node_changed(即调用方的 need_balance)。因为该路径没有修改任何节点状态,保持调用方初始化的 false 正是正确行为(f2fs_write_begin() 入口初始化 bool need_balance = false,已核实)。语义上与旧代码"extent 缓存命中"分支完全等价(旧分支同样用缓存地址 dn.data_blkaddr),只是省掉了 f2fs_get_inode_folio() → 构建 dnode → f2fs_put_dnode() 的整套动作。
f2fs buffered overwrite 快路径 before/after 流程图
图 1:Before/After 对比——左边旧路径:prepare_write_begin() 先 f2fs_get_inode_folio()(红框,冗余节点 folio 查询:xarray 查找 + 锁 + inode 校验和 + sanity)再查 extent 缓存;右边新路径:守卫条件满足后先查便宜的 extent 缓存,命中即得 blk_addr 并 return 0,f2fs_get_inode_folio() 整体跳过(灰框 ✗)。
来源:基于真实 commit diff(fs/f2fs/data.c)绘制
步骤操作目的
① 提前查缓存函数入口先判断 !inline && !compressed && (pos&PAGE_MASK)<i_size,再 f2fs_lookup_read_extent_cache_block(inode, index, blk_addr)命中即直接得物理块地址,跳过节点 folio 查询(前提)
② 命中即返回缓存命中时 return 0,need_balance 保持 false无节点状态变更 → 无需 balance,无需取 inode folio(主体)
③ 保留旧路径inline / 压缩 / 跨 EOF 写仍走原有取 folio + 转换/预留流程特殊路径行为不变,仅优化窄覆盖写场景(收尾)
关键代码片段
@@ -3719,6 +3719,11 @@ static int prepare_write_begin(struct f2fs_sb_info *sbi,
 	int flag = F2FS_GET_BLOCK_PRE_AIO;
 	int err = 0;
 
+	if (!f2fs_has_inline_data(inode) && !f2fs_compressed_file(inode) &&
+	    (pos & PAGE_MASK) < i_size_read(inode) &&
+	    f2fs_lookup_read_extent_cache_block(inode, index, blk_addr))
+		return 0;
+
 	/*
 	 * If a whole page is being written and we already preallocated all the
 	 * blocks, then there is no need to get a block address now.

▲ 这段改动是机制成立的关键:4 个条件把快路径严格限定在"普通覆盖写 + 块映射已缓存"的窄场景。f2fs_lookup_read_extent_cache_block() 命中时把 *blk_addr 填成物理块地址(ei.blk + index - ei.fofs)并返回真,函数立即返回 0——调用方 f2fs_write_begin() 拿到有效 blkaddr 后走 f2fs_submit_page_read() 读旧数据进页(部分页写需要 read-modify-write),行为与旧代码完全一致,只是省掉了取 inode folio。

📈 性能影响

提升角度(方法论分类)
主类为 on-CPU 计算效率 / 热路径开销降低:减少的是每写一次的固定 CPU 开销——节点 folio 在页缓存中的 xarray 查找、folio_lock()、inode 块校验和(CRC)计算、节点 sanity 检查、dnode 构建与释放。这是 Brendan Gregg 视角下的"减少计算量"(on-CPU),不涉及锁等待或 I/O 往返;改动也不增加任何 off-CPU 等待。

次要角度为 内存层次(解读(AI 分析)):免掉对 NODE_MAPPING(sbi) 节点地址空间的一次访问,避免该缓存行/页表项访问,对 CPU cache 与 TLB 略有正面影响。
受益场景
小尺寸、高频率的覆盖写(如 64/256 字节写):节点 folio 查询是固定开销,单次写越小、写频率越高,占比越突出。fio 随机写、数据库日志、WAL、sync_file_range 类频繁小写负载收益最明显。
文件曾读/写(映射已入 extent 缓存)是命中前提;文件块首次写(新分配)或缓存被逐出后,走旧路径,行为不变。
测试环境注意:作者用 QEMU/KASAN x86_64 VM 测量——KASAN 的内存检查会放大绝对耗时,但 before/after 是同一环境的相对比较,f2fs_get_inode_folio() 调用数从 20004→4 直接印证"查询被跳过"这一机制,趋势可信(解读(AI 分析))。
场景/用例运行环境改进前改进后
64-byte 覆盖写QEMU/KASAN x86_64 VM,已有 1MiB 文件1724.93 ns/write1560.24 ns/write(约 -9.6%)
256-byte 覆盖写QEMU/KASAN x86_64 VM,已有 1MiB 文件1713.38 ns/write1577.85 ns/write(约 -7.9%)
20k 次 64B 覆盖写函数级 profilingf2fs_get_inode_folio() 20004 次f2fs_get_inode_folio() 4 次

说明:以上数字均来自作者 commit message 自报("In a QEMU/KASAN x86_64 VM, using a small buffered overwrite workload on an existing 1MiB file, median time improved as follows..."),作者自报,未独立验证。百分比为按前值归一化计算((前-后)/前):64B ≈ 9.55%,256B ≈ 7.91%。KASAN 环境绝对耗时偏大,建议以相对提升与函数调用数变化为准。

buffered overwrite 中位耗时 before/after 对比图
图 2:覆盖写中位耗时对比(ns/write,越低越好)——64B 从 1724.93 降到 1560.24(↓9.6%),256B 从 1713.38 降到 1577.85(↓7.9%)。
来源:作者 commit message 自报数据(未独立验证),基于真实数字绘制

🔄 方案演进

本补丁为 f2fs 树直接合入的主线单版本,无可回溯的公开 lore 补丁系列(如实标注数据源局限):

单版本合入(无多版本演进)
主线 commit 222bc257a151(2026-05-29 提交)为首版即合入版本;2026-06-23 经 Merge tag 'f2fs-for-7.2-rc1'(merge commit 09ca8dc7d634)进入主线。
数据源说明:该 commit message 无 Link:/Message-ID trailer,无法直接定位到 lore 补丁系列;本环境 lore MCP 查询(lore_search/lore_find/lore_substr_subject/lore_count)多次超时,未检索到公开补丁系列与 review 讨论。因此版本演进与讨论细节不作编造,背景/动机以 commit message 原文为准。
社区参与证据
来自 commit message trailer:作者 Wenjie Qi(qiwenjie@xiaomi.com,小米),Reviewed-by: Chao Yu <chao@kernel.org>(f2fs 维护者),Signed-off-by: Jaegeuk Kim <jaegeuk@kernel.org>(f2fs 维护者,合入人)——经 f2fs 维护者常规 review 流程合入主线。

⚠️ 风险与局限

收益成立的前提
需读 extent 缓存命中:文件块映射必须已在内存 read extent 树中(文件曾读/写过、映射未因内存压力被逐出)。f2fs 的 extent 缓存有内存上限(默认 max_extent_cnt/逐出策略),缓存未命中或已被逐出时走旧路径,行为不变(no-op 安全)。
非 inline、非压缩、页起点 < i_size 是硬条件:inline/压缩/跨 EOF 写被明确排除在快路径之外,仍走旧流程,因此这三类场景零行为变化。
KASAN 环境放大绝对数字:benchmark 的绝对 ns/write 在真实硬件上会小很多,但相对收益趋势与"跳过查询"的机制相互印证。
生产落地影响
无新配置/开关/接口,on-disk 格式不变,行为变化集中在"缓存命中时查询顺序",对用户完全透明,升级无迁移动作。
正确性核心在 need_balance(*node_changed):早退路径不置位,依赖调用方 f2fs_write_begin() 初始化为 false——已核实该初始化存在,且该路径确实不改节点状态,故 f2fs_balance_fs() 不会被错误跳过(因为旧代码该场景下 dn.node_changed 也为 false)。
建议在升级后对小写随机覆盖写负载做一次延迟/吞吐冒烟测试验证收益,并观察 f2fs_get_inode_folio() 的 profile 计数是否如预期下降(解读(AI 分析))。
生态/兼容性
无用户态 API、无 sysctl、无 f2fs-tools 依赖变化,向后兼容。blk_addr/need_balance 均为函数调用参数与局部变量,不影响任何导出接口或挂载选项。
review 质疑(若有)
本环境 lore 不可用、未检索到该补丁的公开 review 回复(如实标注);commit 由 f2fs 维护者 Jaegeuk Kim 合入、Chao Yu Reviewed-by,说明常规 review 流程通过。
潜在讨论点(推测):① 早退路径不显式写 *node_changed,依赖调用方初始化为 false——这是"隐式依赖",若未来调用方改变初始化或新增"需平衡"语义需特别注意;② 读 extent 缓存陈旧性——新代码与旧代码命中时使用同一缓存数据源(ei.blk + index - ei.fofs),不引入新的陈旧暴露;既有 truncate/fallocate/punch-hole 路径通过 f2fs_update_read_extent_cache_range() 更新/失效缓存,该机制未受影响。
严重度:MINOR(无 CRITICAL/MAJOR 回归点;改动仅新增一条入口早退,被排除的三类场景路径不变)· 落地场景:最需关注的是「缓存命中 + 非 inline/压缩 + 页起点小于 i_size」这一窄条件的判定边界——写跨越 EOF 的页在页起点仍小于 i_size 时会走快路径,但其数据块已分配故行为正确

🔗 交叉引用

📌 关联工作
git.kernel.org commit 222bc257a151 — f2fs: skip inode folio lookup for cached overwrite — 本补丁主线 commit(fs/f2fs/data.c,+5 行)
git.kernel.org merge 09ca8dc7d634 — Merge tag 'f2fs-for-7.2-rc1' — 本补丁进入主线的 merge commit(2026-06-23)
The Linux Kernel documentation — Flash-Friendly File System (F2FS) — f2fs 官方文档:节点页(node page)、extent cache 与 buffered I/O 机制背景
git.kernel.org 当前 fs/f2fs/data.c — prepare_write_begin() / f2fs_write_begin() 所在源码文件(含本补丁后代码)
⚠️ 免责声明

本站内容均由 AI 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。