为什么需要 Slab 分配器?

在操作系统内核中,有大量频繁创建和销毁的小对象:进程描述符(task_struct)、文件描述符、网络 socket、dentry 缓存等。如果每次都直接调用页分配器(Buddy System)获取内存,会带来三个严重问题:

! 问题一:严重的内部碎片

Buddy System 以页(4KB)为最小分配单位。一个 task_struct 可能只有 1.7KB,却要占用整整一页,浪费超过 50% 的空间。内核中这类小对象成千上万,浪费极其惊人。

! 问题二:频繁分配/回收的开销

每次分配都要搜索空闲页框、修改页表、更新标记位;释放时又要做合并、回收。这些操作涉及多个自旋锁和中断控制,开销巨大。

! 问题三:重复初始化

同一类对象(如 task_struct)被反复分配时,每次都要执行构造函数来初始化数据结构。如果对象频繁创建销毁,重复初始化就是纯粹的浪费。

Slab 分配器的核心思想:为每种内核对象类型预分配一批对象缓存,对象释放后不立即归还给伙伴系统,而是保留在缓存中供下次复用,同时利用"对象构造/析构"机制避免重复初始化。

三级架构:Cache → Slab → Object

Slab 分配器采用三级层次结构,自顶向下依次为:

kmem_cache(缓存层) 每种内核对象一个 Cache,如 task_struct_cache、inode_cache Slab(页面组)— Partial Obj 0 Obj 1 Free Obj 3 Free 满/空闲对象混合,部分使用中 Slab(页面组)— Full Obj Obj Obj Obj 所有对象均已分配 Slab(页面组)— Empty Free Free Free Free Free 所有对象空闲,可被回收 已使用 空闲 已满 Cache 管理多个 Slab | 每个 Slab 由 1~多个连续物理页组成 | 每个 Slab 包含多个同类型 Object
图 1:Slab 分配器三级层次结构

三种 Slab 状态

每个 Slab(由连续物理页组成)始终处于以下三种状态之一:

Full
所有对象已分配
无空闲对象
Partial
部分对象已分配
有空闲对象
Empty
所有对象空闲
可被系统回收
i Cache 内部维护三个双向链表:full、partial、empty。分配时优先从 partial 链分配,释放时优先归还给 partial 链(而非 empty 链),目的是让 partial slab 保留更多可用对象以加速后续分配。

分配与回收的完整流程

对象分配流程
对象释放流程
1

查找目标 Cache

根据对象类型找到对应的 kmem_cache。例如分配 task_struct 就找 task_struct_cache。每个 Cache 保存了对象大小、对齐要求、构造/析构函数等元信息。

2

从 Partial 链获取空闲对象

遍历 partial 链表,从第一个 partial slab 中取出一个空闲对象(通过空闲对象指针数组 O(1) 定位)。取出后更新 slab 的空闲计数。

3

Partial 链为空?从 Empty 链获取

如果 partial 链没有可用的 slab,则从 empty 链取出一个 slab 并升级为 partial。

4

Empty 链也为空?向 Buddy System 申请新页

所有链表都无可用 slab 时,调用伙伴系统分配新页框,创建新的 slab,初始化其中的对象(执行构造函数),然后分配。

5

返回对象指针

返回指向该对象的指针。由于对象已被构造函数初始化(或从缓存复用),调用者可以直接使用,无需再次初始化。

1

定位对象所属 Slab

通过对象的物理地址计算出它所在的 slab(利用页描述符 page 中的 slab 指针,或通过地址对齐计算)。

2

调用析构函数

如果该 Cache 配置了析构函数,先执行析构函数清理对象中的资源(如释放内部指针、复位标志位)。

3

标记对象为空闲

将对象加入 slab 内部的空闲对象链表(或位图标记为空闲),更新 slab 的 inuse 计数。

4

调整 Slab 状态

如果 slab 变成全部空闲,从 partial 链移到 empty 链。系统在内存紧张时可以回收 empty slab 的页框还给伙伴系统。

交互式 Slab 分配演示

下面是一个简化的 Slab 分配器模拟器。你可以亲自体验对象的分配和释放过程。

模拟对象类型:task_struct(大小约 1.7KB,每个 Slab 可容纳 8 个对象)
等待操作...

关键数据结构

理解 Slab 的核心在于掌握三个数据结构之间的关系:

// Linux 内核中的核心结构体(简化版)

struct kmem_cache {
    const char        *name;        // Cache 名称,如 "task_struct"
    unsigned int        object_size;  // 单个对象的大小
    unsigned int        size;        // 含对齐填充后的实际大小
    unsigned int        order;        // 从伙伴系统申请的页框阶数
    unsigned int        num;         // 每个 Slab 可容纳的对象数
    void                (*ctor)(void *);  // 对象构造函数
    void                (*dtor)(void *);  // 对象析构函数
    struct list_head    partial;     // Partial Slab 双向链表
    struct list_head    full;        // Full Slab 双向链表
    struct list_head    empty;       // Empty Slab 双向链表
    unsigned int        gfporder;    // 分配标志
};

