3. Slab分配器初始化流程:核心调用链与关键步骤
Slab分配器的初始化是Linux内核内存管理子系统启动阶段的关键环节。整个初始化过程遵循“自举(Bootstrap)”原则:在通用内存分配器(如伙伴系统)尚未完全就绪时,Slab分配器必须能够先为自己分配管理结构所需的内存。本章节深入剖析kmem_cache_init()、kmem_cache_create()与create_cache()三个核心函数的调用链,并拆解每个阶段的关键步骤。
3.1 初始化总览:三阶段调用链
Slab分配器的初始化并非一蹴而就,而是通过三个主要函数依次推进,形成一个清晰的调用链。下表总结了每个函数的职责与调用时机:
| 函数名 | 调用阶段 | 核心职责 | 关键输出 |
|---|---|---|---|
kmem_cache_init() |
内核启动早期(start_kernel) | 初始化Slab分配器基础架构,创建第一个静态缓存 | kmem_cache引导缓存、slab_caches链表 |
kmem_cache_create() |
子系统初始化及驱动加载 | 用户态/内核态接口,封装参数校验与缓存创建请求 | 缓存描述符指针(struct kmem_cache *) |
create_cache() |
被kmem_cache_create()调用 |
实际分配并初始化kmem_cache结构,设置slab管理参数 |
完整的kmem_cache实例 |
/proc/slabinfo尚未可用。调试早期Slab初始化问题通常需要依赖printk或early_printk,并关注dmesg中与kmem_cache相关的日志。
3.2 第一阶段:kmem_cache_init() —— 自举引导
kmem_cache_init()是Slab分配器的“点火器”。它必须在伙伴系统初始化之后、其他任何内存分配请求之前被调用。其核心挑战在于:如何在没有通用Slab缓存的情况下,为Slab分配器自身分配管理结构?
解决方案是使用静态定义的引导缓存(bootstrap cache)。关键步骤如下:
- 计算元数据大小:确定
struct kmem_cache和struct slab等管理结构的大小。 - 创建静态缓存:使用
create_boot_cache()创建一个名为kmem_cache的静态缓存,用于存放后续所有kmem_cache描述符。 - 初始化全局链表:将引导缓存加入全局链表
slab_caches。 - 创建通用缓存:为常用大小(如32、64、128、256、512、1024、2048、4096字节)创建通用目的缓存(
kmalloc-*系列)。 - 替换引导缓存:当通用缓存就绪后,将引导缓存中的
kmem_cache描述符迁移到通用缓存中,释放静态空间。
kmem_cache_init()阶段的内存分配失败(例如伙伴系统尚未完全初始化),内核将触发panic。这是调试早期启动崩溃时需要优先检查的路径。
下图展示了kmem_cache_init()的内部流程与数据结构关系:
3.3 第二阶段:kmem_cache_create() —— 用户态接口与参数校验
kmem_cache_create()是内核其他模块创建专用Slab缓存的标准入口。它位于mm/slab_common.c中,负责处理来自文件系统、网络栈、设备驱动等子系统的缓存创建请求。
该函数的核心工作流程包括:
- 参数校验:检查
size(对象大小)、align(对齐要求)、flags(标志位)是否合法。例如,size不能为0,flags不能包含冲突位。 - 名称去重:检查全局
slab_caches链表,确保缓存名称唯一(name参数)。 - 计算对齐与填充:根据硬件缓存行大小(
cache_line_size())和align参数,计算最终的对象对齐方式。 - 委托创建:调用内部函数
__kmem_cache_create()或create_cache()执行实际的内存分配与初始化。
kmem_cache_create()的flags参数中,SLAB_HWCACHE_ALIGN强制对象按硬件缓存行对齐,可显著减少伪共享(False Sharing)问题,但会增加内存浪费。对于高频访问的小对象(如网络套接字缓冲区),此标志通常推荐启用。
下表列出了kmem_cache_create()中常见的标志位及其对性能的影响:
| 标志位 | 含义 | 性能影响 | 典型使用场景 |
|---|---|---|---|
SLAB_HWCACHE_ALIGN |
硬件缓存行对齐 | 减少伪共享,增加内存碎片 | 网络协议栈、文件系统元数据 |
SLAB_PANIC |
分配失败时触发panic | 避免静默失败,适合关键缓存 | VMA结构、进程描述符 |
SLAB_RECLAIM_ACCOUNT |
计入可回收内存 | 影响内存回收策略 | 文件系统dentry、inode缓存 |
SLAB_ACCOUNT |
计入memcg统计 | 支持cgroup内存隔离 | 容器环境下的内核对象 |
3.4 第三阶段:create_cache() —— 缓存描述符的完整构建
create_cache()是Slab缓存创建的最终执行者。它接收来自kmem_cache_create()的校验后参数,完成以下关键步骤:
- 分配
kmem_cache结构:从kmem_cache引导缓存(或通用缓存)中分配一个struct kmem_cache实例。 - 初始化基本字段:设置
size、object_size、align、flags、name等。 - 计算Slab管理参数:
- gfporder:每个Slab占用的连续页面阶数(2^order页)。
- num:每个Slab中可容纳的对象数量。
- colour_off:着色偏移量,用于缓存行着色(Cache Coloring)优化。
- 初始化per-CPU缓存:设置
cpu_cache数组,为每个CPU分配本地缓存数组(array_cache)。 - 初始化Slab链表:初始化
slabs_full、slabs_partial、slabs_free三个链表头。 - 注册到全局链表:将新缓存加入
slab_caches链表,并更新/proc/slabinfo接口。
下图展示了create_cache()构建一个完整kmem_cache实例的内部结构:
3.5 初始化流程中的关键数据结构关系
理解初始化流程,必须掌握三个核心数据结构之间的关联。下表总结了它们在初始化过程中的角色:
| 数据结构 | 初始化时机 | 存储位置 | 与初始化流程的关系 |
|---|---|---|---|
struct kmem_cache |
kmem_cache_init() / create_cache() |
引导缓存 → 通用缓存 | 每个缓存对应一个描述符,管理所有Slab |
struct slab |
首次分配对象时创建 | Slab页面内部 | 描述单个Slab的状态(满/部分/空闲) |
struct array_cache |
create_cache()中初始化 |
per-CPU内存区域 | 每个CPU的本地对象缓存,减少锁竞争 |
struct kmem_list3 |
kmem_cache_init()中初始化 |
NUMA节点内存 | 管理每个NUMA节点的Slab链表 |
kmem_cache_init()执行完毕后,可以通过printk打印slab_caches链表中的缓存名称,验证初始化是否完整。例如:list_for_each_entry(cache, &slab_caches, list) printk("cache: %s\n", cache->name);
3.6 初始化流程的完整调用链总结
将三个函数串联起来,形成完整的初始化调用链。下图以SVG形式呈现从start_kernel到缓存创建完成的完整路径:
kmem_cache_init()和kmem_cache_create()两个阶段?答案:kmem_cache_init()解决“先有鸡还是先有蛋”的问题——在通用缓存可用之前,必须用静态内存创建第一个缓存来存放kmem_cache描述符。之后kmem_cache_create()才能依赖通用缓存动态创建其他专用缓存。
通过以上三个函数的调用链分析,可以清晰地看到Slab分配器从无到有的完整构建过程。理解这一流程,对于后续调试Slab内存泄漏、优化缓存着色参数以及分析per-CPU缓存行为,都提供了坚实的基础。