2. slab 分配器核心概念

slab 分配器是 Linux 内核中用于管理小对象(通常小于一页)的高效内存分配机制。其核心设计围绕四个关键概念展开:对象缓存 (object cache)、slab 缓存行、着色 (coloring) 和 每 CPU 缓存 (per-CPU cache)。理解这些概念是掌握 slab 及其后继者 slub 分配器的基础。

2.1 对象缓存 (Object Cache)

对象缓存是 slab 分配器最上层的抽象。内核中频繁创建和销毁的同类型数据结构(如 task_struct、inode、dentry)会对应一个专属的缓存。缓存负责管理该类型对象的分配、释放和复用,避免了频繁调用底层页分配器(如 buddy system)带来的性能开销。

核心思想: 缓存预先分配并维护一组已初始化或未初始化的对象。当内核请求一个新对象时,直接从缓存中获取一个已存在的对象(或从 slab 中分配一个新对象),使用完毕后归还给缓存,而不是释放给系统。这极大地减少了内存碎片和分配延迟。

每个缓存由 struct kmem_cache 结构体描述,主要包含以下关键字段:

字段 描述
name 缓存名称,用于调试和 /proc/slabinfo 标识。
object_size 缓存管理的每个对象的大小(字节)。
align 对象对齐要求。
flags 缓存标志,如 SLAB_HWCACHE_ALIGN(硬件缓存行对齐)。
num 每个 slab 中可容纳的对象数量。
gfporder 每个 slab 占用的页面阶数(2^gfporder 页)。
cpu_cache 指向每 CPU 缓存结构的指针。
slabs_full 所有对象都已分配的 slab 链表。
slabs_partial 部分对象已分配的 slab 链表。
slabs_free 所有对象都空闲的 slab 链表。

下图展示了对象缓存、slab 和对象之间的层次关系:

对象缓存 (kmem_cache) 层次结构 对象缓存 (kmem_cache) slabs_full (已满) slabs_partial (部分) slabs_free (空闲) 对象 对象 对象 对象 对象 对象 对象 对象 对象

2.2 Slab 缓存行

slab 是对象缓存的基本存储单元。一个 slab 由一个或多个连续的物理页面组成(通常为 2^gfporder 页)。slab 被划分为多个大小相等的对象槽位,每个槽位可以存放一个缓存对象。slab 分配器通过管理 slab 的三种状态(满、部分、空闲)来高效地分配和回收对象。

slab 的内部布局包含三个主要部分:

  1. Slab 头部描述符:存储 slab 的元数据,如所属缓存、对象数量、空闲对象链表指针等。对于小对象,头部可能存储在 slab 内部;对于大对象,头部可能存储在 slab 外部。
  2. 对象数组:连续的、大小相等的对象存储区域。
  3. 着色区域 (Coloring Area):用于调整对象起始偏移,优化缓存行对齐(详见 2.3 节)。

下图展示了一个典型 slab 的内部布局:

单个 Slab 内部布局 一个 Slab (2^gfporder 个连续物理页) Slab 头部 (描述符) 空闲链表指针 着色 区域 对象 0 对象 1 对象 2 对象 3 对象 4 说明:着色区域用于调整第一个对象的起始偏移,以优化不同 slab 中对象在 CPU 缓存中的分布。

2.3 着色 (Coloring)

着色是 slab 分配器为了优化 CPU 硬件缓存(L1/L2/L3 Cache)利用率而引入的一种技术。当多个 slab 中的对象映射到相同的 CPU 缓存行时,会导致缓存行冲突,降低缓存命中率。

着色原理: 在每个 slab 的起始位置(头部描述符之后)插入一个大小可变的偏移量(称为“颜色”)。这个偏移量使得不同 slab 中相同索引的对象起始地址在物理内存中错开,从而映射到不同的 CPU 缓存行集合,减少冲突。

关键点: 着色区域的大小是动态计算的,范围从 0 到 (cache_line_size - 1)。着色不会浪费内存,因为它利用了 slab 末尾可能存在的碎片空间(由于对象大小和页面大小不整除导致)。着色区域本质上是将这种碎片空间重新利用为缓存优化偏移。

着色过程的核心步骤:

  • 计算颜色范围: 根据 slab 大小、对象大小和硬件缓存行大小,计算出可用的颜色数量。
  • 分配颜色: 每次创建新 slab 时,分配器选择一个颜色值(通常递增循环),作为该 slab 的起始偏移。
  • 设置偏移: 在 slab 头部描述符中记录该颜色值,并据此计算第一个对象的起始地址。

