引言
在 Linux 内核的广阔版图中,内存管理子系统(Memory Management)是影响系统性能的核心枢纽。内核自身运行时需要频繁分配和释放大量小尺寸对象——task_struct、inode、dentry、file、vm_area_struct 等。如果每次都直接向伙伴系统(Buddy System)申请页面,不仅会产生严重的锁竞争和全局瓶颈,还会因「页内碎片」造成大量内存浪费。
Slab 分配器正是为解决这些问题而诞生的「内核对象缓存层」。它从伙伴系统批量申请页面,切成固定大小的 slot,以 O(1) 速度完成对象分配/释放。SLUB(Unqueued Slab)自 Linux 2.6.23 起成为默认实现,以其极简的设计、最小的元数据开销和卓越的扩展性统治了内核内存分配的半壁江山。
本文将深入 SLUB 分配器的每一个核心环节——从伙伴系统的 page allocator,到 kmem_cache 的构建;从每 CPU freelist 的无锁路径,到 slab partial/full/empty 三链表平衡;从 cmpxchg 双链表 fastpath,到 debugger 与 red zone 的内存安全防线。最后结合实际调优案例与监控手段,构建完整的 SLUB 知识体系。
一、从伙伴系统到 Slab:为什么需要对象缓存
1.1 伙伴系统的局限性
伙伴系统(Buddy System)以 page(通常 4KB)为最小单位管理物理内存,支持 alloc_order 申请 2^order 个连续页面。这一机制适合大块 DMA、用户态 mmap 等场景,但对于内核频繁出现的小对象分配(多数不足 1KB),它存在三重致命缺陷:
锁竞争:伙伴系统的核心数据结构 zone->free_area[order].free_list 受 zone->lock 保护。多核并发 alloc_pages 时自旋锁成为吞吐量天花板。
内部碎片:分配 96 字节的 dentry_object 仍需消耗一个完整 4KB 页,浪费约 97% 空间。
初始化开销:task_struct 每次分配后都需要设置引用计数、链表头、PID 字段等,反复初始化的成本不容忽视。
1.2 Slab 的设计哲学
1994 年 Jeff Bonwick 为 Solaris 首创 Slab allocator,Linux 在 2.0 时期引入并演进为 SLUB。其核心思路是 「以空间换时间,以缓存消开销」:
1) 对象缓存池:按固定大小建立 kmem_cache,提前批量申请页面并切分,避免重复的系统调用与 freepage 查找。
2) 构造/析构复用:分配时调用构造函数(ctor)初始化对象;归还时标记为 free 但保留初始化状态,下次分配直接进入热路径,跳过 memset。
3) 硬件缓存友好:对象按 cache line 对齐,并在 slab page 内部顺序分配,提升 L1/L2 命中率。
4) 着色偏移(Colour Offset):每个 slab 起始处加入随机偏移量「cache colour」,避免多 slab 中同一偏移的对象落在同一 cache line,减少冲突未命中。
1.3 SLUB vs SLOB vs SLAB
Linux 内核目前提供三种 slab 实现:
SLAB——最早的 Linux slab 实现,每个 slab 头部维护共享的 buffctrl 数组(unsigned int 映射 obj→index),适合 SMP 但元数据负载高。
SLUB——默认选择。去掉共享位图,将 freelist 指针直接嵌入对象内存(free list 链表),使用每 CPU page 指针锁定当前 partial slab,实现了步骤最少、扩展性最优的 fastpath。
SLOB(Simple List Of Blocks)——用于嵌入式等极低内存环境(小于64MB),采用首次适应算法,性能最差但元数据最精简。
SLUB 在主流服务器与桌面场景中综合表现最佳:大小对象通吃、NUMA 感知、调试能力(red zone / poison / tracking)完善。
二、SLUB 的核心数据结构
2.1 kmem_cache
struct kmem_cache 是 SLUB 的核心描述符,每个固定大小的对象类型对应一个。通过 kmem_cache_create(const char *name, size_t size, unsigned long flags, ...) 创建。关键字段如下:
object_size:用户请求的裸对象大小(不含元数据)。
size:实际占用空间 = object_size + sizeof(void *)(free list 指针)+ 调试附加区(red zone)。
offset:free list 指针在对象内的偏移,复用对象前 8 字节存储 next 指针。
oo(order + objects):通过 get_order(size) 计算本 slab 的 alloc_order 与 per-slab 对象数。
cpu_slab:指向 struct kmem_cache_cpu 的指针数组(per-CPU)。
node[MAX_NUMNODES]:指向 struct kmem_cache_node 的指针数组(per-NUMA-node)。
可通过 cat /proc/slabinfo 查看当前系统所有 kmem_cache 的活跃对象数、slab 页数与综合利用率。
2.2 kmem_cache_cpu(每 CPU 热路径)
该结构体是 SLUB 性能魔法的基石:
struct page *page:指向当前每 CPU 专属的 slab 页。分配几乎总是从该 page 直接取对象,无需原子操作。
void **freelist:每 CPU 专属 free list 链表头。释放操作默认归还入此链表。
unsigned long tid:事务 ID,配合 cmpxchg 实现无锁化 freelist 操作,避免传统自旋锁开销。
在 fastpath __slab_alloc 中,SLUB 首先检查本地 kmem_cache_cpu->page 是否存在并有空闲对象;若有,直接 freelist = object; return——全程无锁,仅几条汇编。
2.3 kmem_cache_node(NUMA 层面)
每个 NUMA 节点拥有一个 kmem_cache_node,管理本地 partial 与 full slab 表:
struct list_head partial:本地 partial slab 链表(按 node 分组)。当 CPU 私有 slab 耗尽时,会从该 node 的 partial 链表获取备用 slab。
struct list_head full:本地 full slab 链表,仅作管理用不参与快速分配。释放对象入 full slab 时会转为 partial。
节点级操作受 list_lock(spinlock_t)保护,仅在 partial 切换、新增 slab 等 slowpath 才进入,对 fastpath 零干扰。
2.4 slab page(struct page 复用)
SLUB 巧妙复用 struct page 表示管理的 slab 页,不引入新元数据结构:
page->s_mem(union 成员):指向 slab 中第一个对象的起始地址。
page->freelist:空闲对象链表头(所有 CPU 共享版本的 freelist)。
page->inuse:已分配对象计数。
page->objects:本 slab 总对象数。
page->frozen:标记是否绑定特定 CPU(frozen=1 表示 per-cpu exclusive)。
page->slab_cache:反向指针回 kmem_cache。
这样设计把元数据开销降到最低:每 page(4KB)仅需 dozen bytes 的状态信息。
三、SLUB 的分配路径详解
3.1 Fastpath:cmpxchg 无锁分配
单线程或小并发下,SLUB 完全在 CPU 本地缓存上完成分配,执行步骤不超过 5 行核心逻辑:读取 kmem_cache_cpu->page → 取 page->freelist 中对象 → new.s_mem = *(void **)object → cmpxchg 更新 freelist 并校验 tid 一致 → 成功即返回。该路径无锁、无中断、无调度,延迟可低至 20-50纳秒。
3.2 Slowpath:伙伴系统介入
当私有 slab 耗尽(inuse == objects 且无 freelist),SLUB 进入 __slab_alloc_slowpath:检查 per-node partial 链表是否存在可复用 slab;没有则向 alloc_pages(gfp, oo) 申请 oo 阶页面,初始化 page->s_mem/freelist/objects,插入当前 page 指针进入 CPU fastpath。
这是上下文切换、内存压力等场景的兜底路径,涉及 alloc_pages 的 zone 扫描与 shrinker 回收。
3.3 GFP 路径与内存碎片避让
分配 flag 如 GFP_KERNEL、GFP_ATOMIC、GFP_DMA、GFP_KERNEL_ACCOUNT 会影响 zone 选择、回收触发与 cgroup 核算。memory cgroup v2 场景下,GFP_KERNEL_ACCOUNT 调用 memcharge_kernel_account 计入 memory.kernel。
四、SLUB 的释放路径详解
4.1 本地释放(Local Free)
CPU 本地释放走 __slab_free 的快速路径:*(void **)object = page->freelist; page->freelist = object; inuse--。若 inuse 降至 0,slab 变为 empty,视内存压力决定是否归还伙伴系统。
4.2 跨 CPU 释放(Remote Free)
对象跨 CPU 释放时(如 NUMA 节点 X 申请,节点 Y 释放),进入 __slab_free 的慢路径:通过 cmpxchg_double_slab 原子开关链;若 slab 原本绑定另一 CPU(frozen=1),会被拉回 partial 链表。
4.3 Shrink 与 Reclaim
内核注册 kmem_cache_shrink 与 shrinker 接口,在 /sys/kernel/slab/.../shrink 或内存压力下触发 empty slab 归还。默认 SLUB reclaim_account=0 表示永不自动释放 partial slab,需显式写入 drop_cache。
五、调试与安全防线
5.1 Red Zone 与 Poison
开启 CONFIG_DEBUG_SLAB 后,每个 object 前嵌入 Redzone(0x12 字节魔数),后嵌入 Redzone + tracker。释放时填入 0x5a(POISON_FREE),分配时填入 0x6b(POISON_ALLOC)。任何越界写、double-free 都会破坏 red zone,触发 object_err 报警。
5.2 Shadow Memory 与 Freed 对象追踪
CONFIG_MEMCG_KMEM 与 CONFIG_SLUB_DEBUG 联合使得每个 free object 的首 8 字节指向 track_alloc/track_free 栈记录,通过 /sys/kernel/slab/<name>/trace 查看。
5.3 KASAN / KFENCE 的协同
KASAN(Kernel Address Sanitizer)在 slab 层之上构建 shadow memory 映射(1:8),每字节合法性的 bit 被编码到影子页。KFENCE(Kernel Electric Fence)则使用独立 CPU 池 + 采样监控模式:每次 __kfence_alloc 以约 1/500 概率从「kfence 池」插入 guard page,越界即触发 protection fault。二者互补:KASAN 用于系统性调试,KFENCE 用于生产环境概率性捕获。
六、调优与监控实战
6.1 查看 slab 状态
cat /proc/slabinfo 每行显示 name、active_objs、num_objs、objsize、objperslab、pagesperslab、active_slabs、num_slabs。slabtop 提供实时 Top-like 视图。/sys/kernel/slab/<name>/ 下暴露 aliases、alignment、cache_order、cpu_partial、min_partial 等数十个调优节点。
6.2 cache_order / min_partial / cpu_partial 调优
cache_order:单次 alloc_pages 的伙伴系统阶,太小导致 slab 数量爆炸,太大在内存碎片时易失败。通常默认 0(4页/slab),大对象 cache 可调高至 2~3。
min_partial:每 NUMA 节点至少保留的 partial slab 数。过低导致频繁 alloc_pages,过高浪费内存。生产环境若 slab 水波震荡剧烈,可适度提升至 5~8。
cpu_partial:每 CPU 缓存对象阈值。超过该值后释放的对象被弹回 node partial,避免 per-cpu freelist 囤积过多导致远端 NUMA 饥饿。
6.3 实际案例:容器密度提升下的 slab 失衡修复
某运行 1500+ Pod 的 NestOS 节点出现 slab 持续攀升、OOM-killer 误杀关键进程。排查 /proc/meminfo 发现 Slab: 12458792 kB、SReclaimable: 2341576 kB,大部分为 unreclaimable 的 proc_inode_cache 与 dentry。根因是新内核 dentry 的 hash table 预分配机制积极增长但不主动收缩。修复方案:定制 Docker + k8s manifest,设置 vm.min_free_kbytes=524288、vm.vfs_cache_pressure=200,通过 drop_caches=2 定时同步清理 dentry+inode。同时开启 slab_nomerge 便于精准监控。三周后 slab 稳定下降 70%,OOM 事件率降至零。
七、SLUB 在新内核中的演进
7.1 CPU Partial 批量操作(Linux 5.15+)
引入 kmem_cache_cpu->partial 批量操作 API,partial slab 插入/弹出支持 batch count,在 bulk alloc/free 场景下减少 per-node lock 争抢。
7.2 Tight-TLB 与 Lazy FP(Linux 6.x)
SLUB 与 CPU 微架构进一步协同:对 slab page 的 TLB shootdown 更谨慎,避免远程释放 slab 时误触发全核 IPI;FPU 懒保存路径避免 slab 分配期间误碰 kernel_fpu_begin 状态。
7.3 Folio 与大页融合(Linux 6.1+)
SLUB 直接使用伙伴系统的 alloc_pages,不直接支持 THP。但通过 khugepaged 后路径,当某个 kmem_cache 的对象大小较大且开启 transparent_hugepage=always 时,可经合并路径探索大页友好分配。此特性在 Redis MEMKITT 等自定义 cache 中实用性较高。
八、总结与展望
SLUB 分配器以「最小的元数据开销、最快的 fastpath、最大的可调试性」成为 Linux 内核内存管理的中流砥柱。其架构演进体现了简洁设计哲学——复用 struct page、cmpxchg 双链表 fastpath、partial 三链表平衡——历久弥新。
面向未来,SLUB 持续在以下方向发力:一是 NUMA 智能调度,感知访问 locality 动态迁移 slab;二是持久性内存(PMEM)适配,探索 slab 混合分配模式;三是与 cgroup v2 memory.reclaim 联动,提供 OOM 前的精细 reclaim 控制。
理解 SLUB,不仅是打通内存管理全链路的钥匙,更是每个内核性能调优工程师的必修课。

发表评论 取消回复