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() |
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 可以避免未初始化字段导致的安全漏洞。
以下 SVG 展示了 kmalloc 在内核内存层级中的位置与分配流程:
2.3 vmalloc:大块虚拟连续内存
vmalloc 分配虚拟地址连续但物理地址可能不连续的内存区域。它通过修改内核页表建立映射,适合需要大块内存但不需要物理连续性的场景,如模块加载、大缓冲区、视频帧缓存等。
#include <linux/vmalloc.h>
void *vmalloc(unsigned long size);
void vfree(const void *addr);
以下 SVG 对比了 kmalloc 与 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)
2.5 场景选择决策树
以下 SVG 决策图帮助开发者在实际编码中快速选择正确的分配 API:
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; // 潜在的空指针解引用