关注了就能看到更多这么棒的文章哦~
Preserving the mobility of ZONE_MOVABLE
By Jonathan Corbet
January 22, 2021
DeepL assisted translation
https://lwn.net/Articles/843326/
长期以来,内存碎片(memory fragmentation)在 Linux 系统中一直是个现实问题。以至于多年以来人们每次在希望找到两个物理连续的 page 的时候也都需要考虑找不到的情况。不过,内存管理开发人员进行了一些改动之后,这种情况在过去的十年内得到了很大的改善。其中一个改动就是创建了 "movable" 的 memory zone,在需要的时候,可以对其中的 page 进行搬移。不过,如果有人在这些 movable zone 中 pin (锁定)了一个 page,那么大家的努力就白费了。Pavel Tatashin 提出一组 patch set 就是旨在阻止这种情况,但是,可能会在其他场景产生问题。
Linux 内核会非常频繁地对内存 page 进行分配和释放。我们无法预测在某个时刻哪些 page 是空闲的,哪些 page 是正在使用的。那些正在使用的 page 会趋向于散布在物理内存中的各处,从而使得人们在需要分配一块空闲的、物理连续的内存区域的时候碰到各种麻烦。不过,越来越广泛应用的 huge page,更增加了对分配连续区域的需求。如果 kernel 不努力去做些什么事情的话,很可能拿不到这种连续区域。
现代的系统中还可能面临一个与此相关的问题:hotpluggable memory(内存热插拔)。这里指的并不是说有些机器的内存可以机箱里面随时拔出来,而是指在虚拟化场景下的应用。对于那些内存不够导致经常发生 thrashing(内存颠簸,比如经常 swap in/out)的虚拟机,可以让 hyperviso 给它们及时 "plug" 进来更多内存;而那些没有完全利用所有内存的虚拟机,则会剥夺一些内存还给系统。unplugging memory 可以看作是 "large, physically contiguous region" 这类问题的一种特殊情况。因为只有在整个内存范围都是空闲的情况下,才能在不丢失数据的前提下把这部分内存移除掉。并且要 unplug memory 的话,还必须要求这部分内存区域必须位于指定的地址。可以说,在自然情况下是很难满足这些条件的。
在这两种情况下,内核都可以通过迁移(migrating)这些正在使用的 page(正是它们导致我们无法创建所需的 region),当然需要这些 page 确实可以被迁移。实际上,分配给用户空间的 page 是可以随意移动,因为它们总是通过 page-table 来访问的,所以对它们进行移动只是改变相应额 page-table entry 来指向新的位置而已,而用户空间永远不需要了解这里的细节。而分配给内核使用的 page,通常则是不可移动的。从技术上来讲,它们也是通过 page-table 来访问的,但是那些 page-table entry 一般是不能修改,这些 page 必须保持在原来的位置。
大家可能会好奇为什么不能修改 page-table entry,这里(至少)有几个原因。首先,这些 page 通常是通过内核的直接映射(direct mapping)方式来管理的,这完全是物理内存的一个镜像映射,没有什么方法可以对它们重新进行 mapping。另一个原因是,要想移动 page,就需要在移动过程中使 invalidate 这个 page-table entry,而这个时候如何有人访问这些 page 导致 page fault 的话,kernel 并不能正确处理。]
所以,哪怕只有一个不可移动的 page,相应的这个 memory block 就无法移动了,只需要少数几个 page,就可能导致我们无法创建大块连续区域(large contiguous region)。所以,如果将这些不可移动的 page 随机散放在内存中各处,那么一定会出现碎片化。
内核已经根据许多不同特征将内存划分出许多 zone(区域)了,例如根据是否可以进行 DMA 操作,以及属于哪个 NUMA 节点。zone type 的完整列表在<linux/mmzone.h>中。特别需要关注的是被称为 ZONE_MOVABLE 的 zone,这是只会用于分配那些 movable page 的。通过把可以移动以及不可移动的 page 分隔开,内核就可以最大程度上避免因为某个位置不巧的不可移动 page 给大家造成麻烦。在 ZONE_MOVABLE 中,应该可以随时根据需要来移动 page(或者直接进行回收)从而创建更大块的连续区域。
一切都很美好,直到有一些粗暴的进程在 ZONE_MOVABLE 中 pin(锁定)了一个 page 为止。例如,在对 userspace memory 直接进行 I/O 操作的时候,就会有这种 pin page 的情况。当设备正在执行 I/O 时如果移动了这个 page 就会产生错误。通常情况下,这种 pin 操作的持续时间都很短,一般不是一个大问题。但有时,这种 pin 状态可能会持续很长时间,甚至几个星期。用于 RDMA 的 memory buffer 就经常出现这种让人讨厌的行为,当然也还有其他的例子。虚拟地址连续(virtually contiguous)的 user-space buffer 可能底层 page 是分散在物理内存中各个地方的,因此长期对一个 buffer 进行 pin 操作的话,就会导致内存中许多地方的 page 都无法移动,而不是局限在一个地点。
长时间 pin 状态的问题,人们已经有了理解了。内核社区对解决方案经常没有一个清晰定论,于是多年来一直有讨论在想如何改善这种情况。其中一个影响就是又增加了一些基础设施(Explicit pinning of user-space pages, https://lwn.net/Articles/807108/)用来告知这个 page 需要被长时间锁定了。至于内核针对这种长时间锁定的 page 应该做些什么,人们还没有决定,但至少这是一个好的开始了。
而 Tatashin 的 patch set 就是希望利用这些信息来实现一个特定的目标:阻止 ZONE_MOVABLE 中的 page 被 pin 锁定。它利用了一个针对类似问题(连续内存分配器(CMA,contiguous memory allocator)相关区域中的 non-movable page)的现有解决方案。具体来说,在当前的内核中,对 CMA 区域的 page 进行 pin 操作的时候会先将这个 page 移出 CMA 区域,然后才真正进行 pin 操作。
Tatashin 首先修复了这部分代码中的几个问题,其中一个问题就是原来的代码可能会把 page 迁移到 ZONE_MOVABLE 去。现在这部分迁移代码中就专门避开了这个 zone。另一个问题是在当前的内核中如果移动 page 的操作失败,就会出现一种内核空间的尴尬场景:尽管 page 仍留在 CMA 区域,但它还是会被 pin 锁定。这个 patch set 也修改了这个行为,如果一个页面不能被移动到更合适的位置,那么 pin 操作就会失败。
接下来,将这个机制扩展到也包含 ZONE_MOVABLE 中的 page。如果内核试图将 ZONE_MOVABLE 中的 page 锁定,就需要先进行迁移,迁移失败的话则不允许 pin 锁定。
这些改动可以帮助 ZONE_MOVABLE 真正符合它的设计目标。可以让创建 huge page 之类的操作能更容易成功,并且 hot-unplugging memory 的操作也更容易了。另一方面,这些变化增加了 pin 操作失败的几率,这个错误信息多数情况会直接传播给 userspace。这可能会引起一些不满。
在实践中是否会出现问题,目前还不清楚。理论上,迁移失败的情况应该很少。但这种改动在几年后如果在 enterprise kernel 中出现时应该会引起抱怨。看来,内存管理的开发者并没有过分担心这种问题。patch set 已经经历了七次修订,似乎快可以合入了。第七个版本中主要改动是添加 Reviewed-by 标签。所以 ZONE_MOVABLE 在不久的将来终于能更加名副其实了。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~