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)的基础。
关键指标:在典型Linux服务器上,Slab分配器管理的内存量通常占系统总内存的5%~15%,对象数量可达数百万个。

1.2 与伙伴系统的关系

伙伴系统(Buddy System)以页框(通常4KB)为最小单位管理物理内存,而内核对象往往只有几十到几百字节。直接使用伙伴系统分配小对象会导致:

  • 内部碎片严重:一个32字节的对象占用4KB页框,浪费99%以上。
  • 分配/释放开销高:每次操作都需要进行页框合并与分裂,时间复杂度高。
  • 缺乏对象复用:无法利用对象构造/析构的局部性。

Slab分配器在伙伴系统之上构建了对象缓存层,其关系如下表所示:

层次 管理单位 分配粒度 主要职责
伙伴系统 页框(4KB~4MB) 2^N 页 物理页框管理、大块连续内存分配
Slab分配器 对象(字节级) 8B~8KB 小对象缓存、对象复用、着色、每CPU缓存
通用kmalloc 对象(字节级) 2^N 字节 通用小内存分配接口,基于Slab实现
设计哲学:Slab分配器将伙伴系统视为“批发商”,一次性申请多个页框,然后以“零售”方式按对象粒度分发给内核子系统。

1.3 基本设计思想

1.3.1 对象缓存(Object Caching)

Slab分配器为每种内核对象类型维护一个独立的缓存(kmem_cache),每个缓存包含多个Slab(一个或多个连续页框),每个Slab被划分为等大小的对象槽位。核心流程:

  1. 缓存创建:调用kmem_cache_create(),指定对象大小、对齐方式、构造函数/析构函数。
  2. 对象分配:从缓存中取出一个空闲对象,优先从部分满的Slab中分配。
  3. 对象释放:将对象归还到所属Slab的空闲链表,不立即归还给伙伴系统。
  4. 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(三个链表)

分配流程:

  1. 优先从当前CPU的array_cache中获取对象(无锁)。
  2. 如果本地缓存为空,从共享缓存或Slab链表中批量搬运一批对象到本地缓存。
  3. 如果所有Slab都满,向伙伴系统申请新页框创建新Slab。
性能收益:每CPU缓存将分配/释放操作的平均锁持有时间降低到接近零,在128核系统上吞吐量可提升10倍以上。

1.4 整体架构图

下图展示了Slab分配器的整体架构及其与伙伴系统、内核子系统的关系:

Slab分配器整体架构图 Slab分配器整体架构图 内核子系统(文件系统、网络、进程管理、驱动) 通用接口:kmalloc / kfree / kmem_cache_alloc / kmem_cache_free Slab分配器核心 每CPU缓存 (array_cache) 无锁分配/释放 共享缓存 (NUMA节点) 跨CPU对象平衡 Slab链表管理 满/部分满/空链表 着色(Coloring):每个Slab起始偏移不同颜色值,避免CPU缓存行冲突 颜色步长 = L1缓存行大小(64字节),颜色数 = 可用的偏移量数量 Slab实例(每个缓存包含多个Slab) Slab 0(颜色0) Slab 1(颜色1) Slab 2(颜色2) Slab 3(颜色3) ... 伙伴系统(Buddy System)—— 物理页框管理

1.5 三种Slab分配器变体

Linux内核在不同发展阶段实现了三种Slab分配器变体,当前主流是SLUB:

变体 引入版本 核心特点 适用场景
SLAB Linux 2.2 完整着色、每CPU缓存、调试功能丰富 小内存系统、调试需求强的场景
SLUB Linux 2.6.22 简化数据结构、减少元数据开销、默认分配器 大多数通用场景(默认)
SLOB Linux 2.6.16 极简设计、适合内存极度受限 嵌入式系统、RAM < 64MB
当前默认:自Linux 2.6.22起,SLUB成为默认Slab分配器。SLUB保留了对象缓存和每CPU缓存的核心思想,但通过简化Slab元数据结构(将空闲链表指针嵌入对象本身),显著降低了内存开销。

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内存调试器原理和性能调优的基础。