文章目录
- 前言
- 一、SLUB allocator
- 二、SLUB core
- 参考资料
前言
本文来自:https://lwn.net/Articles/229984/
[Posted April 11, 2007 by corbet]
SLAB分配器是用于处理“频繁分配和释放的对象”的对象缓存内核内存分配器。它是内存管理子系统中关键的一部分,对于获得良好性能至关重要。Linux的SLAB分配器对几乎所有人都非常有效;然而,一些人(如SGI)发现其当前设计在某些情况下效率低下。例如,在1K节点/处理器配置中,仅在对象队列(object queues)中就浪费了数GB的内存,而不包括对象本身。当添加适当的NUMA策略支持等功能时,内存管理很快变得过于复杂。
因此,SGI的Christoph Lameter开发了一个名为“SLUB”的新的SLAB分配器,以解决这些问题和其他问题。它的设计更简单,但也解决了一些问题,可能在某些情况下提供更好的性能和更高效的内存使用(请参阅此 提交链接 中的完整设计说明)。它还具有更好的调试功能。可以在Documentation/vm/slabinfo.c中找到一个名为slabinfo的用户空间工具。
SLUB是一种减少缓存行使用的SLAB分配器,相比于管理缓存对象队列的SLAB方法,它采用了一种不同的方式。SLUB通过使用对象的SLAB而不是对象队列来实现每个CPU的缓存。
SLUB的目标是透明地取代SLAB,但在2.6.22中,这个新的SLAB分配器是可选的,并且默认情况下未启用。您可以在编译时启用它(使其成为与SLOB(面向嵌入式的SLAB分配器)一起的第三个选项)。SLUB已经经过了一段时间的测试,并且足够稳定,可以在您的系统上尝试使用,但由于内核的这一部分的重要性,在进行更多的公开和测试之前,它不会完全替代当前的SLAB分配器,因此不建议在生产系统中使用它。非常感谢测试报告,特别是回归测试报告。
SLAB分配器数据结构:
SLUB分配器数据结构:
一、SLUB allocator
SLAB分配器多年来一直是内核内存管理的核心部分。该分配器(建立在底层页面分配器之上)管理特定大小对象的缓存,实现了快速且高效的内存分配。内核开发者很少深入研究SLAB代码,因为它非常复杂,而且基本上运行良好。
然而,Christoph Lameter是那些认为SLAB分配器并不完全适用的人之一。随着时间的推移,他列出的问题清单变得令人印象深刻。SLAB分配器维护了一些对象队列,这些队列可以加快内存分配速度,但也增加了相当多的复杂性。此外,存储开销往往随系统规模的增大而增加:
SLAB对象队列存在于每个节点和每个CPU上。甚至外部缓存队列都有一个队列数组,其中包含每个节点上每个处理器的队列。对于非常大的系统,队列的数量和可能被占用的对象数量呈指数增长。在我们的1k节点/处理器系统上,我们仅仅为存储这些队列中对象的引用就使用了数十亿字节的内存,这还不包括队列上可能存在的对象。整个机器的内存有一天会被这些队列耗尽。
除此之外,每个SLAB(一组用于分配对象的一个或多个连续页面)在开头包含一块元数据,这使得对象的对齐更加困难。当内存紧张时,清理缓存的代码增加了另一层复杂性。等等。
Christoph的解决方案是SLUB分配器,它是对SLAB代码的直接替代。SLUB通过放弃大多数队列和相关开销,简化了SLAB结构的同时保留了当前的SLAB分配器接口,以提供更好的性能和可伸缩性。
在SLUB分配器中,slab只是一组一个或多个页面,其中紧密地打包了给定大小的对象。slab本身没有元数据,唯一的例外是空闲对象构成了一个简单的链表。当进行分配请求时,会定位到第一个空闲对象,将其从链表中移除,并返回给调用者。
SLUB分配器通过将页帧打包为组,并且通过struct page中未使用的字段来管理这些组,来最小化内存开销。
鉴于缺乏每个SLAB的元数据,我们可能会想知道如何找到第一个空闲对象。答案是SLUB分配器将相关信息存储在系统内存映射中,即与构成SLAB的页面相关联的页面结构中。在大规模环境中,增加struct page的大小是不被推荐的,因此SLUB分配器通过添加另一个联合体来进一步复杂化这个结构。最终结果是,struct page获得了三个新字段,这些字段仅在相关页面是SLAB的一部分时才具有意义:
void *freelist;
short unsigned int inuse;
short unsigned int offset;
对于slab使用,freelist指向slab中的第一个空闲对象,inuse表示从slab中已经分配的对象数量,offset告诉分配器下一个空闲对象的指针位置。SLUB分配器可以使用RCU来释放对象,但为了实现这一点,它必须能够将“下一个对象”指针放在对象本身之外;offset指针是分配器跟踪该指针位置的方式。
当分配器首次创建一个slab时,该slab中没有分配任何对象。一旦分配了一个对象,它就成为一个"partial" slab,并被存储在kmem_cache结构中的一个列表中。由于这是一个旨在提高可伸缩性的补丁,实际上系统上的每个NUMA节点都有一个"partial" list。分配器尝试保持节点本地的分配,但在填满系统的partial slabs之前,它也会跨节点进行分配。
此外,还有一个每个CPU的 active slabs 数组,旨在防止NUMA节点内的缓存行跳动。有一个特殊的线程(通过工作队列运行),用于监视 per-CPU slabs 使用情况;如果某个CPU的SLAB未被使用,它将被放回到部分列表中,供其他处理器使用。
SLUB维护一个包含一些空闲对象的页面列表。请注意,它不保留完全分配的页面(在其中包含的至少一个对象被释放之前,可以简单地忘记这些页面);它也不保留完全空闲的页面(这些页面被交回页面分配器)。部分页面包含一个或多个自由对象,这些对象被组织到一个链表中。以这种方式做事有一定的美学价值;它使用空闲内存本身来跟踪空闲对象,从而最大限度地减少对象管理所需的开销。
如果一个slab中的所有对象都被分配,分配器会完全忽略该slab。一旦释放了一个满的slab中的对象,分配器可以通过系统内存映射重新定位包含该slab的页面,并将其放回适当的部分列表中。如果给定slab中的所有对象(通过inuse计数器跟踪)都被释放,整个slab将被归还给页面分配器以供重用。
SLUB分配器的一个有趣特性是它可以合并具有相似对象大小和参数的slabs。结果是系统中的slab caches减少了(据称减少了50%),slab allocations的局部性更好,slab内存的碎片化更少。
SLUB分配器在2.6.22之后进入主线。简化的代码和声称的5-10%性能提升都具有吸引力。
二、SLUB core
这是一个新的 SLAB 分配器,受到现有代码(mm/slab.c)的复杂性的启发。它试图解决现有实现中的各种问题。
A. 对象队列的管理
SLAB 分配器中对象队列的复杂管理是一个特别关注的问题。SLUB 分配器中没有这样的队列。相反,我们为每个分配的 CPU 分配一个内存页,并直接从内存页中使用对象,而不是将它们排队。
B. 对象队列的存储开销
SLAB 分配器中的对象队列是按节点和 CPU 划分的。外部缓存队列甚至有一个包含每个节点上每个处理器的队列的队列数组。对于非常大的系统,队列的数量以及可能在这些队列中排队的对象的数量会呈指数级增长。在我们的系统中,如果有 1k 个节点/处理器,仅用于存储这些队列中对象的引用的空间就有几个 GB。这还不包括可能在这些队列中的对象本身。我们担心有一天整个机器的内存都会被这些队列占满。
C. SLAB 元数据开销
SLAB 分配器在每个内存页的开头都有开销。这意味着数据不能在内存页的开头自然对齐。SLUB 分配器将所有元数据保存在相应的 page_struct 结构中。对象可以在内存页中自然对齐。例如,一个 128 字节的对象将在 128 字节的边界上对齐,并且可以完全适配到 4k 大小的内存页中,不会有剩余的字节。SLAB 分配器无法做到这一点。
D. SLAB 有一个复杂的缓存回收机制
SLUB 在单处理器系统中不需要缓存回收机制。在 SMP 系统中,每个 CPU 的内存页可以被推回到部分使用列表,但该操作很简单,不需要对对象列表进行迭代。SLAB 在缓存回收时会过期处理每个 CPU、共享和外部对象队列,这可能导致奇怪的延迟。
E. SLAB 具有复杂的 NUMA 策略支持
SLUB 将 NUMA 策略处理推到页面分配器中。这意味着分配更加粗粒度(SLUB 在页面级别进行交错),但在 2.6.13 版本之前这种情况也存在。SLAB 将策略应用于在 SLAB 中分配的单个对象,这对性能是一个问题,因为频繁引用内存策略可能导致一系列对象从一个节点分配到另一个节点。SLUB 会从一个节点获取一个内存页的对象,然后切换到下一个节点。
F. 减少部分内存页列表的大小
SLAB 分配器在每个节点上有一个部分内存页列表。随着时间的推移,这些列表上可能会积累大量的部分内存页。只有在特定节点上进行分配时,才能重用这些部分内存页。SLUB 分配器有一个全局的部分内存页池,会从该池中获取内存页,以减少碎片化。
G. 可调参数
SLAB 分配器针对每个 SLAB 缓存具有复杂的调优能力。可以详细调整队列的大小。然而,填充队列仍然需要使用自旋锁来检查 SLAB。SLUB 分配器有一个全局参数(min_slab_order)用于调优。增加最小 SLAB 阶数可以减少锁定开销。SLAB 阶数越大,每个 CPU 和部分列表之间的页面移动就越少,SLUB 的扩展性就越好。
G. SLAB 合并
我们经常有具有相似参数的 SLAB 缓存。SLUB 在启动时会检测到它们,并将它们合并到相应的通用缓存中。这会导致更有效的内存使用。大约有 50% 的缓存可以通过 SLAB 合并来消除。这也将减少 SLAB 的碎片化,因为可以重新填充已分配部分的 SLAB。可以通过在启动时指定 slub_nomerge 来关闭 SLAB 合并。
注意,合并可能会暴露内核中迄今未知的错误,因为损坏的对象现在可能被放置在不同的位置,并且可能破坏相邻的对象。启用健全性检查以发现这些问题。
H. 诊断
当前的 SLAB 诊断工具很难使用,并且需要重新编译内核。SLUB 包含了始终可用的调试代码(但被保持在热代码路径之外)。可以通过设置 “slab_debug” 选项启用 SLUB 诊断。可以指定参数选择单个或一组 SLAB 缓存进行诊断。这意味着系统在正常性能下运行,并且更有可能重现竞态条件。
I. 弹性
如果基本的健全性检查开启,SLUB 能够检测常见的错误条件,并尽可能恢复以允许系统继续运行。
J. 跟踪
可以在启动时通过 “slab_debug=T,” 选项启用跟踪。SLUB 将记录该 SLAB 缓存上的所有操作,并在释放时转储对象内容。
K. 按需创建 DMA 缓存
通常不需要 DMA 缓存。如果使用带有 __GFP_DMA 标志的 kmalloc,则只需创建所需的单个 SLAB 缓存。对于没有 ZONE_DMA 要求的系统,完全消除了支持。
L. 性能提升
一些基准测试显示,在内核测试(kernbench)中的速度提升范围为 5-10%。SLUB 的锁定开销基于底层基本分配大小。如果可以可靠地分配较大的页面,那么可以进一步提高 SLUB 的性能。抗碎片化补丁可能会带来进一步的性能提升。
参考资料
https://lwn.net/Articles/229984/
https://kernelnewbies.org/Linux_2_6_22#New_Slab_allocator:_SLUB
https://events.static.linuxfound.org/sites/events/files/slides/slaballocators.pdf