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 分配路径流程图
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 释放路径流程图
4.2.2 释放路径关键步骤
- 对象有效性检查: 内核通过
kasan_check_free和debug_object_free检查对象是否越界或重复释放(仅在CONFIG_SLUB_DEBUG开启时)。 - 快速路径: 将对象插入 per-CPU 空闲链表头部,更新
freelist指针。如果 per-CPU 缓存未超限,释放结束。 - 批量回收: 如果 per-CPU 缓存中对象数量超过
limit(通常为batch * 2),触发批量回收,将batch个对象移回节点空闲链表。 - slab 页面回收: 如果节点中某个 slab 的所有对象都已释放,该 slab 页面被标记为空闲,可归还给 buddy 系统。
常见错误: 驱动开发中常见的释放错误包括:
• 重复释放同一对象(double-free)
• 释放后继续使用(use-after-free)
• 释放不属于该 cache 的对象(object type confusion)
开启
• 重复释放同一对象(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 分配中的传递路径
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,这是驱动开发中最基本的内存分配规则。