关注了就能看到更多这么棒的文章哦~
The long path toward optimizing short reads
By Jonathan Corbet
October 30, 2025
Gemini flash translation
https://lwn.net/Articles/1043640/
内核的文件I/O子系统多年来一直得到高度优化,旨在为各种工作负载提供最佳性能。然而,有一种工作负载类型在当前内核中表现不佳:即多个进程从同一文件执行大量短读取的应用。Kiryl Shutsemau 一直在努力开发一个补丁,试图优化这种情况,但这项任务正变得比预想的要困难。
正如 Shutsemau(也被称为 Kirill Shutemov)在这个相对较短的补丁的更新日志(changelog)中解释的,执行文件读取的一个步骤是找到页缓存(page cache)中相关的 folio(页帧),并获取该 folio 的引用,以确保在数据复制回用户空间(user space)时其保持稳定。然而,在短读取频繁的情况下,这种引用计数(reference-count)操作成为总成本的重要组成部分;原子操作(atomic operations)开销很大,而且在系统中反复刷新 folio 的缓存行(cache line)会使情况变得更糟。这类工作负载的性能会因这种开销而明显降低。
他着手通过为短读取添加一个新的快速路径(fast path)来降低这种性能开销。第一步是添加一个新的序列计数器(sequence counter)(到 `address_space` 结构中),用于跟踪从页缓存中删除的给定文件的 folio 数量。短读取(在此补丁中定义为小于 1KB)随后被导向一个快速路径,该路径的工作原理是:在将要读取的数据从 folio 复制到内核栈(kernel stack)上的缓冲区(buffer)之前,记录下该序列计数。如果在复制完成后,序列计数没有改变,数据就可以安全地从该缓冲区复制回用户空间;否则内核将回退到较慢的路径。
这种优化似乎有效。一项针对单个小文件的短读取的基准测试结果显示,性能提高了近三倍;其他测试则显示出更适度的改进。强制性的内核编译基准测试——这显然不是此补丁试图优化的工作负载类型——性能提高了约 1%。从表面上看,这个补丁似乎能带来显而易见的好处,并且可以相当容易地通过审查过程,但内存管理(memory management)和文件I/O(file I/O)是微妙的领域,充满了潜在的危险。
使用 1KB 基于栈(stack-based)的缓冲区在过去会引起无数质疑。内核栈不再像以前那么小,但 1KB 仍然是其中相当大的一部分。Linus Torvalds 建议将最大“短读取”大小降至 768 字节,但 Shutsemau 回应说,这种类型的读取只发生在短调用链(call chain)的末尾,因此发生栈溢出(stack overflow)的风险很小。Andrew Morton 也质疑了“1K 猜测或玩笑”的截止点,称一个小得多的缓冲区可能就能处理大多数相关情况。他还想知道是否有可能将数据直接复制到用户空间,而不是通过内核栈进行缓冲;Torvalds 回答说,如果在操作过程中相关 folio 被重用,这样做可能会暴露敏感数据。
另一种批评来自 Dave Chinner,他担心如果这些短读取之一与在同一文件中“打洞”的 `fallocate()` 操作发生竞态(race)会发生什么。`fallocate()` 被指定为原子性的(atomic),这意味着用户空间将看到文件在操作之前或之后的内容,但不会看到中间结果。他说,对新序列计数的检查不会注意到 `fallocate()` 造成的更改,如果数据在被 `fallocate()` 同时修改时被复制以满足读取请求,这可能会导致“短暂的数据损坏事件(transient data corruption event)”。
Torvalds 驳回了这些担忧,将 `fallocate()` 的原子性保证描述为“一个睡前故事”,并断言实现这种行为会代价过高。Chinner 回应说,这种情况与 `truncate()` 相同,在 `truncate()` 中,内核社区将部分结果的暴露视为一个严重的错误。他说,问题不仅仅是暴露部分结果;在 `FALLOC_FL_COLLAPSE_RANGE` 操作的情况下,一个错误的时机读取(badly timed read)可能会返回文件中从未存在过的结果,甚至连部分结果都没有。关于性能,他说 XFS 实现了必要的锁定(locking)来防止这种竞态,即使有了 Shutsemau 的补丁,因此为 `fallocate()` 提供指定的保证不一定慢。
Chinner 也建议不要增加任何参与 `truncate()` 实现工作的开发人员的“认知负担”,因为 `truncate()` 长期以来被认为是文件系统中最难正确实现的操作之一。他问道:“这种小众(niche)工作负载的收益,真的值得为本已非常复杂的行为和交互集增加额外的复杂性吗?” Hugh Dickins 表达了同样的观点,但 Torvalds 表示,页缓存的其他部分会做“*更*可怕的事情”,而且将已知工作负载的性能提高三倍不一定是“小众”的。David Hildenbrand 回应说,请求将此更改推迟到计划中的 folio 释放方式(使用读-复制-更新,read-copy-update)的工作完成后再进行。
讨论暂时平息了,至少目前如此;如果 Shutsemau 在解决了审查中指出的其他一些问题后重新发布补丁,讨论很可能会继续。这项工作以及围绕它的讨论表明了内存管理(memory management)与虚拟文件系统层(virtual filesystem layer)交叉点的微妙之处;每个子系统本身就足够复杂,将它们结合在一起并不会使事情变得更简单。这就是为什么此类更改很难达到接受标准;因为太容易破坏现有功能。因此,这项特定优化能否被接受并进入主线(mainline)还远未确定,但无论如何,这都是一个有趣的实践。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

1万+

被折叠的 条评论
为什么被折叠?



