2. 内核内存分配API

内核模块开发中,内存管理是核心基础。Linux 内核提供了多种内存分配接口,每种接口在设计目标、物理连续性、内存大小、性能开销和使用场景上存在显著差异。错误选择分配器是导致内存泄漏、系统碎片化和性能下降的常见根源。本节将系统梳理四种最常用的内核内存分配 API,并通过对比表格和结构图帮助开发者建立清晰的选择依据。

2.1 核心分配器概览

以下表格从关键维度对比了四种 API 的差异,开发者应熟记于心:

特性 kmalloc kzalloc vmalloc __get_free_pages
物理连续性 物理连续 物理连续 虚拟连续,物理可能不连续 物理连续
分配大小上限 通常 < 4MB(取决于 slab 配置) 同 kmalloc 较大,可达数百 MB 最大 4MB(2^10 页,4KB/页)
分配粒度 字节级(最小 8/16 字节) 字节级 页级(4096 字节对齐) 页级(2^order 页)
性能 高(slab 缓存) 高(slab 缓存 + 清零) 较低(页表操作 + TLB 刷新) 高(直接伙伴系统)
清零行为 不保证清零 分配时自动清零 不保证清零 不保证清零
适用场景 小对象、驱动数据结构、DMA 缓冲区 需要初始化为 0 的数据结构 大块内存、模块加载、视频帧缓冲区 页对齐的大块连续内存
GFP 标志 必须指定 必须指定 GFP_KERNEL 等 必须指定
释放函数 kfree() kfree() vfree() free_pages()
选择口诀:小对象用 kmalloc,要清零用 kzalloc,大块不连续用 vmalloc,页对齐大连续用 __get_free_pages。DMA 缓冲区必须使用物理连续内存,优先考虑 kmalloc 或 __get_free_pages。

2.2 kmalloc 与 kzalloc:驱动开发的主力

kmalloc 是基于 slab 分配器的内核内存分配函数,分配的内存物理连续,适合驱动中大多数数据结构、缓冲区、设备描述符等小对象。其原型为:

#include <linux/slab.h>

void *kmalloc(size_t size, gfp_t flags);
void kfree(const void *ptr);

kzalloc 是 kmalloc 的零初始化版本,等价于 kmalloc(..., flags | __GFP_ZERO)。在分配结构体、链表头、互斥锁等需要初始化为 0 的场景中,使用 kzalloc 可以避免未初始化字段导致的安全漏洞。

常见陷阱:kmalloc 分配的内存不保证清零,返回的内存可能包含之前其他模块使用的敏感数据。如果用于存储用户态数据或密码密钥,务必使用 kzalloc 或手动 memset 清零。

以下 SVG 展示了 kmalloc 在内核内存层级中的位置与分配流程:

kmalloc 分配流程与内核内存层级 用户空间进程 系统调用接口 内核模块 / 设备驱动 kmalloc(size, GFP_KERNEL) Slab 分配器(缓存命中) 伙伴系统(缓存未命中) 物理内存(连续页框)

2.3 vmalloc:大块虚拟连续内存

vmalloc 分配虚拟地址连续但物理地址可能不连续的内存区域。它通过修改内核页表建立映射,适合需要大块内存但不需要物理连续性的场景,如模块加载、大缓冲区、视频帧缓存等。

#include <linux/vmalloc.h>

void *vmalloc(unsigned long size);
void vfree(const void *addr);
性能注意:vmalloc 分配时需要进行页表操作和 TLB 刷新,开销远大于 kmalloc。在中断上下文或性能关键路径中禁止使用 vmalloc。此外,vmalloc 分配的内存不能直接用于 DMA 操作,因为 DMA 需要物理连续地址。

以下 SVG 对比了 kmalloc 与 vmalloc 的物理内存布局差异:

kmalloc vs vmalloc 物理内存布局对比 kmalloc(物理连续) 虚拟地址空间 物理地址空间(连续页框) 页0 页1 页2 页3 vmalloc(虚拟连续,物理离散) 虚拟地址空间(连续) 物理地址空间(不连续) 页A 页B 页C 页D vmalloc 通过页表映射将离散物理页框组合成连续虚拟地址,但每次访问需查页表

