Linux 内核 SLUB 内存分配器深度实战:从对象缓存到生产级性能调优
本文深入剖析 Linux 内核 SLUB(Unqueued Slab)内存分配器的设计原理与工程实践。作为 slab 分配器的第三代演进,SLUB 在 NUMA 感知、调试能力与碎片控制方面实现了质的飞跃。我们将从底层数据结构设计逐层展开,涵盖 per-CPU 缓存机制、NUMA 节点亲和、red zone/debug guard 防护、kmem_cache 接口全解、/proc/slabinfo 分析方法论,以及大规模部署场景下的调优策略,帮助读者建立起完整的内核内存管理工程认知。
1. SLUB 的设计缘起与演进脉络
Linux 内核的 slab 分配器家族经历了三个重要阶段:最早的 slab(1994,Jeff Bonwick 为 Solaris 设计)→ slub(2007,Christoph Lameter 引入)→ 当前默认的 SLUB。每一次演进都针对前代的架构瓶颈做出了根本性重构。
1.1 slab 家族的共同目标
现代操作系统内核需要频繁分配和释放固定大小的对象(如 task_struct、inode、dentry、file 等)。若直接使用页分配器(buddy system),将产生严重的内部碎片(一个 160 字节的 task_struct 塞进 4KB 页是巨大的浪费),且反复初始化/销毁对象带来不可忽略的构造开销。slab 分配器的核心思路是:
- 对象缓存池:为每种高频使用类型维护一个
kmem_cache,对象在 slab 页内预先初始化后复用 - 硬件缓存友好:同一类型的对象紧密排列,减少 cache miss
- per-CPU 热路径:单 CPU 无锁分配,避免全局竞争
- 着色偏移(slab coloring):不同 slab 的起始偏移随机化,降低 cache line 冲突
1.2 为什么 SLUB 取代了 slab 和 slob
原始 slab 分配器的元数据管理异常复杂——每个 slab 页都需要维护 bufctl 数组、本地/全局空闲链表、着色偏移表,代码量庞大且难以调试。slob 是为嵌入式场景设计的极简方案,但仅支持首次适配(first-fit)策略,无法胜任通用负载。
SLUB 的核心哲学是"回归本质":去掉一切不必要的间接层。每个 page 结构体中的 freelist 指针直接指向页内第一个空闲对象,空闲对象自身嵌入 next 指针,形成隐式链表。这种设计将元数据开销压缩到极致,同时保持了 per-CPU 缓存的 O(1) 分配性能。
2. SLUB 核心数据结构与布局
2.1 page 结构体中的 slab 元数据
SLUB 复用通用的 struct page,通过 union 复用字段来存储 slab 元数据:
// include/linux/page-flags-layout.h 及 mm/slab.h 中的简化示意
struct page {
// slab 分配器复用的 union 字段:
struct {
union {
struct list_head slab_list; // 同缓存的 slab 链表节点
struct slab_slab_s slab; // 复合对象
};
struct kmem_cache *slab_cache; // 所属 kmem_cache
void *freelist; // 第一个空闲对象指针
union {
unsigned inuse; // 已分配对象计数
unsigned objects; // 总对象数
};
};
// page 类型标记:
// PG_slab - 标识此页属于 slab 分配器
// PG_head - 首页(compound 页的第一个子页)
// PG_tail - 尾页
}
inuse 字段仅当 PG_head 时作为"已分配对象数"使用,非 head page 的 inuse 字段直接等于总对象数(page->objects),用于快速判断 slab 状态。页数 = 1 << order,每个对象的实际内存从 page->freelist 开始的占位 slot 中分出。
2.2 kmem_cache 结构体
kmem_cache 是 SLUB 分配器的一级元数据结构,每个缓存类型对应一个:
// mm/slub.c 中的核心字段(简化)
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; // per-CPU 热缓存
struct kmem_cache_node *node[MAX_NUMNODES]; // NUMA 节点缓存
unsigned long flags; // SLAB_* flags
unsigned int size; // 对象实际大小
unsigned int object_size; // 用户请求大小
unsigned int offset; // 下一个空闲对象的偏移量(next指针嵌入位置)
unsigned int oo; // min(order, objects_per_slab)
struct kmem_cache_order_objects min; // 最小 slab 配置
slab_flags_t gfpflags; // 分配时使用的 GFP 标志
int refcount; // 引用计数
void (*ctor)(void *); // 构造函数
const char *name; // 缓存名称(用于 /proc/slabinfo)
struct list_head list; // 全局缓存链表
unsigned long random; // 着色随机种子
unsigned int align; // 对齐要求
unsigned int useroffset, usersize; // 用户copy大小(用于Usercopy防御)
} ____cacheline_aligned;
关键设计点:
- addr 的复用:用户请求
kmem_cache_alloc()返回的指针后,下一个空闲对象的指针嵌入在对象的特定偏移处(offset),通过ctor可维护对象的初始化状态 - align:L1 cache line 对齐避免 false sharing,内核通常为 64 字节(CONFIG)
- gfpflags:GFP_KERNEL、GFP_ATOMIC 等,决定页面分配时是否允许睡眠、回收等行为
2.3 三种 slab 状态链表
SLUB 为每个 NUMA 节点维护三个双向链表:
// mm/slub.c 内部
struct kmem_cache_node {
spinlock_t list_lock;
unsigned long nr_partial; // partial slab 计数
struct list_head partial; // partial 链表(部分空闲)
#ifdef CONFIG_SLUB_DEBUG
unsigned long nr_slabs; // 总 slab 页数
unsigned long total_objects; // 总对象数
struct list_head full; // full 链表(全部占用)
#endif
} ____cacheline_aligned_in_smp;
- full:所有对象已分配,仅当使用 CONFIG_SLUB_DEBUG 时保留链表以便调试跟踪
- partial:部分对象空闲、部分已分配 —— 分配优先从此链表取
- empty:无链表,empty slab 直接由 page->slab_list 维护在 kmem_cache->slab_list
分配优先级:per-CPU cache → partial 链表 → buddy system 分配新 slab。释放优先级:归还 per-CPU cache → 若 per-CPU 满则部分归还到 node partial → slab 变 empty 时归还 buddy system。
3. 分配与释放核心路径
3.1 快速路径:per-CPU 分配
SLUB 的分配快速路径极其高效,总开销仅为几条汇编指令:
// mm/slub.c 中的分配简化逻辑
static __always_inline void *slab_alloc(struct kmem_cache *s,
gfp_t gfpflags, unsigned long addr)
{
// 1. 当前 CPU 的 slab 缓存
struct kmem_cache_cpu *c = raw_cpu_ptr(s->cpu_slab);
void *object = c->freelist; // 取第一个空闲对象
if (unlikely(!object)) // per-CPU 无空闲对象
return __slab_alloc(s, gfpflags, addr); // 慢路径
c->freelist = get_freelist(s, object); // 更新 freelist
c->page->inuse++; // 增加已分配计数
return object;
}
// 更新 freelist 的核心宏(object 自身即存储下一个指针)
#define get_freelist(s, object) \
({ void **__f = (void *)(object + s->offset); *__f; })
整个快速路径无锁、无原子操作(除了 inuse++ 对 page 成员的写操作,由于 per-CPU page 的独占性,无竞争)。在执行路径中仅需一次指针解引用和一次指针存储即可完成分配。
3.2 慢路径:获取新 slab
当 per-CPU cache 为空时,__slab_alloc() 进入慢路径处理:
// 慢路径步骤:
// Step 1: 检查当前 CPU 绑定的 page 是否有剩余(首次进入时 page 可能为 NULL)
// Step 2: 关闭抢占(sl_alloc 可能阻塞),重新获取 cpu_slab
// Step 3: 仍然为空 → new_slab() 从 partial 链表或 buddy 系统分配
// Step 4: 切换 cpu_slab->page 到新获取的 slab
// Step 5: 从新的 freelist 中分配对象
static void *__slab_alloc(struct kmem_cache *s, gfp_t gfpflags, ...)
{
struct kmem_cache_cpu *c;
struct page *page;
// 禁用抢占,防止在获取 slab 过程中被调度到不同 CPU
c = this_cpu_ptr(s->cpu_slab);
...
if (unlikely(!c->page)) {
// 当前无绑定 page,从 node partial 分配或创建新 slab
page = new_slab(s, gfpflags);
c->page = page;
c->freelist = page->freelist;
}
...
// 正常继续分配流程
}
值得注意的是,new_slab() 可能调用 alloc_slab_page() → allocate_slab() → __alloc_pages_node() 进入 buddy 系统,此时可能在 node partial 链表的 spin lock 保护下短暂睡眠。因此 GFP 标志中若含 __GFP_WAIT,分配就不能在硬中断上下文中使用。
3.3 释放逻辑
释放是将对象归还 quicklist 的逆过程:
// 释放快速路径:
static __always_inline void slab_free(struct kmem_cache *s, void *x)
{
struct kmem_cache_cpu *c = raw_cpu_ptr(s->cpu_slab);
void **object = (void *)x;
set_freelist(s, object, c->freelist); // 将释放的 object 插入链表头
c->freelist = object;
c->page->inuse--;
// per-CPU 缓存对象数超过 cpu_partial 阈值时,批量归还到 node
if (unlikely(c->page->inuse == 0 && c->partial)) {
// slab 变空,可能归还 buddy
} else if (unlikely(nr_partial > s->cpu_partial)) {
unfreeze_partial();
}
}
4. NUMA 感知与每 CPU 优化
4.1 NUMA 架构下的分配挑战
在 NUMA(非统一内存访问)系统中,CPU 访问本地节点的内存远快于远程节点。SLUB 通过两级缓存路径确保 NUMA 亲和性:
- L1: per-CPU cache — 缓存的 slab 页本身来自某个 NUMA 节点,但对象已预取到 CPU 寄存器缓存级别
- L2: kmem_cache_node (per-node partial) — 释放时若确认 slab 属于本地节点,则归还到本地 partial 链表;否则归还 slab 到 buddy system
4.2 kmem_cache_alloc_node() 与节点亲和
// 显式节点分配
static __always_inline void *kmem_cache_alloc_node(struct kmem_cache *s,
gfp_t gfpflags, int node)
{
// 策略:先从 per-CPU 快速路径尝试
// 若 per-CPU 无缓存,再尝试 node partial 链表
// 若 node partial 为空,从 buddy system 按 NUMA 策略分配新页
// __alloc_pages_node(node, gfpflags) 先尝试目标节点,
// 若本地节点内存不足则 fallback 到其他节点
}
对于网络设备、NVMe 驱动等 NUMA 敏感的组件,可以在设备中断处理中显式指定目标节点,确保内存分配始终来自本地 NUMA 域。
4.3 CPU 迁移与缓存失效
SLUB 通过 __flush_cpu_slab() 处理 CPU 热插拔和调度器迁移场景。当一个 CPU 离线时,其 per-CPU 缓存中的待释放 partial slab 需要被归还到 node partial 链表或 buddy system,避免内存泄漏。
5. 调试与安全防护:SLAB_DEBUG 全家桶
SLUB 提供了丰富的编译时和运行时调试选项,覆盖了内存溢出检测、use-after-free 诊断、用户空间非法拷贝等攻击向量。
5.1 Red Zone 红区检测
CONFIG_DEBUG_PAGEALLOC 或 SLAB_RED_ZONE 会在每个对象尾部附加一个 red zone 区域(通常 16 或 32 字节),填充特定魔数(0xCC 或 0xDEADBEEF)。每次分配和释放操作前后都会校验 red zone 的完整性——若发现被覆盖,内核立即触发 panic 或 BUG_ON,防止溢出蔓延到其他对象。
5.2 Poisoning 毒化模式
SLAB_POISON 标志在对象释放时用特定模式填充(0x5A5A5A5A),使后续的 use-after-free 行为在解引用悬挂指针时更易触发异常。结合 CONFIG_DEBUG_KMEMLEAK 还能定位未释放对象。
5.3 用户空间防护
自 CVE-2017-5123 等漏洞后,SLUB 硬化了 copy_from_user/copy_to_user 路径:
- HARDENED_USERCOPY:通过
useroffset和usersize字段记录用户可访问区域,任何越界 copy 均触发 WARN_ON 或 panic - Freelist hardening:per-CPU freelist 指针在进入 per-CPU 缓存前使用 XOR 随机化加密,防止堆溢出直接控制空闲链表指针
6. kmem_cache 编程接口全解
6.1 内核模块开发者必读
以下是一套完整的内核对象缓存管理 API:
// ============ 缓存生命周期 ============
// 创建缓存(通常在模块 init 中调用)
struct kmem_cache *kmem_cache_create(const char *name, unsigned int size,
unsigned int align, slab_flags_t flags,
void (*ctor)(void *));
// name: 缓存名称,用于 /proc/slabinfo,不可为空
// size: 对象字节大小
// align: 对齐要求(0 表示自然对齐,SMP 通常为 cache line 大小)
// flags: SLAB_HWCACHE_ALIGN | SLAB_POISON | SLAB_RED_ZONE | SLAB_TYPESAFE_BY_CPU
// ctor: 可选构造函数,每次对象从 buddy 取出前调用
// 销毁缓存(通常在模块 exit 中调用)
void kmem_cache_destroy(struct kmem_cache *s);
// ============ 分配与释放 ============
// 通用分配
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t flags);
// NUMA 节点感知分配(用于设备本地内存分配)
void *kmem_cache_alloc_node(struct kmem_cache *s, gfp_t flags, int node);
// 批量分配和释放(kernel 6.6+,性能提升 40%+)
void *kmem_cache_alloc_bulk(struct kmem_cache *s, gfp_t flags, size_t size, void **p);
void kmem_cache_free_bulk(struct kmem_cache *s, size_t size, void **p);
void kmem_cache_free(struct kmem_cache *s, void *x);
6.2 实战示例:字符设备私有数据对象
// 完整的内核模块中使用 SLUB 缓存示例
struct mydev_priv {
int index;
struct cdev cdev;
spinlock_t lock;
struct list_head buffers;
char name[64];
};
static struct kmem_cache *mydev_cache;
static int __init mydev_init(void)
{
mydev_cache = kmem_cache_create("mydev_priv_cache",
sizeof(struct mydev_priv),
0, // 默认对齐
SLAB_HWCACHE_ALIGN | SLAB_POISON,
NULL); // 无构造
if (!mydev_cache)
return -ENOMEM;
// 注册字符设备...
return 0;
}
static void __exit mydev_exit(void)
{
kmem_cache_destroy(mydev_cache);
}
struct mydev_priv *mydev_alloc_priv(void)
{
return kmem_cache_alloc(mydev_cache, GFP_KERNEL);
}
void mydev_free_priv(struct mydev_priv *priv)
{
kmem_cache_free(mydev_cache, priv);
}
6.3 与通用分配器(kmalloc)的关系
kmalloc() 系列 API 底层同样由 SLUB 驱动。内核预建了一系列固定尺寸的 kmem_cache(如 8、16、32、64、96、128、192、256、512、1024、2048 字节等),kmalloc(size, flags) 根据 size 查找最匹配的缓存:
// mm/slab_common.c
static __always_inline
struct kmem_cache *kmalloc_slab(size_t size, gfp_t flags)
{
unsigned int index;
if (size <= kmalloc_caches[0].size) {
index = kmalloc_caches[0].size; // 最小分配从 8 或 16 字节开始
} else if (size > KMALLOC_MAX_SIZE) {
return NULL; // 请求过大
} else {
index = fls(size - 1); // 找到最高位
// 若 size 不是 2 的幂(如 100 字节),从 kmalloc_size_classes[] 映射到对应缓存
return &kmalloc_caches[size_index[size_index_elem(size)]];
}
}
注意:kmalloc() 与 kmem_cache_alloc() 的关键区别在于前者复用通用缓存(共享尺寸相近的对象),后者拥有专属缓存(隔离不同类型对象)。频繁分配的自定义结构体应使用专用缓存而非 kmalloc,以减少碎片和避免跨类型缓存交叉污染。
7. 性能分析与诊断
7.1 /proc/slabinfo 实战
/proc/slabinfo 提供了所有 kmem_cache 的快照视图:
$ 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-1024 1524 1536 1028 16 4 : tunables 0 0 0 : slabdata 96 96 0
kmalloc-512 120452 121856 504 32 4 : tunables 0 0 0 : slabdata 3808 3808 0
dentry 89210 92416 192 42 1 : tunables 0 0 0 : slabdata 2200 2200 0
inode_cache 45123 47872 632 6 8 : tunables 0 0 0 : slabdata 7978 7978 0
task_struct 2150 2264 6368 5 8 : tunables 0 0 0 : slabdata 452 452 0
各列含义:
- active_objs:当前已分配出去的对象数
- num_objs:总对象容量
- objsize:每个对象实际占用大小(含元数据和对齐填充)
- objperslab:每个 slab 可存放的对象数
- pagesperslab:每个 slab 使用的页数
健康指标:
- 若
num_objs / active_objs比值过高(如 >5),说明缓存存在严重低效,cat /proc/slabinfo | awk '{print $4/$2}' | sort -rn | head - slab 废弃率 =
(num_objs - active_objs) / num_objs,废弃率 > 80% 表示分配策略需要优化
7.2 slabtop 实时监控
类似 top,但针对 slab:
$ slabtop -o
Active / Total Objects (% used) : 342761 / 360718 (95.0%)
Active / Total Slabs (% used) : 11885 / 11885 (100%)
Active / Total Caches (% used) : 157 / 160 (98.1%)
Active / Total Size (used) : 713.71K / 750.21K (95.1%)
Minimum / Average / Maximum Object : 0.01K / 0.00K / 16.88K
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
120480 120480 100% 0.50K 3764 32 60224K kmalloc-512
89210 89210 100% 0.19K 2200 42 35200K dentry
45123 45123 100% 0.62K 7978 6 31912K inode_cache
15258 15258 100% 1.00K 954 16 15264K kmalloc-1024
2264 2149 94% 6.21K 452 5 14464K task_struct
7.3 ftrace 跟踪
若需要深入某一缓存的分配路径延迟,可使用 ftrace:
# 跟踪所有 SLUB 分配请求
echo 'kmem_cache_alloc kmem_cache_free' > /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 运行负载后查看
cat /sys/kernel/debug/tracing/trace_pipe | head -100
也可借助 perf 的 probe 功能精确追踪:
perf probe --add 'slab_alloc_alloc mm_slab_alloc'
perf stat -e probe:slab_alloc_alloc -a sleep 10
7.4 kmemleak 检测内存泄漏
CONFIG_DEBUG_KMEMLEAK 可以扫描内存中已分配但无任何指针引用的对象(孤立堆块):
echo scan > /sys/kernel/debug/kmemleak # 触发扫描
cat /sys/kernel/debug/kmemleak # 查看泄漏报告
echo clear > /sys/kernel/debug/kmemleak # 清除历史记录
8. 生产环境最佳实践
8.1 分配器选择决策树
┌─ 嵌入式极低内存(<64MB RAM)
│ → 考虑 CONFIG_SLOB
请求固定大小 │
对象且高频 │ ┌─ 需要 NUMA 感知 / 高性能
├─│─ → SLUB(当前默认,推荐)
│ │
│ └─ 需要极老的硬件支持 / 特殊 ABI
│ → slab(仅历史兼容)
│
└─ 非固定大小或一次性分配
→ kmalloc() / vmalloc()(kmalloc 底层用 SLUB)
8.2 GFP 标志快速参考
| 标志 | 含义 | 可用上下文 |
|---|---|---|
| GFP_KERNEL | 标准分配,允许睡眠回收 | 进程上下文 |
| GFP_ATOMIC | 原子分配,禁止睡眠 | 硬中断、softirq、spinlock 中 |
| GFP_NOIO | 禁止启动磁盘 I/O | 文件系统 I/O 路径,防止递归 |
| GFP_NOFS | 禁止文件系统调用 | 文件系统代码内部,防止死锁 |
| GFP_NOWAIT | 不等待回收,失败即返回 | 不可睡眠但允许直接回收 |
| __GFP_ZERO | 分配后清零 | 需要零初始化缓冲区 |
| __GFP_HIGHMEM | 允许分配高端内存(32位系统) | 用户空间缓冲区 |
8.3 SLUB 参数调优
通过 sysctl 或 /proc/sys 可调参数:
/proc/sys/vm/min_free_kbytes — 低水位线,影响分配器回收激进程度
/proc/sys/vm/vfs_cache_pressure — dentry/inode slab 回收权重(默认 100,增大 = 更积极回收)
/proc/sys/vm/dirty_ratio — 脏页比例,间接影响文件缓存 slab 行为
# SLUB 内核参数(启动时或运行时)
slab_max_order=0 — 限制单次 slab 分配的最大 order(0 = 默认 3,即 32 页)
slab_partial=5 — 单个缓存至少保留的 partial slab 数量
slab_min_objects=4 — 每个 slab 最小对象数
slab_max_object_size=8192 — 超出此大小直接使用 buddy 系统
8.4 常见陷阱与反模式
- 在 GFP_ATOMIC 路径中分配大块内存:atomic 上下文无法触发 page reclaim,大块分配极易失败,应预分配或延迟到进程上下文处理
- 频繁创建/销毁 kmem_cache:创建涉及 slab 全局链表操作,是高成本操作,应在模块 init 中一次性创建并缓存
- kmalloc 大尺寸内存(>8KB):kmalloc 最大尺寸受
KMALLOC_MAX_SIZE限制(通常 4MB 或 8MB),大块内存应使用vmalloc - 忽视构造函数导致的竞态:若 ctor 修改了全局状态,在 SMP 上可能存在 cross-CPU 竞争,需自行加锁或确保只引用本对象
- 模块卸载时遗漏 kmem_cache_destroy:引用的模块其它代码路径仍在持有对象会导致 use-after-free,确保 destroy 时 refcount=0
9. 现代内核演进:SLUB 的未来方向
kernel 6.x 进一步增强了 SLUB:
- Bulk allocator:
kmem_cache_alloc_bulk()和kmem_cache_free_bulk()支持批量操作(典型场景:网络 sk_buff 预分配池) - Tiny:替代 slob 的新超低内存分配器,合并了 slob 的简单性与 SLUB 的调试能力
- Rust for Linux:SLUB 缓存已被 Rust 绑定封装,在内核 Rust 代码中以安全接口分配对象
- cgroup slab controller:通过 memory.slab.limit_in_bytes 限制每个 cgroup 的 slab 用量
- Prevent double-free:释放路径增强指针校验,阻止对同一对象重复 free 导致的安全漏洞
10. 总结
SLUB 分配器以其简洁而高效的设计哲学,成为现代 Linux 内核内存管理的核心基础设施。从底层的 per-CPU 空闲链表、NUMA 节点感知,到上层的 red zone 防护、freelist 随机化,SLUB 在性能、安全和调试三者之间取得了精妙的平衡。
理解 SLUB 的内部机制,不仅有助于在系统层面诊断 OOM、内存抖动等疑难问题,更能指导驱动程序员在内核模块开发中做出正确的技术决策——选择合适的 gfp flags、设计合理的缓存生命周期、避免常见反模式。
在云原生和容器化的今天,SLUB 与 cgroup、namespace 的结合为资源隔离提供了坚实保障。深入理解这套机制,是从"会用 Linux"走向"懂 Linux"的关键一步。

发表评论 取消回复