3. Slab分配器数据结构:kmem_cache、slab、object描述符的详细设计

Slab分配器的核心在于三层数据结构的协同设计:缓存描述符(kmem_cache)、Slab块描述符(slab)以及对象描述符(object)。这三层结构从全局管理到物理页帧,再到单个对象,构成了一个完整的对象缓存体系。理解它们的内存布局与指针关系,是掌握Slab分配器实现的关键。

核心要点: kmem_cache 是工厂,管理一类对象的生产与销毁;slab 是生产车间,持有连续的物理页帧;object 是产品,每个对象对应一个内存块。描述符的设计决定了分配与释放的常数级时间复杂度。

3.1 kmem_cache:缓存管理的大脑

每个 kmem_cache 实例管理一种特定大小的对象类型。它包含了创建、销毁、统计以及链表管理等全部元数据。

字段名称 类型 描述
name const char * 缓存名称,用于 /proc/slabinfo 标识
object_size unsigned int 对象原始大小(字节)
align unsigned int 对象对齐要求(如 cache_line_size)
size unsigned int 实际分配大小(含填充对齐)
num unsigned int 每个 slab 可容纳的对象数量
gfporder unsigned int 每个 slab 占用的连续页框阶数(2^gfporder 页)
flags unsigned long 缓存标志(如 CFLGS_OFF_SLAB 描述符分离)
slabp_cache struct kmem_cache * 指向管理 slab 描述符的专用缓存(off-slab 模式)
ctor void (*ctor)(void *) 对象构造函数指针
list struct list_head 全局缓存链表节点
slabs_full struct list_head 完全分配完毕的 slab 链表
slabs_partial struct list_head 部分分配的 slab 链表
slabs_free struct list_head 完全空闲的 slab 链表
objs_slaboff unsigned int slab 描述符相对于 slab 起始的偏移(on-slab 模式)
设计权衡: 三个链表(full / partial / free)是分配器性能的基石。分配时优先从 slabs_partial 查找,避免扫描全空 slab;释放时若 slab 变空则移入 slabs_free,便于快速回收。

3.2 slab 描述符:连接页帧与对象的桥梁

每个 slab 描述符代表一个或多个连续的物理页帧。它记录了该 slab 中所有对象的状态,以及指向空闲对象链表的指针。

struct slab {
    struct list_head   list;          // 挂入 kmem_cache 的 full/partial/free 链表
    unsigned long      colouroff;     // slab 内的着色偏移(缓存行着色优化)
    void              *s_mem;         // 第一个对象的起始虚拟地址
    unsigned int       inuse;         // 当前已分配的对象数量
    unsigned int       free;          // 空闲对象链表的头索引(或指针)
    unsigned short     nodeid;        // NUMA 节点 ID
};

slab 描述符的存放位置有两种经典模式:

  • On-slab(内置模式): 描述符存放在 slab 自身占用的物理页帧内部,紧跟在对象数据区之后或之前。节省内存,但描述符大小受限于页内剩余空间。
  • Off-slab(外置模式): 描述符从独立的 kmem_cache(slabp_cache)中分配。适用于对象尺寸极小(如 32 字节)的场景,避免描述符浪费过多页内空间。
Slab 描述符布局:On-slab vs Off-slab On-slab 模式 struct slab 描述符(内置) 对象 0 对象 1 ... 对象 N-1 着色填充区(colouroff) Off-slab 模式 struct slab 描述符(独立缓存) 对象 0 对象 1 ... 对象 N-1 着色填充区(colouroff) 分离
避坑指南: Off-slab 模式虽然避免了描述符浪费,但引入了额外的内存分配操作。当对象尺寸大于 512 字节时,通常采用 on-slab 模式;小于 256 字节时,off-slab 模式更优。中间尺寸需根据 slab 内对象数量动态计算。

3.3 object 描述符与空闲链表

Slab 分配器不维护每个对象的独立描述符结构,而是利用对象自身的内存空间构建一个隐式空闲链表。这是其高效的关键设计。

每个空闲对象的前几个字节(通常 4 或 8 字节,取决于指针宽度)被复用为 next 指针,指向下一个空闲对象。分配时从 slab->free 取出头节点,释放时将对象重新链入头部。

// 空闲链表指针复用对象内存
struct freelist_node {
    struct freelist_node *next;
};

// 分配操作伪代码
void *slab_alloc(struct slab *slab) {
    struct freelist_node *node = (struct freelist_node *)slab->free;
    if (!node) return NULL;  // 无空闲对象
    
    slab->free = (unsigned long)node->next;
    slab->inuse++;
    return (void *)node;
}

// 释放操作伪代码
void slab_free(struct slab *slab, void *obj) {
    struct freelist_node *node = (struct freelist_node *)obj;
    node->next = (struct freelist_node *)slab->free;
    slab->free = (unsigned long)node;
    slab->inuse--;
}
对象空闲链表结构(隐式指针复用) struct slab free → 对象 2 对象 2 next → 对象 5 对象 5 next → 对象 1 对象 1 next → NULL 对象 3 已分配(数据区) 对象 4 已分配(数据区) 空闲对象复用前 4/8 字节 作为 next 指针

3.4 三层数据结构的关系总览

将 kmem_cache、slab 和 object 组合在一起,形成完整的层级关系:

  1. kmem_cache 通过三个链表(full / partial / free)管理所有 slab。
  2. 每个 slab 通过 s_mem 指向连续页帧内的对象区起始地址。
  3. 每个 object 在空闲时通过自身内存构建单向链表,slab 的 free 字段指向链表头。
  4. 分配器通过 kmem_cache_alloc() 从 partial 链表的 slab 中取出对象,若 partial 为空则从 free 链表获取 slab 并初始化。
Slab 分配器三层数据结构关系图 kmem_cache slabs_full | slabs_partial | slabs_free slabs_full slabs_partial slabs_free struct slab inuse=num, free=null struct slab inuse=3, free→obj5 struct slab inuse=0, free→obj0 对象数组(连续页帧内) obj0 | obj1 | obj2 | obj3 | obj4 | obj5 s_mem 指向对象区起始 colouroff 控制着色偏移 free 指向空闲链表头

3.5 关键设计指标与性能约束

指标 计算公式 典型值 影响
每 slab 对象数 num = (slab_size - desc_size) / obj_size 8 ~ 256 num 过小增加链表遍历开销;过大浪费内存
着色偏移范围 0 ~ (cache_line_size - 1) * num 0 ~ 256 字节 减少 CPU cache 行冲突
描述符开销比 desc_size / (num * obj_size) < 5% off-slab 模式可降至 < 1%
分配/释放时间复杂度 O(1) 常数级 依赖空闲链表头插头删
性能观察: 在 Linux 内核中,kmem_cache_alloc() 通常只需 30~50 条指令即可完成一次对象分配,远快于 buddy 系统的页分配。这得益于空闲链表的常数级操作和 slab 描述符的快速定位。