引言
在 Linux 内核中,内存管理是性能优化的核心战场。内核频繁地分配和释放大量小对象(如task_struct、inode、dentry、file等),如果直接依赖伙伴系统(Buddy System),不仅会有严重的锁竞争,还会产生大量内部碎片。Slab 分配器正是为解决这一问题而生,而 SLUB(Unqueued Slab)作为 Linux 2.6.23 之后默认的 Slab 实现,以其简洁高效的设计成为内核对象分发的核心引擎。
本文将深入 SLUB 分配器的内部实现,从伙伴系统交互、每 CPU 缓存(Freelist)、Slab Page 管理,到实际调优与故障诊断,构建完整的 SLUB 知识体系。
一、SLUB 的设计哲学
SLUB 的前身是 SLAB(Jeff Bonwick 为 Solaris 设计)和 SLOB(嵌入式场景的精简版)。SLUB 的核心设计目标是极致简化:去掉了 SLAB 中复杂的每 CPU 队列(kmem_bufctl)、着色机制(Colouring)的繁琐逻辑,以及分离的对象缓冲区。SLUB 将对象直接嵌入到 Page 中,利用 Page 结构的 union 和多级链表管理空闲对象,大幅减少了元数据开销。
SLUB 的关键特性:
- 无队列(Unqueue):Freelist 是单链表而非 SLAB 的 bufctl 双向链表,CPU 缓存直接指向空闲对象,无需中间层
- Free Pointer Inlining:空闲对象的内存直接存储下一个空闲指针,利用"对象空闲时其内存可借用"的特性,零额外开销
- 每 CPU Partial Page 链表:减少 NUMA 跨节点分配和锁竞争
- 最简元数据:每个 Slab Page 仅需
struct page中的几个字段(inuse, objects, frozen, freelist 指针通过 page 结构间接管理)
二、SLUB 核心数据结构
SLUB 的实现代码位于 mm/slub.c,核心结构围绕着 struct kmem_cache、CPU kmem_cache_cpu 和 struct page。
2.1 kmem_cache — 缓存描述符
每一个 kmem_cache 代表一种固定大小的对象类型。例如 task_struct_cachep 专门分配 task_struct 对象。
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; // 每 CPU 缓存指针
unsigned long flags;
unsigned long min_partial; // 每 CPU partial 页数阈值
unsigned int size; // 对象实际大小
unsigned int object_size; // 用户请求的对象大小
unsigned int offset; // 空闲指针在对象内的偏移
struct kmem_cache_node *node[MAX_NUMNODES]; // NUMA 节点
struct kmem_cache_order_objects oo;
struct kmem_cache_order_objects max;
struct kmem_cache_order_objects min;
gfp_t allocfp;
int refcount;
void (*ctor)(void *); // 构造函数
const char *name;
// ...
};
2.2 kmem_cache_cpu — 每 CPU 快速路径
这是 SLUB 性能的关键。每个 CPU 都有自己的 kmem_cache_cpu,其中包含当前正在使用的 Slab Page 和空闲对象链表。分配时如果 CPU 缓存有余,直接走 Fast Path(无锁、无 NUMA 感知、无需页分配),延迟极低。
struct kmem_cache_cpu {
void **freelist; // 指向第一个空闲对象
unsigned long tid; // 全局事务 ID(用于防止异步中断重入导致的链表损坏)
struct page *page; // 当前 Active Slab Page
struct page *partial; // 该 CPU 的 Partial 页链表
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct page *freelist_partial; // CPU Partial freelist(V5.12+ 优化)
#endif
};
2.3 SLUB 的对象布局
SLUB 中一个 Slab Page 的内存布局如下:
- Page 结构体(struct page + SLUB 扩展字段)
- 对象区域:连续排列 N 个固定大小的对象
- 红区(Red Zone):在对象末尾加
RED_ZONE标记,检测越界写 - Freelist:空闲对象的起始位置存放下一个空闲对象的地址(LIFO 链表)
三、分配与释放的完整路径
3.1 分配路径(kmem_cache_alloc → __slab_alloc)
SLUB 分配遵循三级降级策略:
- Fast Path(CPU 缓存命中):
cpu->freelist != NULL→ 直接从 freelist 弹出头对象,更新 freelist,page->inuse++。整个过程无需任何锁,关本地中断即可。 - Middle Path(CPU Partial 降级):如果 Active Page 已满(freelist 为 NULL),检查
cpu->partial链表。若有 partial page,将其中一个提升为 Active Page,继续 Fast Path。 - Slow Path(向伙伴系统申请新 Page):若 CPU Partial 也为空,则从 NUMA 节点的 partial 链表(也空)→ 最终调用
new_slab()从伙伴系统获取一个或多个页面(order 由 oo 决定),初始化 Slab(填充 freelist 链表),设定为 Active Page。
顺带一提,SLUB 的 Freelist 指针指向的是 object 内存的起始地址。而在 SLUB 中,active object 的内存就是用户数据(object 起始位置),只有闲置对象才被用作 next 指针。这是经典的"借用空闲内存存指针"技巧。
3.2 释放路径(kmem_cache_free → __slab_free)
释放是分配的逆过程,也有三级路径:
- 快速释放到 Active Page:对象属于当前 Active Page → 对象变 freelist 新头,
page->inuse--。 - 释放到 Partial Page:对象不属于 Active Page 而属于某个 CPU Partial Page → 插入其 freelist。
- Page 全空归还伙伴系统:若释放后 Page 中所有对象均为空闲(
inuse == 0),且该 Page 不在 Node Partial 链表中(即不属于半满),则直接将整页归还伙伴系统。
值得注意的细节:当 Page 从"全部占用"变为"最后一个被释放"时,SLUB 会将其从当前的 freelist 关系中解除,加入kmem_cache_node 的 partial 链表(如果 node->partial 数量不低于 min_partial),否则直接 free 给伙伴系统。这保证了内存利用率与缓存效率的平衡。
四、NUMA 感知与 Node Partial 链表
在 NUMA 架构中,SLUB 将每个 NUMA Node 抽象为 kmem_cache_node,每个 Node 维护自己的 partial 页链表。
分配优先级(NUMA 模式下):
- 本地 CPU 的 Fast Path
- 本地 CPU 的 partial
- 本地 Node 的 partial
- 远程 Node 的 partial(性能下降)
- 向伙伴系统申请新页(默认在本地 Node 上 GFP_THISNODE)
Node partial 页数量由 min_partial 控制,低于此值时不会释放全空 Page,而是留在 Node 的 partial 链表中供后续分配使用,减少伙伴系统交互。
五、SLUB 的 Capturing 机制(CPU Partial 乒乓优化)
早期 SLUB 的问题:多个 CPU 交替从同一个 Page 中分配释放对象,导致 Page 频繁在"满"和"空"之间颠簸,引发大量 Slow Path。
Linux 6.0+ 引入的 frozen 标记和 CPU Partial 机制解决了这一问题:
- 当一个 Page 属于某个 CPU 时,标记为
frozen—— 其他 CPU 即使释放对象也不会将它快速归还伙伴系统 - CPU Partial 链表成为"待回收"缓冲区,Page 先挂上 CPU partial,等待合适时机再批量处理
SLUB_CPU_PARTIAL特性将每 CPU partial 页数限制在slab_max_order· 2 个,防止某一 CPU 占用过多 partial 页
六、SLUB 调试与故障排查
6.1 SLUB Debug(CONFIG_SLUB_DEBUG)
开启后,SLUB 会在对象周围插入:Red Zone(越界写检测)、 Poison(释放后填充 0x5a/0xa5 检测 UAF)、Tracking(记录 alloc/free 的调用栈)。
触发SLUB BUG的场景:
- Red Zone 被覆写:越界写 → "Object overwritten: Red zone not overwritten" 或 红区数值被魔改
- Use-After-Free:释放后对象被再次使用 → "Object was poisoned and freed but still in use"
- Double Free:同一对象被 free 两次 → freelist 成环,后续分配死循环或内核挂起
- Slab 越界访问:访问超出对象边界的内存,命中红区的 0x1234567 或 0x76543210 标记
6.2 /proc/slabinfo 详解
# cat /proc/slabinfo
dentry 91204 96240 192 40 2 : tunables 0 0 0 : slabdata 2406 2406 0
active total size perslab page_order ... slabs partial ...
- active / total:已使用对象数 / 总对象数,比例低说明碎片多
- size:每个对象的大小(含元数据对齐)
- perslab:每页可放对象数
- slabs:已分配的 Slab Page 组数
- partial:节点中 partial 页数
6.3 tracing: slub 事件跟踪
echo 1 > /sys/kernel/debug/tracing/events/kmem/kmem_cache_alloc/enable
echo 1 > /sys/kernel/debug/tracing/events/kmem/kmem_cache_free/enable
cat /sys/kernel/debug/tracing/trace
BCC 工具:slabratetop(每秒各缓存的分配速率)、slabtop(实时 slab 状态)。
七、内核参数调优
| 参数 | 说明 | 建议 |
|---|---|---|
slab_max_order | SLUB 向伙伴系统申请的最大阶数(2^n页) | 默认3(8页),大对象缓存可调至4 |
slab_min_objects | 每阶最少维护的对象数 | 默认4,增大可减少碎片 |
slub_min_order | 最小阶数 | 默认0(单页),一般不改 |
slub_min_partial | 每个 Node 最少保留的 partial 页数 | 默认5,热路径多的缓存可调小 |
slub_debug | 全局调试开关 | U 开启 Poison+Poi...(释放后毒化+红区),F 开启 Red Zone追踪,Z 开启红区,P 开启 Poison |
八、生产案例
案例 1:dentry 缓存爆炸导致抖动
某容器环境 dentry_cache 飙到千万级,/proc/slabinfo 看到 active/total 比极低(约 0.1),大量 partial Page 未回收。原因:应用频繁创建/删除目录,shrink_slab 触发不及时。drop_caches=2 强制回收后内存抖动消失。优化方案:设置 vfs_cache_pressure=200(更积极回收 dentry)。
案例 2:SLUB 跨 NUMA 插入延迟
MySQL 实例在 8 路 NUMA 服务器上出现单核分配毛刺。perf 采样发现 kmem_cache_alloc 在 Slow Path 的 new_slab 中跨越 Node 分配 Page。优化方案:启用 vm.zone_reclaim_mode=1(优先本地节点回收),结合 numactl --cpunodebind 绑核。
案例 3:SLUB Double Free 触发内核死锁
某驱动模块在错误路径中重复 kmem_cache_free(),导致 freelist 链表成环。后续分配进入 while 循环无法退出,触发 hard lockup。SLUB_DEBUG=1 可以在释放时校验指针是否已在 freelist 中(通过 RCU 安全的 slab_full 过滤),尽早发现问题。
九、SLUB 在现代 Linux 中的最新演进
- Linux 5.9+:Triage Allocator引入了
kmem_cache_alloc_bulk的 per-cpu 批量取能力,一次性取 32 个孤立对象,对网络收发包场景大有提升。 - Linux 6.0+:CPU Partial Frozen 机制有效避免多核间 partial page 乒乓问题。
- Linux 6.3+:SLUB支持
CONFIG_SLUB_TINY(替代 SLOB)仅 1.2KB 代码,为极嵌入式场景提供"勉强够用"的 Slab 能力。 - Linux 6.5+:shrinkers通过
SLAB_TYPESAFE_BY_RCU实现无锁 Call-RCU 缓存,大幅提升 RCU 遍历密集型 Workload 的内存效率。
十、总结
SLUB 作为 Linux 内核最核心的 slab 实现,其"极简 + 快速路径优先 + NUMA 友好"的设计思想值得所有系统软件开发者学习。理解 SLUB 不仅对内核开发有益,更能指导用户态高性能内存分配器(如 jemalloc、tcmalloc)的选型与调优。
实践中,掌握 /proc/slabinfo 读懂对象利用率、用 BCC 追踪分配热点、合理配置 SLUB 调试开关,是排查内核内存问题的三板斧。而在 NUMA 大系统中,绑核 + zone_reclaim 的组合拳往往能解决大部分 slab 引起的延迟异常。

发表评论 取消回复