关注了就能看到更多这么棒的文章哦~
The intersection of unstable pages and direct I/O
By Jonathan Corbet
November 12, 2025
Gemini flash translation
https://lwn.net/Articles/1045006/
LWN 的资深读者想必以前就接触过“稳定页(stable pages)”这个概念;它最初在近15年前被报道。在大多数情况下,稳定页旨在解决的问题——即防止用户空间(user space)在I/O进行中修改缓冲区(buffer)时发生错误——已经完成了正确的处理。但最近的讨论表明,仍有一个领域存在问题:直接I/O(direct I/O)。不过,关于这些问题是用户空间错误(user-space bugs)造成的,以及为解决这些问题应该付出多大的性能代价,存在一些分歧。
将一页数据写入块存储设备(block storage device)需要时间。如果一个进程(process)告诉内核(kernel)执行写入操作,那么在操作完成之前会经过一段时间。如果该进程在I/O进行中修改了正在写入的数据,结果就会出现一种数据竞争(data race),导致通常不可预测的结果。最终可能写入旧数据或新数据;在最坏的情况下,旧数据和新数据的组合可能会被写入,从而破坏文件。
在简单的情况下,这种行为的麻烦程度低于其表面看起来的样子;如果应用程序(application)正在修改I/O中的页,它很可能会在稍后再次将它们写入持久存储(persistent storage),最终结果会如预期那样。但如果,例如文件系统(filesystem)在数据旁边写入校验和(checksums),在写入过程中更改数据可能会导致最终的校验和不匹配。其他在I/O期间进行的处理,例如透明压缩(transparent compression)或RAID实现,也可能遇到类似的问题。在这些情况下,对I/O中的页进行一次时机不当的更改,可能导致I/O错误(I/O errors)或数据损坏(data corruption)。
解决这个问题的方案通常是稳定页(stable pages)——内核承诺在数据进行I/O时不会被更改。稳定性可以通过例如简单地延迟任何修改数据的尝试,直到I/O操作完成来实现。对于缓冲I/O(buffered I/O),数据通过页缓存(page cache)进行复制,实现这种方法相对简单,并且已在多年前完成。然而,延迟内存访问(memory access)会降低性能,因此通常只有在明确需要时才这样做。
然而,直接I/O(direct I/O)是一个特殊情况。当一个进程请求直接I/O时,它希望在自己的缓冲区和底层存储设备之间直接传输数据,而无需经过页缓存(page cache)。由于稳定页与页缓存紧密相关,直接I/O操作无法使用它们。直接I/O是一种相对高级、低级的操作,应用程序负责管理自己的缓冲区,因此通常未曾发现并发数据更改引起的问题。不过,越来越多的人对将直接I/O与文件系统完整性功能,或与数据完整性字段(data integrity field,也称为“保护信息”或“PI”)等硬件功能结合使用感兴趣,这些功能提供了端到端校验和保护。
2025年5月,Qu Wenruo 发布了一个补丁(patch),描述了当用户空间(user space)对启用校验和的Btrfs文件系统执行直接I/O,然后在I/O进行中修改其缓冲区时出现的问题。如上所述,这可能导致校验和错误。显然,QEMU可以配置为使用直接I/O,并且容易发生这类故障。最终采用的解决方案(并合并到 6.15 版本中)是,在使用了校验和的文件系统上,简单地回退(fall back)到缓冲I/O。这解决了问题,但代价沉重;直接I/O现在与Btrfs文件系统上的校验和完全不兼容,并且依赖直接I/O来提高性能的应用程序将相应地变慢。目前正在考虑一个类似的补丁,以便在读取时也回退到缓冲I/O,因为对正在读取的缓冲区进行并发修改(concurrent modifications)可能导致校验和失败。
Btrfs 的改动是在没有太多宣传的情况下应用的;Christoph Hellwig 的补丁向 XFS 添加了类似的回退(fallback)机制,却引起了更多的关注。XFS 不像 Btrfs 那样支持数据校验和,但块设备(block devices)可以执行自己的校验和计算;它们需要稳定页才能成功完成这项工作。如前所述,稳定页不适用于直接 I/O,因此,在底层块设备需要稳定页的情况下,Hellwig 的补丁会导致 XFS 回退到缓冲 I/O,即使已经请求了直接 I/O。他对由此产生的性能损失(performance penalty)表示不满,并为“知道自己在做什么”的应用程序提出了一些禁用此回退的方法。
Bart Van Assche 提出了一个替代建议:“只对在 I/O 完成前修改直接 I/O 缓冲区的有缺陷软件(buggy software)回退到缓冲 I/O”。Dave Chinner 也质疑了这个补丁,同样建议只对“已知会损坏基于稳定页的块设备”的应用程序强制使用缓冲 I/O,他怀疑这类应用程序相对较少。Geoff Beck 建议设置页权限(page permissions)以防止内存在直接 I/O 期间被更改。
Hellwig 对这些建议的回应突显了围绕这个话题的分歧核心。在I/O进行中修改缓冲区,对应用程序来说是可以接受的行为吗?Hellwig 反复坚持认为这确实可以接受:“鉴于我们从未声明你不能修改缓冲区,我不会称之为有缺陷(buggy),即使这种行为令人不快。”他表示,并发修改(concurrent modification)在许多情况下都有效;他的目标是让它在其余情况下也能工作,即使以严重降低I/O性能为代价。
然而,Chinner 却坚决认为内核不应该尝试支持执行此类修改的应用程序:
记住:O_DIRECT 意味着应用程序需对正确实现 I/O 并发语义(IO concurrency semantics)负全责。当硬件正在读取或写入 I/O 缓冲区时修改这些缓冲区,一直都是应用程序中的 I/O 并发错误(IO concurrency bug)。
这里讨论的行为,过去是,现在也一直是应用程序的 I/O 并发错误,无论是否存在 PI、稳定写入等。存在这样的应用程序错误 *不是内核或文件系统禁用直接 I/O 的有效理由*。
然而,Hellwig 重申,内核从未有过任何文档要求在直接I/O进行期间缓冲区不能被触动:“我们不能在20多年后才编造出这样的要求。”他还表示,在RAID设备上,修改正在写入的数据可能会损坏整个条带(stripe),并可能影响与问题应用程序无关的数据。在那时,他说,这就会成为一个内核错误,可能让恶意用户(malicious users)破坏他人的数据。Jan Kara 表示,他同意并发修改I/O缓冲区是一个应用程序错误,但他补充说,内核无论如何都需要对此问题采取一些措施。
Darrick Wong 专门针对 PI(保护信息)案例(即硬件验证软件提供的校验和)指出,在大多数情况下,应用程序通过修改直接 I/O 中的缓冲区来损坏自己的数据,而这种损坏不会被硬件检测到。在这些情况下,没有必要回退到缓冲 I/O。然而,RAID 的情况更糟,必须加以预防。Wong 建议添加一个新的块设备属性(block-device attribute),名称为 “mutation_blast_radius”,描述并发修改的严重程度,并仅在最严重的情况下才回退。然而,正如 Hellwig 指出的,该解决方案只解决了 PI 案例,并且会导致 QEMU 失败。
虽然这次讨论没有达成确凿的结论,但显然,无论如何看待应用程序修改正在进行 I/O 的数据的行为,内核可能都需要做些什么,至少要阻止最严重的问题发生。真正的问题将是,是否有可能为行为良好的应用程序提供一种方法,使其避免缓冲 I/O 的额外开销(extra overhead),同时又不会为恶意行为者(malicious actors)创造机会。最终可能结果是,某些类型的存储设备将根本无法在直接 I/O 模式下使用。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

1万+

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



