4. slab 分配与释放:kmem_cache_alloc 与 kmem_cache_free 的完整路径,gfp_flags 标志详解

本章深入剖析 slab 分配器的核心操作路径:从 kmem_cache_alloc 到 kmem_cache_free 的完整执行流程,以及控制内存分配行为的 gfp_flags 标志位系统。理解这些内容,是编写高性能、低碎片化内核驱动的关键。

4.1 kmem_cache_alloc 分配路径

kmem_cache_alloc 是 slab 分配器提供给驱动开发者的标准分配接口。其执行路径分为快速路径、慢速路径和扩容路径三个层级,旨在最大化分配效率。

核心原则: 分配操作优先在本地 CPU 的 slab 缓存(per-CPU cache)中完成,避免锁竞争,这是 slab 分配器高性能的基石。

4.1.1 分配路径流程图

kmem_cache_alloc 完整分配路径 kmem_cache_alloc 快速路径:检查 per-CPU 空闲链表 无锁操作,直接返回对象 空闲链表为空? 慢速路径:从 slab 节点空闲链表获取 获取锁,批量转移对象到 per-CPU 节点空闲链表为空? 扩容路径:分配新 slab 页面 调用 buddy 系统分配物理页面(gfp_flags 生效) 初始化新 slab,分割为对象,加入空闲链表 返回对象指针 说明:快速路径命中率通常 > 90%,是 slab 分配器高性能的关键设计。

4.1.2 分配路径关键步骤

路径层级 触发条件 操作描述 锁开销
快速路径 per-CPU 空闲链表非空 从本地 CPU 的 freelist 头部取出对象,更新 freelist 指针 无锁(local lock 或 disable preempt)
慢速路径 per-CPU 空闲链表为空 获取 slab 节点锁,从节点空闲链表批量转移对象到 per-CPU 缓存 spinlock
扩容路径 节点空闲链表也为空 调用 alloc_pages 分配新物理页面,初始化 slab 结构,分割为对象 spinlock + buddy 锁
驱动开发提示: 在中断上下文或持有自旋锁时,必须确保分配操作走快速路径。如果 per-CPU 缓存耗尽,慢速路径的 spinlock 获取可能导致死锁。建议在初始化阶段预分配足够对象,或使用 kmem_cache_alloc_bulk 批量分配。

4.2 kmem_cache_free 释放路径

释放路径与分配路径对称,同样优先将对象归还到 per-CPU 缓存。当 per-CPU 缓存超过高水位(limit)时,触发批量回收到节点 slab 链表。

4.2.1 释放路径流程图

kmem_cache_free 完整释放路径 kmem_cache_free 快速路径:归还到 per-CPU 空闲链表 无锁操作,对象插入链表头部 per-CPU 缓存超限? 批量回收:转移对象到节点空闲链表 获取节点锁,将批量对象移回 slab 节点 若 slab 全空,可释放回 buddy 系统 释放完成 关键设计:释放操作不立即归还 buddy 系统,而是缓存在 per-CPU 和节点层。 只有当整个 slab 的所有对象都释放后,该 slab 页面才可能被回收。 这种延迟回收策略显著降低了内存分配/释放的抖动。

4.2.2 释放路径关键步骤

  1. 对象有效性检查: 内核通过 kasan_check_free 和 debug_object_free 检查对象是否越界或重复释放(仅在 CONFIG_SLUB_DEBUG 开启时)。
  2. 快速路径: 将对象插入 per-CPU 空闲链表头部,更新 freelist 指针。如果 per-CPU 缓存未超限,释放结束。
  3. 批量回收: 如果 per-CPU 缓存中对象数量超过 limit(通常为 batch * 2),触发批量回收,将 batch 个对象移回节点空闲链表。
  4. slab 页面回收: 如果节点中某个 slab 的所有对象都已释放,该 slab 页面被标记为空闲,可归还给 buddy 系统。
常见错误: 驱动开发中常见的释放错误包括:
• 重复释放同一对象(double-free)
• 释放后继续使用(use-after-free)
• 释放不属于该 cache 的对象(object type confusion)
开启 CONFIG_SLUB_DEBUG 和 KASAN 可在开发阶段捕获这些问题。

4.3 gfp_flags 标志详解

gfp_flags(Get Free Pages Flags)是控制内存分配行为的标志位集合。在 slab 分配器的扩容路径中,最终会调用 alloc_pages 向 buddy 系统申请物理页面,此时 gfp_flags 决定了分配的行为约束。

4.3.1 核心标志位分类

