一、为什么需要 Slab 分配器
Linux 内核每秒要创建和销毁成千上万个 task_struct、inode、dentry 这些固定结构体。用通用分配器 kmalloc 来分配它们,每次都要在空闲链表上搜索合适大小的块,释放后还要合并相邻块防止碎片。对象大小固定、类型已知,却要反复走通用分配器的完整流程,这是浪费。
更关键的问题是缓存。一个 task_struct 刚释放,内存块归还给通用分配器,可能立刻被分配给完全不同类型的数据。CPU 缓存行里刚加载好的数据被新数据覆盖,下次再分配 task_struct 时又要从内存重新加载——缓存局部性被彻底破坏。
还有初始化开销。很多内核对象创建时需要初始化大量字段(锁、链表头、引用计数),销毁时又要逐个清理。如果上一次释放的对象能保持初始化状态,下次直接拿来用,就能省掉构造和析构的开销。
Slab 分配器针对这些问题给出了系统性的方案:按类型建缓存,每个缓存只分配固定大小的对象,释放后不归还给通用分配器,而是留在类型专属的 slab 中等待复用。
二、现实类比
工厂里的模具生产线。你要生产 M6 螺栓,不会每次都临时开模——你有一套 M6 专用的模具,压出来的螺栓尺寸完全一致。模具始终就位,换批生产时只需倒入新原料,不需要重新校准。M6 的模具和 M10 的模具分开存放,互不干扰。
餐厅的桌位安排也类似。2 人桌、4 人桌、8 人桌各有一片区域。来了一对客人,直接领到 2 人桌区找空桌坐下,不用在大厅里到处找”差不多大小”的空位。吃完离场,桌子收拾干净留在原位,下一拨 2 人客人直接坐。
三、核心思想
Slab 分配器为每种对象类型维护一个独立的 缓存(Cache)。每个缓存由若干 Slab 组成,每个 Slab 是一块连续内存(通常由一个或多个物理页构成),里面划分成固定大小的 对象槽位。
3.1 三种 Slab 状态
每个 Slab 处于三种状态之一:
- Partial:部分槽位被占用,部分空闲。分配时优先从这里取
- Full:所有槽位都被占用,无法再分配
- Empty:所有槽位都空闲,内存可以被回收
3.2 对象生命周期
分配一个对象:从 Partial Slab 的空闲槽位中取出一个,标记为已用。如果 Partial Slab 用完了,从 Empty Slab 中取一个变成 Partial。如果连 Empty Slab 也没有,向伙伴系统申请新页创建 Slab。
释放一个对象:归还到它所在的 Slab,标记为空闲。如果 Slab 从 Full 变成 Partial,移入 Partial 链表。如果 Slab 从 Partial 变成 Empty,移入 Empty 链表,等待回收。
分配流程: Partial 链表 → 取空闲对象 → Partial 可能变 Full Partial 为空 → Empty 链表取 Slab → 变 Partial Empty 也空 → 伙伴系统分配新页 → 创建新 Slab
释放流程: 归还对象到所在 Slab → Full 变 Partial(移入 Partial 链表) → Partial 变 Empty(移入 Empty 链表,可回收)3.3 复杂度
| 操作 | 复杂度 | 说明 |
|---|---|---|
| 分配(Partial 有空闲) | O(1) | 直接取空闲槽位,无需搜索 |
| 分配(需新建 Slab) | O(页分配) | 向伙伴系统申请页面 |
| 释放 | O(1) | 归还到已知 Slab 的槽位 |
| 缓存行对齐 | O(1) | 对象按缓存行大小对齐,硬件友好 |
四、变体与对比
4.1 Linux 内核的三种实现
| 实现 | 特点 | 适用场景 |
|---|---|---|
| Slab | 原始实现,每个 Slab 有元数据管理,支持构造/析构回调 | 复杂对象,需要初始化/清理 |
| Slub | 默认实现,元数据嵌入对象本身,调试信息更丰富 | 大多数场景,调试友好 |
| Slob | 极简实现,适合嵌入式,内存开销最小 | 内存极度受限的嵌入式系统 |
Slub 取代 Slab 成为 Linux 默认分配器,主要因为 Slab 的元数据开销太大——每个 Slab 需要额外的管理结构,在大型系统上浪费显著。Slub 把空闲对象串成链表,元数据直接嵌入对象中,省掉了额外开销。
4.2 Slab 与 Arena
Slab 和 Arena 都追求快速分配,但策略不同。Slab 按类型建缓存,每个缓存只放固定大小的对象;Arena 按生命周期分配,同一 Arena 里的对象大小可以不同,但一起释放。Slab 支持单个对象的分配和回收,Arena 只支持整块释放。
4.3 Slab 与 Buddy System
两者是上下级关系。Buddy System 管理物理页面,以 2 的幂次为单位分配和合并。Slab 从 Buddy System 拿到页面后,在页面内部划分固定大小的对象槽位。Buddy 解决”给几个页”的问题,Slab 解决”页里面怎么放对象”的问题。
4.4 Slab 与 sync.Pool
Go 的 sync.Pool 思路类似但更轻量。sync.Pool 没有按类型建立持久缓存结构,对象在 GC 时可能被回收,不保证存活。Slab 的缓存是持久的,Empty Slab 的回收是显式决策而非 GC 驱动。sync.Pool 更适合临时对象的复用,Slab 更适合内核级的高频固定类型分配。
五、多语言实现
5.1 Go 实现
下面实现一个简化版 Slab 分配器,包含类型专属缓存和 Slab 状态管理:
package slab
import ( "sync" "unsafe")
// SlabState 表示 Slab 的状态type SlabState int
const ( Partial SlabState = iota // 部分占用 Full // 全部占用 Empty // 全部空闲)
// Slab 是一块连续内存,划分成固定大小的槽位type Slab struct { objectSize int // 每个对象的大小 count int // 槽位总数 used int // 已用槽位数 data []byte // 连续内存块 freeList []int // 空闲槽位索引栈 freeTop int // 栈顶指针 state SlabState // 当前状态}
// newSlab 创建一个新 Slabfunc newSlab(objectSize, count int) *Slab { s := &Slab{ objectSize: objectSize, count: count, data: make([]byte, objectSize*count), freeList: make([]int, count), freeTop: count, // 栈从顶部往下压 state: Empty, } // 初始化空闲栈:所有槽位都空闲 for i := 0; i < count; i++ { s.freeList[count-1-i] = i } return s}
// Alloc 从 Slab 分配一个对象槽位func (s *Slab) Alloc() unsafe.Pointer { if s.freeTop == 0 { return nil // Slab 已满 } s.freeTop-- slot := s.freeList[s.freeTop] s.used++ s.updateState() // 返回槽位对应的内存地址 return unsafe.Pointer(&s.data[slot*s.objectSize])}
// Free 释放一个对象槽位func (s *Slab) Free(ptr unsafe.Pointer) { // 计算槽位索引 offset := uintptr(ptr) - uintptr(unsafe.Pointer(&s.data[0])) slot := int(offset) / s.objectSize s.freeList[s.freeTop] = slot s.freeTop++ s.used-- s.updateState()}
// updateState 根据使用情况更新 Slab 状态func (s *Slab) updateState() { switch { case s.used == 0: s.state = Empty case s.used == s.count: s.state = Full default: s.state = Partial }}
// Cache 是类型专属的对象缓存type Cache struct { objectSize int slabCount int // 每个 Slab 的对象数 mu sync.Mutex partials []*Slab // Partial 状态的 Slab 链表 empties []*Slab // Empty 状态的 Slab 链表}
// NewCache 创建一个类型专属缓存func NewCache(objectSize, slabCount int) *Cache { return &Cache{ objectSize: objectSize, slabCount: slabCount, }}
// Alloc 从缓存分配一个对象func (c *Cache) Alloc() unsafe.Pointer { c.mu.Lock() defer c.mu.Unlock()
// 优先从 Partial Slab 分配 for _, slab := range c.partials { if ptr := slab.Alloc(); ptr != nil { if slab.state == Full { // Slab 变满,从 partial 链表移除 c.removePartial(slab) } return ptr } }
// Partial 都满了,尝试从 Empty Slab 分配 if len(c.empties) > 0 { slab := c.empties[len(c.empties)-1] c.empties = c.empties[:len(c.empties)-1] ptr := slab.Alloc() c.partials = append(c.partials, slab) return ptr }
// 没有可用 Slab,创建新的 slab := newSlab(c.objectSize, c.slabCount) ptr := slab.Alloc() c.partials = append(c.partials, slab) return ptr}
// Free 释放对象回缓存func (c *Cache) Free(ptr unsafe.Pointer) { c.mu.Lock() defer c.mu.Unlock()
// 在 Partial 链表中找到对象所属的 Slab for i, slab := range c.partials { if c.belongsTo(slab, ptr) { wasFull := slab.state == Full slab.Free(ptr) if slab.state == Empty { // Slab 变空,移入 empty 链表 c.removePartial(slab) c.empties = append(c.empties, slab) } _ = wasFull return } }}
// belongsTo 判断指针是否属于某个 Slabfunc (c *Cache) belongsTo(slab *Slab, ptr unsafe.Pointer) bool { start := uintptr(unsafe.Pointer(&slab.data[0])) end := start + uintptr(slab.objectSize*slab.count) p := uintptr(ptr) return p >= start && p < end}
// removePartial 从 partial 链表移除 Slabfunc (c *Cache) removePartial(target *Slab) { for i, s := range c.partials { if s == target { c.partials = append(c.partials[:i], c.partials[i+1:]...) return } }}关键设计点:空闲槽位用栈管理,分配和释放都是 O(1);Slab 状态自动流转,Partial 优先分配保证缓存局部性;每个 Cache 独立加锁,不同类型的缓存互不影响。
5.2 TypeScript 实现
TypeScript 没有手动内存管理,但 Slab 的思想可以用于对象池化——预分配固定数量的对象槽位,按索引分配和回收:
/** * Slab 风格的对象池 * 每个池只管理一种固定类型的对象,槽位按索引分配 */class SlabPool<T> { private slots: (T | null)[]; private freeStack: number[]; // 空闲槽位索引栈 private state: "empty" | "partial" | "full";
constructor( private factory: () => T, private reset: (obj: T) => void, size: number ) { // 预分配所有槽位 this.slots = Array.from({ length: size }, () => factory()); // 初始化空闲栈:所有索引都空闲 this.freeStack = Array.from({ length: size }, (_, i) => size - 1 - i); this.state = "empty"; }
/** 分配一个对象,返回 { obj, index } */ alloc(): { obj: T; index: number } | null { if (this.freeStack.length === 0) { this.state = "full"; return null; // Slab 已满 } const index = this.freeStack.pop()!; const obj = this.slots[index]; this.updateState(); return { obj: obj!, index }; }
/** 释放对象,归还到槽位 */ free(index: number): void { const obj = this.slots[index]; if (obj !== null) { this.reset(obj); // 重置对象状态,类似 Slab 的析构回调 } this.freeStack.push(index); this.updateState(); }
private updateState(): void { const used = this.slots.length - this.freeStack.length; if (used === 0) this.state = "empty"; else if (used === this.slots.length) this.state = "full"; else this.state = "partial"; }
get slabState(): string { return this.state; }}
// 使用示例:游戏中的子弹对象池interface Bullet { x: number; y: number; speed: number; active: boolean;}
const bulletSlab = new SlabPool<Bullet>( () => ({ x: 0, y: 0, speed: 5, active: false }), (b) => { b.x = 0; b.y = 0; b.active = false; }, 1024 // 每个 Slab 1024 个子弹);
// 发射子弹const bullet = bulletSlab.alloc();if (bullet) { bullet.obj.x = 100; bullet.obj.y = 200; bullet.obj.active = true; // ... 子弹飞行逻辑 ... // 子弹消失后归还 bulletSlab.free(bullet.index);}六、生产验证
| 项目 | 源码位置 | 用途 |
|---|---|---|
| Linux 内核 | mm/slab.c | 内核原始 Slab 分配器,管理 task_struct、inode、dentry 等高频对象 |
| Linux 内核 | mm/slub.c | Slub 分配器,Linux 5.x 起的默认实现,元数据嵌入对象减少开销 |
| Memcached | slabber.c | 按 size class 分组管理内存,不同大小的 item 分到不同 slab class,消除碎片 |
| Nginx | ngx_slab.c | 共享内存中的 Slab 分配器,用于 limit_req、lua_shared_dict 等模块 |
Memcached 的 Slab 实现尤其值得研究。它把内存按 size class(48B、64B、80B、……、1MB)分成不同 slab class,每个 class 只存相近大小的 item。这牺牲了一些内部碎片(比如 50B 的 item 放进 64B 的槽位),但彻底消除了外部碎片,让内存使用完全可预测。
七、小结
何时使用:
- 高频分配/释放同类型对象——内核数据结构、网络连接状态、游戏实体
- 需要缓存行对齐——对象按缓存行边界对齐,减少 cache miss
- 构造/析构开销大——对象释放后保持初始化状态,下次直接复用
- 碎片敏感场景——固定大小分配天然消除外部碎片
何时不用:
- 对象大小差异大——每种大小都要建独立缓存,内存浪费严重
- 分配频率低——通用分配器足够,Slab 的管理开销不值得
- 生命周期不可预测——对象可能长期不释放,Slab 无法回收 Empty 页面
- 内存极度受限——Slab 的元数据和管理结构本身需要额外内存
八、参考资料
- The Slab Allocator: An Object-Caching Kernel Memory Allocator - Bonwick, 1994, Slab 分配器原始论文
- Linux 内核 Slab 文档 - Linux 内核 Slab 分配器官方文档
- Linux 内核 Slub 文档 - Slub 分配器设计与调试指南
- Memcached Slab 分配 - Memcached 的 Slab 机制与 LRU 策略
- Nginx Slab 分配器 - Nginx 共享内存中的 Slab 实现
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






