LWN:针对BPF代码的内存分配器!

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

A memory allocator for BPF code

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

将一个 BPF 程序加载到内核的过程需要很多步骤,包括验证、权限检查、链接到内核中的 helper 函数,以及编译成本地可执行的指令。不过,在所有这些工作之中都隐含着一个简单的任务:分配内存,从而将编译后的 BPF 程序存放在内核的地址空间中。事实证明,在目前的内核中,当前的分配方式可能有些浪费内存,特别是在加载大量 BPF 程序的系统中尤为突出。Liu Song 的一组 patch set 就希望通过在内核中引入另一个专门的内存分配器来解决这个问题。

内核可以接受并执行相当大的 BPF 程序,但实际上,大多数 BPF program 都是很小的。BPF 可以用几百字节的代码就完成很多事情。但是,内核为 BPF 程序分配的空间是以整个 page(通常是 4KB)为单位的,只要该程序还没有被卸载掉,那么超过每个程序代码末尾的所有内存空间其实就被浪费掉了。对于一个小程序来说,被分配来存放代码的 page 中大部分空间是未使用的。如果有许多程序被加载进来,那么浪费的内存数量就会持续增加。

Liu 的补丁集增加了一个新的 "bpf_prog_pack" 分配器来解决这个问题。从表面上看,这会是系统中最简单的内存分配器之一。它维护着一个存放 BPF program 的一系列 huge page 的 list,在需要时会再分配新的 page。每当一个新的 BPF 程序需要空间时,一个简单的、先到先得的(first-fit)算法就会给它分配够内存空间。每个 huge page(或称为 "pack")关联的一个 bitmap 会跟踪记录空闲空间(以 64B 的小块为单位),这样就可以在 BPF program 被卸载时自动把返回给这个内存分配器的内存空间拼接起来。这个分配器并不需要以非常快的速度来执行,因为确实也不需要。哪怕是在一个大量使用 BPF 的系统中,分配和释放操作也是比较少见的。

这个简单的内存分配器的优点相当明显。因为可以把多个程序加载到一个 page 里面,这样就会大大减少由于内部碎片(internal fragmentation)而浪费的内存。把 BPF 程序放到 huge page 中就可以减少 TLB 的压力,有助于提高性能。肯定有人想问,既然内核已经有了经过多年密集开发和调整的内存管理代码,那么为什么还需要再加一个新的内存分配器呢?答案是,BPF 带来了一些现有分配器无法满足的特殊需求。

原有的无法进一步细分内存 page 的那些内核 page 分配器所存在的问题上面已经描述过了,那就是浪费了太多的空间。但是,内核的 slab 分配器其实正是为这个任务而设计的:它们会从 page 分配器中获取大块空间,并把它们分成小块传递给内核的其他部分。因此,在这里使用 slab 分配器看起来是很自然的想法。Liu 没有解释为什么不采用这种做法,但是有几个合理的理由可以来解释。

一个原因是由于 slab 分配器是为分配同等大小的 chunk 而进行过针对性的优化的。但是对于 BPF 程序来说,并不是所有的程序都有相同的 size,所以无法使用一个专用的 slab 以及 kmem_cache_alloc() 来简单支持这个功能。当然,可以使用多个 slab,既可以直接在 BPF 代码中使用,也可以简单地用 kmalloc() 来直接分配空间,但是无论如何都会有内部碎片的问题(尽管这个问题会比之前的问题要小得多了)。kmalloc() 使用的 2 的幂次的分配原则仍然会在每个程序的末尾浪费相当多的空间。

不过,要使用 slab 分配器还无法解决另一个更根本的问题。BPF 程序都是可执行代码,它必须被特别处理过。代码需要在页表配置中有可执行权限,这意味着它不能与数据混放在一起(kmalloc() 就会混放在一起)。毕竟,多年来,开发人员一直在努力确保数据不能在内核中被误执行了。所以,那些旨在管理数据内容的 slab 分配器并不适合这种应用场景。

要使这个 program-packing 方案生效,需要一定的技巧,不仅仅是使用这个专门的分配器就好了的。在一个正在运行的系统中更新代码是非常危险的,如果系统中的 CPU 试图在代码被修改时去执行这部分代码,结果会非常糟糕。内核在很多地方都需要使用那些需要自己修改过的代码,所以已经有机制可以安全地完成这些修改了,但是这个任务仍然必须非常小心地处理。由于无法直接写入可执行内存,再加上内核中的可执行内存区域必须是只读的,这就给 JIT 编译器带来了挑战。

因此,当使用新的分配器来为一个 BPF program 寻找内存空间时,它实际上分配了两个缓冲区。第一个是只读的、可执行的空间,程序必定要放在这里;第二个空间是用 kvmalloc()获得的普通内存。JIT 编译器将会编译输出到第二个 buffer 中,这个区域是可以被写入的,但它生成的任何地址都必须是相对于第一个 buffer 的地址的。等编译过程完成之后,内核的 "text poke" 机制再加上一个新增的 copy 函数就可以把编译好的 program 移动到 read-only 的 buffer 去,并释放这个临时 buffer。

截至目前,这组 patch 已经经历了 8 次修订,收到了许多 review,引出了大量的新的需要 fix 的地方。不过,目前来看可能已经没有更多的东西可以改了。因此,这段代码可能会进入 5.18 版本,然后大量使用 BPF 用户就应该可以节省更多的内存并看到性能有一些提升了。

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

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

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

a9d559d8cdee21e93f5558fc6f52cdaa.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值