2. 核心数据结构:kmem_cache、slab、object 的关系与关键字段解析

Slab 分配器的核心在于三层数据结构的协同:kmem_cache(缓存描述符)、slab( slabs 链表节点)和 object(内存对象)。理解这三者的关系以及关键字段,是调优 Slab 分配器的基础。

核心关系速记: 一个 kmem_cache 管理多个 slab,每个 slab 被划分为多个等长的 object。分配时优先从 slab 的空闲链表中取出 object,释放时归还。

2.1 三层结构总览

下图展示了 kmem_cache、slab 和 object 之间的层次关系与核心指针流向。

kmem_cache、slab、object 三层结构关系图 kmem_cache (缓存描述符) • name: "task_struct" • object_size: 2176 • gfporder: 1 • flags: SLAB_HWCACHE_ALIGN • num: 20 (objects per slab) slab (slab 实例) • s_mem: 0xffff888... (起始地址) • free: 3 (下一个空闲对象索引) • inuse: 12 (已分配对象数) • list: <slabs_full/slabs_partial> slab (slab 实例) • s_mem: 0xffff889... • free: 0 • inuse: 20 • list: <slabs_full> object 数组 object[0] (已分配) object[1] (空闲) object[2] (已分配) object[3] (空闲) ... object[19] (空闲) 每个 object 大小固定 由 object_size 决定 图例说明: kmem_cache:全局缓存描述符,管理 slab 链表 slab:连续物理页框,包含多个 object object:最小分配单元,用户实际使用的内存

2.2 kmem_cache:缓存描述符

kmem_cache 是 Slab 分配器的顶层结构,每个 kmem_cache 对应一种特定大小的对象类型。它负责管理所有属于该类型的 slab 实例。

字段名 类型 说明 调优关注点
name const char * 缓存名称,如 "task_struct"、"inode_cache" 通过 /proc/slabinfo 识别缓存
object_size unsigned int 每个对象的大小(字节),包含对齐 决定 slab 能容纳的对象数量
gfporder unsigned int 每个 slab 占用的连续页框阶数(2^gfporder 页) 影响 slab 大小和内存碎片
num unsigned int 每个 slab 中包含的对象数量 由 object_size 和 gfporder 计算得出
flags unsigned long 缓存标志,如 SLAB_HWCACHE_ALIGN、SLAB_PANIC 控制对齐行为和错误处理
slabs_full struct list_head 所有对象都已分配的 slab 链表 分配时优先跳过
slabs_partial struct list_head 部分对象已分配的 slab 链表 分配时优先从此链表查找
slabs_free struct list_head 所有对象都空闲的 slab 链表 回收时优先释放回页分配器
调优提示: 通过 /sys/kernel/slab/<cache_name>/ 下的 sysfs 接口可以动态调整 gfporder 和 object_size 的对齐方式,从而改变 slab 的密度。

2.3 slab:物理页框容器

slab 结构体描述了一段连续的物理内存(由 gfporder 决定页数),它被划分为多个等长的 object。slab 可以处于三种状态之一:全满、部分满、全空。

字段名 类型 说明 分配/释放时的作用
s_mem void * slab 的起始虚拟地址(第一个 object 的地址) 用于计算 object 的偏移量
free unsigned int 下一个空闲 object 的索引(指向空闲链表头) 分配时直接返回 s_mem + free * object_size
inuse unsigned int 当前已分配的对象数量 当 inuse == num 时 slab 移入 slabs_full
list struct list_head 用于链接到 kmem_cache 的三个链表之一 决定 slab 的归属状态

2.4 object:最小分配单元

object 是 Slab 分配器返回给调用者的内存块。每个 object 的大小由 kmem_cache 的 object_size 字段决定,并且会进行硬件缓存行对齐(如果设置了 SLAB_HWCACHE_ALIGN 标志)。

object 的分配与释放通过 空闲链表 实现,这是一个隐式链表,利用 object 自身的前几个字节存储下一个空闲 object 的索引。

// 分配 object 的核心逻辑(简化版)
void *slab_alloc(struct kmem_cache *cachep) {
    struct slab *slabp;
    void *object;

    // 1. 优先从 slabs_partial 链表获取 slab
    slabp = list_first_entry(&cachep->slabs_partial, struct slab, list);
    if (!slabp) {
        // 2. 如果 slabs_partial 为空,从 slabs_free 获取
        slabp = list_first_entry(&cachep->slabs_free, struct slab, list);
        if (!slabp) {
            // 3. 如果都为空,分配新的 slab
            slabp = kmem_cache_grow(cachep);
        }
    }

    // 4. 从 slab 的空闲链表中取出 object
    object = slabp->s_mem + slabp->free * cachep->object_size;
    slabp->free = *(unsigned int *)object;  // 更新空闲链表头
    slabp->inuse++;

    // 5. 如果 slab 已满,移入 slabs_full 链表
    if (slabp->inuse == cachep->num) {
        list_move(&slabp->list, &cachep->slabs_full);
    }

    return object;
}

2.5 三层结构的协同工作流程

  1. 初始化: 调用 kmem_cache_create() 创建 kmem_cache,指定 object_size、gfporder 等参数。
  2. 分配: 调用 kmem_cache_alloc(),从 kmem_cache 的 slabs_partial 或 slabs_free 链表中找到合适的 slab,然后从该 slab 的空闲链表中取出一个 object。
  3. 释放: 调用 kmem_cache_free(),将 object 归还到所属 slab 的空闲链表,并更新 inuse 计数。
  4. 回收: 当 slab 的所有 object 都释放后(inuse == 0),该 slab 被移入 slabs_free 链表,并可能被回收给页分配器。
性能陷阱: 如果 slabs_partial 链表为空,分配器必须从 slabs_free 获取 slab,这会导致 slab 从全空变为部分满,增加内存占用。频繁的 kmem_cache_grow() 调用会引入页分配器的延迟。

2.6 关键字段对性能的影响

字段 值过大 值过小 最佳实践
gfporder slab 过大,容易导致内存碎片,分配大块连续内存失败率升高 slab 过小,管理开销占比增大,slabs_partial 链表频繁变动 对于常用对象(如 task_struct),使用 gfporder=1(2页)
object_size 浪费内存,每个 slab 能容纳的对象数量减少 对象无法容纳必要数据,导致越界或需要额外分配 保持与硬件缓存行对齐(64字节或128字节)
num slab 内对象过多,slabs_partial 状态持续时间长 slab 内对象过少,频繁触发 kmem_cache_grow() 由 gfporder 和 object_size 自动计算,通常无需手动干预

2.7 数据结构关系总结

下图以流程图形式展示了分配一个 object 时,三层数据结构之间的交互路径。

object 分配流程:三层结构交互 1. kmem_cache_alloc() 2. 检查 slabs_partial 3. 从 slab 取 object 4. 更新 free 和 inuse 5. 返回 object 地址 关键路径: kmem_cache → slabs_partial → slab → object。如果 slabs_partial 为空,则回退到 slabs_free 或调用 kmem_cache_grow()。