LWN:允许其他CPU来清空此CPU的page list!

Linux News搬运工

共 3874字,需浏览 8分钟

 ·

2022-03-01 17:57

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

Remote per-CPU page list draining

By Jonathan Corbet
February 15, 2022
DeepL assisted translation
https://lwn.net/Articles/884448/

有时,一组内核 patch 会有一个令人眼前一亮、非常吸引眼球的标题。不过大多数时候在邮件列表中的 patch 标题都是类似 "remote per-cpu lists drain support" 这样的。对许多人来说这组 patch 就跟这个标题本身一样看起来很枯燥。但是,那些对这方面感兴趣的人——许多 LWN 的读者都是这样的—— Nicolas Saenz Julienne 的这组短小的 patch 给了我们一些启示,告诉开发者们如何可以让内核的 page 分配器尽可能快以及尽可能地强大。

Per-CPU page lists

page allocator 这个名字很直白地说明了它是负责以整个 page 为单位来管理系统的内存的。它跟通常用来处理更小块的内存的 slab allocator 是有区别的。内存的分配和释放动作在内核中频繁发生,使得 page allocator 成为许多性能敏感的关键路径(hot path)的一环。例如,来一个系统调用或者设备中断之后,就可能多次引发对 page allocator 的调用,因此代码需要能飞快执行完毕。有时候,内存管理代码会被认为是限制了内核其他部分性能的瓶颈,尽管人们已经做了很多努力来优化它。

抽象地来说,page allocator 是基于 "buddy allocator" 的,它以 2 的幂次数量的连续 page 为单位来进行内存处理。整体来说,buddy allocator 专注于尽可能将相邻的 page 合并成更大的连续内存块。但是,在面对当代系统的需求中的新特点时,这种抽象就开始出现问题了,因为哪怕现在一个手机里面也可能有许多的 CPU。而维护一个全局的 buddy structure 就意味着会有大量的对这个数据的并发访问操作。这反过来又意味着会引入 locking (互斥锁)机制以及导致 cache miss,这两种情况都会破坏系统的性能。

要想缓解这些因为并发访问(concurrent access)这些共享数据而产生的性能问题,最好方法之一就是避免对共享数据的进行并发访问。只要每个 CPU 能够在自己的私有领域中独立工作,不与其他 CPU 争夺,那么性能就会得到改善。page allocator 就跟内核中许多其他部分一样,通过针对每个 CPU 都维护一个独立的空闲 page 列表(per-CPU list of free pages)来实现这一点。

具体来说,内存管理子系统会在用于描述内存管理区的 zone structure 中存放一个 per-CPU 的空闲 page 列表。虽然实际情况(必定)会更复杂一些,但这个结构确实可以被简化为是一个简单的 page 列表数组,系统中每个 CPU 都有一个列表。每当某个 CPU 需要分配一个 page 时,它首先会在其 per-CPU 的列表中寻找,如果找到了一个可以使用的 page,那么就从中取出来用。当该 CPU 释放了一个 page 的时候,它就会把这个 page 放回到 per-CPU 的列表中。通过这种方式,page allocator 中的许多操作都可以在不需要对全局数据结构进行写入的情况下就能完成,这样就大大地加快了速度。并且在 CPU 上可以更高概率地使用本地仍然在 cache 中的 page,也有助于性能。

当然,这只有在 per-CPU 的列表中有所需数量的 page 时才能起效。如果某个 CPU 需要分配一个 page,但此时它的 per-CPU 列表是空的,那么就必须采取较慢的方式来从全局的 free list 中获取到内存,在这个过程中就可能会与其他 CPU 竞争。相反,如果 per-CPU 的列表变得太长了,那么它就可能会占用原本可以用在其他地方的内存,这里最好把其中一部分 page 交还给全局分配器。不过很多时候,每个 CPU 都可以直接用自己的 free page 列表就能完成工作,大家都很高兴。

