3. 核心数据结构
slab 分配器的核心在于三个抽象层次:高速缓存描述符(kmem_cache)、slab 描述符(slab)和对象(object)。三者构成了从“类型管理”到“物理页框”再到“具体内存单元”的完整链路。理解它们之间的关系,是掌握 slab 分配器运作机制的第一步。
3.1 三层结构总览
下图展示了 kmem_cache、slab 和 object 之间的层次关系与核心字段映射:
核心要点:一个
kmem_cache 管理多个 slab,每个 slab 占用连续物理页框,并将页框划分为等大小的 object。分配时优先从 slabs_partial 链表取对象,其次 slabs_free,最后创建新 slab。
3.2 高速缓存描述符(kmem_cache)字段详解
kmem_cache 是 slab 分配器最上层的管理结构,每个内核对象类型(如 task_struct、inode)对应一个独立的 kmem_cache 实例。下表列出其关键字段及作用:
| 字段名 | 类型 | 说明 |
|---|---|---|
name |
const char * |
缓存名称,用于 /proc/slabinfo 标识和调试 |
object_size |
unsigned int |
单个对象的大小(字节),包含对齐填充 |
gfporder |
unsigned int |
每个 slab 占用的页框阶数(2^gfporder 个连续页框) |
num |
unsigned int |
每个 slab 中可容纳的对象总数 |
offset |
unsigned int |
对象在 slab 内的起始偏移(用于着色) |
slabs_full |
struct list_head |
所有对象均已分配的 slab 链表 |
slabs_partial |
struct list_head |
部分对象空闲的 slab 链表(分配首选) |
slabs_free |
struct list_head |
所有对象均空闲的 slab 链表 |
slabp_cache |
struct kmem_cache * |
用于分配 slab 描述符自身的 kmem_cache(外部 slab 时使用) |
ctor |
void (*)(void *) |
对象构造函数,在分配时初始化对象 |
gfpflags |
gfp_t |
底层页框分配标志(如 GFP_KERNEL) |
性能提示:
slabs_partial 链表是分配路径的“热区”。如果系统频繁出现 slabs_full 链表过长而 slabs_partial 为空,说明缓存对象大小设置不合理,应考虑调整 object_size 或启用 SLAB_HWCACHE_ALIGN 标志。
3.3 slab 描述符字段详解
每个 slab 描述符对应一组连续的物理页框,负责管理该页框内所有对象的分配与释放状态。slab 描述符可以存放在 slab 内部(内部 slab)或由独立缓存分配(外部 slab),取决于 slab 大小。
| 字段名 | 类型 | 说明 |
|---|---|---|
slab_list |
struct list_head |
将 slab 挂入所属 kmem_cache 的 slabs_full/partial/free 链表 |
colouroff |
unsigned long |
着色偏移量,用于将不同 slab 的对象起始地址错开缓存行,减少伪共享 |
s_mem |
void * |
对象区的起始虚拟地址(即第一个对象的地址) |
inuse |
unsigned int |
当前已分配出去的对象数量 |
free |
unsigned int |
当前空闲对象数量(free = num - inuse) |
active |
unsigned int |
活跃对象数量(某些场景下与 inuse 含义相同) |
freelist |
void * |
指向空闲对象链表的头指针,每个空闲对象的前 4/8 字节存储下一个空闲对象索引 |
3.4 对象(object)与空闲链表
对象是 slab 分配器最终返回给调用方的内存单元。每个对象的大小由 kmem_cache->object_size 决定,且通常按 sizeof(void *) 对齐。对象本身不包含元数据,其空闲状态通过隐式空闲链表管理:
- 空闲链表指针:当对象空闲时,其前 4 字节(32 位)或 8 字节(64 位)被用作指向下一个空闲对象的指针(索引)。
- freelist 头:
slab->freelist指向第一个空闲对象,分配时从头部取出,释放时插回头部(LIFO 行为)。 - 无外部元数据:这种设计避免了为每个对象额外维护分配位图,节省了内存并提升了缓存局部性。
注意:空闲链表指针会覆盖对象的前几个字节,因此已分配的对象绝对不能读取其前 4/8 字节作为有效数据。这是 slab 分配器的一个隐含约束,驱动开发者必须注意。
3.5 分配与释放路径中的三者协作
下图展示了从 kmem_cache_alloc() 到最终返回对象的完整路径,以及三个数据结构如何协同工作:
3.6 关键设计权衡
- 内部 slab vs 外部 slab:当 slab 描述符较小(通常 < 512 字节)时,描述符直接存放在 slab 页框的末尾,节省一次额外分配;大 slab 则从
slabp_cache独立分配描述符。 - 着色(colouroff):通过为不同 slab 的对象起始地址添加微小偏移,使对象均匀分布在不同的缓存行中,减少多核环境下的缓存行伪共享(false sharing)。
- LIFO 空闲链表:释放的对象被插回 freelist 头部,下次分配优先返回刚释放的对象,利用 CPU 缓存的热度(temporal locality)。
实战建议:在
/proc/slabinfo 中观察 task_struct 等高频缓存的 slabs_partial 与 slabs_full 比例。若 slabs_full 占比持续超过 90%,说明对象复用率低,可考虑增大 object_size 或启用 SLAB_RECLAIM_ACCOUNT 以允许回收。