下图展示了着色如何影响不同 slab 中对象的缓存行映射:

着色 (Coloring) 对缓存行映射的影响 Slab 0 (颜色偏移 = 0) Obj 0 Obj 1 Obj 2 Slab 1 (颜色偏移 = 16) Obj 0 Obj 1 Obj 2 Slab 2 (颜色偏移 = 32) Obj 0 Obj 1 Obj 2 CPU 缓存行映射效果 • Slab 0 的 Obj 0 映射到缓存行 A • Slab 1 的 Obj 0 映射到缓存行 B (偏移 16) • Slab 2 的 Obj 0 映射到缓存行 C (偏移 32) 不同 slab 的相同索引对象 映射到不同缓存行, 减少了缓存行冲突。 假设缓存行大小为 64 字节。

2.4 每 CPU 缓存 (Per-CPU Cache)

为了进一步减少多核系统中的锁竞争和内存访问延迟,slab 分配器为每个 CPU 维护了一个本地对象缓存(struct array_cache)。当内核在当前 CPU 上分配对象时,优先从该 CPU 的本地缓存中获取,无需获取全局 slab 链表的锁。

每 CPU 缓存的工作流程:

  1. 分配 (alloc): 检查当前 CPU 的本地缓存是否为空。如果不为空,直接从本地缓存中弹出一个对象返回。
  2. 释放 (free): 将对象归还到当前 CPU 的本地缓存中。如果本地缓存已满,则将本地缓存中的一批对象批量归还到全局 slab 链表中。
  3. 缓存刷新 (flush): 当 CPU 间负载均衡或内存压力大时,系统会刷新每 CPU 缓存,将对象归还给全局 slab。
注意事项: 每 CPU 缓存虽然提高了性能,但也引入了内存占用问题。每个 CPU 都维护一份缓存副本,可能导致总内存消耗增加。slub 分配器对此进行了优化,采用了更轻量级的每 CPU 空闲链表。

每 CPU 缓存的结构体定义如下:

struct array_cache {
    unsigned int avail;      // 当前可用对象数量
    unsigned int limit;      // 缓存容量上限
    unsigned int batchcount; // 批量传输的对象数量
    unsigned int touched;    // 是否被访问过(用于缓存收缩决策)
    void *entry[];           // 对象指针数组(变长)
};

下表总结了每 CPU 缓存的关键参数及其作用:

参数 作用 典型值
limit 本地缓存的最大对象数量。超过此值后,新释放的对象会触发批量回收到全局 slab。 动态调整,通常为 2~256
batchcount 每次从全局 slab 批量获取或归还到全局 slab 的对象数量。 通常为 limit 的一半
touched 标记缓存是否被使用过。在内存回收时,未使用的缓存(touched=0)会被优先收缩。 0 或 1

下图展示了每 CPU 缓存与全局 slab 之间的交互流程:

每 CPU 缓存与全局 Slab 交互流程 CPU 0 每 CPU 缓存 (array_cache) CPU 1 每 CPU 缓存 (array_cache) CPU 2 每 CPU 缓存 (array_cache) 全局 Slab 链表 (slabs_full/partial/free) 受 spinlock 保护 批量获取/归还 批量获取/归还 批量获取/归还

2.5 核心概念总结

下表对 slab 分配器的四个核心概念进行了总结对比:

概念 主要目的 实现机制 性能影响
对象缓存 复用对象,减少分配/释放开销 为每种对象类型创建独立 kmem_cache 减少 buddy 系统调用,降低碎片
Slab 缓存行 组织和管理对象存储 将连续页面划分为对象槽位,维护三态链表 提供批量分配/回收能力
着色 优化 CPU 硬件缓存利用率 在 slab 头部后插入可变偏移量 减少缓存行冲突,提高命中率
每 CPU 缓存 减少锁竞争和内存访问延迟 为每个 CPU 维护本地对象缓存 显著提升多核系统分配性能
学习提示: 这四个概念是 slab 分配器的设计基石。slub 分配器作为 slab 的简化替代方案,保留了对象缓存和每 CPU 缓存的思想,但简化了 slab 管理逻辑(去掉了着色和复杂的 slab 链表),从而降低了代码复杂度和元数据开销。后续章节将详细对比两者的差异。