2. Slab分配器核心数据结构

Slab分配器是Linux内核中管理小对象内存的核心机制,其设计围绕一组精确定义的数据结构展开。理解这些数据结构是掌握Slab调试与性能调优的基础。本章将逐一剖析kmem_cache、slab、object、page描述符、着色偏移量以及每CPU数组(array_cache)的作用、字段与相互关系。

核心观点: Slab分配器通过将内存划分为固定大小的对象缓存(kmem_cache),每个缓存管理多个物理连续的页面(slab),页面内再分割为等大小的对象(object)。着色偏移量用于优化CPU缓存行对齐,每CPU数组则用于减少锁竞争,提升分配/释放性能。

2.1 数据结构全景图

下图展示了Slab分配器核心数据结构之间的层次关系与协作流程:

Slab分配器核心数据结构关系图 Slab分配器核心数据结构关系图 kmem_cache 名称、对象大小、对齐、标志 slab列表、每CPU数组、着色参数 每CPU数组 array_cache slab 物理连续页面 page描述符 struct page object 固定大小内存块 着色偏移量 color / colour_off 说明:kmem_cache 是全局管理结构,slab 是物理内存载体,object 是分配单元,page 描述符提供底层映射,着色偏移量优化缓存,每CPU数组加速高频路径。

2.2 kmem_cache —— 缓存描述符

kmem_cache 是Slab分配器的顶层管理结构,每个结构体描述一种特定大小对象的缓存池。内核通过 kmem_cache_create() 创建,通过 kmem_cache_destroy() 销毁。

字段 类型 说明
name const char * 缓存名称,用于 /proc/slabinfo 标识
object_size unsigned int 对象原始大小(字节)
align unsigned int 对象对齐要求
flags unsigned long 创建标志(如 SLAB_HWCACHE_ALIGN)
num unsigned int 每个 slab 中可容纳的对象数量
gfporder unsigned int 每个 slab 占用的页面阶数(2^gfporder 页)
colour unsigned int 着色范围(最大偏移量个数)
colour_off unsigned int 着色偏移的基本单位(字节)
slabp_cache struct kmem_cache * 用于分配 slab 描述符的缓存(若 slab 描述符外部存放)
array struct array_cache * 每CPU数组指针(指向 array_cache 数组)
slabs_full struct list_head 完全分配完的 slab 链表
slabs_partial struct list_head 部分分配的 slab 链表
slabs_free struct list_head 完全空闲的 slab 链表
调试提示: 在 /proc/slabinfo 中,每一行对应一个 kmem_cache。通过观察 object_size 与 num 的比值,可以判断 slab 内部碎片率。若 object_size 远小于页面大小但 num 较小,说明对齐或着色开销较大。

2.3 slab —— 物理内存载体

slab 是 kmem_cache 管理的物理连续内存块,通常由 1 个或多个物理页面组成。slab 描述符(struct slab)记录了该 slab 的元数据。

struct slab {
    struct list_head slab_list;   // 挂入 kmem_cache 的 slabs_full/partial/free 链表
    unsigned long __page_flags;   // 页面标志(复用 struct page 的部分字段)
    unsigned int units;           // 对象数量(等于 kmem_cache->num)
    atomic_t __page_refcount;     // 引用计数
    unsigned long __page_memcg;   // 内存控制组信息
    unsigned int __page_type;     // 页面类型标记
    unsigned long __page_head;    // 复合页头部指针
    void *s_mem;                  // slab 中第一个对象的起始虚拟地址
    unsigned int active;          // 已分配的对象数量
    unsigned int free;            // 空闲对象数量
    unsigned int __page_offset;   // 着色偏移量(字节)
};

slab 描述符可以存放在 slab 内部(内部描述符)或 slab 外部(外部描述符),由 CFLGS_OFF_SLAB 标志决定:

  • 内部描述符: slab 描述符存放在 slab 的起始位置,对象紧随其后。适用于对象较大的缓存。
  • 外部描述符: slab 描述符从独立的 slabp_cache 分配,slab 页面全部用于存放对象。适用于对象较小的缓存(如 < 256 字节)。

2.4 object —— 分配单元

object 是 Slab 分配器向调用者返回的最小内存单元。每个 object 的大小由 kmem_cache->object_size 决定,但实际占用的空间可能因对齐和着色而略有增加。

object 在 slab 内的布局遵循以下规则:

  1. 第一个 object 的起始地址 = slab->s_mem。
  2. 后续 object 的地址 = 前一个 object 地址 + kmem_cache->object_size(经对齐调整)。
  3. 空闲 object 通过 freelist 链表串联,freelist 指针存放在 object 内部(前 4/8 字节)或独立的 freelist 数组中。
