2. slab分配器起源:SunOS的slab设计思想,Linux引入slab的原因

2.1 内存碎片化与对象缓存问题

在Linux内核早期版本(2.0及之前)中,伙伴系统(Buddy System)是唯一的物理内存分配器。伙伴系统以2的幂次阶(order)为单位分配连续物理页,虽然能有效避免外部碎片,但在面对内核中大量频繁创建和销毁的小对象(如task_struct、inode、dentry)时,暴露出两个严重问题:

核心矛盾:伙伴系统最小分配单位为4KB(单页),而内核对象通常只有几十到几百字节。每次分配一整个页框,内部利用率极低,且频繁的分配/释放导致页框内部出现大量无法被其他对象使用的“内部碎片”。
问题类型 描述 影响
内部碎片 对象大小远小于页框大小,页框内剩余空间无法被其他类型对象使用 内存利用率下降30%~60%
外部碎片 长期分配释放后,空闲页框分散,无法满足高阶连续内存请求 系统运行时间越长,分配大块内存越困难
对象初始化开销 每次分配对象都要重新初始化,释放时又要销毁,重复执行构造函数/析构函数 CPU缓存污染,分配延迟增加
CPU缓存利用率低 对象在物理内存中分布随机,缺乏空间局部性 缓存命中率下降,性能抖动

2.2 SunOS的slab设计思想

1994年,Jeff Bonwick在SunOS 5.4中首次提出了slab分配器,其核心设计思想可以概括为“对象缓存”与“着色”两大创新。slab分配器并非从零开始管理物理内存,而是建立在伙伴系统之上的一层“对象缓存层”。

设计哲学:“内核中最频繁的操作不是分配内存,而是分配和释放特定类型的对象。”—— Jeff Bonwick

2.2.1 对象缓存(Object Cache)

slab分配器为每种内核对象类型维护一个独立的缓存(cache)。每个缓存由多个slab组成,每个slab对应一个或多个连续物理页框,页框内部被划分为多个相同大小的对象槽位。

  • 缓存(cache):管理同类型对象的全局结构,包含slab链表、构造函数/析构函数指针、对象大小、对齐要求等。
  • slab:一个或多个连续物理页,被划分为固定大小的对象槽。slab有三种状态:full(全满)、partial(部分空闲)、empty(全空)。
  • 对象(object):slab内划分出的最小分配单元,大小与内核数据结构一致。
slab分配器三层架构(SunOS设计) Cache(对象缓存) 管理同类型对象:task_struct_cache / inode_cache / dentry_cache ... slab(全满) 所有对象已分配 状态:full slab(部分空闲) 部分对象已分配 状态:partial slab(全空) 所有对象空闲 状态:empty 对象槽位(Object Slots) 每个slab内划分为固定大小的对象槽,槽位大小 = 对象类型大小 分配时从partial或empty slab取一个空闲槽,释放时归还到slab 构造函数/析构函数仅在对象首次创建或slab销毁时调用

2.2.2 着色(Coloring)

着色是slab分配器最精妙的设计之一。其目的是让不同slab中对象的起始地址在CPU缓存行(cache line)中错开,避免多个热点对象竞争同一组缓存行,从而减少缓存颠簸(cache thrashing)。

  • 缓存行对齐:每个对象起始地址相对于slab基地址的偏移量称为“颜色”。
  • 颜色偏移:不同slab使用不同的颜色偏移值,使对象在L1/L2缓存中的映射位置分散。
  • 颜色范围:颜色偏移值在[0, slab_size - object_size]范围内,步长通常为缓存行大小(64字节)。
着色效果:假设两个slab分别使用颜色偏移0和64字节,则slab A的第一个对象映射到缓存组0,slab B的第一个对象映射到缓存组1。即使两个对象被频繁交替访问,也不会互相驱逐对方的缓存行。

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分配器的分配和释放流程经过精心设计,以最小化路径长度和锁竞争:

  1. 分配(kmem_cache_alloc):
    • 从slabs_partial链表头部取一个slab,若为空则从slabs_free取。
    • 从slab的空闲对象链表中取出一个空闲对象索引。
    • 更新inuse计数,若slab变满则移至slabs_full链表。
    • 返回对象地址(slab基地址 + 颜色偏移 + 对象索引 × 对象大小)。
  2. 释放(kmem_cache_free):
    • 根据对象地址反查所属slab和cache(通过virt_to_page和page->lru)。
    • 将对象索引放回空闲链表,更新inuse计数。
    • 若slab变空则移至slabs_free链表;若slab变partial则移至slabs_partial链表。
性能数据:在Linux 2.4内核中,slab分配器的对象分配/释放操作平均耗时约200~400纳秒(基于x86 1GHz处理器),而同等条件下伙伴系统分配单页需要约1~2微秒。slab分配器将小对象分配速度提升了5~10倍。

2.6 slab分配器的演进:从SunOS到Linux

Linux的slab实现并非完全照搬SunOS,而是根据Linux内核的特点做了重要调整:

特性 SunOS slab Linux slab
slab描述符位置 存储在slab页框外部(独立分配) 存储在slab页框内部(节省内存)
着色粒度 以缓存行为单位 以缓存行为单位,但支持动态调整
NUMA感知 无 每个NUMA节点独立缓存
调试支持 有限 红区、毒药值、对象跟踪、统计计数
增长策略 按需分配slab 支持批量预分配和回收
Linux slab分配器:从伙伴系统到对象缓存 伙伴系统(Buddy System) 以2的幂次阶分配物理页 最小粒度:4KB(单页) 负责底层物理页管理 alloc_pages slab分配器 对象缓存层 着色机制 对象复用 NUMA感知 kmem_cache_alloc task_struct_cache 对象大小:~2KB | 每个slab 8个对象 inode_cache 对象大小:~600B | 每个slab 28个对象 dentry_cache 对象大小:~200B | 每个slab 80个对象 关键设计原则 ① 每种内核对象类型拥有独立缓存,互不干扰 ② 对象创建后常驻内存,释放后仅标记为空闲,避免重复构造/析构 ③ 着色机制使不同slab的对象在CPU缓存中均匀分布,减少冲突未命中

2.7 本章小结

slab分配器的引入是Linux内核内存管理的一次重要进化。它通过对象缓存、着色、slab状态管理等机制,从根本上解决了伙伴系统在小对象分配场景下的碎片化和性能问题。SunOS的设计思想为Linux提供了理论基石,而Linux根据自身架构特点(NUMA、调试需求、内存布局)进行了大量工程优化,使得slab分配器成为内核中最高效、最稳定的内存分配组件之一。

注意:slab分配器并非万能。对于大对象(超过一个页框)或分配频率极低的对象,slab的维护开销可能超过收益。Linux后续引入了slub分配器(简化版slab)和slob分配器(嵌入式专用),分别针对不同场景优化。这些将在后续章节详细讨论。