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 分配器尚未可用。其执行流程如下:

  1. 初始化静态缓存结构: 内核预先静态定义了一个 kmem_cache 实例(cache_cache),用于管理 kmem_cache 结构本身。此阶段将其内部链表、锁、统计计数器初始化。
  2. 创建第一个 slab 缓存: 调用 kmem_cache_create() 为 cache_cache 创建 slab 缓存。由于此时没有可用的 slab 分配器,该函数会直接调用伙伴系统的 __get_free_pages() 分配物理页,并手动构建 slab 描述符和对象布局。
  3. 创建通用缓存(malloc 后备): 依次创建 kmalloc_caches[] 数组中的各个通用缓存(如 32B、64B、128B、… 8MB)。这些缓存使用刚刚建立起来的 cache_cache 来分配其 kmem_cache 结构,标志着自举的完成。
  4. 设置全局变量: 将 slab_state 从 DOWN 推进到 PARTIAL,表示分配器已部分可用。
自举关键点: 在 kmem_cache_init() 执行期间,kmem_cache 结构的内存来自静态定义的 cache_cache,而 slab 的物理页来自伙伴系统。直到通用缓存创建完成后,新的 kmem_cache 结构才能从 slab 分配器自身分配。

下面的 SVG 图展示了 kmem_cache_init() 的自举流程与依赖关系:

kmem_cache_init() 自举流程 ① 静态 cache_cache 初始化链表/锁/统计 ② 创建自身 slab 调用 __get_free_pages() ③ 创建 kmalloc 缓存 32B ~ 8MB 通用缓存 依赖关系与自举原理 • 阶段① 使用静态内存,不依赖 slab 分配器 • 阶段② 通过伙伴系统获取物理页,手动构建 slab 描述符 • 阶段③ 使用阶段②建立的缓存分配 kmem_cache 结构,完成自举 slab_state = PARTIAL kmem_cache_init_late() 第二阶段 启用 per-CPU 缓存与优化

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 初始化在整个内核启动路径中的位置和上下文:

slab 初始化在内核启动中的位置 start_kernel() mm_init() kmem_cache_init() sched_init() kmem_cache_init_late() 初始化阶段的内存可用性 • 伙伴系统初始化完成 → 页级分配可用 • kmem_cache_init() 完成 → slab 对象级分配可用 (但无 per-CPU 缓存,性能较低) • 调度器初始化完成 → per-CPU 数据区可用 • kmem_cache_init_late() 完成 → 完整 slab 功能 (per-CPU 缓存、NUMA 优化、回收机制)

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 问题的有效手段。