4. Slab分配器初始化:系统启动时Slab系统的初始化流程与关键步骤
Slab分配器的初始化是Linux内核内存管理子系统启动的关键环节。在系统引导阶段,当伙伴系统(Buddy System)完成基本页帧管理后,Slab层必须从零开始构建其核心数据结构,包括cache_chain、kmem_cache管理对象以及用于分配这些管理对象的静态Slab缓存。本章将详细拆解这一自举(Bootstrap)过程。
核心挑战:Slab分配器本身需要动态内存来管理其内部数据结构(如kmem_cache描述符),但在初始化初期,动态分配机制尚未就绪。因此,必须采用“自举”策略——先使用静态定义或预分配的内存建立最小运行环境,再逐步扩展。
4.1 初始化阶段总览
Slab系统的初始化大致分为三个递进阶段,每个阶段依赖前一阶段建立的基础设施:
| 阶段 | 名称 | 关键动作 | 依赖条件 |
|---|---|---|---|
| Phase 1 | 静态自举 | 创建kmem_cache引导结构,初始化cache_chain链表 | 伙伴系统可用,但Slab未就绪 |
| Phase 2 | 基础缓存注册 | 注册kmem_cache、kmem_list3等内部对象缓存 | Phase 1完成,最小Slab可用 |
| Phase 3 | 通用缓存扩展 | 创建通用大小缓存(如kmalloc-32/64/128...) | Phase 2完成,内部管理完备 |
关键术语:“自举”(Bootstrap)指系统使用自身尚未完全初始化的组件来初始化自身的过程。在Slab中,这意味着先用静态内存创建第一个kmem_cache,再用这个缓存去分配更多管理结构。
4.2 静态自举:创建第一个kmem_cache
在Phase 1中,系统无法调用kmem_cache_create(),因为该函数本身依赖Slab分配器。解决方案是使用静态定义的引导结构:
/* 内核源码中的静态引导缓存定义(简化示意) */
static struct kmem_cache kmem_cache_boot = {
.name = "kmem_cache",
.object_size = sizeof(struct kmem_cache),
.offset = 0,
.flags = SLAB_NO_REAP,
.num = 0,
.gfporder = 0,
.color = 0,
.slabp_cache = NULL,
.list = LIST_HEAD_INIT(kmem_cache_boot.list),
};
/* 初始化入口函数 */
void __init kmem_cache_init(void)
{
/* 1. 将引导缓存加入全局cache_chain */
list_add(&kmem_cache_boot.list, &cache_chain);
/* 2. 设置cache_cache指针指向引导缓存 */
cache_cache = &kmem_cache_boot;
/* 3. 为引导缓存分配第一个Slab页框 */
cache_cache->num = calculate_slab_order(cache_cache);
cache_cache->slabp_cache = NULL; /* 小对象,slab头内嵌 */
/* 4. 分配第一个Slab的物理页 */
struct page *page = alloc_pages(GFP_KERNEL, cache_cache->gfporder);
setup_slab(cache_cache, page, cache_cache->num);
printk(KERN_INFO "Slab: Bootstrap kmem_cache initialized.\n");
}
注意:引导缓存
kmem_cache_boot必须使用SLAB_NO_REAP标志,防止在初始化过程中被回收机制错误地销毁。同时,其slabp_cache字段为NULL,表示Slab头部信息直接存放在数据页内(适用于小对象)。
4.3 基础缓存注册:让Slab管理自身
Phase 2的核心任务是利用Phase 1建立的引导缓存,为Slab分配器自身的核心数据结构创建专用缓存。这些数据结构包括:
- kmem_cache:每个缓存的描述符(约200-300字节)
- kmem_list3:每个NUMA节点/CPU的Slab链表头(约64-128字节)
- array_cache:每个CPU的本地对象池(大小可配置)
| 数据结构 | 大小(典型值) | 用途 | 注册时机 |
|---|---|---|---|
| kmem_cache | 256字节 | 描述一个对象缓存(名称、大小、Slab参数) | Phase 2 第一步 |
| kmem_list3 | 96字节 | 管理每个NUMA节点的partial/full/free链表 | Phase 2 第二步 |
| array_cache | 32 + 4*批量值 | 每个CPU的快速对象缓存(免锁访问) | Phase 2 第三步 |
/* Phase 2 核心代码逻辑 */
void __init kmem_cache_init_late(void)
{
/* 1. 为kmem_cache自身创建专用缓存 */
cache_cache = kmem_cache_create("kmem_cache",
sizeof(struct kmem_cache),
0, SLAB_PANIC, NULL);
/* 2. 为kmem_list3创建专用缓存 */
kmem_list3_cache = kmem_cache_create("kmem_list3",
sizeof(struct kmem_list3),
0, SLAB_PANIC, NULL);
/* 3. 为array_cache创建专用缓存 */
array_cache_cache = kmem_cache_create("array_cache",
sizeof(struct array_cache),
0, SLAB_PANIC, NULL);
/* 4. 销毁引导缓存,切换到正式缓存 */
kmem_cache_destroy(&kmem_cache_boot);
printk(KERN_INFO "Slab: Internal caches registered.\n");
}
设计要点:Phase 2中调用
kmem_cache_create()时,Slab分配器已经具备基本分配能力(通过引导缓存),因此可以正常分配内存来创建新的缓存描述符。创建完成后,引导缓存被销毁,系统完全切换到正式缓存。
4.4 通用缓存扩展:创建kmalloc系列缓存
Phase 3是Slab初始化最后一步,也是最实用的一步:创建一系列通用大小缓存,供内核其他子系统通过kmalloc()使用。这些缓存覆盖从32字节到数兆字节的常见分配粒度。
通用缓存的创建遵循2的幂次或特定粒度策略:
/* 通用缓存大小定义(典型配置) */
static const size_t kmalloc_sizes[] = {
32, 64, 128, 256, 512, 1024, 2048, 4096, 8192,
16384, 32768, 65536, 131072,
};
/* Phase 3 初始化函数 */
void __init kmem_cache_init_generic(void)
{
int i;
char name[32];
for (i = 0; i < ARRAY_SIZE(kmalloc_sizes); i++) {
snprintf(name, sizeof(name), "kmalloc-%zu", kmalloc_sizes[i]);
struct kmem_cache *cache = kmem_cache_create(
name,
kmalloc_sizes[i], /* 对象大小 */
0, /* 对齐(默认) */
SLAB_PANIC, /* 失败时panic */
NULL /* 构造函数(无) */
);
/* 注册到kmalloc映射表 */
kmalloc_caches[i] = cache;
}
/* 设置kmalloc最小/最大索引 */
kmalloc_min_size = kmalloc_sizes[0];
kmalloc_max_size = kmalloc_sizes[ARRAY_SIZE(kmalloc_sizes) - 1];
printk(KERN_INFO "Slab: %d generic caches created.\n", i);
}
性能提示:通用缓存的数量和大小直接影响内存利用率。过多的缓存会增加管理开销,过少则导致内部碎片。现代内核通常支持13-16个通用缓存,覆盖从32字节到4MB的范围。对于超过最大通用缓存大小的分配,Slab会直接回退到伙伴系统分配整页。
4.5 初始化完成后的系统状态
当三个阶段全部完成后,Slab分配器进入完全运行状态。此时系统的内存管理栈如下:
- 用户态/内核态模块 → 调用
kmalloc()/kmem_cache_alloc() - Slab分配器 → 从对应缓存分配对象,管理Slab链表
- 伙伴系统 → 为Slab提供连续页框(2^order页)
- 页分配器 → 管理物理页帧
| 组件 | 状态 | 说明 |
|---|---|---|
| cache_chain | 已初始化 | 包含所有注册缓存(内部+通用) |
| kmem_cache引导结构 | 已销毁 | Phase 2完成后释放 |
| 内部缓存(3个) | 运行中 | 管理Slab分配器自身数据结构 |
| 通用缓存(13个) | 运行中 | 提供kmalloc接口 |
| 每个CPU的array_cache | 已创建 | 提供免锁快速路径 |
初始化成功标志:当
kmem_cache_init_generic()返回后,内核可以安全地调用kmalloc()分配任意大小的内存。此时,Slab分配器完全取代了早期引导阶段使用的临时内存分配机制(如memblock或bootmem)。
4.6 初始化过程中的关键避坑点
Slab初始化涉及多个微妙的依赖关系,以下是实际开发中容易出错的关键点:
- 循环依赖:创建kmem_cache缓存时,不能使用kmalloc分配内存,因为kmalloc依赖kmem_cache。必须通过引导缓存打破循环。
- NUMA感知:在Phase 2创建kmem_list3缓存时,必须确保每个NUMA节点都有对应的链表头,否则跨节点分配会触发空指针。
- 缓存对齐:通用缓存的对象大小必须考虑硬件缓存行对齐(通常64字节),否则会导致伪共享(False Sharing)性能问题。
- 销毁顺序:引导缓存的销毁必须在Phase 2所有内部缓存创建完成后进行,且不能有未释放的对象。
调试建议:在初始化代码中加入
BUG_ON(cache_cache == NULL)和BUG_ON(kmem_list3_cache == NULL)断言,可以快速定位初始化顺序错误。此外,使用slabinfo工具(在初始化完成后)可以验证所有缓存的状态。
通过以上三个阶段的精心设计,Slab分配器在系统启动时完成了从无到有的自举过程,为内核提供了高效、可扩展的对象缓存机制。这一初始化流程是Slab分配器正确运行的基础,也是理解后续性能优化章节的前提。