2. 内存分配器基础:页分配器(alloc_pages)、slab分配器、kmalloc与kzalloc
Linux 内核的内存分配体系呈分层结构,从最底层的物理页管理到上层面向对象的缓存分配,每一层都服务于特定的场景。理解 页分配器、slab 分配器 以及 kmalloc / kzalloc 的层级关系与适用边界,是驱动开发者正确管理内存的前提。
2.1 内核内存分配器层级总览
下图展示了从物理页到驱动常用 API 的完整调用链路:
核心原则:驱动开发者应优先使用
kmalloc / kzalloc,仅在需要分配整页或大量连续物理内存时,才直接调用 alloc_pages 系列接口。
2.2 页分配器:alloc_pages 与 __get_free_pages
页分配器是内核最底层的内存分配接口,直接操作物理页帧。它基于伙伴系统算法,分配粒度固定为 2^order 个连续物理页(order 从 0 到 MAX_ORDER-1,通常 MAX_ORDER=11,即最大 2MB 连续物理内存)。
2.2.1 核心 API
| 函数 | 返回值类型 | 说明 |
|---|---|---|
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order) |
struct page * |
分配 2^order 个连续物理页,返回首页的 page 结构体指针 |
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) |
虚拟地址(内核逻辑地址) | 分配连续物理页并返回线性映射的虚拟地址(直接映射区) |
void free_pages(unsigned long addr, unsigned int order) |
— | 释放由 __get_free_pages 分配的页 |
void __free_pages(struct page *page, unsigned int order) |
— | 释放由 alloc_pages 分配的页 |
2.2.2 典型用法
#include <linux/gfp.h>
#include <linux/mm.h>
/* 分配 4 个连续物理页 (order=2) */
struct page *pages = alloc_pages(GFP_KERNEL, 2);
if (!pages) {
pr_err("alloc_pages failed\n");
return -ENOMEM;
}
/* 获取虚拟地址(仅适用于直接映射区) */
void *virt = page_address(pages);
/* 使用完毕后释放 */
__free_pages(pages, 2);
注意:在 ARM64 等非直接映射架构上,
page_address() 可能返回 NULL。对于 DMA 或需要虚拟地址的场景,应使用 dma_alloc_coherent 或 vmalloc 替代裸页分配。
2.3 slab 分配器:面向对象的缓存管理
slab 分配器专为内核中频繁创建/销毁的 小对象 设计(如 task_struct、inode、sk_buff)。它维护了每个对象类型的缓存池,避免反复调用页分配器导致的碎片和性能开销。
2.3.1 核心概念
- cache(缓存):每个对象类型对应一个
kmem_cache,内部包含多个 slab。 - slab:由一个或多个连续物理页组成,被划分为等大小的对象槽位。
- 着色(coloring):通过偏移调整使不同 slab 的对象对齐到不同 cache line,减少伪共享。
2.3.2 自定义 slab 缓存 API
| 函数 | 作用 |
|---|---|
kmem_cache_create(name, size, align, flags, ctor) |
创建一个新的 slab 缓存,指定对象大小、对齐方式和构造函数 |
kmem_cache_alloc(cache, gfp_flags) |
从缓存中分配一个对象 |
kmem_cache_free(cache, obj) |
将对象归还给缓存 |
kmem_cache_destroy(cache) |
销毁缓存(必须确保所有对象已释放) |
2.3.3 驱动中的使用示例
#include <linux/slab.h>
/* 定义驱动私有结构体 */
struct my_dev_priv {
int id;
char name[32];
/* ... */
};
static struct kmem_cache *my_cache;
/* 模块初始化时创建缓存 */
static int __init my_init(void)
{
my_cache = kmem_cache_create("my_priv_cache",
sizeof(struct my_dev_priv),
0, /* 默认对齐 */
SLAB_HWCACHE_ALIGN, /* 硬件缓存行对齐 */
NULL); /* 无构造函数 */
if (!my_cache)
return -ENOMEM;
return 0;
}
/* 分配对象 */
struct my_dev_priv *priv = kmem_cache_alloc(my_cache, GFP_KERNEL);
if (!priv)
return -ENOMEM;
/* 释放对象 */
kmem_cache_free(my_cache, priv);
/* 模块退出时销毁缓存 */
static void __exit my_exit(void)
{
kmem_cache_destroy(my_cache);
}
性能优势:slab 分配器在分配/释放相同大小的对象时,时间复杂度为 O(1),且能显著减少内存碎片。对于驱动中频繁创建的小结构体(如网络 skb、USB URB),强烈建议使用自定义 slab 缓存。
2.4 kmalloc 与 kzalloc:驱动最常用的分配接口
kmalloc 是 slab 分配器的最上层封装,驱动开发者最常接触。它返回内核虚拟地址,分配的内存物理连续(在直接映射区)。kzalloc 是 kmalloc 的零初始化版本,等价于 kmalloc(..., GFP_ZERO)。
2.4.1 函数原型与行为
#include <linux/slab.h>
void *kmalloc(size_t size, gfp_t flags);
void *kzalloc(size_t size, gfp_t flags);
void kfree(const void *objp);
- size:请求分配的字节数。最大限制通常为 4MB(取决于架构和 slab 配置),超过此限制应使用
vmalloc或页分配器。 - flags:GFP 标志,控制分配行为(见下表)。
- 返回值:成功返回虚拟地址,失败返回 NULL。
2.4.2 常用 GFP 标志
| 标志 | 含义 | 使用场景 |
|---|---|---|
GFP_KERNEL |
普通内核分配,可能睡眠 | 进程上下文(系统调用、ioctl) |
GFP_ATOMIC |
原子分配,不会睡眠 | 中断上下文、软中断、自旋锁内 |
GFP_DMA |
分配 DMA 可访问的内存(低端内存) | 传统 ISA DMA 设备 |
GFP_DMA32 |
分配 32 位地址空间可访问的内存 | 32 位 DMA 设备 |
GFP_HIGHUSER |
分配高端内存(用户空间可映射) | 用户态缓冲区分配 |
2.4.3 典型驱动用法
/* 分配 256 字节缓冲区,零初始化 */
char *buf = kzalloc(256, GFP_KERNEL);
if (!buf) {
pr_err("kzalloc failed\n");
return -ENOMEM;
}
/* 使用缓冲区... */
/* 释放 */
kfree(buf);
最佳实践:在驱动中,除非明确需要未初始化的内存(如即将被硬件填充的 DMA 缓冲区),否则始终使用
kzalloc 代替 kmalloc,避免因未初始化数据导致的安全漏洞或难以调试的 bug。
2.5 分配器选择决策树
下面的 SVG 图帮助你在驱动开发中快速选择正确的分配接口:
2.6 关键对比总结
| 特性 | alloc_pages | slab 分配器 | kmalloc / kzalloc |
|---|---|---|---|
| 分配粒度 | 2^order 个物理页(最小 4KB) | 任意大小(由 cache 创建时指定) | 任意大小(最大约 4MB) |
| 物理连续性 | 保证连续 | 保证连续(基于页分配器) | 保证连续(直接映射区) |
| 分配速度 | 较慢(伙伴系统合并/分裂) | O(1),极快 | O(1),极快(基于 slab) |
| 内存碎片 | 容易产生外部碎片 | 无外部碎片,内部碎片可控 | 同 slab 分配器 |
| 适用场景 | 大块连续物理内存、DMA 缓冲区 | 频繁分配/释放的相同大小对象 | 通用小到中等大小内存分配 |
| 释放接口 | __free_pages / free_pages |
kmem_cache_free |
kfree |
常见错误:
- 在中断上下文使用
GFP_KERNEL(会导致睡眠,引发内核 panic)。 - 对
kmalloc返回的地址调用__free_pages(类型不匹配,破坏 slab 元数据)。 - 分配超过 4MB 的内存使用
kmalloc(返回 NULL 或触发警告)。 - 忘记调用
kfree导致内存泄漏(使用kmemleak工具检测)。
2.7 本章小结
- 页分配器 是基石,提供连续物理页,适合大块分配和 DMA 场景。
- slab 分配器 通过对象缓存大幅提升小对象分配效率,驱动中应善用
kmem_cache_create。 - kmalloc / kzalloc 是驱动开发的首选通用接口,简单安全,默认使用
kzalloc避免未初始化数据。 - 选择分配器时遵循 大小 → 频率 → 上下文 的决策路径,确保正确性与性能。