分类 标志位 含义 典型使用场景
分配区域 __GFP_DMA 从 ZONE_DMA(16MB 以下)分配 ISA DMA 设备驱动
__GFP_DMA32 从 ZONE_DMA32(4GB 以下)分配 32位 DMA 设备驱动
__GFP_HIGHMEM 允许从 ZONE_HIGHMEM 分配 用户空间页面缓存
行为控制 __GFP_WAIT 允许睡眠等待内存回收 进程上下文分配
__GFP_IO 允许执行磁盘 I/O 进行内存回收 文件系统操作
__GFP_FS 允许调用文件系统操作 VFS 层分配
回收策略 __GFP_NORETRY 不重试,分配失败立即返回 NULL 实时系统、中断上下文
__GFP_RETRY_MAYFAIL 允许有限重试,但可能失败 中等优先级分配
__GFP_NOFAIL 无限重试直到成功(慎用!) 关键内核结构分配
调试与统计 __GFP_ZERO 分配后立即清零 安全敏感数据
__GFP_ACCOUNT 计入 cgroup 内存限制 容器化环境

4.3.2 常用组合标志

内核预定义了若干常用组合标志,驱动开发者应优先使用这些组合,而非手动拼接位掩码:

组合标志 展开位 适用上下文
GFP_KERNEL __GFP_WAIT | __GFP_IO | __GFP_FS 进程上下文,可睡眠
GFP_ATOMIC __GFP_HIGH(紧急保留池) 中断上下文、持有自旋锁
GFP_NOWAIT __GFP_NORETRY 不允许任何等待
GFP_DMA __GFP_DMA 需要 DMA 内存
GFP_HIGHUSER __GFP_HIGHMEM | __GFP_WAIT 用户空间大块分配

4.3.3 gfp_flags 在 slab 分配中的传递路径

gfp_flags 传递路径 驱动调用 kmem_cache_alloc(cache, GFP_KERNEL) slab 分配器 传递 gfp_flags 到扩容路径 buddy 系统 alloc_pages(gfp_flags, order) gfp_flags 在传递过程中的关键影响 1. GFP_KERNEL:允许睡眠、I/O 和文件系统操作,buddy 系统可执行激进的内存回收。 2. GFP_ATOMIC:禁止睡眠,只能从紧急保留池(min watermark)分配,失败概率更高。 3. GFP_NOWAIT:完全禁止等待,若 per-CPU 缓存耗尽且 buddy 无空闲页,立即返回 NULL。 4. __GFP_ZERO:在 buddy 返回页面后,slab 层负责将对象内存清零。 5. __GFP_ACCOUNT:slab 层将分配计入当前 cgroup 的内存使用量。

4.3.4 驱动开发中的 gfp_flags 选择指南

黄金法则: 在满足功能需求的前提下,使用最受限的 gfp_flags。过度使用 GFP_KERNEL 可能导致不可预期的睡眠,而滥用 GFP_ATOMIC 会增加分配失败风险。
  • 进程上下文(无锁): 使用 GFP_KERNEL,允许内核执行内存回收和 compaction。
  • 中断上下文 / 软中断: 必须使用 GFP_ATOMIC,禁止睡眠。
  • 持有 spinlock / rwlock: 使用 GFP_ATOMIC 或 GFP_NOWAIT。
  • 实时线程: 使用 GFP_KERNEL | __GFP_NORETRY,避免不可预测的延迟。
  • DMA 缓冲区: 根据设备 DMA 地址宽度,添加 __GFP_DMA 或 __GFP_DMA32。
  • 安全敏感数据: 添加 __GFP_ZERO 防止信息泄露。
性能调优提示: 在 slab 分配器中,gfp_flags 不仅影响 buddy 系统的行为,还会影响 slab 层的缓存策略。例如,GFP_ATOMIC 分配的对象倾向于留在 per-CPU 缓存中,因为回收路径可能无法立即处理。理解这一行为有助于设计低延迟驱动。

4.4 本章小结

  • 分配路径: 快速路径(per-CPU)→ 慢速路径(节点链表)→ 扩容路径(buddy 系统),逐级降级,性能递减。
  • 释放路径: 快速路径(per-CPU)→ 批量回收(节点链表)→ slab 页面回收(buddy 系统),延迟回收策略减少抖动。
  • gfp_flags: 控制分配区域、行为约束、回收策略和调试特性。驱动开发者应优先使用预定义组合标志,并根据执行上下文谨慎选择。
  • 上下文匹配: 中断上下文用 GFP_ATOMIC,进程上下文用 GFP_KERNEL,这是驱动开发中最基本的内存分配规则。