跳转到内容

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 的洞见可以拆成 三步

  1. 按类型开 cache:每种对象类型(inode、task_struct)有一个独立的 object cache。cache 内部维护若干个 slab——每个 slab 是一片连续内存(通常 1 页),切成 N 个同类对象槽。

  2. 保留初始化状态:第一次往 slab 填对象时跑 constructor(ctor)做初始化;slab 整片被回收时才跑 destructor(dtor)。中间的分配/释放不重新 init——已经初始化好的字段(自旋锁、链表头)原样保留。

  3. 着色避 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 1
inode_cache 42891 43200 608 13 2
task_struct 248 265 3392 9 8
kmalloc-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

踩过的坑

  1. 把 slab 当通用 malloc:slab 是按类型预开 cache 的,类型在启动时固定。变长任意 size 走 buddy + 直接页分配,不是 slab 的活。

  2. 大对象不该走 slab:对象 > 半页时,slab 内部碎片惊人。Linux 大 alloc 自动绕开 slab 走 __get_free_pages

  3. ctor 副作用误解:ctor 只在 slab 第一次填充和后续扩张时跑,不是每次 alloc 跑。dtor 在 slab 整片回收时跑——不是每次 free 跑。把”alloc 时初始化”逻辑塞 ctor 里就错了。

  4. SLOB 误用:SLOB 是 first-fit 极简版,给 < 16MB 嵌入式用。在现代服务器上启用会慢得离谱——它没有 magazine,没有按类型分桶。

  5. 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 的设计比代码活得久。

学到什么

  1. 分配器要懂调用者的语义——通用 malloc 不知道你要存什么,slab 知道,所以更快。这是典型的”特化打败通用”。
  2. 保留状态是性能金矿——每次重新初始化看起来便宜,乘以每秒百万次就要命。
  3. per-CPU cache + 全局慢路径是高并发分配器的标准模板:jemalloc / tcmalloc / Go 的 mcache / Rust 的 mimalloc 都用同一招。
  4. 设计活得久过实现——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 — 内联缓存的诞生