LWN:get_user_pages() 仍在带来麻烦!

关注了就能看到更多这么棒的文章哦~

The ongoing trouble with get_user_pages()

By Jonathan Corbet
May 4, 2023
DeepL assisted translation
https://lwn.net/Articles/930667/

2018 年的 Linux 存储、文件系统和内存管理(LSFMM)会议包括一个关于 get_user_pages()的环节,这个接口是内核内部的一个接口,但是在某些情况下的使用方式会导致数据损坏或内核崩溃。随着 2023 年 LSFMM+BPF 活动的临近,这个问题仍然没有解决,也仍然是人们持续讨论的一个话题。来自 Lorenzo Stoakes 的一组 patch 就是又一次尝试一个部分解决方案,这也是最近的主要关注点。

The problem

get_user_pages() API 有很多变种;这一系列 API 通常被简称为 "GUP"。它的用途就是让内核可以直接访问用户空间内存;这需要确保相关 page 驻留在 RAM 中,并且在需要访问的时候不可以出于被驱逐的状态(evicted, 意思是说不在 RAM 里了,比如放到了 swap 里)。get_user_pages()的问题的根源在于可能导致出现一种新情况,就是有两条独立的路径可以访问到相关的内存。

用户空间进程通过在其页表中映射的虚拟地址来访问其内存。这些地址只在拥有该内存的进程中有效。当内核需要暂时限制对用户空间内存中的特定区域的访问时,page table 可以提供一个很方便的处理方法。常见的例子是把已经被修改的文件内容 page 写回到持久存储中去。文件系统会把这些 page(在相关的页表中)标记为只读,在回写过程中就能阻止被继续修改掉了。如果属主进程试图继续写入这些 page 的话,会保持被阻止,直到回写完成之后。到那时,只读保护会触发 page fault,文件系统就可以收到这个通知,了解到该 page 的内容被再次修改过了。

对 get_user_pages()的调用将返回指向内核 page 结构的指针,这些 page 结构代表了存放用户空间内存内容的物理 page,这个函数调用就可以用来获得这些 page 的内核虚拟地址。这些地址在内核的地址空间中,通常是在内核的直接映射(direct map)中,涵盖了所有的物理内存(在 64 位系统中是这样)。它们跟用户空间的地址不一样,因此访问控制也不一样。只要是不包含可执行 text 内容的直接映射的内存(几乎)一直可以被内核写入。

用户空间可以使用 mmap()将一个文件 map 到它自己的地址空间,创建一系列对应了文件内容的(file-backed)page。这些 page 最初会被标记为只读,哪怕它们在做映射上来打算进行写入的。这样以来,当其中一个 page 被改变时,文件系统可以收到通知。如果内核使用 get_user_pages()来获得对文件内容对应 page 的写入权限,底层文件系统将被相应地通知这些 page 已经变成可写的了。在未来的某个时候,该文件系统会将这些页面写回持久存储,再将其改为只读。不过,这种保护性的改动只适用于这些 page 的用户空间地址。内核地址空间中的映射仍然是可写的。

问题就出在这里:如果内核在文件系统写回这些 page 之后再对内核映射的地址空间进行写入的话,文件系统不会知道这些 page 已经改变了。内核代码可以将这些 page 标记为 dirty page,当一个看似只读的 page 被写入时,可能会导致文件系统层面出现意料之外的结果。另外还有一些情况,尽管内容已经被写入,但这些 page 可能永远不会被标记为 dirty page,在这种情况下,写入的内容可能永远不会出现在磁盘上。无论哪种情况都不是什么好事。

这个问题一直是一系列 LSFMM 讨论的主题,也同样是 LWN 文章中无休止的一个主题,但这并不是一个容易解决的问题。有些时候内核确实是需要访问用户空间的内存,通常是为了性能方面。一个经常重复的例子是使用 RDMA 直接向文件内容对应的 page 读取数据。允许具有 DMA 功能的设备直接将数据写入用户空间的 page 就需要把该 page 锁定(pin)在内存里,也许要 pin 很久。事实证明,要找到一种可靠的方法来实现这种写入用户空间的通道是很困难的。

