LWN:BPF allocator 碰到麻烦了!
关注了就能看到更多这么棒的文章哦~
The BPF allocator runs into trouble
By Jonathan Corbet
April 29, 2022
DeepL assisted translation
https://lwn.net/Articles/892743/
5.18 内核合并的一个改动是为已经加载到内核里的 BPF 程序提供一个专门的内存分配器(memory allocator)。不过在那之后,这个功能遇到了相当多的困难,几乎可以肯定在 5.18 的最终版本中会被禁用。走到这一步,部分原因在于分配器本身的 bug,但是这项工作也很不幸地引发了内核的内存管理子系统中一些更古老、隐藏更深的 bug。
在目前的内核中,BPF 程序的内存空间(经过 JIT 代码转换之后)的分配代码,就是用为 loadable module 分配内存的那些代码来分配的。这看起来很有道理,因为两种情况都是要申请空间用于在内核中运行的可执行代码。但是,这两种使用场景之间有一个关键区别。内核 module 是相对静态的,它们一旦被加载就几乎不会被删除。相反,BPF 程序可以频繁地出现和消失。在系统生命周期中可能有成千上万次的加载和卸载动作。
这一差异目前看来影响很大。可执行代码的内存必然要设置执行权限(execute permission),因此也必须是只读的。这就要求这部分内存在 page table 中有自己的 mapping,也就意味着它必须从内核的 direct mapping(可能是 huge-page 方式管理的)中分离出来。这就要把 direct mapping 分解成更小的 page。随着时间的推移,就会让 direct map 越来越碎片化,可能会对性能产生明显的影响。BPF 分配器的主要目标是将这些分配动作都采用一组专用的 huge page 中,避免产生这种碎片化。
不过,在这段代码合并后不久,就开始涌现出一些 regression 问题报告,以及相关的担忧。这引起了 Linus Torvalds 和其他开发者的注意,并揭示了一系列的问题。虽然其中一些 bug 是在 BPF 分配器本身,但最具破坏性的问题是在另一个子系统(vmalloc()分配器)中以前做出的一个改动。
vmalloc() and huge pages
vmalloc()(及其多种变种函数)与其他的内核内存分配接口的差异在于,它返回的内存虚拟地址连续,但物理上可能是分散的。因此,它适合于分配较大的、并且不需要物理连续的内存区域。由于 vmalloc() 的开销较大,而且 32 位系统的可用地址空间不足,人们曾经不希望大量使用 vmalloc(),但是随着时间的推移,人们的态度已经发生了变化。现在,使用 vmalloc()作为一种避免进行较大 size 分配时因内存碎片化而失败的方法在普遍使用了。近年来,像 kvmalloc() 这样的函数也被添加进来,它将在普通分配未能成功时自动退回到 vmalloc()。
在 2021 年,Nick Piggin 增强了 vmalloc()的功能,如果申请的 size 足够大,就可以分配 huge page 了。人们可能会想为什么需要这样做,毕竟 vmalloc() 明确是为了不需要物理连续的内存分配而设计的。答案就是 huge page 可以通过减少对 CPU 的 translation lookaside buffer (TLB)的压力来提升性能。内核有一些大 size 的分配场景可以从这个改动中受益,所以它被合并到了 5.13 内核中。
哪怕在合入的时候,人们也提出了一些注意事项。内核中有些地方会因为收到 vmalloc() 调用返回的 huge page 而无法正常使用。比如说 PowerPC module loader。所以 Piggin 也添加了一个 flag VM_NO_HUGE_VMAP,它要求只使用 base page。当然,vmalloc() 不接受任何 flag,所以要想避免分配到 huge page 的话,只能通过更底层的 __vmalloc_node_range() 函数来分配了,直到 5.13 周期的后期添加 vmalloc_no_huge() 为止。当时 x86 架构也没有启用 Huge-page 分配,因为没有人去花时间寻找并解决那里的潜在问题。
BPF-allocator 系列的第一个 patch 在 vmalloc()中启用了 x86 架构的 huge page 分配;这对 BPF 分配器来说是有必要的,因为需要使用 huge page。这一切看起来都很好,直到开始了更广泛的测试,就发现了问题。看来在 x86 上启用 vmalloc() 中的 huge page 可能不是一个好主意。不过其实这个问题实际上与 x86 架构本身并没有什么关系。
当 vmalloc()(正如它在 5.18 周期开始时的行为)在响应一个申请分配 huge page 的请求时,会返回一个 compound page——这是一组连续的 base page,表现得像一个单一的、更大的 page 而已。这些 page 的组织方式是有差异的,其中关于它要如何使用的大部分信息,都存储在第一个 base page ("head")的 page structure 中。接下来的那些("tail")page 的 page structure 大多只包含了一个指向 head page 的指针。务必注意不要把 tail page 当作是独立页面,否则会出现问题。
问题就这么发生了。事实证明,内核里面有很多代码都是假定可以直接将来自 vmalloc() 的内存视为由 base page 组成的;这种代码会调整这个 page structure 而没有注意到它可能正在处理的是 tail page。这导致了系统内存 mapping 损坏,一旦检测到这种 mapping 损坏,内核就会出现 oops。一个已知会发生这种情况的案例是 Rick Edgecombe 首先注意到的,即驱动代码调用 vmalloc_to_page() 来获取 vmalloc() 分配中的某个 page structure(因此,可能在一个 compound page 的中间位置)。事实证明,有相当多的驱动程序使用 vmalloc_to_page()。如果相关内存是由 compound page 组成的,那么每一个驱动程序几乎都会碰到问题。
这个具体问题最终被 Piggin 修复了。现在的代码是将分配的 huge page 分割成 base page(同时保留 huge page mapping),不再使用 tail page。但是在 vmalloc() 子系统中还潜伏着一些其他的问题,随着越来越多暴露出来,Torvalds 得出结论:"HUGE_VMALLOC 的设计有严重的错误"。他说,从一开始的设计就有问题,问题现在才出现,是因为在 x86 架构上启用该功能导致了更广泛的测试。
Resolutions for 5.18
Piggins 的 fix 被合并到 5.18-rc4 prepatch 中。与此同时,BPF 分配器 patch 的作者 Song Liu 正在努力寻找一套解决方案,从而能安全地使用该分配器,于是就产生了一组由四个 patch 组成的方案。
删除了 VM_NO_HUGE_VMAP flag,改用新的 VM_ALLOW_HUGE_VMAP 这个变种。这改变了该 flag 的含义,从而让 huge page 分配成为一个可以要求使用的功能,而不是要求禁止的。
让 alloc_large_system_hash() (用于为大型哈希表来分配空间)来优先使用 huge page 分配,因为这里使用是肯定安全的。
增加了一个名为 module_alloc_huge() 的函数,它也能实现 huge-page 的分配。
使用 module_alloc_huge() 来分配 BPF 分配器所使用的空间。
如果在 vmalloc()中广泛使用了 huge page 是唯一的问题,那么这些措施可能已经足够了。然而,Torvalds 也不喜欢他在 BPF 分配器代码中看到的一些内容。其中之一是,它在没有初始化内存的情况下就启用了所分配的内存的可执行权限,这就给内核的地址空间里增加了一堆随机的可执行代码段。他总结说 "我真的不认为这已经准备好大规模使用了"。
因此,他决定只合入 Liu 的第一个 patch,也就是完全禁用 vmalloc()中的 huge page 分配(因为还没有什么地方使用了新的 opt-in 方式的 flag)。最初他打算到此为止,但后来判定第二个 patch 也可以安全地合入。然后他甚至更进一步添加了一个他自己的 patch,在 kvmalloc() 中实现了 huge page 分配。这样做的理由是,从该函数返回的内存可能来自一个 slab 分配器,所以拿到这块内存的用户无论如何都不应该对相应的 page structure 使用底层操作技巧。
此后,Liu 在另一组 patch 中修复了未初始化内存的问题。BPF 维护者 Alexei Starovoitov 试图说明这项工作也应该被合入,使 BPF 分配器在 5.18 版本中可用。不过 Torvalds 仍然未被说服,所以这项工作似乎更有可能是 5.19(甚至可能是更晚)才会合入了。也就是说 BPF 用户可能只需要再等一个发布周期就可以使用专门的内存分配器了。
从这个小插曲中可以得出一些结论。调整底层的内存管理特性是很棘手的,可能会在出人意料的地方产生问题。更为流行的架构所带来的广泛测试有很大的价值,它会发现那些在用户基数较小的架构上可能被隐藏起来的错误。但是,也许最重要的是,它说明了在内存管理子系统之外就不应该去访问 page structure。将这种底层的细节暴露在整个内核中,总是会导致这种意外发生。让内核的其他部分不再使用 page structure(这才刚刚开始)将是一个漫长而困难的任务,但还是很值得去承受这些辛苦的。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~