从零写 OS 内核-第十五篇:Slab 分配器 —— 高效管理内核小对象
"Buddy 系统擅长分配大块内存,但内核频繁申请 32 字节、128 字节的小对象怎么办?
今天,我们实现 Slab 分配器,让内核内存分配快如闪电!"
在上一篇中,我们构建了完整的内存管理体系:
- Buddy 系统管理物理页(4KB 粒度)
- 用户态 malloc 通过
brk/mmap申请内存
但内核自身也有大量小对象需要频繁分配/释放:
✅ 进程控制块(PCB)
✅ 文件描述符(file)
✅ VFS 目录项(dentry)
✅ 网络缓冲区(skb)
如果每次用 kmalloc 都向 Buddy 申请 4KB 再切一小块,内部碎片会高达 99%!
今天,我们就来实现 Slab 分配器——专为内核小对象设计的高效内存池,
让你的内核分配 PCB 时零碎片、零初始化开销!
🧱 一、为什么需要 Slab?Buddy 的痛点
Buddy 系统的局限:
| 场景 | 问题 | |——|——| | 分配 32 字节 | Buddy 返回 4KB 页,浪费 99.2% | | 频繁分配/释放 | 每次都要清零内存(安全要求) | | 缓存局部性差 | 对象分散在不同页,CPU 缓存命中率低 |
Slab 的核心思想:
"预分配一批同类型对象,用时即取,废时即还"
- Slab:一个或多个连续物理页,划分为固定大小的对象
- Cache:管理同类型对象的 Slab 池(如
task_cache、file_cache) - 对象复用:释放的对象不归还 Buddy,而是放入空闲链表,下次直接复用
💡 Slab = 对象缓存 + 空闲链表 + 构造/析构回调
📦 二、Slab 核心数据结构
1. kmem_cache(缓存描述符)
struct kmem_cache {
char name[32]; // 缓存名(如 "task_struct")
size_t obj_size; // 对象大小
size_t align; // 对齐要求
size_t offset; // 空闲指针偏移(嵌入对象内部)
// 构造/析构回调
void (*ctor)(void *obj);
void (*dtor)(void *obj);
struct list_head slabs_full; // 完全分配的 slab
struct list_head slabs_partial; // 部分分配的 slab
struct list_head slabs_free; // 完全空闲的 slab
};
2. slab(内存页描述符)
struct slab {
struct kmem_cache *cache; // 所属缓存
void *pages; // 物理页起始地址
unsigned int num_objs; // 总对象数
unsigned int free_objs; // 空闲对象数
void *first_free; // 第一个空闲对象(嵌入式链表)
struct list_head list; // 挂在 cache 的 full/partial/free 链表
};
🔑 关键技巧:空闲对象的指针直接存放在对象内存的开头(节省额外内存)!
⚙️ 三、Slab 操作流程
1. 创建缓存(kmem_cache_create)
struct kmem_cache *kmem_cache_create(
const char *name,
size_t size,
size_t align,
void (*ctor)(void*),
void (*dtor)(void*)
) {
struct kmem_cache *cache = kmalloc(sizeof(struct kmem_cache));
strcpy(cache->name, name);
cache->obj_size = size;
cache->align = align ? align : sizeof(void*);
cache->offset = 0; // 空闲指针放在对象开头
cache->ctor = ctor;
cache->dtor = dtor;
INIT_LIST_HEAD(&cache->slabs_full);
INIT_LIST_HEAD(&cache->slabs_partial);
INIT_LIST_HEAD(&cache->slabs_free);
return cache;
}
2. 分配对象(kmem_cache_alloc)
void *kmem_cache_alloc(struct kmem_cache *cache) {
struct slab *slab;
void *obj;
// 1. 优先从 partial slab 分配
if (!list_empty(&cache->slabs_partial)) {
slab = list_first_entry(&cache->slabs_partial, struct slab, list);
}
// 2. 其次从 free slab 分配
else if (!list_empty(&cache->slabs_free)) {
slab = list_first_entry(&cache->slabs_free, struct slab, list);
}
// 3. 没有可用 slab,新建一个
else {
slab = allocate_new_slab(cache);
if (!slab) return NULL;
list_add(&slab->list, &cache->slabs_partial);
}
// 4. 从 slab 取第一个空闲对象
obj = slab->first_free;
slab->first_free = *(void**)obj; // 空闲链表指针存在对象开头
slab->free_objs--;
// 5. 如果 slab 变满,移到 full 链表
if (slab->free_objs == 0) {
list_move(&slab->list, &cache->slabs_full);
}
// 6. 调用构造函数(如果存在)
if (cache->ctor) {
cache->ctor(obj);
}
return obj;
}
3. 释放对象(kmem_cache_free)
void kmem_cache_free(struct kmem_cache *cache, void *obj) {
struct slab *slab = get_slab_from_obj(obj, cache);
// 1. 调用析构函数
if (cache->dtor) {
cache->dtor(obj);
}
// 2. 将对象放回空闲链表
*(void**)obj = slab->first_free;
slab->first_free = obj;
slab->free_objs++;
// 3. 调整 slab 所在链表
if (slab->free_objs == 1) {
// 从 full 移到 partial
list_move(&slab->list, &cache->slabs_partial);
} else if (slab->free_objs == slab->num_objs) {
// 从 partial 移到 free
list_move(&slab->list, &cache->slabs_free);
}
}
🏗️ 四、初始化内核关键缓存
在内核启动早期,创建常用对象的缓存:
void slab_init() {
// 进程控制块缓存
task_cache = kmem_cache_create(
"task_struct",
sizeof(task_t),
0,
task_ctor,
NULL
);
// 文件对象缓存
file_cache = kmem_cache_create(
"file",
sizeof(struct file),
0,
file_ctor,
NULL
);
// inode 缓存
inode_cache = kmem_cache_create(
"inode",
sizeof(struct inode),
0,
inode_ctor,
NULL
);
}
构造函数示例(初始化 PCB):
void task_ctor(void *obj) {
task_t *task = (task_t*)obj;
memset(task, 0, sizeof(task_t));
task->state = TASK_RUNNING;
// 初始化内核栈、链表头等
}
✅ 从此,创建新进程只需
kmem_cache_alloc(task_cache),无需手动初始化!
🔄 五、与 Buddy 系统的协作
Slab 分配器底层仍依赖 Buddy 系统获取物理页:
struct slab *allocate_new_slab(struct kmem_cache *cache) {
// 1. 计算需要多少页
size_t objs_per_slab = (PAGE_SIZE * pages_per_slab) / cache->obj_size;
size_t total_size = objs_per_slab * cache->obj_size;
int order = get_order(total_size);
// 2. 从 Buddy 申请连续页
void *pages = buddy_alloc(order);
if (!pages) return NULL;
// 3. 初始化 slab 结构
struct slab *slab = kmalloc(sizeof(struct slab));
slab->cache = cache;
slab->pages = pages;
slab->num_objs = objs_per_slab;
slab->free_objs = objs_per_slab;
// 4. 构建空闲链表(对象内部指针)
void *obj = pages;
for (int i = 0; i < objs_per_slab - 1; i++) {
*(void**)obj = (char*)obj + cache->obj_size;
obj = *(void**)obj;
}
*(void**)obj = NULL; // 最后一个
slab->first_free = pages;
return slab;
}
🌟 Slab 是 Buddy 的上层优化,两者协同工作!
🧪 六、测试:内核对象分配性能
传统方式(无 Slab):
// 每次分配都要初始化
task_t *task = (task_t*)kmalloc(sizeof(task_t));
memset(task, 0, sizeof(task_t));
task->state = TASK_RUNNING;
// ... 其他初始化
Slab 方式:
task_t *task = kmem_cache_alloc(task_cache);
// 对象已初始化,直接使用!
性能对比(假设分配 1000 个 PCB):
| 指标 | 传统方式 | Slab 方式 | |——|———-|———–| | 分配次数 | 1000 次 Buddy 调用 | ~4 次 Buddy 调用(每个 slab 256 个对象)| | 初始化开销 | 1000 次 memset | 256 次(只在 slab 创建时)| | 内存碎片 | 高(对象分散) | 低(对象紧凑)| | CPU 缓存命中 | 低 | 高(局部性好)|
✅ Slab 在频繁分配小对象时,性能提升 10 倍以上!
⚠️ 七、Slab 的进阶优化
- 着色(Slab Coloring)
- 调整对象起始偏移,避免不同缓存的对象映射到同一 CPU 缓存行
- 提升多核性能
- 硬件缓存对齐
- 确保对象大小是 L1 缓存行(64 字节)的倍数
- NUMA 感知
- 在多 CPU 插槽系统中,优先分配本地内存
- 对象冻结
- 当 slab 完全空闲时,延迟释放物理页(避免频繁 Buddy 调用)
💡 Linux 的 SLAB/SLUB/SLOB 分配器正是基于这些思想演进而来!
💬 写在最后
Slab 分配器看似复杂,
但它解决了内核内存管理的根本矛盾:
频繁分配小对象 vs 内存效率与性能。
今天你实现的 kmem_cache_alloc,
正是 Linux 中 kmalloc 背后的核心机制之一。
🌟 高效的内存管理,是高性能操作系统的隐形翅膀。
📬 动手挑战:
为你的 ext2 inode 实现 Slab 缓存,并测量创建 100 个文件的性能提升。
欢迎在评论区分享你的性能对比数据!
👇 下一篇你想看:写时复制(CoW)实现,还是 内存压缩(zswap)?
#操作系统 #内核开发 #内存管理 #Slab分配器 #kmalloc #内核对象 #从零开始
📢 彩蛋:关注后回复关键词 "slab",获取:
- 完整 Slab 分配器代码(含 cache/slab 结构)
- kmem_cache_create/alloc/free 实现
- 内核关键对象(task/file/inode)缓存初始化模板