性能陷阱: 当 object 大小接近或超过一个页面时,slab 内部碎片会急剧增加。此时应考虑使用 kmalloc 或直接使用页分配器。调试时可通过 slabinfo -s 查看每个缓存的 object_size 与 slab_size 比值。

2.5 page描述符 —— 底层映射

Linux 内核使用 struct page 描述每个物理页面。Slab 分配器复用了 struct page 的部分字段来管理 slab 元数据,从而避免额外的内存开销。

struct page 字段 Slab 复用含义
flags 设置 PG_slab 标志,标记该页面属于 Slab 分配器
lru 作为 slab 链表节点(slab_list)
slab_cache 指向所属的 kmem_cache
freelist 指向空闲对象链表头
s_mem 第一个对象的虚拟地址
active 已分配对象计数

当 PG_slab 标志被设置时,内核的内存管理子系统知道该页面由 Slab 分配器控制,不应被页回收或移动。调试时可通过 cat /proc/pagetypeinfo 观察 slab 页面的分布。

2.6 着色偏移量 —— CPU缓存优化

着色(Coloring)是 Slab 分配器的一项关键优化技术,旨在减少 CPU 缓存行冲突。其核心思想是:同一个 kmem_cache 的不同 slab,其内部对象的起始地址相对于页面起始的偏移量不同。

着色偏移量原理示意图 着色偏移量原理示意图 Slab 0: 偏移量 = 0 ... 对象连续排列 Slab 1: 偏移量 = colour_off ... 对象偏移后排列 Slab 2: 偏移量 = 2 × colour_off ... 对象进一步偏移 说明:黄色区域表示着色填充字节,不同 slab 的相同索引对象映射到不同的 CPU 缓存行,减少冲突未命中。

着色参数由 kmem_cache 的以下字段控制:

  • colour_off: 每次偏移的字节数,通常等于 CPU 缓存行大小(64 字节)或 object_size 的对齐值。
  • colour: 最大偏移次数,即 colour_off × colour 不应超过一个 slab 内可用于偏移的空间。
  • 实际偏移量: 每个 slab 创建时,从 0 到 colour-1 循环选取一个偏移序号,乘以 colour_off 得到该 slab 的起始偏移。
调优参数: 在创建缓存时指定 SLAB_HWCACHE_ALIGN 标志,内核会自动将 colour_off 设置为 L1 缓存行大小。对于高频分配的对象(如 task_struct、inode),着色优化可带来 5%~15% 的性能提升。

2.7 每CPU数组(array_cache)

每CPU数组(struct array_cache)是 Slab 分配器实现无锁分配/释放的关键组件。每个 CPU 核心拥有一个独立的 array_cache,用于缓存最近释放的对象,从而避免在分配/释放时获取全局 slab 链表锁。

struct array_cache {
    unsigned int avail;       // 当前可用对象数量
    unsigned int limit;       // 数组容量上限
    unsigned int batchcount;  // 批量转移对象数量
    unsigned int touched;     // 是否被访问过的标志
    spinlock_t lock;          // 自旋锁(用于 NUMA 节点间共享时)
    void *entry[];            // 变长数组,存放对象指针
};

分配与释放的快速路径如下:

操作 快速路径(无锁) 慢速路径(需加锁)
分配 从 array_cache->entry[--avail] 直接返回对象 从 slab 的 slabs_partial 或 slabs_free 链表批量填充 array_cache
释放 将对象放入 array_cache->entry[avail++] 当 array_cache 已满时,将 batchcount 个对象批量归还给 slab
每CPU数组分配/释放流程图 每CPU数组分配/释放流程图 分配请求 array_cache 有可用? 直接返回对象 从 slab 批量填充 释放请求 array_cache 未满? 放入 array_cache 批量归还 slab
性能调优: batchcount 和 limit 是关键的调优参数。对于高频分配/释放的缓存,适当增大 limit(如 256 或 512)可以减少慢速路径的触发频率。可通过 /sys/kernel/slab/<cache_name>/ 下的 limit 和 batchcount 文件动态调整。

2.8 数据结构协作总结

下表总结了各数据结构在分配生命周期中的角色:

阶段 涉及结构 关键操作
缓存创建 kmem_cache 计算 object_size、align、colour、colour_off、num、gfporder
对象分配(快速) array_cache 从 entry[--avail] 返回对象指针
对象分配(慢速) kmem_cache → slab → page → object 从 slabs_partial/slabs_free 获取 slab,更新 freelist
对象释放(快速) array_cache 将对象放入 entry[avail++]
对象释放(慢速) object → slab → kmem_cache 将对象归还 slab,更新 active/free,移动 slab 链表
缓存销毁 kmem_cache → slab → page 释放所有 slab 页面,销毁 array_cache