2.4 __get_free_pages:页级连续大块内存

__get_free_pages 直接从伙伴系统分配 2^order 个连续物理页框,返回虚拟地址。它适合需要页对齐、物理连续的大块内存场景,如 DMA 缓冲区、页表分配等。

#include <linux/gfp.h>

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);

// 辅助宏
#define alloc_pages(gfp_mask, order) \
    alloc_pages_node(numa_node_id(), gfp_mask, order)
#define __free_page(page) put_page(page)
order 限制:order 最大值为 10(即 2^10 = 1024 页,约 4MB)。order 越大,分配成功率越低,因为伙伴系统需要连续的空闲页块。在内存碎片化严重的系统中,order > 3 的分配很容易失败。建议优先使用 kmalloc,仅在明确需要页对齐且大小超过 kmalloc 上限时使用 __get_free_pages。

2.5 场景选择决策树

以下 SVG 决策图帮助开发者在实际编码中快速选择正确的分配 API:

内核内存分配 API 选择决策树 需要分配内存 大小 < 1MB 且需要物理连续? 是 需要清零? 是 kzalloc 否 kmalloc 否 需要物理连续? 是 __get_free_pages 否 vmalloc 关键约束提醒 • DMA 缓冲区:必须物理连续,优先 kmalloc,大块用 __get_free_pages • 中断上下文:只能用 GFP_ATOMIC,禁止 vmalloc • 大块缓存(>1MB):优先 vmalloc,避免伙伴系统碎片化

2.6 GFP 标志位详解

所有分配 API 都需要指定 GFP(Get Free Pages)标志,它决定了分配行为:是否允许睡眠、是否允许 I/O、是否允许内核回收内存等。以下是驱动开发中最常用的 GFP 标志:

GFP 标志 含义 允许睡眠 适用上下文
GFP_KERNEL 标准内核分配,可能阻塞 是 进程上下文(驱动 ioctl、读写、open/close)
GFP_ATOMIC 原子分配,不睡眠 否 中断上下文、软中断、自旋锁持有中
GFP_DMA 分配 DMA 区域内存(低 16MB) 取决于组合 ISA DMA 设备、旧硬件
GFP_DMA32 分配 32 位 DMA 地址内存 取决于组合 32 位 DMA 设备
GFP_HIGHUSER 分配高端内存(用户空间可映射) 是 用户态缓冲区分配
GFP_NOIO 禁止 I/O 操作 是 块设备层避免递归
GFP_NOFS 禁止文件系统操作 是 文件系统内部避免递归
最佳实践:在进程上下文中始终使用 GFP_KERNEL;在中断上下文或持有自旋锁时使用 GFP_ATOMIC。错误使用 GFP_KERNEL 在中断上下文中会导致内核崩溃(睡眠在原子上下文中)。

2.7 常见错误与调试建议

  • 错误 1:使用 kmalloc 分配超过 4MB 的内存,导致分配失败。应改用 vmalloc 或分多次分配。
  • 错误 2:在中断上下文中使用 vmalloc,导致内核调度异常。中断上下文只能使用 kmalloc(..., GFP_ATOMIC)。
  • 错误 3:使用 kfree 释放 vmalloc 分配的内存,导致内核崩溃。必须配对使用 vfree。
  • 错误 4:忘记检查分配返回值是否为 NULL。所有分配 API 在内存不足时都可能返回 NULL,必须检查。
// 正确示例:带错误检查的 kmalloc 使用
struct my_device *dev;

dev = kmalloc(sizeof(*dev), GFP_KERNEL);
if (!dev) {
    pr_err("Failed to allocate my_device\n");
    return -ENOMEM;
}
// 使用 dev...
kfree(dev);

// 错误示例:未检查返回值
struct my_device *dev = kmalloc(sizeof(*dev), GFP_KERNEL);
// 如果 dev 为 NULL,以下访问将导致内核崩溃
dev->id = 0;  // 潜在的空指针解引用