2. 内存分配器基础:页分配器(alloc_pages)、slab分配器、kmalloc与kzalloc

Linux 内核的内存分配体系呈分层结构,从最底层的物理页管理到上层面向对象的缓存分配,每一层都服务于特定的场景。理解 页分配器、slab 分配器 以及 kmalloc / kzalloc 的层级关系与适用边界,是驱动开发者正确管理内存的前提。

2.1 内核内存分配器层级总览

下图展示了从物理页到驱动常用 API 的完整调用链路:

内核内存分配器层级架构 内核内存分配器层级架构 伙伴系统 (Buddy System) 页分配器 (alloc_pages / __get_free_pages) 通用分配器 (kmalloc / kzalloc) slab 分配器 (kmem_cache) 驱动常用 kmalloc/kzalloc → slab 分配器 → 页分配器 → 伙伴系统 kmem_cache_create/kmem_cache_alloc 直接使用 slab 分配器,绕过 kmalloc 封装
核心原则:驱动开发者应优先使用 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 图帮助你在驱动开发中快速选择正确的分配接口:

内存分配器选择决策树 驱动内存分配器选择决策树 需要分配内存? 大小 < 4MB? kmalloc/kzalloc alloc_pages/vmalloc 频繁分配相同大小? 自定义 slab 缓存 kmalloc 即可 提示:中断上下文必须使用 GFP_ATOMIC;DMA 缓冲区需考虑 GFP_DMA/GFP_DMA32 或使用 dma_alloc_coherent

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 避免未初始化数据。
  • 选择分配器时遵循 大小 → 频率 → 上下文 的决策路径,确保正确性与性能。