赞
踩
关注了就能看到更多这么棒的文章哦~
By Jonathan Corbet
September 5, 2022
DeepL assisted translation
https://lwn.net/Articles/906852/
内核在很多方面都有让人惊讶的可扩展性,但是在内存管理子系统中就有个长期存在的痛点,一直在阻碍所有消除此痛点的尝试,这就是 mmap_lock。在 2022 年的 Linux 存储、文件系统、内存管理和 BPF 峰会(LSFMM)上,这个锁不可避免地拥有着自己的话题,其中提出了使用 per-VMA lock 的想法。Suren Baghdasaryan 已经发布了 patch 来实现了这个想法,但是对这些 lock 的实现方式有一个有趣的变动。
mmap_lock(以前叫 mmap_sem)是一个控制对进程地址空间的访问权限的的 reader/writer
锁;在对地址空间进行改动之前(例如想要 map 一个新的地址区域),内核就必须先获取这个 lock。page fault 的处理也必须先获取 mmap_lock(在 reader 模式下),以确保地址空间在 fault 处理完毕时不会出现一些让人意外的改动。一个进程可能有非常大的地址空间,以及许多同时运行的线程(并产生 page fault),从而 mmap_lock 就变成了一个重要的瓶颈。即使 lock 本身没有冲突,这里不断产生的 cache-line bouncing 也会损害性能。
有许多尝试希望解决 mmap_lock 可扩展性问题,它们都采取了 speculative page-fault handling 的方式,这样解决 fault 的工作是在不使用 mmap_lock 的情况下完成的,希望在此期间地址空间不会发生变化。如果发生了 concurrent access,那么这些 speculative page-fault handling 代码将放弃它所做的工作,并在取得 mmap_lock 后再重新进行。多年来已经有了多种不同的实现方式,并且显示出性能上的优势,但是这些解决方案都很复杂,没有一个能够说服足够多的开发者来将其并入 mainline kernel。
还有一个经常被考虑的替代方法就是 range locking。对整个地址空间来上锁其实可能只是为了改变这个地址空间中的一小部分,而 range locking 确保了对感兴趣的地址范围的独占访问,并且允许同一时刻有对地址空间的其他部分的访问。不过,range locking 也是挺棘手的,而且没有任何一个代码实现已经达到了可以被考虑 merge 的状态。
一个进程的地址空间是由一连串的 virtual memory areas(VMAs)来描述的,使用 vm_area_struct 结构表示。每个 VMA 对应了一个独立的地址空间范围;例如,一个 mmap()调用通常会创建一个新的 VMA。具有相同特征的连续的 VMA 可以被合并;VMA 也可以被分割,例如,如果一个进程改变了对其中部分范围的内存保护的时候就会发生这种情况。VMA 的数量因进程的不同而不同,但它可以增长到非常多;用来撰写本文的 Emacs 进程有超过 1100 个 VMA,而 gnome-shell 有超过 3100 个 VMA。
在今年的 LSFMM 会议上,Matthew Wilcox 建议可以通过将 range-locking 问题变成 VMA-locking 问题从而实现简化。由于每个 VMA 覆盖了地址空间的一个范围,锁定了 VMA 就相当于锁定了这个范围。得到的结果将比真正的 range-locking 会有更粗略的粒度,但它可能仍然是足够好的,因此值得尝试。
Baghdasaryan 的 patch set 就是试图找出是否是这样的情况。但是正如我们所预料的,它立即碰到了内存管理子系统中 locking 的复杂性问题。在 VMA 上有两种不同类型的锁需要获取:
page-fault 处理需要确保 VMA 在 fault 被解决时仍然存在,并且改动中不能出现问题。不过,这项工作可以与其他 fault 的处理或其他一些任务同时进行。因此,page fault 处理程序需要获取的锁本质上是一个 read lock。
地址空间的改变就需要对一个或多个 VMA 进行独占访问了;例如当一个 VMA 被分割时,内核的其他部分不可以在此时对这个 VMA 中的任何部分进行操作。所以这种类型的变化需要一个 write lock。
最初的想法是使用一个 reader/writer lock 来完成这项任务,但这导致了另一个问题:write lock 经常需要同时应用于多个 VMAs。用 reader/writer lock 来实现这个功能是可行的,但是,正如 Baghdasaryan 在 patch 的说明邮件中指出的:"跟踪所有被 lock 的 VMA,避免递归锁定和其他复杂情况,会使代码更加复杂"。在核心内存管理代码中,开发者们出乎意料地不希望看到更多的复杂性,所以他去寻找了一个不同的解决方案。
最终出现的方案是一个 reader/writer lock 和一个 sequence number 序列号的组合,这个序列号被添加到每个 VMA 中,同时也添加到描述整个地址空间的 mm_struct 结构中。如果某一个 VMA 的序列号跟 mm_struct 的序列号相等,那么这个 VMA 就被认为是已经被锁定的,在 concurrent page-fault 处理中不可以访问这部分。如果这两个数字不一致,则说明没有被 lock ,可以进行并发访问。
当发生了一个 page fault 时,处理程序将首先尝试读取锁定每个 VMA 的锁;如果失败了的话,它就会像现在这样回退到去获取完整的 mmap_lock 的老路上。如果 read lock 成功了,处理程序还必须检查序列号,只要相关 VMA 的序列号与 mm_struct 中的序列号一致(只要 mmap_lock 被持有就不能改变),那么同一时刻有其他的改动正在发生,fault 处理程序必须同样退回到获取 mmap_lock 的路径上。否则的话就说明 VMA 可用,并且可以在不锁定整个地址空间的情况下处理该 fault。在该任务完成之后,read lock 将被释放。
当内存管理系统必须对地址空间进行改变时,它必须要将每个将受影响的 VMA 都 lock 下来。第一步是在 mmap_lock 上取得一个 write lock,然后,对于每个 VMA,它将在 write 模式下取得 reader/writer lock(可能要等待现有的所有 reader 都先释放 lock)。不过,该 lock 只需要持有很短的时间,完成把 VMA 的序列号与 mm_struct 的序列号设置相等的工作就够了,在完成这个任务后,尽管 reader/writer lock 已经被释放了,VMA 仍在 lock 状态。
还可以有另一种描述方式,那就是每个 VMA 的 reader/writer lock 实际上只是为了保护对 per-VMA 序列号的访问,这才是一个真正的 per-VMA lock。
在内核获取了所有相关的 VMA 的锁之后,就可以进行需要的改动了。在这段时间内,不可能在这些 VMA 内处理 page fault(现在也是这样的),但是地址空间的其他部分将不受影响。等工作完成之后,所有这些 VMA 都可以通过简单地增加 mm_struct 的序列号来解锁。没有必要再去查每个锁定的 VMA,甚至都不需要记住它们是谁。
当然,还有很多其他的细节在这里被跳过了,包括需要将 VMA 置于 read-copy-update 的保护之下,以便它们可以在不持有 mmap_lock 的情况下进行 look up 查询。但锁定方案是使这一切工作的核心。根据 Baghdasaryan 的说法,由此带来的性能提升大约是 speculative page-fault patch 的 75%,所以它仍然消耗了一些性能。但是,他说 "不过,由于代码复杂度更低,所以这种方法可能更可取"。
这项工作目前被认为是一个概念验证(proof-of-concept)。目前它只处理匿名页的 fault,并且还是那些不在 swap 中的 page。他说,如果这个方法看起来值得继续推进的话,今后可以增加对 swapped page 以及 file-backed pages 的支持。回答这个问题可能需要一段时间;core memory-management patch 往往不会很快被合并,而且这种讨论才刚刚开始。但是,如果它成功了,这组 patch set 可能是朝着希望已久的进程地址空间进行 range-locking 机制的方向迈出了一步。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。