struct slab {
    struct list_head    list;        // 链表节点(挂在 cache 的某个链表上)
    unsigned int        inuse;       // 已使用的对象数量
    unsigned int        free;        // 空闲对象数量
    void               *s_mem;       // 指向第一个对象的指针
    void               **freelist;   // 空闲对象指针数组
};
    
i 空闲对象管理:Slab 内部用指针数组(freelist)管理空闲对象。每个空闲对象的起始位置存储着指向下一个空闲对象的指针,形成隐式链表。分配时 O(1) 弹出,释放时 O(1) 压入,非常高效。

对象大小对齐与 Slab 着色

为了提高 CPU 缓存命中率,Slab 分配器引入了着色(coloring)机制:

Slab A (color=0) 对齐偏移 0 Obj 0 Obj 1 Slab B (color=16) 对齐偏移 16B Obj 0 Obj 1 Slab C (color=32) 对齐偏移 32B Obj 0 Obj 1 着色(Coloring):不同 Slab 的对象起始地址错开,避免映射到同一 CPU Cache Line,减少冲突失效
图 2:Slab 着色机制 — 错开对象在缓存行中的映射位置

与其他分配器的对比

特性 Buddy System Slab 分配器 kmalloc
最小分配单位 1 页 (4KB) 单个对象 字节级(按 2^n 分类)
适用对象大小 页级(大块) 固定大小的内核对象 任意小尺寸
内部碎片 严重(小对象) 极小 较小
分配速度 较慢 极快(O(1))
对象初始化 每次重新初始化 构造函数 + 缓存复用 不清零(可设 GFP_ZERO)
硬件缓存友好 一般 优秀(着色机制) 一般
! kmalloc 与 Slab 的关系:kmalloc 底层实际上就是 Slab 分配器。内核预创建了一组不同大小的通用 Cache(如 32B、64B、128B...),kmalloc 根据请求大小找到最匹配的 Cache,然后从中分配。所以 kmalloc 本质上是 Slab 的一种"通用封装"。

SLAB vs SLUB vs SLOB

Linux 内核中实际上有三种 Slab 分配器的实现:

SLAB 原始实现(Jeff Bonwick) • 对象队列 + 颜色缓存 • 每个 CPU 有 per-CPU 缓存 • 元数据开销较大 • 代码复杂度高 已逐步淘汰 SLUB 当前默认分配器 • 极简元数据(页描述符复用) • 更好的 NUMA 支持 • 调试功能强大 • 大规模系统性能优异 Linux 默认选择 SLOB 极简实现 • 简单的 first-fit • 内存开销最小 • 适合嵌入式/小内存 • 性能一般 嵌入式系统使用
图 3:三种 Slab 实现的对比

内核中的实际使用

// 1. 创建一个新的 Slab Cache
struct kmem_cache *task_struct_cachep;

task_struct_cachep = kmem_cache_create(
    "task_struct",      // Cache 名称
    sizeof(struct task_struct), // 对象大小
    ARCH_MIN_TASKALIGN,  // 对齐要求
    SLAB_PANIC | SLAB_NOTRACK, // 标志位
    NULL                // 构造函数(可选)
);

// 2. 从 Cache 中分配一个对象
struct task_struct *task;
task = kmem_cache_alloc(task_struct_cachep, GFP_KERNEL);

// 3. 使用完毕后释放
kmem_cache_free(task_struct_cachep, task);

// 4. 通用分配(kmalloc 底层使用 Slab)
void *buf = kmalloc(256, GFP_KERNEL);
kfree(buf);
    

如何查看内核中的 Slab 信息

// 在 Linux 系统中,可以通过以下方式查看

// 方式 1:查看 /proc/slabinfo
$ cat /proc/slabinfo
slabinfo - version: 2.1
# name            <active_objs>  <num_objs>  <objsize>  ...
task_struct          512       640      1792  ...    1    8 : tunables ...

// 方式 2:使用 slabtop 命令(实时监控)
$ slabtop

// 方式 3:查看 /sys/kernel/slab/(SLUB 特有)
$ ls /sys/kernel/slab/task_struct/
aliases      align       object_size  order  partial ...
    

核心要点回顾

Slab 分配器的五大核心优势
1
消除内部碎片 — 将页级分配降为对象级,大幅减少内存浪费
2
对象缓存复用 — 释放的对象保留在 Cache 中,避免重复分配/回收开销
3
构造/析构缓存 — 对象初始化结果被保留,避免重复初始化的开销
4
O(1) 分配速度 — 空闲链表操作,分配和释放都是常数时间
5
硬件缓存友好 — 着色机制让不同 Slab 的对象映射到不同 Cache Line

恭喜你学完了 Slab 分配机制的核心知识!

你可以回到上方操作交互式演示加深理解,或在内核中用 slabtop 实际观察。