引言

在 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 分配遵循三级降级策略:

  1. Fast Path(CPU 缓存命中):cpu->freelist != NULL → 直接从 freelist 弹出头对象,更新 freelist,page->inuse++。整个过程无需任何锁,关本地中断即可。
  2. Middle Path(CPU Partial 降级):如果 Active Page 已满(freelist 为 NULL),检查 cpu->partial 链表。若有 partial page,将其中一个提升为 Active Page,继续 Fast Path。
  3. 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)

释放是分配的逆过程,也有三级路径:

  1. 快速释放到 Active Page:对象属于当前 Active Page → 对象变 freelist 新头,page->inuse--。
  2. 释放到 Partial Page:对象不属于 Active Page 而属于某个 CPU Partial Page → 插入其 freelist。
  3. 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 模式下):

  1. 本地 CPU 的 Fast Path
  2. 本地 CPU 的 partial
  3. 本地 Node 的 partial
  4. 远程 Node 的 partial(性能下降)
  5. 向伙伴系统申请新页(默认在本地 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_orderSLUB 向伙伴系统申请的最大阶数(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 引起的延迟异常。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部