引言
Linux 内核的内存管理是一个精密的分层系统。在用户态,我们通过 malloc/free 分配堆内存,底层由 ptmalloc、jemalloc 或 tcmalloc 等分配器管理。但在内核态,情况完全不同——内核不能依赖用户态的分配器,必须自行管理物理内存。Slab 分配器正是为此而生。
从 Linux 2.1.23 引入 Slab 分配器解决内核对象频繁创建/销毁的性能问题,到如今 Slub 作为默认分配器统治内核内存分配,这一子系统经历了三次架构演进。本文将深入剖析 Slab/Slob/Slub 三大分配器的设计哲学、核心数据结构、性能特征以及在真实内核模块开发中的应用实战。
一、为什么内核需要专用分配器
1.1 页分配器的局限
Linux 内存管理的基础是Buddy System(伙伴系统),它以物理页(通常 4KB)为最小单位进行分配。然而,内核中绝大多数内存需求远小于一页:
- task_struct(进程描述符)约 8KB
- inode(索引节点)约 700 字节
- dentry(目录项缓存)约 200 字节
- file(文件对象)约 300 字节
如果直接用 Buddy 分配这些对象,内部碎片将浪费大量内存。更致命的是,内核对象的创建/销毁频率极高(每次 open() 分配 file 对象,每次 fork() 复制 task_struct),频繁的页分配/释放会导致:
- TLB 抖动:频繁的页表操作冲刷 TLB 缓存
- Cache 失效:新分配的页面不携带任何热缓存数据
- 锁竞争:mm_struct->mmap_lock 成为多线程瓶颈
1.2 对象复用的红利
内核对象有一个关键特征:类型相同、生命周期规律、可复用。一个 file 对象被释放后,其内存布局与下一个需要的 file 对象完全一致。Slab 分配器正是利用这一特征,实现了:
- 避免内部碎片:按对象实际大小精准分配,无页内浪费
- 避免初始化开销:通过构造函数/析构函数延迟初始化,对象复用时不重复清零
- 硬件 Cache 友好:相同类型的对象集中排列,提升缓存命中率
二、Slab 分配器:经典架构
2.1 核心数据结构
Slab 分配器的三层架构如下:
┌──────────────────────────────────────────────────────┐
│ slab_cache (Cache) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ name = "dentry_cache" │ │
│ │ object_size = 200 │ │
│ │ align = 8 │ │
│ │ lists.slabs_full ──→ [slab1] → [slab2] │ │
│ │ lists.slabs_partial ──→ [slab3] → [slab4] │ │
│ │ lists.slabs_free ──→ [slab5] │ │
│ │ constructor = dentry_ctor │ │
│ │ destructor = dentry_dtor │ │
│ └─────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
一个 slab(slab_t):
┌──────────────────────────────────────────────────────┐
│ mem (首地址,指向页内存起始) │
│ inuse (已使用对象数量) │
│ free (第一个空闲对象索引) │
│ s_mem 数组: [obj0|obj1|obj2|...|objN] │
│ freelist: obj5 → obj2 → obj7 → NULL │
└──────────────────────────────────────────────────────┘
2.2 Slab 描述符
每个 slab 由一个 slab 描述符管理,包含以下关键字段:
struct slab {
struct list_head; /* 链接到 cache 的三个链表之一 */
unsigned long s_mem; /* 第一个对象的虚拟地址 */
unsigned int inuse; /* 已分配对象计数 */
unsigned int free; /* 下一个空闲对象的索引 */
};
为了提升性能,现代 Slab 实现将 slab 描述符与对象数据物理分离:对象数据存放在页面内存中,而 slab 描述符存放在独立的内存区域。这使得对象数据页面完全用于数据,避免了描述符嵌入导致的缓存行浪费。
2.3 着色(Coloring)机制
Slab 分配器中一个精妙的设计是Cache Coloring。同一类型的 slab,其对象相对于页面起始的偏移量可以不同:
Slab A: 页面起始 → | obj0 | obj1 | obj2 | ... | color=0
Slab B: 页面起始 → | pad | obj0 | obj1 | obj2 | ... | color=16
Slab C: 页面起始 → | pad | pad | obj0 | obj1 | obj2 | ... | color=32
这样做的目的是让不同 slab 中的相同索引对象映射到 CPU Cache 的不同行,减少 Cache Line 冲突。可用颜色数量由 cache_ colour 决定(通常为 0~255 字节的对齐偏移)。
三、Slub 分配器:现代默认方案
3.1 Slab 的问题
经典 Slab 分配器在大型多核系统中暴露出严重问题:
- 元数据开销大:每个 slab 一个描述符,每个 CPU 维护三个链表,在拥有数千 CPU 的系统中,一个 cache 的 CPU 槽位可能占用数 MB 内存
- 缓存管理复杂:Full/Partial/Free 三个链表在不同 slab 间频繁移动对象,锁竞争激烈
- 调试困难:Red Zone、Poison 等调试机制添加大量不可见元数据
3.2 Slub 的革命性简化
Slub(Unqueued Slab)由 Christoph Lameter 设计,其核心理念是将元数据嵌入对象页面本身:
Slub 页面布局:
┌──────────────────────────────────────────────────────┐
│ Page Header ( struct page ) │
│ ├─ freelist: 指向第一个空闲对象 │
│ ├─ inuse: 已使用数量 │
│ ├─ objects: 对象总数 │
│ ├─ frozen: 是否被 CPU 独占 │
│ └─ next: 下一个 slab 页面 │
├──────────────────────────────────────────────────────┤
│ 对象区域: │
│ [obj0 | obj1 | obj2 | ... | objN] │
│ (空闲对象的前 8/16 字节存储 next 指针) │
└──────────────────────────────────────────────────────┘
关键创新是" freelist 复用对象内存 ":当一个对象被释放,其内存前几个字节被用来存储 freelist 指针。这意味着元数据存储零额外开销。
3.3 CPU-Cache 与 Node-Cache
Slub 设计了三级缓存策略:
分配路径(快速路径):
1. CPU kmem_cache_cpu-

发表评论 取消回复