A partial solution?

在四月下旬的时候,Stoakes 决定针对这个问题中的一部分发布了一个 patch,直接禁止那些对文件内容对应的 page 申请写入权限的 get_user_pages()调用。但他认识到,有些情况还是真的需要这种映射方式,因此他还包括一个新的 flag,即 FOLL_ALLOW_BROKEN_FILE_MAPPING,从而绕过这个限制;一些 InfiniBand 控制器驱动就改为使用该 flag 了。Stoakes 说,做出这一改动 "是朝着获得更可靠的 GUP 迈出的重要一步,并明确指出哪些调用位置可能会在后续暴露出问题"。

在接下来的一周左右,该系列经历了几次修订。最重要的改动也许是放弃了 FOLL_ALLOW_BROKEN_FILE_MAPPING flag,而是只禁止提供 FOLL_LONGTERM 标志的 get_user_pages() 调用(并且是对文件支持的 page 进行写访问的情况下),这个 flag 表明该映射可能会持续很长一段时间。短时间的 mapping 并不是不会出现这个问题,但由于是短期映射,它们触发这个问题的可能性要小得多。这一改动相当于承认仍然不可能完全解决(或者说阻止)这一问题。

这一提议引发了相当多的讨论。克里斯托夫-赫尔维格(Christoph Hellwig)担心这会破坏那些使用 direct I/O 写入文件内容对应映射区域的用户,但杰森-冈索普(Jason Gunthorpe)质疑是否真的有这样的使用情况,他说尝试过的人 "在测试中应该不用多久就会发现数据损坏和内核崩溃"。David Hildenbrand 则建议,一些虚拟化的环境可能会因这一改动而被破坏;Gunthorpe 再次表示怀疑现在是否有任何此类用例可以成功运行。

Hildenbrand 对这个 patch 还有一些担心,包括它没有解决全部的问题:"如果我们想把它作为一个安全的东西来推销,我们必须把它 完全 屏蔽掉,然后再 CC 稳定下来。其他一切方案在我看来都是一个应急的创可贴而已。"。他抱怨说,它没有解决 get_user_pages()API 的 "GUP-fast" 这一系列 API 的问题,不过 Stoakes 后来修复了这个遗漏点。他建议把这个话题带到今年的 LSFMM+BPF 会议上(5月 8 日开始),这会是一个合理的下一步动作。

Ted Ts'o 描述了一个由这个问题导致的 ext4 错误;文件系统在意料之外的情况下看到 page dirty 就会可能导致崩溃。一个相关 fix 被合并到 5.18 中以防止 crash,但 Ts'o 说,这可能不是正确的解决方案,因为它 "显然会让人们没有动力去真正真正解决这个问题,而把它掩盖了起来"。他表示,在文件系统开发者看来,通过 get_user_pages() 来对文件内容对应 page 进行写入就是一个错误,"you get to keep both pieces"。

Gunthorpe 认为 Ts'o 的信息就是阻止对文件内容对应 page 进行写入的又一个理由:

仅仅这一点就足以成为阻止这种写入的理由。我已经厌倦了这样的轮回,我认为我们应该结束了,mm 会努力去保证这个观点落实。文件只能通过 PTE 来写入。如果这让人们不高兴,他们可以努力去 fix,但至少我们就不会再有这些内核崩溃问题以及内容不一致的问题要处理。

不过,人们仍然没有完全同意这个观点,也不同意现有的这个部分方案被合并到 mainline 里。主要是担心它最终会破坏用户空间的应用,或者担心合并相对容易的 fix 方案会导致完整解决方案就遥遥无期了,这些担心不会轻易消失。因此,似乎逃不过再一次 LSFMM+BPF 讨论了;事实上,Stoakes 似乎很期待这个会议: "我认为在 LSF/MM 的讨论也是一个明智的想法,而且你知道,买些啤酒就能很好地解决这些问题了:] "。因此,这个长期的讨论看起来暂时尚未结束。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

format,png

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值