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)带来的性能开销。
每个缓存由 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 和对象之间的层次关系:
2.2 Slab 缓存行
slab 是对象缓存的基本存储单元。一个 slab 由一个或多个连续的物理页面组成(通常为 2^gfporder 页)。slab 被划分为多个大小相等的对象槽位,每个槽位可以存放一个缓存对象。slab 分配器通过管理 slab 的三种状态(满、部分、空闲)来高效地分配和回收对象。
slab 的内部布局包含三个主要部分:
- Slab 头部描述符:存储 slab 的元数据,如所属缓存、对象数量、空闲对象链表指针等。对于小对象,头部可能存储在 slab 内部;对于大对象,头部可能存储在 slab 外部。
- 对象数组:连续的、大小相等的对象存储区域。
- 着色区域 (Coloring Area):用于调整对象起始偏移,优化缓存行对齐(详见 2.3 节)。
下图展示了一个典型 slab 的内部布局:
2.3 着色 (Coloring)
着色是 slab 分配器为了优化 CPU 硬件缓存(L1/L2/L3 Cache)利用率而引入的一种技术。当多个 slab 中的对象映射到相同的 CPU 缓存行时,会导致缓存行冲突,降低缓存命中率。
着色原理: 在每个 slab 的起始位置(头部描述符之后)插入一个大小可变的偏移量(称为“颜色”)。这个偏移量使得不同 slab 中相同索引的对象起始地址在物理内存中错开,从而映射到不同的 CPU 缓存行集合,减少冲突。
着色过程的核心步骤:
- 计算颜色范围: 根据 slab 大小、对象大小和硬件缓存行大小,计算出可用的颜色数量。
- 分配颜色: 每次创建新 slab 时,分配器选择一个颜色值(通常递增循环),作为该 slab 的起始偏移。
- 设置偏移: 在 slab 头部描述符中记录该颜色值,并据此计算第一个对象的起始地址。
下图展示了着色如何影响不同 slab 中对象的缓存行映射:
2.4 每 CPU 缓存 (Per-CPU Cache)
为了进一步减少多核系统中的锁竞争和内存访问延迟,slab 分配器为每个 CPU 维护了一个本地对象缓存(struct array_cache)。当内核在当前 CPU 上分配对象时,优先从该 CPU 的本地缓存中获取,无需获取全局 slab 链表的锁。
每 CPU 缓存的工作流程:
- 分配 (alloc): 检查当前 CPU 的本地缓存是否为空。如果不为空,直接从本地缓存中弹出一个对象返回。
- 释放 (free): 将对象归还到当前 CPU 的本地缓存中。如果本地缓存已满,则将本地缓存中的一批对象批量归还到全局 slab 链表中。
- 缓存刷新 (flush): 当 CPU 间负载均衡或内存压力大时,系统会刷新每 CPU 缓存,将对象归还给全局 slab。
每 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 之间的交互流程:
2.5 核心概念总结
下表对 slab 分配器的四个核心概念进行了总结对比:
| 概念 | 主要目的 | 实现机制 | 性能影响 |
|---|---|---|---|
| 对象缓存 | 复用对象,减少分配/释放开销 | 为每种对象类型创建独立 kmem_cache |
减少 buddy 系统调用,降低碎片 |
| Slab 缓存行 | 组织和管理对象存储 | 将连续页面划分为对象槽位,维护三态链表 | 提供批量分配/回收能力 |
| 着色 | 优化 CPU 硬件缓存利用率 | 在 slab 头部后插入可变偏移量 | 减少缓存行冲突,提高命中率 |
| 每 CPU 缓存 | 减少锁竞争和内存访问延迟 | 为每个 CPU 维护本地对象缓存 | 显著提升多核系统分配性能 |