Linux 内核 SLUB 分配器深度工程实战:从 Per-CPU 缓存到 NUMA 感知的内存分配
引言
内核内存分配器是 Linux 性能基础设施的基石。SLUB(Unqueued Slab Allocator)自 2.6.23 起取代 SLAB 成为默认分配器,其核心设计哲学是"极简即高效"——去除复杂的队列管理,利用 Per-CPU 本地缓存和 cmpxchg 无锁路径实现极致的快速分配。
本文将从工程实战角度深入剖析 SLUB 分配器的完整工作机制:从 Per-CPU _partial 队列的二阶缓存策略,到 NUMA 节点本地内存池的组织方式;从 kmem_cache 的自定义创建与调试接口,到跨 CPU 缓存污染(cache bouncing)的排查与优化策略;再到内存碎片化检测、POISON/REDFZONE 安全机制以及 kmemleak 与 KASAN 的联动分析。
一、SLUB 的数据结构模型
1.1 核心结构体关系
SLUB 的内存管理建立在三个核心结构体之上:
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; // Per-CPU 快速路径
struct kmem_cache_node *node[MAX_NUMNODES]; // NUMA 节点级管理
unsigned long object_size; // 对象实际大小
unsigned long size; // 含元数据的对象大小
unsigned int offset; // 下一个空闲对象偏移
struct kmem_cache_order_objects oo; // 最佳 slab 阶数
// ...
};
struct kmem_cache_cpu {
void **freelist; // 空闲对象链表头
unsigned long tid; // 全局事务 ID(防 ABA)
struct page *page; // 当前正在使用的 slab 页
struct page *partial; // Per-CPU partial 链表
};
struct kmem_cache_node {
spinlock_t list_lock;
unsigned long nr_partial;
struct list_head partial; // Node 级 partial slab 链表
};
1.2 对象生命周期与空闲链表
SLUB 使用内嵌式空闲链表(intrusive free list)管理 slab 内的空闲槽位。一个空闲对象的内存位置被复用为 void *next 指针:
static inline void *get_freep(struct kmem_cache *s, void *object)
{
return *(void **)(object + s->offset);
}
static inline void set_freep(struct kmem_cache *s, void *object, void *fp)
{
*(void **)(object + s->offset) = fp;
}
当 slab 全满时,该 slab 脱离 CPU 控制;当 slab 中部分对象被空闲时,根据「最近使用」策略挂载到 Per-CPU partial 或 Node partial 链表。
二、快速路径:kmem_cache_alloc 的执行流
2.1 热路径(无锁快速分配)
SLUB 最快路径仅需约 10 条汇编指令,完全不涉及任何锁:
static __always_inline void *slab_alloc_node(struct kmem_cache *s,
gfp_t gfpflags, int idx, unsigned long addr)
{
struct kmem_cache_cpu *c = raw_cpu_ptr(s->cpu_slab);
struct page *page = c->page;
void *object;
if (unlikely(!c->freelist))
return __slab_alloc(s, gfpflags, idx, addr, c);
object = c->freelist; // 取空闲链表头
c->freelist = get_freep(s, object); // 更新链表头
c->tid = next_tid(c->tid); // 事务 ID 递增
return object;
}
这条路径的关键约束是无锁——每 CPU 独占 cpu_slab,除 slab 页面切换外不存在任何竞争。
2.2 慢路径:Per-CPU Page 耗尽
当 c->page 的 freelist 为空时,进入 __slab_alloc():
__slab_alloc:
1. 将 c->page 从 Per-CPU 移除(可能移入 node->partial 或释放回 buddy)
2. 尝试从 c->partial 获取新 page
3. 若 partial 也为空,从 node->partial 借一个
4. 若 node->partial 也为空,调用 new_slab() 从 buddy 系统申请
5. 将新 page 设为 c->page,重试热路径
2.3 事务 ID 与 RCU 安全的关联
tid(Transaction ID)用于实现 lock-free 的 RCU 读取端保护。cmpxchg 操作结合 tid 的检测可以在不加锁的情况下检测期间是否有其他 CPU 修改了 per-cpu 状态:
void *ptr = c->freelist;
unsigned long tid = c->tid;
preempt_disable(); // 阻止被抢占导致 slab 被迁移
ptr = ____cmpxchg(&c->freelist, ptr, ...);
preempt_enable();
三、NUMA 感知与 Node 级管理
3.1 NUMA 本地分配策略
SLUB 在每个 NUMA 节点上维护独立的 partial 列表,以尽量减少远程内存访问:
struct kmem_cache_node {
spinlock_t list_lock; // node 级锁(跨 CPU 共享)
unsigned long nr_partial;
struct list_head partial; // 跨 CPU 共享的 partial 链表
#ifdef CONFIG_SLUB_DEBUG
unsigned long nr_slabs;
unsigned long total_objects;
struct list_head full; // 全满链表仅用于调试
#endif
};
分配优先级(由 kmem_cache_alloc_node 决定):
1. CPU 本地 freelist
2. CPU 本地 partial
3. 同一 NUMA node 的 partial
4. 远程 NUMA node 的 partial(fallback)
5. 从 buddy system 申请新 slab
3.2 kmem_cache_alloc_node 的实现
static __always_inline void *slab_alloc_node(struct kmem_cache *s,
gfp_t gfpflags, int node, unsigned long addr)
{
// 若 node 为 NUMA_NO_NODE(-1),则使用当前 CPU 的 node
if (node == NUMA_NO_NODE)
node = numa_mem_id();
// ... 快速路径失败后尝试 node partial
if (node != NUMA_NO_NODE)
n = get_node(s, node);
}
四、内存碎片化问题
4.1 内部碎片 vs 外部碎片
SLUB 主要面临两类碎片问题:
- 内部碎片(Internal Fragmentation):一个 object 实际需要的空间比
kmem_cache->size小(由于元数据和对齐造成的浪费) - 外部碎片(External Fragmentation):partial slab 中散布的空闲对象无法合并成完整 slab
4.2 /proc/slabinfo 解析
$ 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 2304 2304 128 32 1 : tunables 0 0 0 : slabdata 72 72 0
dentry 18540 18540 192 21 2 : tunables 0 0 0 : slabdata 883 883 0
kmalloc-96 4186 4256 96 42 1 : tunables 0 0 0 : slabdata 101 101 0
关键字段说明:
| 字段 | 含义 |
|---|---|
| active_objs | 正在使用的对象数 |
| num_objs | 总分配对象数 |
| objsize | 单个对象大小(含元数据) |
| objperslab | 每 slab 对象数 |
| active_slabs | 活跃 slab 数 |
| num_slabs | 总 slab 数 |
4.3 碎片评估指标
# 碎片率 = 1 - (active_objs / num_objs)
# 高碎片率意味着大量内存被"半空 slab"占用
# 使用 slabtop 实时监控
watch -n 1 'sudo slabtop -o'
五、Slab 合并与抗碎片策略
5.1 Slab 合并(Slab Merging)
某些大小和生命周期的 kmem_cache 在初始化阶段会被自动合并以减少碎片:
// mm/slub.c
static struct kmem_cache *create_kmalloc_cache(const char *name,
unsigned int size)
{
struct kmem_cache *s = kmem_cache_create(name, size, 0,
SLAB_HWCACHE_ALIGN, NULL);
// 若存在相近大小的现有 cache,可能合并
return s;
}
SLUB 内部对相似大小的 cache 不做自动合并(区别于 SLAB 的部分合并),但通过精心设计的 kmalloc 层次(kmalloc-{8,16,32,64,96,128,192,256,512,1024,2048,4096,8192})来覆盖大部分分配需求。
5.2 CONFIG_SLUB_CPU_PARTIAL 优化
内核参数 slub_max_slabs(通过 /sys/kernel/slab/ 控制)决定了 Per-CPU partial 链表的最大 slab 数:
# 查看和设置 CPU partial 限制
cat /sys/kernel/slab/dentry/cpu_partial
echo 20 > /sys/kernel/slab/dentry/cpu_partial
较高的值能减少跨 Node 分配但也增加内存占用;较低的值相反。
六、调试与安全机制
6.1 KASAN(Kernel Address Sanitizer)与 SLUB 的集成
KASAN 通过 shadow memory 检测 use-after-free 和越界访问。与 SLUB 配合时:
- 每个 SLUB 对象周围放置 redzone 区域
- 释放的对象进入 freelist 但 shadow memory 被标记为不可访问
- 分配时清除 shadow 标记
// KASAN 释放流程
void kasan_kfree_large(struct kpage *page)
{
// 标记整个页不可访问
kasan_poison_shadow(page_address(page), page_size(page),
KASAN_KFREE);
}
6.2 SLUB_DEBUG 的运行时检测
SLUB_DEBUG 提供多层次的安全检查:
- F(Free)检测:释放后填充
0x6b/0x5a模式,读取时检查是否被篡改 - P(Poison)检测:释放时填充
0x5a模式,分配时清除 - U(User Tracking)检测:跟踪每个对象的分配/释放调用栈
- R(Redzone)检测:在对象边界添加哨兵区域
- Z(Tracking Zero)检测:跟踪未初始化但已分配的对象
- T(Trace)检测:打印每次分配/释放的调用栈
# 开启特定 cache 的调试
echo 1 | sudo tee /sys/kernel/slab/kmalloc-256/redzone
echo 1 | sudo tee /sys/kernel/slab/kmalloc-256/poison
echo tty_*_p | sudo tee /sys/kernel/slab/kmalloc-256/trace
6.3 通过 kmemleak 检测内存泄漏
kmemleak 通过扫描内存发现不可达但未被释放的内核对象:
# 触发扫描
echo scan > /sys/kernel/debug/kmemleak
# 查看泄漏报告
cat /sys/kernel/debug/kmemleak
# 清除当前扫描结果(重新基线)
echo clear > /sys/kernel/debug/kmemleak
# 典型泄漏报告
unreferenced object 0xffff888123456000 (size 256):
comm "insmod", pid=1234, jiffies 5432100
backtrace:
[<00000000c0ffee12>] kmem_cache_alloc_trace+0x1a2/0x230
[<00000000deadbeef>] my_module_init+0x45/0x100
[<00000000cafebabe>] do_one_initcall+0x4a/0x1c0
七、性能调优实践
7.1 kmem_cache_create 的最佳实践
// 自定义对象缓存 - 替代 kmalloc 以获得确定性的性能
struct my_struct {
spinlock_t lock;
void *data;
struct list_head node;
// 频繁创建销毁的结构体适合专用缓存
};
static struct kmem_cache *my_cache;
// 模块初始化时创建
my_cache = kmem_cache_create("my_struct_cache",
sizeof(struct my_struct),
L1_CACHE_BYTES, // 缓存行对齐
SLAB_HWCACHE_ALIGN | SLAB_ACCOUNT,
NULL);
// 分配(比 kmalloc 更快,无对齐惩罚)
struct my_struct *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
// 批量预分配优化
for (i = 0; i < BATCH; i++)
pool[i] = kmem_cache_alloc(my_cache, GFP_KERNEL);
7.2 percpu 计数器与 SLUB 的协同
高速场景中可以结合 percpu 计数器减少对 SLUB 的分配压力:
// 分配 percpu 数据,完全绕过 SLUB
struct my_stats __percpu *stats;
stats = alloc_percpu(struct my_stats); // 每个 CPU 一份,NUMA-local
// 使用
struct my_stats *s = get_cpu_ptr(stats);
s->counter++;
put_cpu_ptr(s);
7.3 CPU Cache Bouncing 排查
当多个 CPU 频繁操作同一 slab_cache 中的不同对象时,可能引发缓存行乒乓(cache bouncing):
# 方法 1:使用 perf 检测 LLC miss
perf stat -e cache-misses -e cache-references -C 0-7 -a sleep 5
# 方法 2:统计每 slab cache 的分配频率
perf probe 'kmem_cache_alloc name=+0(%di):string'
perf record -e probe:kmem_cache_alloc -aR sleep 10
# 方法 3:使用 ftrace 跟踪
echo kmem_cache_alloc > /sys/kernel/debug/tracing/set_ftrace_filter
echo function_graph > /sys/kernel/debug/tracing/current_tracer
cat /sys/kernel/debug/tracing/trace_pipe
八、SLUB 与 SLAB 的工程权衡
8.1 何时 SLUB 不如 SLAB
虽然 SLUB 是默认选择,但在以下场景下可能并不适合:
| 场景 | 分析 |
|---|---|
| 极小对象(<64B) | SLUB 的元数据开销占比过高 |
| 非常大的对象(>PAGE_SIZE/2) | 都走 page allocator,无差异 |
| 高频 bulk 分配释放 | SLAB 的批量回收更高效 |
| NUMA 跨节点频繁访问 | SLAB 的 NUMA-aware 着法更成熟(但 SLUB 已追平) |
8.2 SLUB 与 SLOB:嵌入式场景
SLOB(Simple List Of Blocks)是面向极内存受限嵌入式设备的分配器:
- 代码量极小(约 1500 行)
- 使用首次适应(first-fit)简单算法
- 无 per-cpu 缓存,因此开销极低但性能不稳定
选择优先级:
- 服务器/桌面/移动: SLUB(默认)
- 嵌入式且内存 < 64MB: SLOB
- 实时性要求极高: 自定义静态分配或 mempool
九、实战问题排查案例
9.1 案例1:系统内存被 slab "吃掉"
现象:free -m 显示 buff/cache 很高,且 Slab 字段持续上升。
排查过程:
# 1. 查看哪些 cache 占用最多
sudo slabtop -o --sort=a
# 2. 检查是否是 dentry/inode 缓存过多
cat /proc/sys/vm/vfs_cache_pressure # 默认 100
# 值越高,内核越倾向于回收缓存
# 3. 手动触发回收
sync
echo 2 > /proc/sys/vm/drop_caches # 清除 slab 可回收项
9.2 案例2:SLUB_DEBUG 揭示 use-after-free
现象:开启 SLUB_DEBUG 后,slab 错误日志:
=============================================================================
BUG kmalloc-256 (Tainted: P C): Redzone overwritten
INFO: 0x00000000deadbeef-0x00000000cafebabe. First byte 0x6b vs 0x6a
INFO: Allocated in module_init+0x45/0x100 [my_mod] age=1234 cpu=3 pid=5678
INFO: Freed in my_work_handler+0x8a/0x120 [my_mod] age=1111 cpu=2 pid=1234
根因:工作队列处理线程在释放对象后仍在使用它。需要在 kfree 前确保无其他引用。
9.3 案例3:启动参数调试
# 启动时关闭 SLUB 合并/Slab debug 验证
slub_debug=- # 全局禁用
slub_debug=FZPU # 强制开启所有调试特性
# 查看当前配置
cat /proc/slabinfo | grep -E "kmalloc|dentry|inode"
十、内核新进展
5.14+ 版本引入的特性
- slub_kunit:KUnit 测试框架覆盖核心分配/释放路径
- Batch freeing:一次性多个对象释放时降级为批量链表操作,减少 cmpxchg 次数
- Per-object metadata reduction:减少非调试模式下的元数据开销
6.x 方向
- Memory Tiering Support:SLUB 增加对
MEMORY_HOTPLUG和异构内存(如 CXL)的 tiering 感知分配 - Folio-based Allocation:将 SLUB 与大页(Folio)结合,提升大对象分配效率
- MTE (Memory Tagging Extension):ARM64 的硬件内存标签支持与 SLUB 深度集成,提供硬件级 use-after-free 检测
总结
作为 Linux 内核的默认内存分配器,SLUB 在设计上坚持了"热路径极简"的工程哲学:无锁快速通道、Per-CPU 本地缓存、NUMA 感知 fallback 策略三者的组合使其在绝大多数场景下都能提供稳定且优异的性能。掌握其内部机制不仅有助于内存问题的精准调优,同时也是理解内核内存管理全貌的关键入口。
对于高性能驱动或内核模块开发者,建议:高频分配对象的场景使用 kmem_cache_create 创建专用缓存;调试阶段开启 SLUB_DEBUG 确保内存安全;生产环境结合 KASAN 和 kmemleak 持续监控内存健康状态。

发表评论 取消回复