一、为什么需要 Slab 分配器?伙伴系统的局限性
Linux 内核内存管理采用伙伴系统(Buddy System)+ Slab 分配器的两层架构。伙伴系统是底层物理页框分配器,以 2^n 页为单位管理内存。但对于内核中大量的小对象分配(task_struct、inode、dentry 等,通常几十到几百字节),伙伴系统直接分配 4KB 页面会造成严重的内部碎片。
Slab 分配器由 Jeff Bonwick 于 1994 年为 Solaris 设计,Linux 2.0 引入。其核心思想是对象缓存:为高频使用的内核对象建立专属缓存池,预先分配页面并切片为固定大小的对象,分配时直接返回空闲对象,无需初始化;释放时标记为空闲,保留数据结构供下次复用。
Slab 解决的核心问题:
- 减少内部碎片:伙伴系统按页分配,Slab 在页内按对象大小精确切割
- 加速对象分配:已初始化的对象直接从空闲链表取出,避免重复构造/析构
- 降低锁竞争:每 CPU 本地缓存避免全局锁争用
- 提升 Cache 命中率:Slab 着色技术减少 Cache Line 冲突
二、Slab 分配器核心三层架构
2.1 架构总览
Slab 分配器采用三级结构:
┌─────────────────────────────────────────┐
│ layer 3: kmem_cache │ 每个对象类型一个 cache
│ (管理同类型对象的多个 slab 链表) │
├─────────────────────────────────────────┤
│ layer 2: slab │ 一个或多个连续物理页
│ (物理内存容器,被切成固定大小格子) │
├─────────────────────────────────────────┤
│ layer 1: 伙伴系统 │ 物理页框分配器
│ (alloc_pages / free_pages) │
└─────────────────────────────────────────┘
kmem_cache是管理同一种对象的缓存,如task_struct_cachep、dentry_cache、inode_cache。每个 cache 维护三个 slab 链表:slabs_partial(部分空闲)、slabs_full(全部占用)、slabs_free(全部空闲)。
slab是从伙伴系统分配的一个或多个连续物理页,被切割为固定数量的对象格子,包含空闲对象链表。
2.2 数据结构关系
struct kmem_cache {
struct kmem_cache_cpu *cpu_slab; // 每 CPU 本地缓存
struct kmem_cache_node *node[MAX_NUMNODES]; // 每 NUMA 节点 slab 链表
unsigned int size; // 对象实际大小(含对齐)
unsigned int object_size; // 用户请求的对象大小
unsigned int flags; // SLAB flags
unsigned int num; // 每个 slab 中的对象数量
unsigned int gfporder; // 每个 slab 占用的页数(2^n)
colour_t colour; // 着色偏移量
colour_t colour_off; // 着色增量
const char *name; // cache 名称
// ...
};
struct kmem_cache_cpu {
void **freelist; // 空闲对象链表头
unsigned long tid; // 全局事务 ID
struct page *page; // 当前使用的 slab 页
struct page *partial; // 本地 partial 链表
} ____cacheline_aligned_in_smp;
三、kmem_cache 缓存创建与管理
3.1 创建专用缓存
对于自定义的内核数据结构(如驱动中的私有结构体),应创建专属 kmem_cache:
#include <linux/slab.h>
static struct kmem_cache *my_cache;
// 模块初始化时创建缓存
static int __init my_module_init(void) {
my_cache = kmem_cache_create(
"my_object", // 缓存名称(/proc/slabinfo 中可见)
sizeof(struct my_obj), // 对象大小
0, // 对齐偏移(0 = 自然对齐)
SLAB_HWCACHE_ALIGN, // 标志:对齐到硬件 Cache Line
NULL // 构造函数(可为 NULL)
);
if (!my_cache) {
pr_err("Failed to create my_cache\n");
return -ENOMEM;
}
return 0;
}
// 模块退出时销毁缓存
static void __exit my_module_exit(void) {
kmem_cache_destroy(my_cache);
}
3.2 从缓存分配和释放对象
// 分配对象(类似 malloc)
struct my_obj *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
if (!obj)
return -ENOMEM;
// 使用对象...
obj->field = value;
// 释放对象
kmem_cache_free(my_cache, obj);
3.3 通用分配接口 kmalloc
当对象大小不固定或无需专属缓存时,使用 kmalloc(实际上也是 Slab 分配器):
// kmalloc 根据大小选择最近的 kmem_cache
void *buf = kmalloc(128, GFP_KERNEL);
void *big_buf = kmalloc(65536, GFP __GFP_NOWARN);
// kzalloc 分配并清零
void *zero_buf = kzalloc(256, GFP_KERNEL);
// 释放
kfree(buf);
kmalloc 大小上限:默认 4MB(KMALLOC_MAX_SIZE),超过需使用 vmalloc()。可分配的最大 slab 缓存大小为 8KB * 8 = 64KB(KMALLOC_MAX_CACHE_SIZE),更大的走专用路径。
四、每 CPU 本地缓存(cpu_slab)
4.1 设计原理
多处理器系统中,全局 slab 缓存的链表操作需要加锁,成为性能瓶颈。SLUB 分配器为每个 CPU 分配了per-cpu freelist:每个 CPU 有自己的空闲对象链表,分配/释放时先操作本地链表,仅当本地为空时才从 Node 的 partial 链表补充。
4.2 分配路径详解
/**
* SLUB 分配路径(简化版):
* 1. 检查 cpu_slab->freelist 是否非空 -> 直接 pop 对象返回
* 2. freelist 为空 -> 检查 cpu_slab->partial 是否有页 -> 切换到 partial 页
* 3. 仍无 -> 从 kmem_cache_node->partial 链表获取 slab
* 4. 仍无 -> 从伙伴系统 alloc_pages 分配新 slab
*/
static __always_inline void *slab_alloc(struct kmem_cache *s,
gfp_t gfpflags, unsigned long addr)
{
redo:
freelist = cpu_slab->freelist; // 读取本地链表头
if (freelist)
goto load_freelist; // 快速路径:直接分配
// 慢速路径:本地为空,从 partial 或伙伴系统补充
freelist = get_partial(s, ...);
if (freelist)
goto load_freelist;
// 必须从伙伴系统申请新 slab
page = new_slab(s, gfpflags);
if (page) {
cpu_slab_set(page);
goto redo;
}
return NULL; // 分配失败
}
4.3 释放路径
/**
* SLUB 释放路径:
* 1. 将对象追加到 cpu_slab->freelist 头部(无锁,原子操作)
* 2. 若本地缓存超额(超过 s->cpu_partial),归还部分到 Node partial 链表
*/
static __always_inline void slab_free(struct kmem_cache *s, void *object)
{
// 直接头插法入链
set_freelist(s, object, freelist);
// 检查是否需要批量归还
if (unlikely(slab_want_init_on_free(...)))
// 安全清零逻辑...
// 检查本地缓存是否超额
if (unlikely(node_partial_unused(s) > s->cpu_partial_slabs))
flush_cpu_slab(s);
}
优势总结:热路径分配/释放无锁、无原子操作、无内存屏障,性能接近 O(1)。
五、Slab 着色(Cache Coloring)原理
5.1 为什么需要着色?
CPU Cache 采用set-associative映射(如 64 组 8 路),物理地址的 bit[6:11] 决定落在哪个 Cache Set。如果多个 slab 的相同偏移位置的对象被同时访问,会导致 Cache Set 冲突(Cache Thrashing),降低命中率。
5.2 着色实现机制
Slab 的着色(Colouring)通过在每个 slab 起始位置添加不同大小的偏移量(称为 colour offset),使得不同 slab 中相同位置对象的 Cache Set 分布均匀。
/**
* 着色参数:
* colour = 可用偏移量数量(基于剩余空间计算)
* colour_off = 每次偏移增量(对齐到 cache line)
*/
struct kmem_cache {
colour_t colour; // 着色范围内可用颜色数
colour_t colour_off; // 增量 = cache_line_size()
};
// 创建 slab 时计算偏移
static inlinecolour_t get_colour(struct kmem_cache *s)
{
return s->colour_next++ & (s->colour - 1);
}
struct page *new_slab(struct kmem_cache *s, gfp_t flags, int node)
{
colour = get_colour(s);
slab_size = colour_off * colour; // 起始偏移
// ... 分配页面
}
5.3 着色效果分析
假设:L1 D-Cache 32KB、64 字节 Cache Line、4-way set associative。可确认 Cache Set 数 = 32KB / (64B × 4) = 128 组。着色使得偏移差异在 [0, colour_off × colour) 范围内,确保相同对象在不同 slab 中以不同 Cache Set 访问,减少冲突失效。实测着色可提升密集场景下 Cache 命中率 3-8%,具体效果取决于对象大小和访问模式。
六、SLUB —— Linux 现代 Slab 分配器
6.1 SLUB 设计目标
SLUB(Unqueued Slab Allocator)自 Linux 2.6.23 起成为默认分配器,替代传统的 SLAB。其设计原则:
- 简化元数据:放弃独立的 slab 管理结构,利用 struct page 空闲区域内嵌 freelist 指针
- NUMA 感知:每个 NUMA 节点维护独立 slab 列表,减少跨节点访问
- 减少内存开销:相比 SLAB 的 slab 描述符数组,SLUB 内存 overhead 更低
- 更好的可扩展性:per-cpu freelist 设计天然适配多核
6.2 SLUB 核心结构
/**
* SLUB 使用 struct page 内嵌的 union 存储 metadata
* 当页被分配为 slab 时:
* page->freelist -> slab 中的空闲对象列表
* page->inuse -> 已使用对象数
* page->objects -> slab 中总对象数
* page->frozen -> 是否冻结(per-cpu 专属)
* page->slab_cache -> 指向 kmem_cache
*/
struct page {
union {
struct { /* SLUB */
union {
struct {
void *freelist; // 空闲对象头
union {
unsigned long counters;
struct {
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
};
};
struct { /* 每页 slab 描述符 */
struct { };
void *freelist;
void *next;
} kmem_cache_node;
};
};
};
};
6.3 SLAB vs SLUB vs SLOB
| 特性 | SLAB | SLUB | SLOB |
|---|---|---|---|
| 引入时间 | 1994 (Solaris) | 2007 (Linux 2.6.23) | 2006 (嵌入式) |
| 元数据结构 | 独立 slab_t 描述符 | struct page 内联 | 对象内链表 |
| 默认分配器 | 否 | 是(Linux 主流) | 否 |
| 多核扩展性 | 较差(全局锁) | 优秀(per-cpu) | 差 |
| 内存开销 | 较高 | 最低 | 最低 |
| 调试支持 | 完善 | 完善完善(Red Zone/Poison) | 有限 |
| 适用场景 | 服务器(已废弃) | 通用、高性能 | 内存受限嵌入式 |
七、调试与监控
7.1 /proc/slabinfo 解读
# 查看所有 slab 缓存
cat /proc/slabinfo
# 输出格式:
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
# 示例:
cat /proc/slabinfo | head -20
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
kmalloc-128 24514 24576 128 32 1 : tunables 0 0 0 : slabdata 768 768 0
dentry 18992 21315 192 21 2 : tunables 0 0 0 : slabdata 1015 1015 0
inode_cache 15403 15820 608 13 2 : tunables 0 0 0 : slabdata 1217 1217 0
task_struct 369 420 5928 5 8 : tunables 0 0 0 : slabdata 84 84 0
关键指标含义:
- active_objs:当前已使用的对象数
- num_objs:总对象数(含空闲)
- objsize:对象实际大小(含对齐)
- objperslab:每个 slab 切多少个对象
- active_slabs / num_slabs:活跃 slab 比例(高比例=内存浪费少)
内存占用量估算:num_objs × objsize / 1024 KB。若 active_objs 远小于 num_objs,有大量空闲对象可回收。
7.2 slabtop 实时监控
# 类似 top,实时查看 slab 缓存使用情况
watch -n 1 "cat /proc/slabinfo | sort -k3 -rn | head -10"
# 或使用 slabtop
echo 1 /proc/sys/vm/drop_caches # 仅清除 dentries/inodes(不影响 slab 分配器)
7.3 启用 SLUB 调试
# 启动参数启用完整 SLUB 调试
slab_debug=FZP # F=Fault-injection Z=Red-Zone P=Poison
# F(Fault-injection):故意注入分配失败,测试错误处理路径
# Z(Red-Zone):在对象边界插入哨兵值,检测越界写
# P(Poison):释放后填充 0xdead007,检测 use-after-free
# 对单个 cache 启用(内核调试):
echo 1 > /sys/kernel/slab/dentry/validate
八、内存泄漏检测实战
8.1 kmemleak:内核内存泄漏检测器
kmemleak 通过追踪所有分配的内存块,定期扫描未被任何指针引用的内存块,标记为泄漏。
# 编译内核时启用
CONFIG_DEBUG_KMEMLEAK=y
# 查看泄漏报告
cat /sys/kernel/debug/kmemleak
# 输出示例:
unreferenced object 0xffff888123456780 (size 192):
comm "test_module", pid 1234, jiffies 4294967295
hex dump (first 32 bytes):
00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff .."3DUfw..........
backtrace:
[<ffffffff81234567>] kmem_cache_alloc+0x3c/0xe0 [<task_struct>]
[<ffffffff81abcdef>] do_init_module+0x56/0x1a0
[<ffffffff81123456>] load_module+0x1a3/0x5b0
# 手动触发扫描
echo scan > /sys/kernel/debug/kmemleak
# 清除旧报告
echo clear > /sys/kernel/debug/kmemleak
8.2 /proc/meminfo 中的 Slab 信息
$ cat /proc/meminfo | grep -i slab
Slab: 3846124 kB # 总共分配的 slab 内存
SReclaimable: 3124520 kB # 可回收的 slab(如 dentry/inode 缓存)
SUnreclaim: 721604 kB # 不可回收的 slab(活跃对象持有的 page cache)
8.3 使用 drgn 做内存分析
#!/usr/bin/env python3
import drgn
from drgn import container_of
from drgn.helpers.linux.slab import for_each_slab_cache, slab_cache_size, slab_cache_total_used
prog = drgn.program() # 连接到崩溃转储或 live kernel
# 遍历所有 slab cache
for cache in for_each_slab_cache(prog):
name = cache.name.string_().decode()
active_objs = cache.active_slabs.counter
objs = cache.total_slabs.counter
obj_size = cache.size.value_()
waste = (objs - active_objs) * obj_size
if waste > 1024*1024: # 超过 1MB 浪费
print(f"{name}: {active_objs}/{objs} objs, "
f"objsize={obj_size}B, waste={waste//1024}KB")
九、生产环境调优与最佳实践
9.1 驱动模块中的 Slab 使用建议
/*
* 最佳实践 1: 为高频分配的对象创建专属 cache
* 避免使用 kmalloc/kfree(存在 cache 查找和 CPU 迁移开销)
*/
static struct kmem_cache *driver_cache;
static int driver_probe(struct pci_dev *pdev, ...)
{
driver_cache = kmem_cache_create("driver_desc",
sizeof(struct driver_desc),
SMP_CACHE_BYTES, // 对齐到 L1 Cache Line
SLAB_HWCACHE_ALIGN,
NULL);
// 分配
desc = kmem_cache_alloc(driver_cache, GFP_KERNEL);
}
/*
* 最佳实践 2: GFP 标志选择
* GFP_KERNEL : 可睡眠分配(进程上下文)
* GFP_ATOMIC : 不可睡眠(中断、持有自旋锁)
* GFP_NOIO : 不触发 I/O(块层内部使用,避免死锁)
* GFP_NOFS : 不调用文件系统(文件系统内部使用,避免死锁)
*/
/*
* 最佳实践 3: 模块退出时必须销毁 cache,否则内存泄漏
*/
static void driver_remove(struct pci_dev *pdev)
{
kmem_cache_free(driver_cache, desc);
kmem_cache_destroy(driver_cache);
}
9.2 sysctl 调优
# /etc/sysctl.conf 推荐配置
# 1. slab 回收积极性(默认 100,增大更积极回收 vm.slab_reclaim_factor = 200
# 2. 限制 slab 内存占总内存比例(默认无显式限制,通过 min_free_kbytes 间接控制)
vm.min_free_kbytes = 262144 # 1GB 内存建议设为 256MB 级别
# 3. 开启 slab 合并(减少碎片)
# 自 Linux 3.15 起 SLUB 支持同大小 cache 合并,默认开启
slab_nomerge=0
# 4. 调优 slab 缓存大小分配策略(按需调整)
# /sys/kernel/slab/$cache_name/ 中的参数可以运行时调整
echo 32 > /sys/kernel/slab/kmalloc-128/cpu_partial # 增加每 CPU 缓存量
9.3 NUMA 感知分配
# 查看 NUMA 拓扑(影响 slab node 分布)
numactl --hardware
# 强制在特定 NUMA 节点分配
numactl --membind=0 ./my_workload
# 在驱动中指定节点分配
desc = kmem_cache_alloc_node(driver_cache, GFP_KERNEL, node_id);
十、高级话题:Slab 分配器的未来演进
10.1 Folio 与 Slab
Linux 5.16 引入 Folio(大页封装结构),为 2 的幂次大小连续页框提供服务。Folio 与分配器的协作被认为是处理各种连续大小内存请求的方向,长期目标是实现更灵活的 slab 基础结构。
10.2 cgroup 与 Slab 内存控制
Linux 6.0 逐步引入系统化内存控制,cgroup 逐步获得对回收、分配行为的更多控制。未来 cgroup v2 将影响 slab 分配器,因为内存压力指标直接影响 slab 回收。
10.3 BPF 与 Slab 分配器
eBPF 子系统大量使用BPF 本地存储(per-cpu/hash/array map),依赖 SLUB 分配器管理 map 元数据。高频 BPF map 操作对 SLUB 分配器提出了低延迟要求。Linux 6.8+ 对 BPF map 缓存进行了优化。
10.4 自动合并大小策略
Linux 3.15+ SLUB 支持合并相同或相似大小的 cache(slab 合并)。通过减少碎片,提高 cache 利用率,同时对内核编译时即创建的缓存效果尤为显著。
十一、总结与学习路径
Slab 分配器是 Linux 内核内存管理的基石。从原理到实践:
学习阶段一 - 原理理解:阅读 mm/slab.c 早期实现,理解 kmem_cache、slab 和伙伴系统的协作关系。
学习阶段二 - SLUB 深入:阅读 mm/slub.c 核心代码,理解 per-cpu freelist、partial 列表管理、着色算法。
学习阶段三 - 调试与调优:使用 /proc/slabinfo、slabtop、kmemleak 分析系统内存使用,通过 sysctl 和 pfn 参数优化。
学习阶段四 - 模块实战:编写内核模块,使用 kmem_cache_create 管理私有对象,结合 drgn 做深度分析。
推荐资源:
- 内核源码 mm/slub.c:真正的权威文档
Documentation/vm/slub.rst:内核官方文档Understanding the Linux Kernel第 8 章:经典教材- kernel 邮件列表 (mm/tree):最新的 Slub 演进
理解 Slab 分配器不仅是理解 Linux 内核内存管理的必经之路,也是开发高性能内核模块和驱动的基础。在内存敏感的场景(网络、存储、容器)中,合理的 Slab 策略可以显著减少内存浪费和 Cache 提升系统整体吞吐。

发表评论 取消回复