3. 核心数据结构

slab 分配器的核心在于三个抽象层次:高速缓存描述符(kmem_cache)、slab 描述符(slab)和对象(object)。三者构成了从“类型管理”到“物理页框”再到“具体内存单元”的完整链路。理解它们之间的关系,是掌握 slab 分配器运作机制的第一步。

3.1 三层结构总览

下图展示了 kmem_cache、slab 和 object 之间的层次关系与核心字段映射:

kmem_cache、slab、object 三层结构关系图 kmem_cache(高速缓存描述符) struct kmem_cache { const char *name; // 缓存名称,如 "task_struct" unsigned int object_size; // 对象大小(字节) unsigned int gfporder; // 每个 slab 占用的页框阶数 unsigned int num; // 每个 slab 可容纳对象数 unsigned int offset; // 对象在 slab 内的偏移 struct list_head slabs_full; // 完全用尽的 slab 链表 struct list_head slabs_partial; // 部分空闲的 slab 链表 struct list_head slabs_free; // 完全空闲的 slab 链表 struct kmem_cache *slabp_cache; // slab 描述符自身缓存 void (*ctor)(void *obj); // 对象构造函数 gfp_t gfpflags; // 底层页框分配标志 }; slabs_partial / slabs_free 链表 slab(slab 描述符) struct slab { struct list_head slab_list; // 挂入 kmem_cache 的链表节点 unsigned long colouroff; // 着色偏移(缓存行对齐) void *s_mem; // 对象区起始虚拟地址 unsigned int inuse; // 已分配对象数量 unsigned int free; // 空闲对象数量 unsigned int active; // 活跃对象数量 void *freelist; // 空闲对象链表头 }; s_mem + offset * index object[0] | object[1] | object[2] | ... | object[num-1]
核心要点:一个 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() 到最终返回对象的完整路径,以及三个数据结构如何协同工作:

slab 分配路径:kmem_cache → slab → object kmem_cache_alloc() 检查 slabs_partial 链表 取 slab->freelist 头节点 更新 freelist = next slab->inuse++ 返回对象地址 分配路径说明: 1. 优先从 slabs_partial 链表取 slab 2. 若 slabs_partial 为空,从 slabs_free 取 3. 若 slabs_free 也为空,调用 kmem_getpages() 分配新页框,初始化新 slab 4. 从 slab->freelist 取出第一个空闲对象 5. 更新 freelist 指向下一个空闲对象 6. slab->inuse 递增 7. 若 slab 变满,移入 slabs_full 链表

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 以允许回收。