LWN:回收未用的页表页的多种方法!
关注了就能看到更多这么棒的文章哦~
Ways to reclaim unused page-table pages
By Jonathan Corbet
May 9, 2022
LSFMM
DeepL assisted translation
https://lwn.net/Articles/893726/
内存管理子系统最重要的工作之一就是回收未使用(或很少使用)的内存,能够更好地利用内存。但是,如果涉及到内存管理的核心数据结构之一——页表(page table)的时候,这个子系统往往无法处理好。在 2022 年的 Linux 存储、文件系统、内存管理和 BPF 峰会(LSFMM)上,David Hildenbrand 主持了一个关于缺乏 page table 回收机制所带来的问题的会议,并探讨了改善这种情况的方案。
众所周知,页表包含了虚拟地址和物理地址之间的映射;一个进程使用的每个虚拟地址都必须由 CPU 硬件来根据页表项进行转换。这些页表是按照一个分层结构来管理的,一般深度是三到五层,并且本身就会占用相当多的内存。在 X86 系统上,对 2TB 的内存进行映射会需要 4GB 空间来放置最底层页表(bottom-level page table),8MB 来放置下层页表(PMD, 也就是 next-level table),以及少数空间来放置最高一层的页表。这些表是不能 swap 或移动的,有时会阻碍如 hot-unplug 内存这种操作。
现在,page table 在某些情况下会被内存管理子系统回收。例如,用 munmap() 来删除一段范围的地址空间的时候也会清理相关的页表。但是有些情况下,还是会出现没有被回收的空页表。例如,内存分配器(memory allocators)倾向于每次尽量对更大范围内存进行映射,并且在任何时候都只使用该区域的一部分。当其中某个子区域不再需要之后,分配器将使用 madvise()来释放相关 page,但相关的页表却不会被释放。还有 large mapped files 也会引发空页表。这些都是合法的使用情况,而且它们已经会导致不少问题了,可以很容易地写出一个恶意程序,用空页表项来填满内存从而使系统变得停顿,这就属于不那么合法的用法了。
在这些情况之中,产生的许多页表都是空的,没有任何实际用途。它们可以被回收,完全不会有什么损害。Hildenbrand 说,朝这个方向迈出过一步,就是来自 Qi Zheng 的一组 patch set。它为每个 page table page 加了一个引用计数(pte_ref);当某个 page 的引用计数下降到 0 时,该 page 就可以被回收了。
另一个空页表 page 的来源是对共享的 zero page 的映射。这个 zero page 是由内核维护的一个 page,内容全部都是 0,用来初始化匿名内存的映射。这种映射是采用 copy-on-write 方式的,所以如果用户空间对以这种方式映射的 page 进行写入的话,它会将得到一个新的 page。用户空间很容易创建大量的指向 zero page 的页表项之后进行内存分配,从而创造了很多不可回收的内存。可以简单地对映射为 anonymous memory 的区域中,每隔 2MB 的读取一个字节,就能实现这个效果了。这些页表页也可以很容易地被回收,不过需要内核能够检测到这种情况;一个充满 zero page 的页表 page 和一个丢失的页表 page 是没有什么区别的(至少用户空间看来没有区别)。最坏情况下,如果页表 page 中的一个地址被用户空间再次引用了,那么将不得不重新创建这个页表页。
用户空间还可以使用其他方式来创建大量页表页。方法有很多,其中之一就是使用 userfaultfd() 系统调用。
那么该怎么做呢?回收空的页表页的工作就像是摘 "低垂的果实",而且已经存在这样的 patch 了。内核也可以在扫描内存时删除对 zero page 的映射,这最终就会把只包含 zero page 映射的页表页删除掉,并允许把这些页表页也回收掉。普通的回收就可以解除对文件支持的页面(file-backed page)的映射(unmap),这就可以创建出空的页表页,但这不是现在的明确目标。也许有可能让回收工作也处理那些彼此紧挨着的 page,从而能清空这些 page table page,然后这些 page 也可以被回收了。
不过,还有一些其他问题需要回答。有一些页表页可能需要被积极使用,即使它们只是映射了 zero page。如果它们不得不再次被很快重新创建的话,那么对它们进行回收可能会损害性能了。还有一个问题是,回收更高级别的 page table 是否有意义。Hildenbrand 说,回收空的 PMD page 可能还是值得的,但是对更高级别页表项的 page 进行回收就不太可能有价值了。
这种回收工作应该在何时进行?关于空的 page-table entry,答案很简单,就是当它们变成空的时候。对于 zero page mapping,就有点难决定了。它们可以通过 scan 来找到,但 scan 本身也有开销。因此,也许这种扫描应该只在内核确定有可疑的 zero page 相关活动时才进行。但是,对 "可疑" 的定义是很困难的。例如,虚拟机本质上就倾向于创建大量的 zero page mapping,所以这个行为本身并不可疑,可能需要依靠更主观的一些启发式方法来决定了。其中有一个可能是检测某个进程的页表页与它的整个常驻集(resident-set)size 的占比是否很高。
他说,即使是识别出了一个可以移除的 page table page,那么可能删除动作本身也不是一件小事。人们强烈希望避免获取 mmap_lock,因为抓这个锁就会有竞争的问题。这就限制了何时可以进行这些移除动作。移除更高级别的页表页更难,因为普通的扫描可能会长时间地持有相关引用。在等待这个引用被释放的时候,就可能会导致回收过程被无限期推后了。
关于如何打败那些恶意的用户空间进程,Hildenbrand 说,页表页的分配必须以某种方式来进行速度控制(throttled)。最好的方法可能是对那些行为可疑的进程来强制回收页表页。找到这种进程的方法之一就是对用于页表页的内存量设置一个阈值,但恶意攻击者也可以通过生成大量的进程来规避这个限制。
会议在此时结束了。Yang Shi 在会议结束时建议,如果 multi-generational LRU 的工作被合并的话,也许会有一些帮助。反正它也要扫描页表,所以增加对空页表的检测可能会比较容易实现了。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~