1. Slab 分配器概述:内存管理核心机制

1.1 Slab 分配器在 Linux 内存管理中的角色

Linux 内核内存管理分为多个层级,Slab 分配器位于伙伴系统(Buddy System)之上,专门负责管理小块内存对象的分配与释放。它解决了伙伴系统以页(通常 4KB)为最小分配粒度带来的内部碎片问题,为内核中频繁创建和销毁的小型数据结构(如 task_struct、inode、dentry)提供高效的内存复用机制。

核心角色: Slab 分配器是内核对象缓存的实现者,它将伙伴系统分配的大块物理页(2order 页)切分为固定大小的对象,并维护每个对象的初始化状态,实现“分配即就绪”的零开销复用。

Slab 分配器在整个内存管理栈中的位置如下图所示:

Linux 内存管理层次架构 用户态进程(malloc / free / mmap) 系统调用接口(sys_brk / sys_mmap) 虚拟内存管理(VMA / 缺页异常) Slab 分配器(对象缓存 / 通用缓存) 伙伴系统(Buddy System / 页分配器)

1.2 与伙伴系统的关系

Slab 分配器与伙伴系统是“消费者-生产者”关系。伙伴系统负责管理物理页框的分配与回收,而 Slab 分配器在此基础上实现更细粒度的内存管理。两者在职责、粒度、性能特征上存在显著差异:

对比维度 伙伴系统(Buddy System) Slab 分配器
最小分配单位 物理页(通常 4KB) 任意大小对象(8B ~ 几 KB)
分配粒度 2order 页(order=0~10) 按对象大小对齐(cache_line 对齐)
内部碎片 平均 50%(页内浪费) 极低(对象大小精确匹配)
分配速度 O(log N) 合并/分裂 O(1) 从空闲链表取对象
缓存亲和性 无对象级缓存管理 着色(coloring)优化 L1/L2 缓存
主要用途 大块内存、DMA、页表 内核数据结构、网络缓冲区、文件系统元数据
性能关键点: Slab 分配器通过“对象复用”避免了伙伴系统每次分配/释放页时的 TLB 刷新和页表操作。对于每秒数万次的内核对象分配场景,Slab 分配器比直接使用伙伴系统快 10~100 倍。

Slab 分配器向伙伴系统申请内存的流程如下:

  1. 缓存创建:调用 kmem_cache_create() 时,Slab 分配器仅初始化元数据,不立即分配物理页。
  2. 首次分配:当第一个对象分配请求到达时,Slab 分配器调用 alloc_pages() 从伙伴系统获取 2order 个连续物理页。
  3. 对象切分:将获取的物理页划分为多个等大小的对象,并初始化对象头部(如 kmem_bufctl_t 链表)。
  4. 着色优化:对每个 Slab 内的对象起始地址进行偏移(着色),使不同 Slab 的对象落在不同的缓存行上,减少 CPU 缓存冲突。
  5. 对象回收:当 Slab 内所有对象都释放后,Slab 分配器将整组物理页归还给伙伴系统。

1.3 基本设计思想

Slab 分配器的设计围绕三个核心思想展开:对象缓存、状态复用和缓存着色。

1.3.1 对象缓存(Object Caching)

内核中许多数据结构(如 task_struct、socket、inode)被频繁创建和销毁。Slab 分配器为每种数据结构维护一个独立的缓存(kmem_cache),缓存中预分配了多个已初始化好的对象。分配时直接从缓存中取出一个“热”对象,释放时将其放回缓存,避免重复的构造/析构开销。

1.3.2 状态复用(State Reuse)

传统分配器在释放对象后,下次分配需要重新初始化。Slab 分配器通过 ctor(构造函数)和 dtor(析构函数)机制,在对象首次分配时执行一次初始化,后续分配直接复用已初始化的状态。对于不需要完整构造/析构的简单对象,Slab 分配器仅维护空闲链表,分配时从链表头部取出,释放时插入链表头部,实现 O(1) 操作。

1.3.3 缓存着色(Cache Coloring)

缓存着色是 Slab 分配器独有的优化技术。当多个 Slab 中的对象映射到同一 CPU 缓存组时,会产生缓存行冲突(cache line bouncing)。Slab 分配器通过在 Slab 起始地址上增加一个偏移量(着色偏移),使不同 Slab 的对象均匀分布在不同的缓存行上,显著降低缓存未命中率。

Slab 分配器核心架构 kmem_cache • object_size: 256B • align: 64B (cache line) • flags: SLAB_HWCACHE_ALIGN • gfporder: 0 (1 page) slabs_full slabs_partial slabs_free struct slab • s_mem: 对象起始地址 • inuse: 已分配对象数 • free: 空闲对象链表头 • colouroff: 着色偏移 设计思想总结: 1. 对象缓存:每种数据结构一个 kmem_cache,避免重复构造/析构 2. 状态复用:对象释放后保持初始化状态,下次分配直接使用 3. 缓存着色:通过偏移量将对象分布在不同缓存行,减少冲突未命中

1.3.4 三种 Slab 状态

每个 kmem_cache 维护三个 Slab 链表,用于高效管理对象的分配与回收:

  • slabs_full:所有对象都已分配出去的 Slab。当对象释放时,该 Slab 移入 slabs_partial 链表。
  • slabs_partial:部分对象已分配、部分空闲的 Slab。分配请求优先从该链表查找,避免创建新 Slab。
  • slabs_free:所有对象都已释放的 Slab。当系统内存紧张时,这些 Slab 可以被回收并归还给伙伴系统。
调优注意: 如果 slabs_free 链表过长,说明缓存中存在大量空闲 Slab 未被回收。可以通过调整 batchcount 和 limit 参数控制缓存回收的激进程度,避免内存浪费。

1.4 关键数据结构

Slab 分配器的核心数据结构包括 kmem_cache、slab 和 kmem_bufctl_t。以下是一个简化的结构定义:

// 每个 Slab 缓存描述符
struct kmem_cache {
    unsigned int        object_size;    // 对象大小(含对齐)
    unsigned int        size;           // 实际分配大小(含元数据)
    unsigned int        align;          // 对齐要求
    slab_flags_t        flags;          // 标志位(如 SLAB_HWCACHE_ALIGN)
    unsigned int        gfporder;       // 每个 Slab 占用的页阶(2^gfporder 页)
    unsigned int        num;            // 每个 Slab 包含的对象数
    struct list_head    slabs_full;     // 全满 Slab 链表
    struct list_head    slabs_partial;  // 部分空闲 Slab 链表
    struct list_head    slabs_free;     // 全空 Slab 链表
    unsigned int        colour;         // 着色范围(缓存行数)
    unsigned int        colour_off;     // 着色偏移量(字节)
    void (*ctor)(void *obj);           // 构造函数
    void (*dtor)(void *obj);           // 析构函数
};

// 每个 Slab 描述符(位于 Slab 内部或单独分配)
struct slab {
    struct list_head    list;           // 链表节点(链接到 kmem_cache 的链表)
    unsigned long       colouroff;      // 当前 Slab 的着色偏移
    void               *s_mem;          // 对象数据区起始地址
    unsigned int        inuse;          // 已分配对象数
    unsigned int        free;           // 空闲对象链表头索引
};

1.5 本章小结