LWN:近期在讨论的一些内存管理相关patch!
关注了就能看到更多这么棒的文章哦~
Some upcoming memory-management patches
By Jonathan Corbet
November 12, 2021
DeepL assisted translation
https://lwn.net/Articles/875587/
内存管理子系统仍然是内核中最复杂的部分之一,为了提升性能,它总是依赖于各种启发式规则。因此不出意料,开发人员仍在继续尝试改进这部分的功能。目前有许多内存管理 patch 在讨论中。继续阅读,来了解一下页表页(page-table pages)的 free、kvmalloc() flag、内存清除(memory clearing)以及 NUMA "home node" 等的进展。
Freeing page-table pages
每当用户空间在分配内存时,内核自然必须要找到一些 page 来满足这个分配内存请求。但是内核同时也必须要分配一些 page-table page 来将这部分新分配的内存地址创建映射(mapping )。在一个拥有 4KB 的 page size 以及 64 位地址的系统中,每 512 个普通的内存 page 就需要一个 page-table page(如果没有使用 huge page 的话)。对于那些具有非常庞大的地址空间的应用程序来说,仅仅是用在 page-table page 上的内存数量就很可观了。
在用户空间要用的 page 被回收的时候,在一些比如进行了 madvise() 之类调用的情况下,这些内存最终会被浪费掉。也就是说用户空间的 page 实际上从 working set 移除掉了,但是用来给它们创建 mapping 的这些 page-table page 继续保持占用状态。如果某个 page-table page 中映射了的所有 page 都被回收了的话,那么 page-table page 本身就是空的,没有任何作用。进行了大量内存分配然后又释放掉这些内存的应用程序就会导致很多没有用处的 page-table page 积累起来,这不是人们期望的状态。
来自 Qi Zheng 的一组 patch 就希望能解决这个问题。它为 page-table page 增加了一个引用计数(利用了 struct page 另一个可以重用的字段)。这个 page 的用户,比如 page-fault handler 等等,会负责递增这个引用计数,确保自己在工作的过程中这个 page 不会突然消失了。给用户空间的一个 page 添加一个 page-table entry 的动作也会递增这个引用计数,而回收一个 page 的时候就会递减这个引用计数。等引用计数下降到零时,内核就知道这个 page-table entry 实际上不包含任何 page-table entry,可以被释放了。
patch set 测试结果表明对这些空的 page-table page 进行回收就可以释放出很多内存,可供其他用途之用。另一方面,对 page-fault handler 在几个方面可以感受到影响。当然,这些影响之一就是每次添加 page-table entry 的时候增加了维护引用计数这个成本。但是对 page-table page 进行 free 这个动作也是有额外开销的。后续如果这部分地址空间要再次被使用的时候,就必须要分配一个新的 page-table page 了。总的来说,这个改动会导致在 page-fault 处理流程中的性能下降 1%。
这个开销可能就会导致一些用户无法承受了。不过,现在还没有相关的 sysctrl 开关选项来打开或关闭这种行为。如果这组 patch set 被合并的话,那么所有的系统都会对空闲的 page-table page 进行 free。也许有些场景下,只有有大量的 page-table page 牵涉其中,那么释放这些内存所带来的性能提升就可以超过 page-fault 这里的性能损失,但对于其他一些类型的应用可能就并不适用了。
More flags in vmalloc()
在 kernel 中用 vmalloc() 分配的内存必须要被 map 到内核地址空间的一个特殊位置。这跟 kmalloc()或 page allocator 分配出来的内存并不一样,无法直接通过内核的 direct memory mapping 来进行访问。曾经有一段时间大家不鼓励使用 vmalloc(),因为 vmalloc() 区域的地址空间很小,而且创建额外的 mapping 也会带来额外开销。不过,随着时间的推移,使用 vmalloc() 的地方已经越来越多了。64 位系统的出现消除了地址空间的限制,而且代码中需要进行多个 page 的分配的地方也越来越多。如果这个 buffer 中的所有 page 不需要是物理连续的,那么成功完成分配这个 buffer 的工作的可能性就越高。
然而,vmalloc()接口从没有支持过那些传递给 kmalloc() 的各种 GFP_* 标志(这些是用来调整内存的分配方式的),这个限制也同样存在于衍生的 kvmalloc() 等函数中。kvmalloc 函数会尝试调用 kmalloc(),并在失败时取而代之地采用 vmalloc()。对于一些内核子系统,尤其是文件系统来说,这是一个真是存在的问题,因为它们都需要能够在分配内存时带有 GFP_NOFS、GFP_NOIO 或 GFP_NOFAIL 等 flag。因此,一些文件系统中都在避免使用 kvmalloc(),而其他的文件系统比如 Ceph 则推出了自己的内存分配函数来解决这个问题。
Michal Hocko 用一个 patch set 解决了这个问题,他在 vmalloc() 子系统中加入了对上述 GFP_ 各种 flag 的支持,同时也让 kvmalloc() 支持起来了。这样一来这些函数就可以用在文件系统环境中了,也能删除掉 Ceph 特有的分配函数了。截至目前,这组补丁中的一个前提 patch 已经进入了 mainline,但其余的还没有 merge。很可能会在 5.16 合并窗口结束前完成合入,敬请关注。
Uncached memory clearing
现代计算机中总是喜欢使用大量的 memory cache 原因很简单:cache 可以提高性能。因此,看到 Ankur Arora 的这个 patch set 就会感到很有意思,因为它声称是通过绕过 cache 来提高内存性能。大家也许可以猜到,这是一个只在特定情况下有效果的改动。
如果内核需要将某个 memory page 里的内容全部都清零,那么肯定是需要依次执行一系列普通带有 cache 支持的写操作。这样就可以让系统自己挑选合适的时机来将 cache 中存放的数据写入到主内存中。一个刚刚被清零的 page,很大可能会很快被写入其他数据,因此将该 page 放在 cache 中就会加快这些写入操作的速度,其实可能根本不需要将最初的清零内容写入内存中。因此,使用 cache 在这种场景是有性能优势的。
但是当需要对大量的内存内容进行清零时情况就会发生变化了。如果需要清零的内存数量超过了最终一级(last level)cache 的大小,那么直接写入内存其实要比等待 cache 内容写到主内存去要快。因为这种大量的 write 操作也会把其他内容从 cache 中挤出来,而其中一些数据很可能在不久的将来就会需要直接使用了。所以,对于这种大范围的清零操作,更好的方法还是应该绕过 cache。
因此,Arora 的 patch set 修改了内核,当需要对一个 huge page(2MB)或 gigantic page(1GB)进行清零操作时,就使用 uncached write。这类情况经常发生在运行虚拟化 guest 的系统中,一个新的 guest 在最开始时需要基于 host 提供的大片清零过的内存区域,并且这些区域会是尽可能使用 huge page 或 gigantic page 管理的。这组 patch set 的测试结果显示,虚拟机创建时的性能提高了 1.6 倍到 2.7 倍。这应该足以说服大家接受这个改动了。
Setting a home NUMA node
NUMA 系统的特点是访问位于本地 NUMA node(或附近的 node)的内存要比访问远程节点的内存速度更快。这意味着我们可以通过控制哪些节点用来做内存分配,这样可能会有很大范围上的性能提升。内核提供了许多控制这类分配的操作,包括最近增加的 MPOL_PREFERRED_MANY,LWN 在 7 月份已经介绍过。
不过,Aneesh Kumar K.V.想再添加一个新的系统调用:
int set_mempolicy_home_node(unsigned long start, unsigned long len,
unsigned long home_node, unsigned long flags);
这个系统调用将把 home_node 参数对应的 NUMA node 中从 start 开始的 len 字节的地址范围称为 "home node"。目前,flags 参数还没有使用。
home node 这个概念需要与 MPOL_PREFERRED_MANY 或 MPOL_BIND 内存分配策略结合使用。这些策略可以指定一组用于进行新的内存分配请求的 node,但并不指定哪一个节点是首选的分配 node。如果用 set_mempolicy_home_node() 设置了一个 home node,那么只要有可能,都会尽量在该 node 上进行分配。如果失败的话,内核就会根据 in-force policy 中所允许的 node 来 fallback 到相应 node 上进行分配,会优先选择离 home node 最近的 node。
目的是让应用程序对内存分配拥有更多的掌控,同时避免使用 slow node 的内存。目前还没有提及 NUMA 开发者们何时会放弃当前的做法,改为完全由用户空间提供的 BPF 程序来决定 memory-allocation 策略。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~