2. Slab分配器核心数据结构
Slab分配器是Linux内核中管理小对象内存的核心机制,其设计围绕一组精确定义的数据结构展开。理解这些数据结构是掌握Slab调试与性能调优的基础。本章将逐一剖析kmem_cache、slab、object、page描述符、着色偏移量以及每CPU数组(array_cache)的作用、字段与相互关系。
2.1 数据结构全景图
下图展示了Slab分配器核心数据结构之间的层次关系与协作流程:
2.2 kmem_cache —— 缓存描述符
kmem_cache 是Slab分配器的顶层管理结构,每个结构体描述一种特定大小对象的缓存池。内核通过 kmem_cache_create() 创建,通过 kmem_cache_destroy() 销毁。
| 字段 | 类型 | 说明 |
|---|---|---|
name |
const char * | 缓存名称,用于 /proc/slabinfo 标识 |
object_size |
unsigned int | 对象原始大小(字节) |
align |
unsigned int | 对象对齐要求 |
flags |
unsigned long | 创建标志(如 SLAB_HWCACHE_ALIGN) |
num |
unsigned int | 每个 slab 中可容纳的对象数量 |
gfporder |
unsigned int | 每个 slab 占用的页面阶数(2^gfporder 页) |
colour |
unsigned int | 着色范围(最大偏移量个数) |
colour_off |
unsigned int | 着色偏移的基本单位(字节) |
slabp_cache |
struct kmem_cache * | 用于分配 slab 描述符的缓存(若 slab 描述符外部存放) |
array |
struct array_cache * | 每CPU数组指针(指向 array_cache 数组) |
slabs_full |
struct list_head | 完全分配完的 slab 链表 |
slabs_partial |
struct list_head | 部分分配的 slab 链表 |
slabs_free |
struct list_head | 完全空闲的 slab 链表 |
/proc/slabinfo 中,每一行对应一个 kmem_cache。通过观察 object_size 与 num 的比值,可以判断 slab 内部碎片率。若 object_size 远小于页面大小但 num 较小,说明对齐或着色开销较大。
2.3 slab —— 物理内存载体
slab 是 kmem_cache 管理的物理连续内存块,通常由 1 个或多个物理页面组成。slab 描述符(struct slab)记录了该 slab 的元数据。
struct slab {
struct list_head slab_list; // 挂入 kmem_cache 的 slabs_full/partial/free 链表
unsigned long __page_flags; // 页面标志(复用 struct page 的部分字段)
unsigned int units; // 对象数量(等于 kmem_cache->num)
atomic_t __page_refcount; // 引用计数
unsigned long __page_memcg; // 内存控制组信息
unsigned int __page_type; // 页面类型标记
unsigned long __page_head; // 复合页头部指针
void *s_mem; // slab 中第一个对象的起始虚拟地址
unsigned int active; // 已分配的对象数量
unsigned int free; // 空闲对象数量
unsigned int __page_offset; // 着色偏移量(字节)
};
slab 描述符可以存放在 slab 内部(内部描述符)或 slab 外部(外部描述符),由 CFLGS_OFF_SLAB 标志决定:
- 内部描述符: slab 描述符存放在 slab 的起始位置,对象紧随其后。适用于对象较大的缓存。
- 外部描述符: slab 描述符从独立的
slabp_cache分配,slab 页面全部用于存放对象。适用于对象较小的缓存(如 < 256 字节)。
2.4 object —— 分配单元
object 是 Slab 分配器向调用者返回的最小内存单元。每个 object 的大小由 kmem_cache->object_size 决定,但实际占用的空间可能因对齐和着色而略有增加。
object 在 slab 内的布局遵循以下规则:
- 第一个 object 的起始地址 =
slab->s_mem。 - 后续 object 的地址 = 前一个 object 地址 +
kmem_cache->object_size(经对齐调整)。 - 空闲 object 通过 freelist 链表串联,freelist 指针存放在 object 内部(前 4/8 字节)或独立的 freelist 数组中。
kmalloc 或直接使用页分配器。调试时可通过 slabinfo -s 查看每个缓存的 object_size 与 slab_size 比值。
2.5 page描述符 —— 底层映射
Linux 内核使用 struct page 描述每个物理页面。Slab 分配器复用了 struct page 的部分字段来管理 slab 元数据,从而避免额外的内存开销。
| struct page 字段 | Slab 复用含义 |
|---|---|
flags |
设置 PG_slab 标志,标记该页面属于 Slab 分配器 |
lru |
作为 slab 链表节点(slab_list) |
slab_cache |
指向所属的 kmem_cache |
freelist |
指向空闲对象链表头 |
s_mem |
第一个对象的虚拟地址 |
active |
已分配对象计数 |
当 PG_slab 标志被设置时,内核的内存管理子系统知道该页面由 Slab 分配器控制,不应被页回收或移动。调试时可通过 cat /proc/pagetypeinfo 观察 slab 页面的分布。
2.6 着色偏移量 —— CPU缓存优化
着色(Coloring)是 Slab 分配器的一项关键优化技术,旨在减少 CPU 缓存行冲突。其核心思想是:同一个 kmem_cache 的不同 slab,其内部对象的起始地址相对于页面起始的偏移量不同。
着色参数由 kmem_cache 的以下字段控制:
- colour_off: 每次偏移的字节数,通常等于 CPU 缓存行大小(64 字节)或
object_size的对齐值。 - colour: 最大偏移次数,即
colour_off × colour不应超过一个 slab 内可用于偏移的空间。 - 实际偏移量: 每个 slab 创建时,从
0到colour-1循环选取一个偏移序号,乘以colour_off得到该 slab 的起始偏移。
SLAB_HWCACHE_ALIGN 标志,内核会自动将 colour_off 设置为 L1 缓存行大小。对于高频分配的对象(如 task_struct、inode),着色优化可带来 5%~15% 的性能提升。
2.7 每CPU数组(array_cache)
每CPU数组(struct array_cache)是 Slab 分配器实现无锁分配/释放的关键组件。每个 CPU 核心拥有一个独立的 array_cache,用于缓存最近释放的对象,从而避免在分配/释放时获取全局 slab 链表锁。
struct array_cache {
unsigned int avail; // 当前可用对象数量
unsigned int limit; // 数组容量上限
unsigned int batchcount; // 批量转移对象数量
unsigned int touched; // 是否被访问过的标志
spinlock_t lock; // 自旋锁(用于 NUMA 节点间共享时)
void *entry[]; // 变长数组,存放对象指针
};
分配与释放的快速路径如下:
| 操作 | 快速路径(无锁) | 慢速路径(需加锁) |
|---|---|---|
| 分配 | 从 array_cache->entry[--avail] 直接返回对象 |
从 slab 的 slabs_partial 或 slabs_free 链表批量填充 array_cache |
| 释放 | 将对象放入 array_cache->entry[avail++] |
当 array_cache 已满时,将 batchcount 个对象批量归还给 slab |
batchcount 和 limit 是关键的调优参数。对于高频分配/释放的缓存,适当增大 limit(如 256 或 512)可以减少慢速路径的触发频率。可通过 /sys/kernel/slab/<cache_name>/ 下的 limit 和 batchcount 文件动态调整。
2.8 数据结构协作总结
下表总结了各数据结构在分配生命周期中的角色:
| 阶段 | 涉及结构 | 关键操作 |
|---|---|---|
| 缓存创建 | kmem_cache | 计算 object_size、align、colour、colour_off、num、gfporder |
| 对象分配(快速) | array_cache | 从 entry[--avail] 返回对象指针 |
| 对象分配(慢速) | kmem_cache → slab → page → object | 从 slabs_partial/slabs_free 获取 slab,更新 freelist |
| 对象释放(快速) | array_cache | 将对象放入 entry[avail++] |
| 对象释放(慢速) | object → slab → kmem_cache | 将对象归还 slab,更新 active/free,移动 slab 链表 |
| 缓存销毁 | kmem_cache → slab → page | 释放所有 slab 页面,销毁 array_cache |