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,再用这个缓存去分配更多管理结构。
Slab分配器初始化三阶段流程 Phase 1: 静态自举 创建引导kmem_cache 初始化cache_chain Phase 2: 基础注册 注册kmem_cache自身 注册kmem_list3 Phase 3: 通用扩展 创建kmalloc缓存 注册通用对象缓存 自举依赖关系说明: Phase 1 使用静态数组(slab_bootstrap_cache)绕过动态分配, Phase 2 利用Phase 1建立的缓存分配真正的kmem_cache对象, Phase 3 在内部管理完备后,向系统提供通用内存分配接口。

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分配器已经具备基本分配能力(通过引导缓存),因此可以正常分配内存来创建新的缓存描述符。创建完成后,引导缓存被销毁,系统完全切换到正式缓存。
Phase 2完成后Slab核心数据结构关系 cache_chain 全局链表 kmem_cache 缓存 管理所有缓存描述符 kmem_list3 缓存 管理NUMA链表头 array_cache 缓存 管理CPU本地池 实例化关系: 1. 每个kmem_cache实例包含一个kmem_list3数组(每个NUMA节点一个) 2. 每个kmem_list3包含三个Slab链表:slabs_partial、slabs_full、slabs_free 3. 每个kmem_cache还包含一个array_cache数组(每个CPU一个)

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)。
Slab初始化完成后的内存管理层次栈 内核模块 / 文件系统 / 网络栈 / 驱动 Slab 分配器 kmem_cache_alloc / kmalloc 伙伴系统 (Buddy System) alloc_pages / free_pages 物理页帧管理 (Page Allocator)

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分配器正确运行的基础,也是理解后续性能优化章节的前提。