mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2111 字
6 分钟
Slab 分配器(Slab Allocator)
2026-06-13

一、为什么需要 Slab 分配器#

Linux 内核每秒要创建和销毁成千上万个 task_structinodedentry 这些固定结构体。用通用分配器 kmalloc 来分配它们,每次都要在空闲链表上搜索合适大小的块,释放后还要合并相邻块防止碎片。对象大小固定、类型已知,却要反复走通用分配器的完整流程,这是浪费。

更关键的问题是缓存。一个 task_struct 刚释放,内存块归还给通用分配器,可能立刻被分配给完全不同类型的数据。CPU 缓存行里刚加载好的数据被新数据覆盖,下次再分配 task_struct 时又要从内存重新加载——缓存局部性被彻底破坏。

还有初始化开销。很多内核对象创建时需要初始化大量字段(锁、链表头、引用计数),销毁时又要逐个清理。如果上一次释放的对象能保持初始化状态,下次直接拿来用,就能省掉构造和析构的开销。

Slab 分配器针对这些问题给出了系统性的方案:按类型建缓存,每个缓存只分配固定大小的对象,释放后不归还给通用分配器,而是留在类型专属的 slab 中等待复用

二、现实类比#

工厂里的模具生产线。你要生产 M6 螺栓,不会每次都临时开模——你有一套 M6 专用的模具,压出来的螺栓尺寸完全一致。模具始终就位,换批生产时只需倒入新原料,不需要重新校准。M6 的模具和 M10 的模具分开存放,互不干扰。

餐厅的桌位安排也类似。2 人桌、4 人桌、8 人桌各有一片区域。来了一对客人,直接领到 2 人桌区找空桌坐下,不用在大厅里到处找”差不多大小”的空位。吃完离场,桌子收拾干净留在原位,下一拨 2 人客人直接坐。

三、核心思想#

Slab 分配器为每种对象类型维护一个独立的 缓存(Cache)。每个缓存由若干 Slab 组成,每个 Slab 是一块连续内存(通常由一个或多个物理页构成),里面划分成固定大小的 对象槽位

flowchart TD subgraph Cache["kmalloc-64 缓存"] direction LR subgraph Partial["Partial Slab"] O1["obj ✅"] --- O2["obj 🈳"] --- O3["obj ✅"] --- O4["obj 🈳"] end subgraph Full["Full Slab"] O5["obj ✅"] --- O6["obj ✅"] --- O7["obj ✅"] --- O8["obj ✅"] end subgraph Empty["Empty Slab"] O9["obj 🈳"] --- O10["obj 🈳"] --- O11["obj 🈳"] --- O12["obj 🈳"] end end Partial --- Full --- Empty

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 创建一个新 Slab
func 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 判断指针是否属于某个 Slab
func (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 链表移除 Slab
func (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_structinodedentry 等高频对象
Linux 内核mm/slub.cSlub 分配器,Linux 5.x 起的默认实现,元数据嵌入对象减少开销
Memcachedslabber.c按 size class 分组管理内存,不同大小的 item 分到不同 slab class,消除碎片
Nginxngx_slab.c共享内存中的 Slab 分配器,用于 limit_reqlua_shared_dict 等模块

Memcached 的 Slab 实现尤其值得研究。它把内存按 size class(48B、64B、80B、……、1MB)分成不同 slab class,每个 class 只存相近大小的 item。这牺牲了一些内部碎片(比如 50B 的 item 放进 64B 的槽位),但彻底消除了外部碎片,让内存使用完全可预测。

七、小结#

何时使用:

  • 高频分配/释放同类型对象——内核数据结构、网络连接状态、游戏实体
  • 需要缓存行对齐——对象按缓存行边界对齐,减少 cache miss
  • 构造/析构开销大——对象释放后保持初始化状态,下次直接复用
  • 碎片敏感场景——固定大小分配天然消除外部碎片

何时不用:

  • 对象大小差异大——每种大小都要建独立缓存,内存浪费严重
  • 分配频率低——通用分配器足够,Slab 的管理开销不值得
  • 生命周期不可预测——对象可能长期不释放,Slab 无法回收 Empty 页面
  • 内存极度受限——Slab 的元数据和管理结构本身需要额外内存

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Slab 分配器(Slab Allocator)
https://blog.souloss.com/posts/programming/memory/memory-slab-allocator/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时