2. slab分配器起源:SunOS的slab设计思想,Linux引入slab的原因
2.1 内存碎片化与对象缓存问题
在Linux内核早期版本(2.0及之前)中,伙伴系统(Buddy System)是唯一的物理内存分配器。伙伴系统以2的幂次阶(order)为单位分配连续物理页,虽然能有效避免外部碎片,但在面对内核中大量频繁创建和销毁的小对象(如task_struct、inode、dentry)时,暴露出两个严重问题:
| 问题类型 | 描述 | 影响 |
|---|---|---|
| 内部碎片 | 对象大小远小于页框大小,页框内剩余空间无法被其他类型对象使用 | 内存利用率下降30%~60% |
| 外部碎片 | 长期分配释放后,空闲页框分散,无法满足高阶连续内存请求 | 系统运行时间越长,分配大块内存越困难 |
| 对象初始化开销 | 每次分配对象都要重新初始化,释放时又要销毁,重复执行构造函数/析构函数 | CPU缓存污染,分配延迟增加 |
| CPU缓存利用率低 | 对象在物理内存中分布随机,缺乏空间局部性 | 缓存命中率下降,性能抖动 |
2.2 SunOS的slab设计思想
1994年,Jeff Bonwick在SunOS 5.4中首次提出了slab分配器,其核心设计思想可以概括为“对象缓存”与“着色”两大创新。slab分配器并非从零开始管理物理内存,而是建立在伙伴系统之上的一层“对象缓存层”。
2.2.1 对象缓存(Object Cache)
slab分配器为每种内核对象类型维护一个独立的缓存(cache)。每个缓存由多个slab组成,每个slab对应一个或多个连续物理页框,页框内部被划分为多个相同大小的对象槽位。
- 缓存(cache):管理同类型对象的全局结构,包含slab链表、构造函数/析构函数指针、对象大小、对齐要求等。
- slab:一个或多个连续物理页,被划分为固定大小的对象槽。slab有三种状态:
full(全满)、partial(部分空闲)、empty(全空)。 - 对象(object):slab内划分出的最小分配单元,大小与内核数据结构一致。
2.2.2 着色(Coloring)
着色是slab分配器最精妙的设计之一。其目的是让不同slab中对象的起始地址在CPU缓存行(cache line)中错开,避免多个热点对象竞争同一组缓存行,从而减少缓存颠簸(cache thrashing)。
- 缓存行对齐:每个对象起始地址相对于slab基地址的偏移量称为“颜色”。
- 颜色偏移:不同slab使用不同的颜色偏移值,使对象在L1/L2缓存中的映射位置分散。
- 颜色范围:颜色偏移值在
[0, slab_size - object_size]范围内,步长通常为缓存行大小(64字节)。
2.3 Linux引入slab的原因
Linux内核在2.2版本(1999年)正式引入slab分配器,由SunOS的slab设计启发,但针对Linux的体系结构和性能需求做了大量改造。引入slab的核心驱动力包括:
| 驱动因素 | 具体问题 | slab解决方案 |
|---|---|---|
| 对象复用 | 频繁分配/释放导致大量时间消耗在构造和析构上 | 对象创建后不销毁,保留在slab中复用,仅调用构造函数一次 |
| 内部碎片 | 伙伴系统最小粒度4KB,小对象浪费严重 | slab内对象紧密排列,无内部碎片(对象大小对齐到缓存行) |
| CPU缓存友好 | 对象随机分布导致缓存命中率低 | 着色机制使对象在缓存中均匀分布,减少冲突未命中 |
| NUMA支持 | 多处理器系统中内存访问延迟不对称 | 每个NUMA节点维护独立的slab缓存,优先分配本地内存 |
| 调试能力 | 内核内存错误(越界、释放后使用)难以定位 | slab支持红区(Red Zone)、毒药值(Poison)等调试功能 |
2.4 Linux slab分配器的核心数据结构
Linux slab分配器的实现围绕三个核心数据结构展开:kmem_cache、slab和kmem_bufctl_t。以下代码展示了slab分配器的核心结构定义(简化版):
/* 缓存描述符:管理同类型对象的所有slab */
struct kmem_cache {
const char *name; /* 缓存名称,如"task_struct" */
size_t object_size; /* 对象实际大小 */
size_t size; /* 对齐后对象大小(含填充) */
size_t align; /* 对齐要求 */
unsigned int gfporder; /* 每个slab占用的页框阶数 */
unsigned int num; /* 每个slab包含的对象数量 */
unsigned int colour; /* 颜色偏移最大值 */
unsigned int colour_off; /* 颜色偏移步长(通常为缓存行大小) */
struct list_head slabs_full; /* 全满slab链表 */
struct list_head slabs_partial; /* 部分空闲slab链表 */
struct list_head slabs_free; /* 全空slab链表 */
void (*ctor)(void *obj); /* 构造函数 */
void (*dtor)(void *obj); /* 析构函数 */
};
/* slab描述符:管理一个连续页框组内的对象 */
struct slab {
struct list_head list; /* 链接到cache的slab链表 */
unsigned long colouroff; /* 当前slab的颜色偏移 */
void *s_mem; /* 第一个对象的起始地址 */
unsigned int inuse; /* 已分配的对象数量 */
unsigned int free; /* 空闲对象数量 */
kmem_bufctl_t *free; /* 空闲对象索引链表 */
};
2.5 slab分配流程与性能优势
slab分配器的分配和释放流程经过精心设计,以最小化路径长度和锁竞争:
- 分配(kmem_cache_alloc):
- 从
slabs_partial链表头部取一个slab,若为空则从slabs_free取。 - 从slab的空闲对象链表中取出一个空闲对象索引。
- 更新
inuse计数,若slab变满则移至slabs_full链表。 - 返回对象地址(slab基地址 + 颜色偏移 + 对象索引 × 对象大小)。
- 从
- 释放(kmem_cache_free):
- 根据对象地址反查所属slab和cache(通过
virt_to_page和page->lru)。 - 将对象索引放回空闲链表,更新
inuse计数。 - 若slab变空则移至
slabs_free链表;若slab变partial则移至slabs_partial链表。
- 根据对象地址反查所属slab和cache(通过
2.6 slab分配器的演进:从SunOS到Linux
Linux的slab实现并非完全照搬SunOS,而是根据Linux内核的特点做了重要调整:
| 特性 | SunOS slab | Linux slab |
|---|---|---|
| slab描述符位置 | 存储在slab页框外部(独立分配) | 存储在slab页框内部(节省内存) |
| 着色粒度 | 以缓存行为单位 | 以缓存行为单位,但支持动态调整 |
| NUMA感知 | 无 | 每个NUMA节点独立缓存 |
| 调试支持 | 有限 | 红区、毒药值、对象跟踪、统计计数 |
| 增长策略 | 按需分配slab | 支持批量预分配和回收 |
2.7 本章小结
slab分配器的引入是Linux内核内存管理的一次重要进化。它通过对象缓存、着色、slab状态管理等机制,从根本上解决了伙伴系统在小对象分配场景下的碎片化和性能问题。SunOS的设计思想为Linux提供了理论基石,而Linux根据自身架构特点(NUMA、调试需求、内存布局)进行了大量工程优化,使得slab分配器成为内核中最高效、最稳定的内存分配组件之一。