长文103: 内存管理-内核篇:Slab 分配器深度设计 —— 对比 Linux 实现
“Buddy 系统擅长分配大块内存,但内核频繁申请 32 字节的 task_struct 怎么办? 本文将深入 Slab 分配器的设计哲学,对比 Linux 的三种实现(SLAB/SLUB/SLOB), 并构建一个高性能、低碎片的工业级 Slab 系统。”
引言:小对象分配的困境
在操作系统内核中, 小对象 (Small Objects)的分配极其频繁:
进程创建
:
task_struct
(约 1.5KB)
文件操作
:
file
(约 200 字节)、
dentry
(约 192 字节)
网络栈
:
sk_buff
(约 256 字节)
VFS 层
:
inode
(约 600 字节)
如果直接使用 Buddy 系统分配这些对象:
内部碎片高达 75%+
:分配 4KB 页仅用 200 字节
初始化开销巨大
:每次
memset
清零整个页
缓存局部性差
:对象分散在不同页,CPU 缓存命中率低
Slab 分配器 正是为解决这些问题而生!它通过 对象缓存 + 空闲链表 ,实现:
零初始化开销
:对象复用,构造函数按需调用
零内部碎片
:精确分配对象大小
高缓存命中率
:同类对象紧凑存储
本文将系统性地剖析 Slab 分配器的设计,并 深度对比 Linux 的三种实现 (SLAB/SLUB/SLOB),最后提供一个可运行的工业级框架。
第一章:Slab 理论模型与核心思想
1.1 为什么需要 Slab?
Buddy 系统的小对象分配问题:
| 问题 | 后果 | Slab 解决方案 |
|---|---|---|
| 内部碎片 | 分配 4KB 页仅用 200 字节,浪费 95% | 精确分配对象大小 |
| 初始化开销 | 每次分配都 memset 清零 | 对象复用,按需构造 |
| 缓存局部性 | 对象分散在不同页 | 同类对象紧凑存储 |
| TLB 压力 | 频繁分配/释放导致 TLB 刷新 | 减少页分配次数 |
Slab 的核心思想:
“预分配一批同类型对象,用时即取,废时即还”
Slab
:一个或多个连续物理页,划分为固定大小的对象
Cache
:管理同类型对象的 Slab 池(如
task_cache
)
对象复用
:释放的对象不归还 Buddy,而是放入空闲链表
1.2 Slab 三级架构
Slab 系统采用三级层次结构:
关键设计决策:
- 空闲指针内嵌
:空闲对象的指针直接存放在对象内存开头,
零额外内存开销 -
构造函数延迟调用
:仅在对象首次分配时调用
ctor
,复用时不调用 -
Slab 按需创建
:无空闲对象时才向 Buddy 申请新页
1.3 Slab 操作流程
分配对象(kmem_cache_alloc):
- 优先从
partial slab
分配(有空闲对象) - 其次从
free slab
分配(全空闲) - 若无可用 slab,
新建 slab
(向 Buddy 申请页) - 从 slab 取第一个空闲对象
- 调用构造函数
(如果存在且是首次使用)
释放对象(kmem_cache_free):
- 调用析构函数
(如果存在)
- 将对象放回 slab 的空闲链表
- 调整 slab 状态(full → partial,partial → free)
- 若 slab 全空闲且系统内存紧张,
延迟释放
(避免频繁 Buddy 调用)
第二章:Linux Slab 实现演进史
Linux 内核历史上存在 三种 Slab 实现 ,反映了不同设计哲学的演进:
2.1 SLAB(经典实现,1990s-2008)
设计特点:
复杂但功能完整
:支持对象着色、硬件缓存对齐
内存开销大
:每个 slab 需额外元数据(
struct slab
)
锁粒度粗
:每个 cache 一个全局锁
核心数据结构:
优点:
对象着色
(Slab Coloring):避免不同 cache 的对象映射到同一 CPU 缓存行
硬件优化
:L1 缓存行对齐
缺点:
内存开销大
:元数据占 10-20% 内存
复杂度高
:代码超过 10,000 行
2.2 SLUB(现代默认,2008-至今)
设计哲学:
“简化、去中心化、每 CPU 优化”
关键改进:
- 移除 slab 结构
:元数据直接嵌入页描述符(
struct page
) -
每 CPU 缓存无锁
:分配/释放完全无锁
-
简化链表管理
:partial 链表全局共享
-
动态调试支持
:运行时启用红区(Redzone)、对象跟踪
核心数据结构:
优势:
内存开销极小
:元数据仅 16 字节/页
性能卓越
:99% 分配路径无锁
调试友好
:
slabinfo
、
/sys/kernel/slab/
提供详细统计
2.3 SLOB(嵌入式简化版)
设计目标:
内存极度受限
(<64MB RAM)
代码最小化
(<1000 行)
实现原理:
简单 first-fit 分配器
无 cache 概念
:所有对象共享一个堆
使用 Buddy 页作为堆
适用场景:
嵌入式 Linux(路由器、IoT 设备)
内核配置
CONFIG_SLOB=y
2.4 三种实现对比
| 特性 | SLAB | SLUB | SLOB |
|---|---|---|---|
| 代码复杂度 | 高(10k+ 行) | 中(5k 行) | 低(1k 行) |
| 内存开销 | 高(10-20%) | 低(<5%) | 极低 |
| 性能 | 中 | 高 | 低 |
| 调试支持 | 有限 | 丰富 | 无 |
| 适用场景 | 旧版内核 | 现代默认 | 嵌入式 |
💡 Linux 5.15+ 默认使用 SLUB ,因其在性能、内存、调试三者间取得最佳平衡。
第三章:工业级 Slab 设计框架
3.1 设计目标与约束
核心目标:
- 高性能
:分配/释放 O(1) 时间,多核无锁
-
低内存开销
:元数据 < 5% 总内存
-
高缓存命中率
:同类对象紧凑存储
-
调试友好
:支持红区、对象跟踪
约束条件:
32 位系统
:指针对齐 4 字节
4KB 页大小
:slab 大小为页的整数倍
无动态重定位
:对象地址固定
3.2 数据结构设计
缓存描述符(kmem_cache)
每 CPU 缓存(kmem_cache_cpu)
元数据嵌入页描述符
3.3 关键算法实现
创建缓存(kmem_cache_create)
分配对象(kmem_cache_alloc)
释放对象(kmem_cache_free)
3.4 slab 创建与销毁
分配新 slab
释放 slab
第四章:kmalloc 接口与多级缓存
4.1 kmalloc 的设计挑战
kmalloc 需要支持任意大小(8-8192 字节)的分配,但:
不能为每个大小创建 cache
(缓存爆炸)
需要快速映射大小到 cache
解决方案: 多级大小类 (Size Classes)
Linux 将分配大小分为 离散的大小类 ,每个类对应一个 cache:
| 大小范围 | Cache 名称 | 实际分配大小 |
|---|---|---|
| 8-16 | kmalloc-16 | 16 |
| 17-32 | kmalloc-32 | 32 |
| 33-64 | kmalloc-64 | 64 |
| … | … | … |
| 4097-8192 | kmalloc-8k | 8192 |
大小类计算:
4.2 kmalloc 实现框架
初始化 kmalloc caches
kmalloc/kfree 接口
4.3 地址反查 cache 机制
问题:如何从对象地址找到所属 cache?
SLAB
:通过
obj_to_index
计算 cache
SLUB
:
元数据嵌入页描述符
,直接访问
SLUB 方案(工业级):
✅ SLUB 的页描述符嵌入方案是性能关键 !
第五章:高级优化与调试特性
5.1 对象着色(Slab Coloring)
问题:不同 cache 的对象映射到同一 CPU 缓存行
后果
:缓存颠簸(Cache Thrashing)
解决方案
:
着色
(Coloring)
实现原理:
每个 slab 的
起始偏移
不同
偏移量 =
color L1_CACHE_BYTES
确保对象起始地址在不同缓存行
代码框架:
💡 SLUB 移除了显式着色,依赖硬件预取优化
5.2 红区(Redzone)与调试支持
红区设计:
在对象前后添加
保护区
(通常 8-16 字节)
填充特定模式(如
0x5a5a5a5a
)
释放时检查是否被覆盖
SLUB 调试框架:
5.3 内存泄漏检测
对象跟踪(Object Tracking):
记录每个对象的分配/释放轨迹
通过
/sys/kernel/slab/
查看
实现:
结论:Slab 设计的工程权衡
Slab 分配器的设计体现了操作系统内核开发的核心哲学: 在性能、内存、复杂度之间寻找最优平衡 。
SLAB
选择了
功能完整
,但牺牲了内存效率
SLUB
选择了
简化与性能
,成为现代默认
SLOB
选择了
极致精简
,适用于嵌入式
对于我们的实现, SLUB 风格是最佳选择 :
元数据嵌入页描述符
:内存开销最小
每 CPU 无锁缓存
:多核性能卓越
动态调试支持
:开发友好
理解 Slab 系统不仅有助于内核开发,更能培养 缓存友好编程 (Cache-Friendly Programming)的意识——这是高性能系统软件的基石。
附录:关键数据结构与函数速查
核心数据结构
| 结构 | 作用 | 文件 |
|---|---|---|
| struct kmem_cache | 缓存描述符 | slab.h |
| struct kmem_cache_cpu | 每 CPU 缓存 | slab.h |
| struct page (slab) | slab 元数据 | mm_types.h |
关键函数
| 函数 | 功能 | 文件 |
|---|---|---|
| kmem_cache_create | 创建缓存 | slab_common.c |
| kmem_cache_alloc | 分配对象 | slub.c |
| kmem_cache_free | 释放对象 | slub.c |
| kmalloc / kfree | 通用接口 | slab_common.c |
调试接口
| 接口 | 用途 |
|---|---|
| /sys/kernel/slab/ | 查看所有 cache 信息 |
| slabinfo | 命令行工具 |
| echo 1 > /sys/kernel/slab/ |
启用轨迹跟踪 |
评论