4. slab 分配器初始化:slab 系统初始化流程
slab 分配器的初始化是 Linux 内核启动早期内存子系统建立的关键环节。由于 slab 分配器自身需要依赖 kmem_cache 结构来管理内存,而 kmem_cache 本身又必须通过 slab 分配器来分配,这就形成了一个经典的“先有鸡还是先有蛋”的问题。内核通过精心设计的两阶段初始化流程——kmem_cache_init() 和 kmem_cache_init_late()——优雅地解决了这一自举难题。
4.1 初始化整体流程
slab 系统的初始化分为两个主要阶段,分别对应不同的启动上下文和可用资源。下表总结了两个阶段的核心差异:
| 阶段 | 函数 | 调用时机 | 可用内存机制 | 关键动作 |
|---|---|---|---|---|
| 第一阶段(早期) | kmem_cache_init() |
start_kernel() 早期,伙伴系统已就绪 | 伙伴系统 + 静态缓存 | 创建 slab 管理结构自身所需的缓存 |
| 第二阶段(后期) | kmem_cache_init_late() |
内核线程创建后,调度器激活 | 完全功能的 slab 分配器 | 启用 per-CPU 缓存、优化性能路径 |
核心设计思想: 先使用静态分配的
kmem_cache 结构作为“种子”,通过伙伴系统获取原始内存页,构建出最基础的 slab 缓存;待分配器具备自我复制能力后,再替换为动态分配的缓存,并开启全部优化特性。
4.2 kmem_cache_init():早期自举阶段
该函数在 start_kernel() 中、mm_init() 之后被调用。此时伙伴系统已经初始化完毕,但 slab 分配器尚未可用。其执行流程如下:
- 初始化静态缓存结构: 内核预先静态定义了一个
kmem_cache实例(cache_cache),用于管理kmem_cache结构本身。此阶段将其内部链表、锁、统计计数器初始化。 - 创建第一个 slab 缓存: 调用
kmem_cache_create()为cache_cache创建 slab 缓存。由于此时没有可用的 slab 分配器,该函数会直接调用伙伴系统的__get_free_pages()分配物理页,并手动构建 slab 描述符和对象布局。 - 创建通用缓存(malloc 后备): 依次创建
kmalloc_caches[]数组中的各个通用缓存(如 32B、64B、128B、… 8MB)。这些缓存使用刚刚建立起来的cache_cache来分配其kmem_cache结构,标志着自举的完成。 - 设置全局变量: 将
slab_state从DOWN推进到PARTIAL,表示分配器已部分可用。
自举关键点: 在
kmem_cache_init() 执行期间,kmem_cache 结构的内存来自静态定义的 cache_cache,而 slab 的物理页来自伙伴系统。直到通用缓存创建完成后,新的 kmem_cache 结构才能从 slab 分配器自身分配。
下面的 SVG 图展示了 kmem_cache_init() 的自举流程与依赖关系:
4.3 kmem_cache_init_late():后期优化阶段
当内核调度器初始化完成、per-CPU 数据区可用后,kmem_cache_init_late() 被调用。此阶段不再需要处理自举问题,而是专注于性能优化和功能完善:
- 启用 per-CPU 缓存(array_cache): 为每个 CPU 分配并初始化本地对象缓存。这使得对象分配和释放可以无锁进行,极大提升性能。
- 调整缓存参数: 根据 NUMA 拓扑信息优化 slab 的节点分配策略,设置合适的批处理大小(batch count)和限制(limit)。
- 激活回收机制: 注册 slab 回收回调到内存压力处理路径,使 slab 缓存能够在系统内存紧张时释放部分空闲 slab。
- 更新全局状态: 将
slab_state推进到FULL,标志着 slab 分配器完全可用。
注意事项: 在
kmem_cache_init_late() 完成之前,任何尝试使用 per-CPU 缓存或依赖完整 slab 特性的代码都可能触发内核崩溃。驱动开发者应确保在 module_init() 中使用的 slab API 调用发生在初始化后期之后。
4.4 初始化前后状态对比
下表详细列出了 slab 分配器在初始化三个阶段(DOWN、PARTIAL、FULL)的关键能力差异:
| 特性 / 能力 | DOWN(初始化前) | PARTIAL(早期初始化后) | FULL(后期初始化后) |
|---|---|---|---|
| kmem_cache 分配 | 不可用 | 仅限静态 cache_cache | 完全可用 |
| kmalloc/kfree | 不可用 | 可用(无 per-CPU 缓存) | 可用(带 per-CPU 缓存) |
| per-CPU 缓存 | 未初始化 | 未启用 | 已启用 |
| NUMA 感知 | 不支持 | 基本支持 | 完整支持 |
| 内存回收回调 | 未注册 | 未注册 | 已注册 |
| 并发安全 | 单 CPU | 多 CPU(加锁) | 多 CPU(无锁路径) |
4.5 关键数据结构初始化细节
在 kmem_cache_init() 过程中,以下几个核心数据结构的初始化顺序至关重要:
/* 静态定义的 kmem_cache 结构,用于自举 */
static struct kmem_cache cache_cache = {
.name = "kmem_cache",
.object_size = sizeof(struct kmem_cache),
.size = sizeof(struct kmem_cache),
.flags = SLAB_NO_REAP,
.num = 0,
.gfporder = 0,
/* 其他字段初始化为 0 */
};
/* kmem_cache_init() 中的关键调用序列 */
void __init kmem_cache_init(void)
{
int i;
/* 1. 初始化 cache_cache 的内部链表和锁 */
INIT_LIST_HEAD(&cache_cache.list);
spin_lock_init(&cache_cache.lock);
/* 2. 为 cache_cache 自身创建 slab 缓存 */
kmem_cache_create(&cache_cache, "kmem_cache",
sizeof(struct kmem_cache),
__alignof__(struct kmem_cache),
0, NULL);
/* 3. 创建通用 kmalloc 缓存 */
for (i = 0; i < KMALLOC_SHIFT_HIGH; i++) {
struct kmem_cache *c = &kmalloc_caches[i];
unsigned int size = 1 << (i + 3); /* 8B, 16B, 32B, ... */
kmem_cache_create(c, NULL, size, size, 0, NULL);
}
/* 4. 更新状态 */
slab_state = PARTIAL;
}
初始化顺序的必然性: 第 2 步中
kmem_cache_create(&cache_cache, ...) 的调用看似递归——用 slab 分配器创建管理 slab 分配器自身的缓存。实际上,kmem_cache_create() 在 slab_state == DOWN 时会走特殊路径:它直接使用伙伴系统分配物理页,并手动填充 slab 描述符,从而绕过了对自身缓存的依赖。
4.6 初始化流程图
下面的 SVG 图从更高维度展示了 slab 初始化在整个内核启动路径中的位置和上下文:
4.7 初始化过程中的常见问题与调试
slab 初始化阶段的错误通常会导致内核在启动早期崩溃,且难以调试。以下是几个典型问题及其排查思路:
- 自举循环死锁: 如果在
kmem_cache_init()完成前调用了kmalloc(),由于通用缓存尚未创建,会导致空指针解引用。内核通过slab_state标志位进行保护,非法调用会触发 BUG_ON。 - per-CPU 缓存越界:
kmem_cache_init_late()中如果 per-CPU 缓存大小计算错误,可能导致 CPU 热插拔时访问越界。可以通过CONFIG_DEBUG_SLAB启用边界检查。 - NUMA 节点不匹配: 在 NUMA 系统上,如果
kmem_cache_init()阶段未能正确识别所有内存节点,后续的 slab 分配可能落在错误的节点上。检查/sys/devices/system/node/下的内存信息可以辅助诊断。
调试建议: 在初始化代码中添加
pr_info() 输出 slab_state 的转换点和各缓存创建时的参数,配合 early_printk 可以在串口终端看到完整的初始化日志,是定位早期 slab 问题的有效手段。