不过,当系统整体在面临内存压力时,就会出现一种情况。如果内存管理子系统在所有可能的地方都去寻找可用的 page 的时候,它很快就会来盯上这个 per-CPU 的 list,寻找其中闲置的、可供使用的内存。不幸的是,这里不可以直接去抢过来使用,因为 per-CPU 的列表只有在每个 CPU 都能确保是自己独占访问这个 list 时才能发挥作用。如果有其他的 CPU 插手进来的话,整个系统就会完全乱套了。

那么,当系统需要夺取 per-CPU 列表中的空闲 page 时,应该怎么做呢?当前的做法是,内核要求每个 CPU 从自己的 list 中释放(drain)这些 page。这个动作是由一个特殊的、per-CPU 的 workqueue 来完成的,这个 workqueue 中会有多个 callback 在排队来释放 page。等这个 CPU 开始 schedule 调度的时候,此 workqueue 就会立即运行,通常这个过程会很快。

不过这个解决方案并不完美。最起码它会导致每个目标 CPU 发生一次上下文切换来运行 drain 操作的 callback 函数。但是,如果有一个目标 CPU 是运行在 tickless 模式下的,或者如果它正在运行一个高优先级的实时任务,那么 workqueue 可能在很长一段时间内根本就不会有机会运行。因此,该 CPU 上的所有 free page 都会被锁在其本地列表中,搞不好大多数 free page 都会堆在这里了。

Draining the lists remotely

这个 patch 就是为了缓解这个问题的,有了它,就可以远程(即从别的 CPU 上进行)地从这个目标 CPU 的本地列表中获取到 page。上一次的尝试是增加了 spinlock 来控制对 per-CPU list 的访问,这本质上是打破了 per-CPU 的特性,因此那个解决方案虽然是有效的,但它所增加的开销正是我们创建出 per-CPU 列表所想避免的。因此这些 patch 并没有进入内核。

目前这组 patch 的做法不同,它采用了内核社区最喜欢的处理扩展性问题(scalability problem)的工具之一:read-copy-update(RCU)。这反过来又利用了计算机科学的最原始的一个技巧:添加一层间接抽象(a layer of indirection)。使用这组 patch 之后,每个 CPU 现在有两组存放空闲 page 的列表了,其中一组 list 是在任何时候都使用的,而另一组则被预留下来(是个空 list)。在 zone structure 里面添加了一个新的指针,指向当前正在使用的是哪组 list;每当一个 CPU 需要访问其本地的 list 时,它必须使用这个指针来取到。

当需要抢夺一个 CPU 的 free page 时,抢夺者所在的 CPU 将使用一个原子操作的比较和交换动作(atomic compare-and-swap operation)来把目标 CPU 的指针切换到第二组(空的)列表上。不过,哪怕是切换动作完成之后,目标 CPU 仍可能在继续使用前一组列表,所以抢夺者的 CPU 必须等到 RCU 的 grace period 完成之后才可以去正式访问原来的列表。由于这是个 per-CPU data structure,在 RCU 的 grace period 期限到了之后,目标 CPU 就不能再持有对旧列表的引用了。届时就完全可以对旧列表进行提取和清空了。与此同时,目标 CPU 会仍然正常地进行工作,尽管它没有了任何一个本地空闲 page,但它也没有被人打断过。

这种方法也不是完全没有性能损失的,它在内存分配的 hot path 中增加了一个额外的指针解析动作(pointer dereference),会增加一些开销。各种基准测试结果看起来在大多数情况下没有什么区别,而在一些情况下会有 1-3% 的性能损失。patch 的说明邮件中认为这种开销是可以接受的。

目前还不清楚其他的内存管理开发者是否会同意这个结论。内核开发者会为了 1% 的性能提升而努力工作很长时间,他们可能不会愿意为了一部分使用场景特有的好处而放弃这么多的性能。不过无论如何这里所要解决的问题是真实存在,而且尚不清楚是否还有更好的解决方案。无论这组 patch 的标题是否让读者们喜欢与激动,这个 remote per-CPU list draining 的改动很可能会是未来的内核里的一部分。

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

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

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



浏览 28
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报