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 被划分为多个等大小的对象。
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 分配器为例,对象分配与释放的完整流程如下:
- 分配请求:调用
kmem_cache_alloc(),传入 Cache 指针和 GFP 标志。 - 本地 CPU 快速路径:检查当前 CPU 的
freelist(空闲对象链表),若有空闲对象则直接返回,无锁操作。 - Slab 慢速路径:若本地 freelist 为空,从
partial链表中获取一个部分空闲的 Slab,将其 freelist 挂到当前 CPU。 - 伙伴系统介入:若所有 Slab 都已满,从伙伴系统分配新页(通常为 2^order 页),创建新 Slab 并加入 Cache。
- 释放对象:对象释放时,若 Slab 变为全空且系统内存紧张,Slub 会直接归还页给伙伴系统,减少内存占用。
3.7 性能对比与选择建议
在实际生产环境中,选择哪种 Slab 分配器需要根据工作负载特征决定:
- NUMA 大型服务器:传统 Slab 的着色特性可减少跨 Node 内存访问,适合数据库、HPC 场景。
- 通用云服务器:Slub 的简化设计降低了锁竞争,在多数 Web/微服务场景中表现更优。
- 嵌入式/IoT 设备:Slob 的极低元数据开销(约 2-5% 内存节省)对 64MB 以下系统意义重大。
快速切换: 在 GRUB 内核启动参数中添加
slab=slub 或 slab=slab 即可切换分配器,无需重新编译内核。重启后通过 cat /proc/slabinfo | head -1 确认当前使用的分配器类型。