Slab Allocator 1994 — 内核按对象类型开缓存,不是按字节切
是什么
Slab 是一种内核里的内存分配器,专门给”反复创建又销毁的同类对象”用。日常类比:餐厅外卖打包盒不会每来一单都重新生产,而是按盒型预备一摞——同款盒子叠在一起,要用就抽一个,用完洗洗放回去。
内核里 inode、task_struct、socket 这种结构每秒被分配释放上千次。传统 kmalloc(sizeof(struct foo)) 把它当成”给我 N 字节”,每次都要找空位、对齐、初始化。Bonwick 看穿一件事:这些对象的类型是固定的,与其当字节,不如按类型开缓存。
为什么重要
不理解 slab,下面这些事都没法解释:
- 为什么 Linux 内核每秒做几百万次分配却几乎不卡——slab 让分配只是”摘一个链表节点”
- 为什么
cat /proc/slabinfo能看到 dentry / inode / kmalloc-256 这些条目 - 为什么 SLUB(默认)/ SLAB(已删)/ SLOB(嵌入式)三个名字都带 SL——它们都是这篇 1994 论文的变体
- 为什么 jemalloc / tcmalloc 这些用户态分配器的”per-thread cache + size class”思路和它如此相似
核心要点
Bonwick 的洞见可以拆成 三步:
-
按类型开 cache:每种对象类型(inode、task_struct)有一个独立的 object cache。cache 内部维护若干个 slab——每个 slab 是一片连续内存(通常 1 页),切成 N 个同类对象槽。
-
保留初始化状态:第一次往 slab 填对象时跑 constructor(ctor)做初始化;slab 整片被回收时才跑 destructor(dtor)。中间的分配/释放不重新 init——已经初始化好的字段(自旋锁、链表头)原样保留。
-
着色避 cache 冲突:每个 slab 起始位置故意错开几字节(叫 coloring)。这样不同 slab 中”同偏移位置的对象”会落到不同 CPU cache line,缓解硬件 cache 抖动。
后续 1998 年 Bonwick 加了 magazine——每 CPU 一个本地对象池,分配先走本地不用全局锁。这是现在 SLUB 性能的真正来源。
实践案例
案例 1:Linux 里的 slabinfo
$ cat /proc/slabinfo | head# name <active_objs> <num_objs> <objsize> ...dentry 125632 138978 192 21 1inode_cache 42891 43200 608 13 2task_struct 248 265 3392 9 8kmalloc-256 5120 5120 256 16 1读懂三件事:
dentry这种类型在内核里有 13 万多个对象,每个 192 字节kmalloc-256是给”任意 256 字节请求”准备的通用 cache——kmalloc(200)就走它- 每行就是一个独立的 cache,互不抢锁
案例 2:分配热路径有多便宜
伪代码看 SLUB 的 fast path:
void *kmem_cache_alloc(cache) { obj = cpu_local_freelist; // 1. 看本 CPU magazine if (obj) { cpu_local_freelist = obj->next; // 2. 摘一个 return obj; // 3. 返回 } return slow_path(cache); // 才走全局锁}无锁、3 条指令、命中率 95%+。这就是为什么内核能扛住每秒百万次分配。
案例 3:ctor 只跑一次的陷阱
struct foo { spinlock_t lock; struct list_head head; int counter; };
void foo_ctor(void *p) { struct foo *f = p; spin_lock_init(&f->lock); // 只在 slab 第一次填充时跑 INIT_LIST_HEAD(&f->head);}
cache = kmem_cache_create("foo", sizeof(struct foo), 0, 0, foo_ctor);约定:alloc 出来的 foo,lock 已经 init 好、head 已经是空链表——但 counter 是上次 free 时的残留值。新人常踩这个坑:以为 alloc 出来全 0。要 0 得自己 memset 或加 __GFP_ZERO。
踩过的坑
-
把 slab 当通用 malloc:slab 是按类型预开 cache 的,类型在启动时固定。变长任意 size 走 buddy + 直接页分配,不是 slab 的活。
-
大对象不该走 slab:对象 > 半页时,slab 内部碎片惊人。Linux 大 alloc 自动绕开 slab 走
__get_free_pages。 -
ctor 副作用误解:ctor 只在 slab 第一次填充和后续扩张时跑,不是每次 alloc 跑。dtor 在 slab 整片回收时跑——不是每次 free 跑。把”alloc 时初始化”逻辑塞 ctor 里就错了。
-
SLOB 误用:SLOB 是 first-fit 极简版,给 < 16MB 嵌入式用。在现代服务器上启用会慢得离谱——它没有 magazine,没有按类型分桶。
-
per-CPU cache 不是免费的:每 CPU 一份 magazine 意味着内存放大 N 倍。NUMA 大机器上这个开销不能忽视,需要调
slub_max_order等参数。
适用 vs 不适用场景
适用:
- 反复创建销毁的同类对象(inode、socket、bio、task_struct)
- 对象有固定初始化状态(锁、链表头)想保留
- 高并发分配场景——magazine 提供无锁 fast path
不适用:
- 任意大小的一次性分配 → buddy + 页分配器
- 对象 > 半页 → 直接
alloc_pages - 用户态程序 → 用 jemalloc / tcmalloc / mimalloc,思想类似但实现不同
- 嵌入式 < 16MB 内存 → SLOB(不要 SLAB/SLUB)
历史小故事(可跳过)
- 1994 年:Bonwick 在 Sun,给 SunOS 5.4 写了第一版 slab,论文只有 12 页,提出”按对象类型缓存 + 保留已初始化状态”两个洞见。
- 1998 年:SunOS 7 加 magazine 解决多 CPU 锁竞争。这部分另有一篇论文《Magazines and Vmem》。
- 2001 年:Mark Hemment 把 slab 移植到 Linux 2.4,叫 SLAB。
- 2007 年:Christoph Lameter 提出 SLUB——不存元数据在 slab 内,直接用 page 结构体,简单但够快——合入 2.6.22 成为默认。
- 2024 年:Linux 6.8 移除 SLAB(同名实现),剩 SLUB + SLOB。Bonwick 1994 的设计比代码活得久。
学到什么
- 分配器要懂调用者的语义——通用 malloc 不知道你要存什么,slab 知道,所以更快。这是典型的”特化打败通用”。
- 保留状态是性能金矿——每次重新初始化看起来便宜,乘以每秒百万次就要命。
- per-CPU cache + 全局慢路径是高并发分配器的标准模板:jemalloc / tcmalloc / Go 的 mcache / Rust 的 mimalloc 都用同一招。
- 设计活得久过实现——SLAB 实现死了,SLUB 实现换了,但 1994 的对象 cache + ctor/dtor + coloring 三件套 30 年没变。
延伸阅读
- 论文 12 页 PDF:Bonwick 1994(密度不高,有内核背景能直接读)
- 后续:Magazines and Vmem (USENIX 2001)(讲 magazine + vmem 资源分配器)
- Linux 内核源码:
mm/slub.c(SLUB 实现,Lameter 简化版) - Robert Love《Linux Kernel Development》第 12 章——把 slab 接口讲得最清楚
- buddy-allocator —— slab 的下一层,slab 向 buddy 要整页
- linux-rcu —— slab 对象释放常配 call_rcu 延迟释放
关联
- buddy-allocator —— 物理页分配器,slab 在它之上
- linux-rcu —— 释放 slab 对象时常需要 RCU 延迟
- afs-1988 —— 同时代 Sun 系统软件,分布式文件系统也是 cache 重度用户
- aries-1992 —— 同样把”按操作类型设计专用结构”思想用到数据库恢复
- immix-mark-region —— GC 里的”region + 同类对象一起”思想与 slab 同源
- self-pic —— 内联缓存也是”特化打败通用”的另一个经典案例
反向链接
- afs-1988 —— AFS 1988 — 客户端缓存 + 回调失效让分布式文件系统真正能扩展
- aries-1992 —— ARIES 1992 — 数据库崩溃后怎么把账目对回来
- immix-mark-region —— Immix — 把”扫”和”搬”两种垃圾回收揉成一个
- self-pic —— Self / PIC — 内联缓存的诞生