3. Slab 分配器:Slab/Slub/Slob 区别、对象缓存、kmem_cache

Slab 分配器是 Linux 内核中用于管理小对象(通常小于一页)内存分配与释放的核心机制。它通过缓存已初始化的对象来避免频繁的创建与销毁开销,是内核性能优化的关键组件。本章将深入剖析三种主流实现(Slab、Slub、Slob)的差异、对象缓存的工作原理以及 kmem_cache 的编程接口。

3.1 为什么需要 Slab 分配器?

内核中频繁分配和释放的小对象(如 task_struct、inode、dentry)如果直接使用伙伴系统(Buddy System)按页分配,会产生严重的内部碎片和性能瓶颈。Slab 分配器专为此场景设计,其核心优势包括:

  • 对象缓存:复用已分配但未使用的对象,减少初始化开销。
  • 硬件缓存友好:通过着色(Coloring)技术使对象在 CPU Cache 中均匀分布,减少 Cache Miss。
  • 碎片控制:按固定大小管理内存,避免外部碎片。
性能提示: 在数据库、Web 服务器等高频创建/销毁内核对象的场景中,Slab 分配器的效率直接影响系统吞吐量。调优 slab_max_size 或切换分配器类型可带来显著收益。

3.2 三种实现:Slab / Slub / Slob

Linux 内核根据系统规模和硬件能力提供了三种 Slab 分配器实现。下表从设计目标、内存开销、适用场景等维度进行对比:

特性 Slab(传统) Slub(默认) Slob(精简)
设计目标 通用、功能完整 简化代码、降低元数据开销 极小内存占用
元数据开销 高(每个 Slab 有完整描述符) 低(复用页描述符) 极低(链表式管理)
CPU Cache 着色 支持 不支持(简化设计) 不支持
调试能力 强(Red Zoning、Poisoning) 中等 弱
适用系统 大型 NUMA 服务器 通用桌面/服务器(默认) 嵌入式/小型系统
内核配置 CONFIG_SLAB CONFIG_SLUB CONFIG_SLOB
关键决策: 自 Linux 2.6.23 起,Slub 成为默认分配器。对于大多数通用场景,Slub 在性能和内存开销之间取得了最佳平衡。嵌入式系统(如 OpenWrt)常选择 Slob 以节省内存。

3.3 Slab 分配器核心架构

下图展示了 Slab 分配器的三层架构:kmem_cache 管理特定大小的对象池,每个 Cache 包含多个 Slab(物理连续页),每个 Slab 被划分为多个等大小的对象。

Slab 分配器三层架构 kmem_cache Slab 0 (full) Slab 1 (partial) Slab 2 (empty) obj0 obj1 obj2 obj0 obj1 obj2 obj0 obj1 obj2 说明: • 每个 kmem_cache 管理一种固定大小的对象类型。 • Slab 状态:full(全满)、partial(部分空闲)、empty(全空)。 • 分配优先从 partial Slab 中取,释放后若 Slab 变 empty 则归还伙伴系统。

3.4 对象缓存与 kmem_cache 接口

kmem_cache 是 Slab 分配器的核心数据结构,每个 Cache 对应一种对象类型。内核模块可以通过以下 API 创建和使用自定义对象缓存:

// 1. 创建 kmem_cache
struct kmem_cache *cache = kmem_cache_create(
    "my_object_cache",      // 名称(/proc/slabinfo 中可见)
    sizeof(struct my_obj),  // 对象大小
    0,                      // 对齐(0 表示自然对齐)
    SLAB_HWCACHE_ALIGN,     // 标志(硬件缓存对齐)
    NULL                    // 构造函数(通常为 NULL)
);

// 2. 分配对象
struct my_obj *obj = kmem_cache_alloc(cache, GFP_KERNEL);

// 3. 释放对象
kmem_cache_free(cache, obj);

// 4. 销毁 Cache
kmem_cache_destroy(cache);
注意事项:
  • 构造函数在对象首次分配时调用,后续复用时不重复调用,因此对象状态需由调用者维护。
  • SLAB_HWCACHE_ALIGN 标志会强制对象对齐到 L1 Cache Line 大小,对性能敏感场景至关重要。
  • 销毁 Cache 前必须确保所有对象已释放,否则会导致内存泄漏。

3.5 查看 Slab 使用情况

系统管理员可以通过以下文件实时监控 Slab 分配器的状态:

文件路径 内容说明 常用命令
/proc/slabinfo 所有 kmem_cache 的统计信息(对象数、Slab 数、大小等) cat /proc/slabinfo | head -20
/sys/kernel/slab/<cache_name>/ 每个 Cache 的详细调优参数(如 cpu_slab、order) ls /sys/kernel/slab/task_struct/
/proc/meminfo Slab 总内存占用(SReclaimable + SUnreclaim) grep Slab /proc/meminfo
调优示例: 若发现 dentry Cache 占用大量内存,可通过 echo 2 > /proc/sys/vm/drop_caches 回收可回收 Slab(SReclaimable),或调整 vfs_cache_pressure 参数控制回收倾向。

3.6 Slub 分配器的工作流程

以默认的 Slub 分配器为例,对象分配与释放的完整流程如下:

  1. 分配请求:调用 kmem_cache_alloc(),传入 Cache 指针和 GFP 标志。
  2. 本地 CPU 快速路径:检查当前 CPU 的 freelist(空闲对象链表),若有空闲对象则直接返回,无锁操作。
  3. Slab 慢速路径:若本地 freelist 为空,从 partial 链表中获取一个部分空闲的 Slab,将其 freelist 挂到当前 CPU。
  4. 伙伴系统介入:若所有 Slab 都已满,从伙伴系统分配新页(通常为 2^order 页),创建新 Slab 并加入 Cache。
  5. 释放对象:对象释放时,若 Slab 变为全空且系统内存紧张,Slub 会直接归还页给伙伴系统,减少内存占用。
Slub 分配器工作流程 kmem_cache_alloc() freelist 非空? 快速路径:取 freelist 慢速路径:取 partial 分配新页(伙伴系统) 返回对象

3.7 性能对比与选择建议

在实际生产环境中,选择哪种 Slab 分配器需要根据工作负载特征决定:

  • NUMA 大型服务器:传统 Slab 的着色特性可减少跨 Node 内存访问,适合数据库、HPC 场景。
  • 通用云服务器:Slub 的简化设计降低了锁竞争,在多数 Web/微服务场景中表现更优。
  • 嵌入式/IoT 设备:Slob 的极低元数据开销(约 2-5% 内存节省)对 64MB 以下系统意义重大。
快速切换: 在 GRUB 内核启动参数中添加 slab=slub 或 slab=slab 即可切换分配器,无需重新编译内核。重启后通过 cat /proc/slabinfo | head -1 确认当前使用的分配器类型。