LWN:BPF专用的内存分配器!

共 3560字,需浏览 8分钟

 ·

2022-07-24 21:25

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

A BPF-specific memory allocator

By Jonathan Corbet
June 30, 2022
DeepL assisted translation
https://lwn.net/Articles/899274/

内核现在并不缺内存分配器(memory allocator),所以人们可能不理解为什么又要加一个分配器。正如 Alexei Starovoitov 的这组 patch set 所表明的,BPF 子系统觉得就是有这个必要。其他提出的新的内存分配器目标是提高 BPF program 中进行内存分配的可靠性,毕竟 BPF program 可能的执行环境会是各种各样的。

在内核中分配内存总是很棘手的,哪怕是在最容易的情况之下。如果出现了分配请求不能立即被满足的情况,那么内存管理子系统根据当时的执行环境可能会有一些可以进一步采取的措施,也可能完全没有办法做什么。例如,可以通过将一些内存中的内容搬移到持久性存储设备中来释放这部分 memory,但是如果是某个文件系统正在申请内存,那么再使用该文件系统来写出数据就可能会导致死锁,因此这不是一个可行的措施。在内核中的许多场景下,不可能通过 sleep 来等待内存空闲出来。如果内核目前正在处理一个来自 CPU 的 non-maskable interrupt(NMI),那么能做的就更加少了。

大多数内核代码都是为了在一个特定的环境中运行而撰写的,并且它知道此时可用哪些内存分配选项;这些信息是通过分配请求中的 GFP flag 来传递给内存管理子系统的。当某个特定的函数需要在多个上下文中被调用的话,它通常就必须先假设它是在最严格的上下文中运行一样来进行内存分配;这对开发者来说是很不方便的。

近年来已经开发出一些像内存池("mempools")这样的机制,它会预先分配一定数量的内存,以确保在需要的时候可以直接使用,从而使开发者更轻松一些。很自然地,内存池很快产生了一个新的问题:内核开发者采用了内存池作为一种使自己免受内存分配失败影响的方式。不久之后,内核的大部分内存被捆绑在 mempools 中,无法用在实际需要内存的地方。这些年来人们在过度使用 mempool 以及无法在关键情况下分配内存之间已经找到了一个平衡点。

Mempools for BPF

BPF 程序可以在人们可以想象到的任意一个上下文中运行;尤其是针对 tracing program 来说更是如此。某个特定的 BPF program 可能被 attach 到一个在 atomic 上下文中运行的函数,从而来响应硬件中断,甚至是一个处理 NMI 的函数。这使得内存的分配成为一件不那么确定的事情,而这反过来又使 BPF verifier 的工作更加困难,verifier 的目的是确保 BPF program 在所有情况下都能表现良好。BPF 程序通常不直接分配内存,但它们可以间接地进行分配,例如,在 BPF map 中存储数据。这样的操作如果失败了就会造成不少后续麻烦。

patch 中提出的 BPF 专用分配器可以被认为是一种为 BPF 程序的需要而专门设计的 mempool。与 mempool 一样,其目的是在一个相对宽松的环境中创建一个放满了预分配内存的 cache 区,以便在条件变得更严格的时候可以随时使用这部分内存。虽然 mempool 的核心是一个由相同大小的可用 object 的列表组成的 cache 区,但这种 BPF cache 还是要更复杂一些。最少来说,针对系统中的每个 CPU,一个 BPF cache 由两个相同大小的 free object list 组成(一个用于大多数情况下的分配,另一个用于处理 NMI 时的分配)。而一个更复杂的变种方案就是每个 CPU 有 11 对 size 各不相同的 object list;也就是每个 CPU 有 22 个 free-object list。

其中的核心逻辑非常简单:当 BPF program 需要分配内存时,它就像调用 kmem_cache_alloc()(对于相同大小的 cache)或调用 kmalloc()(用在可变 size 的对象上)那样来从 cache 中分配。调用者申请的时候会把一个 free object 从当前 CPU 的相应 list 中移出来,并返回给调用者;只有在当时正在处理 NMI 的情况下,才会使用 NMI 专用的 list。在释放 object 的时候,它们会被返还到当前 CPU 的相应的 per-CPU list 中,这可能并不是它们之前被分配出来时的那个 CPU 了。这些操作是在禁用中断和 migration 的情况下进行的,所以它们进行的时候就可以不需要任何其他类型的 lock 了。假设 cache 没有被耗尽,那么 BPF program 就能够在任何上下文里面安全地进行内存分配。

当第一次建立 cache 时,在每一个普通 list 里面会最多放 4 个 object,并且每个 NMI 列表中会针对每个 size 来准备一个 object(当然是针对每个 CPU)。不过,每当从 list 中分配一个对象时,剩余可供分配的 object 的数量会跟一个低水位进行比较(也就是 32 个 object );如果低于这个值,就会使用 irq_work 机制来调用一个函数,将 list 扩展到 64 项。在非实时内核中,这项工作会是在内核线程中实现的,在那里内存分配相对来说不受太多限制。

同样,每当一个 object 被返回到 BPF 分配器时,list 的长度会与高水位(96 个)进行比较;如果 list 太长了,就会把一些 object 返回给内核,从而把数量减少到 64 个。这可以防止 cache 积累过多的 object;这部分代码还可以处理 object 在一个 CPU 上分配而在另一个 CPU 上 free 所导致的可能的不平衡问题。

Memory use

值得注意的是,这些 cache 并不是 global object;它们是为每个 user 所独立分配的。例如,为 BPF 程序使用的每一个 hash map 会建立一个 cache,会有很多这样的 hash map。因此,我们很自然地想知道,在一个繁忙的系统中,这些 cache 可能会消耗多少内存。上面描述的行为显然是为了使内存可以随时进行分配,同时限制 cache 本身的总内存使用量。正常来说,大多数用户不会在每个可用的 CPU 上都针对所有可能 size 都分配 object,而且大多数 object 也不会在响应 NMI 时运行。保留数百个 object 以备不时之需是很浪费的,所以 BPF 的 memory cache 会最少量的 object 数量来开始,每个 list 只有在该 list 发生了实际分配时才会填充更多 object 进来。

不过,还是需要担心这些 cache 是否会增长过多导致消耗了太多的内存,尤其是在有很多 cache 的时候。如果有 memory cgroup 的 limit 的话,cache 的分配也会被计算在内,但是对于专门用于 BPF cache 的内存并没有其他限制。如果内核发现自己需要其他地方的内存,也没有 shrinker 类型的机制可以减少 cache 的 size,并且也统计能显示当前有多少这样的 cache,或者目前有多少内存被分配给它们。

这个 patch set 增加的唯一使用位置就是 hash map 的实现代码;它会使用固定 size 的方式。不过,这里存在 kmalloc() 方式的选项,就说明还有其他的一些需要这种方式的代码在考虑近期也进来调用。

作为对邮件的回应,Christoph Hellwig 建议 Starovoitov 在继续工作之前先与现有的 slab allocator 的维护者进行一下讨论。不过到目前为止,内存管理开发者对这组 patch 的回应很少,所以不清楚他们对此事的想法。这个新的 allocator 完全包含在 BPF 代码中,不涉及内存管理子系统,所以原则上可以在没有他们参与的情况下进行开发与合入。不过,内存管理方面的开发人员在这个领域有一些专业知识,他们可能会对如何解决这个问题有一些有价值的想法。

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

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

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



浏览 19
点赞
评论
收藏
分享

手机扫一扫分享

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

手机扫一扫分享

分享
举报