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 字节)的场景,避免描述符浪费过多页内空间。
避坑指南: 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--;
}
3.4 三层数据结构的关系总览
将 kmem_cache、slab 和 object 组合在一起,形成完整的层级关系:
- kmem_cache 通过三个链表(full / partial / free)管理所有 slab。
- 每个 slab 通过
s_mem指向连续页帧内的对象区起始地址。 - 每个 object 在空闲时通过自身内存构建单向链表,slab 的
free字段指向链表头。 - 分配器通过
kmem_cache_alloc()从 partial 链表的 slab 中取出对象,若 partial 为空则从 free 链表获取 slab 并初始化。
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 描述符的快速定位。