引言:为什么内核需要专用内存分配器?
Linux 内核是一个庞大而复杂的系统,它对内存的管理远不止简单的页面分配。当我们编写内核模块或深入理解系统性能时,Slab 分配器是一个绕不开的核心机制。它诞生于 1994 年 SunOS 的 Jeff Bonwick 之手,后来被引入 Linux 2.1.23,至今已演进三代:SLAB(经典实现)、SLUB(默认分配器)和 SLOB(精简版)。
本文将从零开始,深入剖析 Slab 分配器的设计哲学、内部数据结构、三代实现差异,并结合生产环境实战案例,带你彻底掌握这一内核内存管理的基石。
第一章:Buddy System 与 Slab 的分层协作
理解 Slab 必须先理解 Buddy System(伙伴系统)。Buddy System 以 页(Page,通常 4KB) 为最小分配单位,管理物理内存。但内核中大量对象的尺寸远小于一页——如 task_struct(约 7KB)、inode(约 600 字节)、dentry(约 200 字节)。如果每次分配都使用页面级分配器,内部碎片将极其惊人。
Slab 分配器正是为了解决这个问题:它在 Buddy System 之上构建对象缓存机制,将页面划分为多个等大小的对象槽,实现细粒度的内存分配与回收,消除内部碎片。
两者协作模型:
- Buddy System:管理页面级连续物理内存,解决外部碎片(通过迁移和合并)
- Slab 分配器:管理小于一页的频繁分配/释放的对象,解决内部碎片
- kmalloc:基于 Slab 的通用分配器,支持 8 字节到 8MB 的任意大小
第二章:Slab 的核心数据结构与缓存层次
2.1 Cache 结构
每个对象类型对应一个 kmem_cache 结构,核心字段如下:
struct kmem_cache {
struct kmem_cache_cpu *cpu_slab; // Per-CPU 快速分配缓存
struct kmem_cache_node *node[MAX_NUMNODES]; // 各 NUMA 节点的半空 slab 列表
unsigned int size; // 对象实际大小(对齐后)
unsigned int object_size; // 对象原始大小
unsigned int offset; // 空闲对象链表指针偏移
slab_flags_t flags; // 构造函数等标志
unsigned int num; // 每个 slab 包含的对象数量
void (*ctor)(void *); // 对象构造函数
const char *name; // 缓存名称(如"dentry_cache")
};
2.2 Slab 描述符
一个 slab 是物理上连续的内存区域(1-N 页),由 struct slab(新版,替代了旧版 page 内嵌 slab 描述)管理:
struct slab {
unsigned long __page_flags;
struct list_head slab_list; // 指向 cache 的 partial/full/empty 列表
void *freelist; // 空闲对象链表头指针
unsigned inuse; // 已使用对象数量
unsigned objects; // 对象总数
unsigned long __page_flags_2;
};
2.3 三层分配策略
Slab 缓存维护三类链表,形成优先级明确的分配路径:
- cpu_slab(hot cache):Per-CPU 专属缓存,无锁分配,性能最优。每个 CPU 维护 freelist 指针和当前 slab 页,分配/释放无需加锁。
- Node Partial:NUMA 节点的半空 slab 列表。当 cpu_slab 中的 slab 用尽或空闲完毕时,从 Node 的 partial 列表中取用。
- Buddy System:当所有 partial 列表均空时,向 Buddy System 申请新页面,创建新 slab。
这种设计完美契合现代多核 NUMA 架构:优先在本地 CPU 无锁操作,其次考虑 NUMA 亲和性,最后才申请新内存。
第三章:SLAB / SLUB / SLOB 三代实现对比
3.1 SLAB(经典版,1994-2005 主流)
特点:
- 每个 slab 内部维护
bufctl数组记录空闲对象索引 - 使用 slab 描述符嵌入在 page 结构体的前几个字段
- 着色(Color)机制利用硬件 Cache Line 对齐,减少 Cache 冲突
- 元数据管理复杂,在多核扩展性上存在瓶颈
3.2 SLUB(Unqueued Slab,当前默认,2007 至今)
SLUB 是目前 Linux 内核的默认分配器,由 Christoph Lameter 设计,核心改进:
- 取消 per-slab 队列:直接利用 page 结构体内嵌 slab 描述符,大幅减少元数据
- 简化 free list:空闲对象链表直接存在对象内存的第一个指针位,freelist 指针链式串联
- 更优的多核扩展:每个 CPU 独立管理 freelist,减少全局锁争用
- Red-Zone 越界检测:在对象末尾填充特殊标记,释放时检查是否被越界写入
- Poison 毒化:释放时填充 0x5a5a5...,捕获 use-after-free
- 合并到 page 结构体中,减少独立数据结构带来的开销
3.3 SLAB freelist 硬核布局
SLUB 的核心设计之一是 freelist 的存储方式。空闲对象的前 8 字节被用作 next 指针:
// SLUB freelist 原理:复用对象内存存储链表指针
struct kmem_cache *cache = dentry_cache;
void *object = cache->cpu_slab->freelist;
// 分配过程(无锁):
void *next_object = *(void **)object; // 读出 next 指针
cache->cpu_slab->freelist = next_object;
// 返回给调用者的对象前8字节已被"垃圾"数据覆盖
// 用户无需关心这段元数据
3.4 SLOB(精简版,适用于嵌入式)
专为内存极度受限的嵌入式系统设计:
- 极小的代码量(约 600 行 vs SLUB 数千行)
- 简单的第一适应(First-fit)分配策略
- 不支持 Per-CPU 缓存和 NUMA 优化
- 使用 Buddy System 直接的碎片管理能力较弱
三代实现性能对比:
| 维度 | SLAB | SLUB | SLOB |
|---|---|---|---|
| 多核扩展性 | 中等(全局缓存锁) | 优(Per-CPU 无锁) | 差(全局锁) |
| 内存开销 | 高(bufctl 数组) | 极低(内嵌元数据) | 最低(最简元数据) |
| 碎片控制 | 好(着色优化) | 好 | 较差(第一适应) |
| 调试功能 | 基础 | 强(RedZone/Poison) | 无 |
| 代码复杂度 | 高 | 中 | 极低 |
| 适用场景 | 已淘汰 | 通用服务器、桌面 | 嵌入式 < 64MB RAM |
第四章:kmalloc 与 kmem_cache_alloc 的使用实战
4.1 kmalloc:内核通用分配接口
kmalloc 是大多数内核代码使用的接口,它背后是一组预定义的 kmem_cache:
// 内核源码 mm/slab_common.c 中的尺寸分级
static struct kmem_cache *(*[KMALLOC_SHIFT_HIGH + 1]) = ...
// 如 kmalloc-8, kmalloc-16, kmalloc-32, ..., kmalloc-8K, ... kmalloc-64K
// 使用示例
char *buf = kmalloc(256, GFP_KERNEL); // 匹配到 kmalloc-256 缓存
strcpy(buf, "Hello Slab!");
kfree(buf);
kmalloc 分配的内存物理连续,适合 DMA 等需要物理连续性的场景,但大尺寸分配可能因 Buddy System 碎片失败。
4.2 kmem_cache_alloc:自定义对象缓存
对于频繁分配/释放的固定大小对象(如自定义结构体),应创建专用 cache:
static struct kmem_cache *my_obj_cache;
// 模块初始化时创建缓存
my_obj_cache = kmem_cache_create(
"my_obj_cache", // 缓存名称(/proc/slabinfo 可见)
sizeof(struct my_object), // 对象大小
0, // 对齐偏移(0 表示自然对齐)
SLAB_HWCACHE_ALIGN | SLAB_POISON, // 属性标志
my_ctor // 可选构造函数
);
// 分配对象
struct my_object *obj = kmem_cache_alloc(my_obj_cache, GFP_KERNEL);
// 使用对象...
obj->value = 42;
// 释放对象
kmem_cache_free(my_obj_cache, obj);
// 模块退出时销毁缓存
kmem_cache_destroy(my_obj_cache);
4.3 GFP 标志详解
| 标志 | 含义 | 适用场景 |
|---|---|---|
| GFP_KERNEL | 标准分配,允许睡眠等待换页 | 普通进程上下文 |
| GFP_ATOMIC | 高优先级,不睡眠(使用紧急内存池) | 中断上下文、持有自旋锁 |
| GFP_DMA | 分配 ZONE_DMA(0-16MB)内存 | 老式 ISA DMA 设备 |
| GFP_DMA32 | 分配 ZONE_DMA32(0-4GB)内存 | 32 位 DMA 地址线设备 |
| GFP_NOWARN | 分配失败时抑制警告日志 | 预期可能失败的分配 |
| __GFP_ZERO | 分配后清零 | 避免旧数据泄露的安全场景 |
| GFP_NOIO | 允许分配但禁止启动 I/O 换页 | 块层、文件系统内部 |
第五章:NUMA 感知与分配拓扑
现代多路服务器采用 NUMA(非统一内存访问)架构,CPU 访问本地节点内存延迟约 100ns,跨节点访问延迟可达 200-300ns。Slab 分配器深度集成了 NUMA 优化:
- 每个 NUMA 节点维护独立的
kmem_cache_node包含 partial/full/empty slab 链表 - CPU slab 优先使用本地节点内存分配新 slab
- 跨节点偷取(stealing):当本地节点 slab 用尽时,从其他节点的 partial 列表借用而非立即申请新页
- NUMA 调度器配合 Slab 分配策略,可将跨节点访问比例控制在 15% 以下
struct kmem_cache_node {
spinlock_t list_lock; // 保护链表的自旋锁
unsigned long nr_partial; // partial slab 数量
struct list_head partial; // partial slab 链表(主要分配来源)
#ifdef CONFIG_SLUB
unsigned long nr_slabs; // 总 slab 数(仅统计用)
unsigned long total_objects; // 总对象数
struct list_head full; // 满 slab 链表
#endif
};
第六章:调试、监控与性能调优
6.1 /proc/slabinfo 详解
$ cat /proc/slabinfo | head -20
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : ...
dentry 283792 301250 192 21 1 : ...
inode_cache 142056 143232 648 28 8 : ...
kmalloc-64 128453 131072 64 64 1 : ...
task_struct 1024 1024 7616 5 8 : ...
6.2 slabtop 实时监视
$ slabtop -o
Active / Total Objects (% used) : 523491 / 587123 (89.2%)
Active / Total Slabs (% used) : 12847 / 13082 (98.2%)
Active / Total Caches (% used) : 142 / 211 (67.3%)
Active / Total Size (% used) : 234.52K / 258.91K (90.6%)
Minimum / Average / Maximum Object : 0.01K / 0.44K / 8.00K
6.3 性能调优实践
场景一:dentry 缓存膨胀导致内存压力
大量文件操作会导致 dentry 缓存占用大量内存。诊断与调优:
# 查看 dentry 缓存占用
$ grep dentry /proc/slabinfo
dentry 301250 301250 192 21 1
# 计算:301250 * 192 = 约 55MB
# 清理 dentry 和 inode 缓存(谨慎操作)
$ sync
$ echo 2 > /proc/sys/vm/drop_caches # 清理 slab 缓存
# 调整 vfs_cache_pressure(默认 100)
# 值越大,内核越积极回收 dentry/inode
$ sysctl -w vm.vfs_cache_pressure=500
场景二:/proc/slabinfo 中 active_objs/num_objs 持续接近
说明缓存趋于饱和,无空闲对象可回收。解决方案:
- 检查是否为特定工作负载导致的正常现象(如大量并发文件描述符)
- 确认是否有内存泄漏(active_objs 持续增长)
- 对于自定义 cache,可通过
kmem_cache_shrink()主动释放空 slab
场景三:SLUB 调试——捕获越界写和释放后使用
// 开启 Red-Zone 和 Poison 检测(编译内核时配置)
CONFIG_SLUB_DEBUG=y
CONFIG_SLUB_DEBUG_ON=y
// 运行时对指定缓存开启调试
echo 1 > /sys/kernel/slab/dentry/validate
// 捕获越界写(Red-Zone 被破坏)
// 内核会在 dmesg 中打印:
// slub: corrupted data after object name=dentry ...
//
// 捕获释放后使用(Poison 0x5a5a5a... 被覆盖)
// slub: Object use after free ...
第七章:现代演进——SLUB 与 cgroup 内存控制
Linux 5.x+ 内核中,SLUB 与 cgroup v2 内存控制器深度集成:
- 每个 cgroup 可独立统计 SLAB 内存使用量(
memory.slabinfo) - cgroup 销毁时自动释放其关联的所有 slab 对象
- memory.max 限制包含 SLAB 缓存使用的内存
- 这对容器化环境至关重要:防止某个容器的 dentry/inode 无限膨胀挤占宿主机内存
$ cat /sys/fs/cgroup/memory.slabinfo | head -10
# name <active> <total>
dentry 123456 130000
inode_cache 98765 100000
kmalloc-64 54321 55000
第八章:问题诊断与常见问题排查
Q1: kmalloc 返回 NULL 怎么办?
- 检查 GFP 标志:GFP_KERNEL 分配失败通常是因为内存枯竭触发 OOM Killer
- 使用 GFP_NOWARN 避免刷屏式警告
- 使用
show_free_areas()查看各 Zone 水位线 - 检查是否存在内存泄漏(/proc/meminfo 中 Slab 持续增长)
Q2: SLUB 如何调试一个"Bad page state"崩溃?
- 这通常意味着对已释放的 slab 页面进行了操作
- 开启 CONFIG_SLUB_DEBUG_ON=y 自动检测
- 使用 slabinfo -v 验证 slab 状态
- 注意跨 CPU 释放问题:在 CPU A 分配、CPU B 释放可能导致 CPU hot cache 延迟释放
Q3: 为什么 /proc/slabinfo 中仍有大量 partial slab?
这是正常现象——slab 分配器蓄水池化设计。只有在内存压力下才会将 partial slab 的内容归还 buddy system。如需强制回收:
# 触发同步回收
echo 3 > /proc/sys/vm/drop_caches
# 调整 min_free_kbytes 提升水位线(提前回收)
sysctl -w vm.min_free_kbytes=262144 # 256MB
总结与展望
Slab 分配器从诞生至今已近 30 年,但其核心理念——减少碎片、提升缓存命中率、NUMA 感知——依然指导着现代内核开发。随着 CXL(Compute Express Link)内存扩展和异构计算架构的兴起,SLUB 持续演进:
- 未来 SLUB 将更好地管理多层级内存(HBM/DDR/CXL) 的对象分配策略
- cgroup v3 将为 slab 资源控制提供更细粒度的 QoS 策略
- Rust for Linux 驱动中,kmem_cache 与 Rust 分配器 trait 结合的安全封装正在进行中
- io_uring 的 registered buffer 机制与 Slab 的交互正在持续优化
掌握 Slab 分配器不仅是内核开发的必经之路,更是诊断系统性能疑难杂症的利器——理解它,你就能看懂 /proc/slabinfo 中的内存密码,在 OOM 黑暗中点亮一盏明灯。

发表评论 取消回复