3. slab分配器初始化:slab系统初始化流程,kmem_cache_init与kmem_cache_create源码分析
slab分配器是Linux内核中用于管理小对象(通常小于一页)的核心内存分配机制。其初始化过程分为两个关键阶段:slab系统自举(kmem_cache_init)和通用缓存创建(kmem_cache_create)。理解这两个函数的执行流程,是掌握slab分配器工作原理的基石。
3.1 slab系统初始化整体流程
在系统启动早期,内存管理子系统尚未完全就绪时,slab分配器需要通过一个“自举”过程来建立自身所需的数据结构。整个初始化流程分为四个主要阶段,如下表所示:
| 阶段 | 函数/步骤 | 核心任务 | 关键数据结构 |
|---|---|---|---|
| 1 | kmem_cache_init (早期) |
创建静态的cache_cache,用于管理slab缓存描述符自身 |
struct kmem_cache (静态实例) |
| 2 | kmem_cache_init (中期) |
创建malloc_sizes数组中的前几个通用缓存(如32B、64B) |
cache_sizes、kmem_list3 |
| 3 | kmem_cache_init (后期) |
创建剩余的通用缓存,并启用per-CPU缓存(array_cache) |
array_cache、kmem_list3 |
| 4 | kmem_cache_create |
为驱动或内核子系统创建专用缓存 | struct kmem_cache (动态分配) |
关键概念:slab分配器的初始化是一个“鸡生蛋”问题——分配器本身需要内存来管理内存。内核通过先使用静态定义的数据结构,再逐步过渡到动态分配的方式解决这一矛盾。
3.2 kmem_cache_init 源码分析
kmem_cache_init 是slab分配器的入口函数,位于 mm/slab.c。它负责建立slab分配器运行所需的所有基础设施。其核心执行逻辑如下:
- 初始化
cache_cache:这是一个静态定义的struct kmem_cache实例,专门用于管理“slab缓存描述符”本身。内核通过init_cache宏或直接赋值来设置其初始值。 - 创建
malloc_sizes中的前几个缓存:使用kmem_cache_create创建大小为32、64、96、128字节的通用缓存。此时,kmem_cache_create内部会从cache_cache中分配新的struct kmem_cache对象。 - 启用per-CPU缓存:为每个CPU分配
array_cache结构,并挂接到对应的struct kmem_cache上。这大大提高了多核系统下的分配性能。 - 创建剩余通用缓存:创建所有
malloc_sizes中定义的缓存(如256B、512B、1KB等),完成通用内存分配器的初始化。
源码关键片段 (简化逻辑):
// mm/slab.c
void __init kmem_cache_init(void)
{
int i;
// 1. 初始化 cache_cache (静态定义)
cache_cache = (struct kmem_cache) {
.objsize = sizeof(struct kmem_cache),
.flags = SLAB_NO_REAP,
.num = 0, // 稍后计算
.gfporder = 0,
// ... 其他字段初始化
};
// 2. 创建前几个通用缓存 (使用 cache_cache 分配)
for (i = 0; i < NUM_INITIAL_CACHES; i++) {
kmem_cache_create(malloc_sizes[i].name,
malloc_sizes[i].size,
0, 0, NULL);
}
// 3. 启用 per-CPU 缓存
for_each_online_cpu(cpu) {
for (i = 0; i < NUM_CACHES; i++) {
// 分配并初始化 array_cache
// 设置 shared 和 alien 指针
}
}
// 4. 创建剩余通用缓存
for (; i < NUM_CACHES; i++) {
kmem_cache_create(malloc_sizes[i].name,
malloc_sizes[i].size,
0, 0, NULL);
}
// 标记初始化完成
slab_state = UP;
}
3.3 kmem_cache_create 源码分析
kmem_cache_create 是驱动开发者最常接触的slab API。它用于创建一个新的专用缓存,并返回一个 struct kmem_cache * 指针。其核心流程包括参数校验、slab计算、对象布局和per-CPU缓存初始化。
3.3.1 函数原型与参数
struct kmem_cache *
kmem_cache_create(const char *name, size_t size, size_t align,
slab_flags_t flags, void (*ctor)(void *));
| 参数 | 含义 | 典型值/说明 |
|---|---|---|
name |
缓存名称,用于/proc/slabinfo标识 | 如"my_driver_cache" |
size |
每个对象的大小(字节) | 必须大于0,通常对齐到sizeof(void *) |
align |
对象对齐要求(字节) | 0表示使用默认对齐(L1缓存行) |
flags |
控制slab行为的标志位 | SLAB_HWCACHE_ALIGN、SLAB_PANIC等 |
ctor |
构造函数,每次分配对象时调用 | 通常为NULL,用于初始化对象 |
3.3.2 核心执行步骤
- 参数校验与对齐:检查
size是否合法,根据align和flags计算最终的对象对齐值。如果flags包含SLAB_HWCACHE_ALIGN,则对齐到L1缓存行大小。 - 计算slab布局:确定每个slab中包含多少个对象(
num),以及需要多少个物理页面(gfporder)。这是通过calculate_slab_order函数完成的,它会尝试不同的gfporder,直到找到一个能容纳整数个对象且碎片最小的配置。 - 分配
struct kmem_cache:从cache_cache中分配一个新的缓存描述符。如果kmem_cache_init尚未完成,则使用静态预分配的空间。 - 初始化per-CPU缓存:为每个在线CPU分配
array_cache结构,并设置其容量(limit)和批量传输大小(batchcount)。 - 初始化节点列表:为每个内存节点初始化
kmem_list3结构,用于管理该节点上的slab链表(full、partial、free)。 - 注册到全局链表:将新创建的缓存添加到
cache_chain全局链表中,使其对kmem_cache_alloc可用。
驱动开发注意:在模块卸载时,必须调用
kmem_cache_destroy 销毁通过 kmem_cache_create 创建的缓存。如果缓存中仍有未释放的对象,内核会打印警告并阻止卸载。
3.4 slab初始化流程SVG架构图
下图展示了从系统启动到驱动创建专用缓存的完整初始化流程:
3.5 关键数据结构关系
在初始化过程中,以下几个核心数据结构相互关联,构成了slab分配器的骨架:
struct kmem_cache:每个缓存的描述符,包含对象大小、slab布局、per-CPU缓存指针、节点列表等。struct array_cache:per-CPU的本地对象缓存,用于无锁快速分配。struct kmem_list3:每个内存节点的slab链表管理结构,包含full、partial、free三个链表。struct slab:描述一个物理连续的内存块(通常为1页或多页),包含对象分配位图。
初始化顺序依赖关系:
cache_cache (静态)
→ 用于分配 struct kmem_cache
→ 用于创建通用缓存 (malloc_sizes)
→ 用于创建 array_cache (per-CPU)
→ 完整的 slab 分配器就绪
→ 驱动调用 kmem_cache_create 创建专用缓存
3.6 驱动开发实践要点
对于Linux驱动开发者,理解slab初始化流程有助于正确使用内存分配API:
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 频繁分配/释放固定大小结构体 | 使用 kmem_cache_create + kmem_cache_alloc |
避免内存碎片,提高分配效率 |
| 对象大小超过一页 | 使用 __get_free_pages 或 vmalloc |
slab分配器针对小对象优化 |
| 需要对齐到特定边界 | 设置 align 参数或 SLAB_HWCACHE_ALIGN 标志 |
避免伪共享,提高缓存性能 |
| 模块初始化时创建缓存 | 在 module_init 函数中调用 kmem_cache_create |
确保在首次分配前缓存已就绪 |
| 模块卸载时销毁缓存 | 在 module_exit 函数中调用 kmem_cache_destroy |
防止内存泄漏 |
常见错误:在
kmem_cache_create 中指定 SLAB_PANIC 标志时,如果内存不足,内核会直接 panic。在驱动中应谨慎使用,或确保在系统内存充足时调用。
通过深入理解 kmem_cache_init 和 kmem_cache_create 的源码实现,驱动开发者可以更高效地利用slab分配器,避免常见的性能陷阱和内存管理错误。