4. slab 分配器初始化流程:kmem_cache_init()、kmem_cache_create()、gfp 标志与行为
slab 分配器的初始化是 Linux 内核启动早期内存管理子系统建立的关键步骤。与伙伴系统(Buddy System)管理物理页框不同,slab 分配器专门负责内核中大量小对象(如 task_struct、inode、dentry)的分配与释放。本章深入分析 slab 分配器的初始化入口 kmem_cache_init()、缓存创建接口 kmem_cache_create(),以及控制分配行为的 gfp 标志体系。
4.1 初始化入口:kmem_cache_init()
kmem_cache_init() 在内核启动早期被调用,此时伙伴系统已可用,但 slab 分配器自身尚未就绪。该函数采用“自举(bootstrap)”策略,分阶段构建 slab 缓存系统。
核心要点:kmem_cache_init() 必须解决“先有鸡还是先有蛋”的问题——分配 slab 缓存描述符本身也需要 slab 内存。
4.1.1 初始化阶段划分
| 阶段 | 操作 | 关键数据结构 |
|---|---|---|
| 阶段 0 | 使用静态定义的 kmem_cache_boot 作为临时缓存描述符 |
struct kmem_cache 静态实例 |
| 阶段 1 | 创建 kmem_cache 类型的缓存(cache_cache) |
cache_cache 全局变量 |
| 阶段 2 | 创建通用大小缓存(如 32、64、128 字节等) | malloc_sizes[] 数组 |
| 阶段 3 | 启用每 CPU 缓存(array_cache) | struct array_cache |
| 阶段 4 | 将静态引导缓存切换为动态分配 | 释放 kmem_cache_boot |
自举技巧:阶段 0 使用静态分配的
kmem_cache_boot 来分配第一个真正的 slab 缓存描述符,之后用这个新缓存来分配所有后续的缓存描述符,完成“自我复制”。
4.1.2 初始化流程 SVG 图
4.2 缓存创建接口:kmem_cache_create()
kmem_cache_create() 是内核模块和子系统创建专用 slab 缓存的统一入口。它接收对象大小、对齐要求、构造函数等参数,返回一个 struct kmem_cache * 句柄。
4.2.1 函数原型与参数
struct kmem_cache *
kmem_cache_create(const char *name, // 缓存名称(用于 /proc/slabinfo)
unsigned int size, // 对象大小(字节)
unsigned int align, // 对齐要求(0 为自然对齐)
slab_flags_t flags, // 缓存标志
void (*ctor)(void *)); // 构造函数(可为 NULL)
4.2.2 创建流程步骤
- 参数校验:检查 size 是否在合理范围(通常 32B ~ 4MB),name 是否非空。
- 对齐调整:根据 align 参数和硬件缓存行(cache line)对齐对象起始地址。
- 计算 slab 布局:确定每个 slab 中对象数量、着色区(coloring)偏移、管理对象(kmem_bufctl_t)位置。
- 分配缓存描述符:从
cache_cache分配一个新的struct kmem_cache实例。 - 初始化 per-CPU 缓存:为每个 CPU 分配
struct array_cache,设置批处理量(batch count)。 - 注册到全局链表:将新缓存加入
cache_chain全局链表,供/proc/slabinfo导出。
注意:构造函数(ctor)在 slab 分配器中性能开销较大。每次从 slab 分配对象时,如果对象之前被释放过,构造函数不会重复调用;只有新分配的 slab 页中的对象才会调用 ctor。现代内核倾向于避免使用构造函数,改用
kmem_cache_zalloc() 清零初始化。
4.2.3 缓存创建流程 SVG 图
4.3 gfp 标志与分配行为
gfp(Get Free Pages)标志是 Linux 内核内存分配的核心控制参数。slab 分配器在底层调用伙伴系统获取页框时,会将 gfp 标志传递给 __get_free_pages()。不同的 gfp 组合决定了分配行为:是否允许睡眠、是否允许 I/O、是否允许高内存、是否允许回收等。
4.3.1 常用 gfp 标志分类
| 分类 | 标志 | 含义 | 典型使用场景 |
|---|---|---|---|
| 上下文 | GFP_KERNEL |
允许睡眠、允许 I/O、允许回收 | 进程上下文,大部分内核分配 |
| 上下文 | GFP_ATOMIC |
不允许睡眠,使用紧急内存池 | 中断处理程序、自旋锁内 |
| 上下文 | GFP_NOIO |
不允许 I/O,但允许睡眠 | 块设备层避免递归 |
| 上下文 | GFP_NOFS |
不允许文件系统 I/O | 文件系统内部分配 |
| 区域 | __GFP_DMA |
从 ZONE_DMA(16MB 以下)分配 | ISA DMA 设备 |
| 区域 | __GFP_HIGHMEM |
允许从 ZONE_HIGHMEM 分配 | 用户空间页缓存 |
| 行为 | __GFP_ZERO |
分配后清零内存 | 安全敏感数据结构 |
| 行为 | __GFP_RETRY_MAYFAIL |
允许重试但可能失败 | 大块分配 |
| 行为 | __GFP_NORETRY |
不重试,立即返回 NULL | 试探性分配 |
4.3.2 gfp 标志在 slab 中的传递路径
kmem_cache_alloc(cachep, gfpflags)
└─ __cache_alloc(cachep, gfpflags, caller)
└─ ____cache_alloc(cachep, gfpflags)
├─ 尝试 per-CPU 缓存 (array_cache)
├─ 尝试 slab 空闲链表 (slab 级)
└─ cache_grow() 分配新 slab 页
└─ __get_free_pages(gfpflags, cachep->gfporder)
└─ 伙伴系统分配页框
关键行为差异:
GFP_KERNEL分配时,如果 per-CPU 缓存为空,可以进入cache_alloc_refill()并可能触发直接回收(direct reclaim)。GFP_ATOMIC分配时,不会触发直接回收,只能从内存池(min watermark)中获取;如果失败则返回 NULL。- slab 分配器在
cache_grow()中会检查 gfp 标志中的__GFP_WAIT(旧内核)或__GFP_RECLAIM(新内核)来决定是否允许回收。
4.3.3 gfp 标志选择决策树 SVG 图
4.4 本章小结
- kmem_cache_init() 采用 5 阶段自举策略,从静态引导缓存逐步过渡到完整的动态 slab 系统。
- kmem_cache_create() 是创建专用缓存的统一接口,内部完成对齐、布局计算、per-CPU 缓存初始化等步骤。
- gfp 标志 控制 slab 分配器的底层行为:
GFP_KERNEL用于进程上下文,GFP_ATOMIC用于原子上下文,__GFP_DMA/__GFP_HIGHMEM控制物理内存区域选择。 - 理解初始化流程和 gfp 标志体系,是后续分析 slab 分配器性能优化和内存调试的基础。