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 图

kmem_cache_init() 自举初始化流程 阶段 0 静态 kmem_cache_boot 阶段 1 创建 cache_cache 阶段 2 通用大小缓存 阶段 3 启用 per-CPU 缓存 阶段 4 切换动态分配 自举关键点 • 阶段 0 使用静态 slab 分配阶段 1 的缓存 • 阶段 1 创建 cache_cache 后,slab 可自给 • 阶段 2-4 逐步完善通用缓存和 per-CPU 加速 初始化完成

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 创建流程步骤

  1. 参数校验:检查 size 是否在合理范围(通常 32B ~ 4MB),name 是否非空。
  2. 对齐调整:根据 align 参数和硬件缓存行(cache line)对齐对象起始地址。
  3. 计算 slab 布局:确定每个 slab 中对象数量、着色区(coloring)偏移、管理对象(kmem_bufctl_t)位置。
  4. 分配缓存描述符:从 cache_cache 分配一个新的 struct kmem_cache 实例。
  5. 初始化 per-CPU 缓存:为每个 CPU 分配 struct array_cache,设置批处理量(batch count)。
  6. 注册到全局链表:将新缓存加入 cache_chain 全局链表,供 /proc/slabinfo 导出。
注意:构造函数(ctor)在 slab 分配器中性能开销较大。每次从 slab 分配对象时,如果对象之前被释放过,构造函数不会重复调用;只有新分配的 slab 页中的对象才会调用 ctor。现代内核倾向于避免使用构造函数,改用 kmem_cache_zalloc() 清零初始化。

4.2.3 缓存创建流程 SVG 图

kmem_cache_create() 内部流程 kmem_cache_create() ① 参数校验 ② 对齐调整 ③ 计算 slab 布局 ④ 分配缓存描述符 ⑤ 初始化 per-CPU 缓存 布局计算关键 • 对象数 = (PAGE_SIZE - 管理区) / (对象大小 + 对齐) • 着色偏移 = 缓存行大小 × 着色序号 • gfporder = 0 表示单页 slab • 大对象使用多页(gfporder > 0)

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 图

slab 分配 gfp 标志决策树 分配上下文? 进程上下文 GFP_KERNEL 允许睡眠 / 回收 / I/O 可触发直接回收 中断 / 自旋锁 GFP_ATOMIC 不允许睡眠 / 无 I/O 使用紧急内存池

4.4 本章小结

  • kmem_cache_init() 采用 5 阶段自举策略,从静态引导缓存逐步过渡到完整的动态 slab 系统。
  • kmem_cache_create() 是创建专用缓存的统一接口,内部完成对齐、布局计算、per-CPU 缓存初始化等步骤。
  • gfp 标志 控制 slab 分配器的底层行为:GFP_KERNEL 用于进程上下文,GFP_ATOMIC 用于原子上下文,__GFP_DMA/__GFP_HIGHMEM 控制物理内存区域选择。
  • 理解初始化流程和 gfp 标志体系,是后续分析 slab 分配器性能优化和内存调试的基础。