去年帮客户排查一台机器的内存问题,free 一看 available 只剩 15%,但 top 里所有进程的 RSS 加起来都解释不了这些内存去哪了。后来用 slabtop 一拉,真相大白:某个内核对象的 cache 占了将近 2GB,num_objs 涨到了几百万。从那以后,我对 Linux 内核这套 SLAB/SLUB 专用内存缓存池就特别上心。
这篇文章我打算把 SLAB 和 SLUB 讲透,包括它们到底解决什么问题、各自的经典设计、为什么 SLUB 能取代 SLAB 成为默认分配器,以及实际排查内存问题时怎么用 /proc/slabinfo 和 slabtop 定位问题。如果你是写内核模块、驱动,或者做嵌入式 Linux,又或者正在准备内核相关的面试,这篇文章应该能帮你把很多零散的知识串起来。我尽量用大白话讲,也会带上一些我实际踩过的坑。
1. 为什么内核必须自己搞一套对象级缓存池
先说一个最基础的问题:内核里到处都是几十字节、几百字节的小对象,比如文件系统的 dentry、inode,进程的 task_struct,网络连接的 socket 结构,它们被频繁创建和销毁。如果每次都直接找伙伴系统(Buddy Allocator)要内存,会怎样?
1.1 伙伴系统解决不了的对象级分配问题
伙伴系统管理的是物理页,最小粒度是 4KB。一个 dentry 结构体通常只有 192 字节,为了它单独分配一页 4KB 内存,内部碎片极其严重。更麻烦的是,每次分配都要经过复杂的页块拆分,释放时又要合并回大块,这个过程的开销和锁竞争非常可观。
你可以把伙伴系统想象成一个只按“整箱”出货的仓库。你只是要一根笔,它也得给你一箱。用完再还回去,仓库还要拆开箱子检查、重新归类。如果内核里所有对象都这么干,CPU 时间全耗在页表的拆拆合合上了。
1.2 缓存池要解决的三个核心问题
所以内核需要一套机制,把同一类大小的对象集中管理起来,这就是缓存池(Cache)。SLAB 分配器最早就是干这个的,SLUB 是它的改良版。一个合格的缓存池至少要解决三件事:
- 对象复用:对象释放后不还给伙伴系统,而是留在池子里,下次分配直接拿回来用。很多对象只需要重置少量字段,不用从零初始化,省了大量开销。
- 内部碎片控制:把一个或多个物理页切成若干大小相等的对象,按需分配,避免“一页用几个字节”的浪费。
- 无锁快速路径:利用 per-CPU 缓存,让同一个 CPU 上的分配和释放在绝大多数情况下不碰全局锁,这是性能的关键。
在 Linux 2.6.23 之前,默认分配器是 SLAB;从 2.6.23 开始,SLUB 成为默认。但内核代码里至今还保留着 SLAB 的实现,很多老驱动和嵌入式 BSP 也还在用 SLAB。所以两者都得懂。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. SLAB 的经典设计:着色、per-CPU 缓存与三链表
SLAB 的命名就是它的设计核心:“slab” 这个词指的是把一页或多页物理内存分成一组对象。理解 SLAB,可以先从它的组织单位开始。
2.1 slab 页组与对象划分
假设你有一个 cache,对象大小是 256 字节。如果每个 slab 占一页(4KB),那么这一个 slab 就能切成 16 个对象,objperslab = 16,pagesperslab = 1。如果对象很大,比如 4KB,那一个 slab 可能就需要多页,pagesperslab 会大于 1。
每个 cache 有一个名字,比如 kmalloc-256、dentry、filp。kmem_cache_create() 创建 cache 时传进去的 name 就是 /proc/slabinfo 里看到的名字。这个细节很重要,后面排查内存问题时你会靠它认人。
2.2 着色(coloring)解决什么问题
SLAB 里有一个很经典的设计叫 cache coloring,通常翻译成“着色”,但跟颜色没关系。它的动机是:不同 slab 中编号相同的对象,如果它们的起始地址相对页边界的位置完全一样,那么这些对象会被映射到 CPU 硬件 cache 的同一组 cache line 上。当内核频繁交替访问不同 slab 里的同编号对象时,就会互相“踢掉”对方的缓存行,造成 cache thrashing。
解决办法是在每个 slab 开头加一个不同的偏移量(颜色),让对象在内存里的起始位置尽量错开。你可以理解成停车场里每辆车错开一个车位停,大家都好进出。
不过在现代 CPU 大 L2/L3 cache、多路组相联的背景下,着色的收益已经很小了,而且它让 SLAB 的元数据管理和计算变得复杂。SLUB 基本砍掉了这个功能,换来的是更简单的代码路径。
2.3 SLAB 的三链表与 per-CPU array_cache
SLAB 为每个 cache 维护了三张 slab 链表:
slabs_full:所有对象都被占用的 slab。slabs_partial:有一部分对象空闲的 slab。slabs_free:所有对象都空闲的 slab。
分配对象时,优先从 slabs_partial 里找空闲对象,因为它还有剩余;如果 partial 也没了,就从 slabs_free 里取一个 slab 重新初始化并转去 partia
