1. Slab分配器概述
Slab分配器是Linux内核中位于伙伴系统之上的核心内存缓存层,专门用于管理内核中频繁分配和释放的小对象(如task_struct、inode、dentry等)。它通过对象缓存、着色和每CPU缓存三大设计思想,解决了伙伴系统在小对象场景下的碎片化和性能瓶颈问题。
1.1 在Linux内核中的地位
Slab分配器处于内核内存管理子系统的中间层,向上服务于内核通用对象分配(kmalloc/kfree)和专用缓存(kmem_cache_alloc/kmem_cache_free),向下依赖伙伴系统提供连续物理页框。其核心地位体现在:
- 性能关键路径:文件系统、网络协议栈、进程管理等子系统频繁调用Slab分配器,其效率直接影响系统吞吐量。
- 内存碎片控制:通过对象复用和着色机制,显著降低外部碎片和内部碎片。
- 调试基础设施:提供红区(Red Zone)、毒药模式(Poisoning)、引用计数追踪等调试能力,是内核内存调试器(如KASAN、kmemleak)的基础。
1.2 与伙伴系统的关系
伙伴系统(Buddy System)以页框(通常4KB)为最小单位管理物理内存,而内核对象往往只有几十到几百字节。直接使用伙伴系统分配小对象会导致:
- 内部碎片严重:一个32字节的对象占用4KB页框,浪费99%以上。
- 分配/释放开销高:每次操作都需要进行页框合并与分裂,时间复杂度高。
- 缺乏对象复用:无法利用对象构造/析构的局部性。
Slab分配器在伙伴系统之上构建了对象缓存层,其关系如下表所示:
| 层次 | 管理单位 | 分配粒度 | 主要职责 |
|---|---|---|---|
| 伙伴系统 | 页框(4KB~4MB) | 2^N 页 | 物理页框管理、大块连续内存分配 |
| Slab分配器 | 对象(字节级) | 8B~8KB | 小对象缓存、对象复用、着色、每CPU缓存 |
| 通用kmalloc | 对象(字节级) | 2^N 字节 | 通用小内存分配接口,基于Slab实现 |
1.3 基本设计思想
1.3.1 对象缓存(Object Caching)
Slab分配器为每种内核对象类型维护一个独立的缓存(kmem_cache),每个缓存包含多个Slab(一个或多个连续页框),每个Slab被划分为等大小的对象槽位。核心流程:
- 缓存创建:调用
kmem_cache_create(),指定对象大小、对齐方式、构造函数/析构函数。 - 对象分配:从缓存中取出一个空闲对象,优先从部分满的Slab中分配。
- 对象释放:将对象归还到所属Slab的空闲链表,不立即归还给伙伴系统。
- Slab回收:当Slab中所有对象都空闲时,可将其整页释放回伙伴系统。
1.3.2 着色(Coloring)
着色技术用于解决CPU缓存行冲突问题。当多个Slab中的对象在物理内存中具有相同的偏移量时,它们会映射到相同的CPU缓存行,导致频繁的缓存行失效。着色通过在Slab起始位置添加不同的偏移量(颜色),使对象在物理内存中的分布错开:
- 颜色偏移量:每个Slab在分配时选择一个颜色值(0~color_count-1),乘以颜色步长(通常为L1缓存行大小,64字节),作为Slab起始地址的偏移。
- 效果:不同Slab中的相同索引对象映射到不同的CPU缓存行,减少缓存行颠簸。
- 代价:每个Slab浪费少量内存(最多color_count × 颜色步长字节),通常可忽略。
1.3.3 每CPU缓存(Per-CPU Cache)
为了消除多核系统中的锁竞争,Slab分配器为每个CPU维护一个本地对象缓存(array_cache):
| 组件 | 作用 | 数据结构 |
|---|---|---|
| 每CPU缓存 | 无锁分配/释放热对象 | struct array_cache(数组+批量传输阈值) |
| 共享缓存 | 跨CPU对象平衡 | struct array_cache(可选,NUMA节点级) |
| Slab链表 | 管理部分满/满/空Slab | struct list_head(三个链表) |
分配流程:
- 优先从当前CPU的
array_cache中获取对象(无锁)。 - 如果本地缓存为空,从共享缓存或Slab链表中批量搬运一批对象到本地缓存。
- 如果所有Slab都满,向伙伴系统申请新页框创建新Slab。
1.4 整体架构图
下图展示了Slab分配器的整体架构及其与伙伴系统、内核子系统的关系:
1.5 三种Slab分配器变体
Linux内核在不同发展阶段实现了三种Slab分配器变体,当前主流是SLUB:
| 变体 | 引入版本 | 核心特点 | 适用场景 |
|---|---|---|---|
| SLAB | Linux 2.2 | 完整着色、每CPU缓存、调试功能丰富 | 小内存系统、调试需求强的场景 |
| SLUB | Linux 2.6.22 | 简化数据结构、减少元数据开销、默认分配器 | 大多数通用场景(默认) |
| SLOB | Linux 2.6.16 | 极简设计、适合内存极度受限 | 嵌入式系统、RAM < 64MB |
1.6 关键数据结构关系
Slab分配器的核心数据结构关系如下:
struct kmem_cache { // 每个对象类型一个缓存
const char *name; // 缓存名称(如 "task_struct")
unsigned int object_size; // 对象大小(含对齐)
unsigned int offset; // 空闲指针在对象内的偏移
struct kmem_cache_node *node[]; // 每个NUMA节点一个管理结构
struct array_cache __percpu *cpu_cache; // 每CPU缓存
unsigned int colour_off; // 颜色步长(字节)
unsigned int colour; // 颜色数量
};
struct kmem_cache_node { // 每个NUMA节点的Slab管理
struct list_head slabs_partial; // 部分满Slab链表
struct list_head slabs_full; // 满Slab链表
struct list_head slabs_free; // 空Slab链表
unsigned long nr_partial; // 部分满Slab数量
};
struct array_cache { // 每CPU缓存
unsigned int avail; // 可用对象数量
unsigned int limit; // 最大容量
unsigned int batchcount; // 批量传输数量
void *entry[]; // 对象指针数组
};
batchcount和limit是每CPU缓存的关键调优参数。增大batchcount可减少从Slab链表搬运对象的次数,但会增加每CPU缓存的内存占用。在NUMA系统中,kmem_cache_node的nr_partial控制部分满Slab的回收策略,直接影响内存占用和分配延迟。
1.7 本章小结
Slab分配器通过三层设计——对象缓存(消除内部碎片)、着色(优化CPU缓存利用率)、每CPU缓存(消除锁竞争)——在内核内存管理体系中扮演了承上启下的关键角色。理解这些基本设计思想,是后续深入学习Slab内存调试器原理和性能调优的基础。