3. kmalloc 基础:函数原型、内存分配大小限制与 slab 分配器
kmalloc 是 Linux 内核中最常用的内存分配函数,用于在进程上下文或中断上下文中分配物理连续的内存块。与用户空间的 malloc 不同,kmalloc 直接操作内核的物理内存映射,并依赖 slab 分配器来管理底层内存对象。理解 kmalloc 的函数原型、分配大小限制以及 slab 分配器的工作原理,是掌握内核驱动内存管理的第一步。
3.1 kmalloc 函数原型与参数
kmalloc 的函数原型定义在 <linux/slab.h> 中,其核心签名如下:
#include <linux/slab.h>
void *kmalloc(size_t size, gfp_t flags);
| 参数 | 类型 | 说明 |
|---|---|---|
| size | size_t | 请求分配的字节数。内核 slab 分配器会将其向上对齐到最接近的 slab 缓存对象大小。 |
| flags | gfp_t | 内存分配标志,控制分配行为:是否允许睡眠、是否允许 I/O、是否允许高优先级回收等。 |
| 返回值 | void * | 成功时返回分配到的虚拟地址(物理连续);失败时返回 NULL。 |
关键提示: kmalloc 返回的地址是内核虚拟地址(直接映射区或 vmalloc 区),但保证物理连续。驱动必须始终检查返回值是否为 NULL,尤其是在原子上下文中调用时。
3.2 内存分配大小限制
kmalloc 并非可以分配任意大小的内存。其大小受限于 slab 分配器预定义的缓存对象尺寸以及底层页面分配器(page allocator)的能力。下表列出了常见的分配大小限制与行为:
| 分配大小范围 | 分配来源 | 最大限制 | 说明 |
|---|---|---|---|
| 8 B ~ 8 KB | slab 通用缓存(kmalloc-8 ~ kmalloc-8k) | 8 KB | 最常用范围,分配效率最高,碎片最少。 |
| 8 KB ~ 4 MB | 页面分配器(直接分配 2^order 个连续页面) | 4 MB(order=10) | 超过 8 KB 时 kmalloc 内部转为 alloc_pages,物理连续要求高。 |
| > 4 MB | 不推荐使用 kmalloc | 无保证 | 分配大块连续物理内存容易失败,应使用 vmalloc 或 CMA。 |
避坑指南: 在 32 位系统上,kmalloc 最大分配大小通常限制在 128 KB 左右(受低端内存限制)。在 64 位系统上,虽然理论上可达 4 MB,但实际分配超过 1 MB 时失败率显著上升。驱动应避免在原子上下文中分配超过 32 KB 的内存。
3.3 从 slab 分配器分配内存
kmalloc 的底层实现依赖于 slab 分配器。slab 分配器维护了一组预分配的缓存(cache),每个缓存管理固定大小的对象。当驱动调用 kmalloc 时,内核按以下流程分配内存:
- 大小对齐与缓存查找: 将请求的 size 向上对齐到 2 的幂次或预定义尺寸,查找对应的 kmem_cache(如 kmalloc-64、kmalloc-128)。
- 从 slab 缓存分配: 从对应缓存的空闲对象链表(slab 中的空闲列表)中取出一个对象。
- 缓存未命中处理: 如果当前 slab 已耗尽,slab 分配器调用页面分配器(alloc_pages)分配新的页面,并将其分割成多个对象加入缓存。
- 返回虚拟地址: 将对象的内核虚拟地址返回给调用者。
性能优势: slab 分配器通过对象复用、着色(coloring)和每 CPU 缓存(per-CPU cache)大幅减少了内存碎片和分配延迟。kmalloc 在大多数情况下的分配时间在纳秒级别。
3.4 kmalloc 与 slab 分配器关系图
下图展示了 kmalloc 从调用到最终从 slab 分配器获取内存的完整路径:
3.5 常用 gfp_t 标志速查
gfp_t 标志决定了 kmalloc 的行为上下文。以下是最常用的标志组合:
| 标志 | 含义 | 典型使用场景 |
|---|---|---|
| GFP_KERNEL | 允许睡眠、允许 I/O、允许回收页面 | 进程上下文,非原子路径 |
| GFP_ATOMIC | 不允许睡眠,立即返回 | 中断处理程序、软中断、自旋锁持有中 |
| GFP_DMA | 分配 DMA 可访问的内存(低 16 MB 或 32 位地址空间) | 传统 ISA DMA 设备 |
| GFP_DMA32 | 分配 32 位地址空间内的内存 | 64 位系统上的 32 位 DMA 设备 |
| GFP_HIGHUSER | 优先分配高端内存,允许用户空间映射 | 大块用户空间缓冲区 |
最佳实践: 在驱动 probe 函数或工作队列中始终使用 GFP_KERNEL;在中断上下文或持有自旋锁时使用 GFP_ATOMIC。错误使用 GFP_KERNEL 在原子上下文中会导致内核调度异常(kernel scheduling while atomic)。
3.6 kmalloc 与 kfree 配对规则
kmalloc 分配的内存必须通过 kfree 释放,且必须遵循以下规则:
- 释放原指针: 必须传入 kmalloc 返回的原始指针,不能偏移。
- 不要重复释放: 对同一指针调用两次 kfree 会导致内核崩溃(double free)。
- 释放后置 NULL: 建议在 kfree(ptr) 后立即将 ptr = NULL,防止悬空指针。
- 大小无关: kfree 不需要传入 size 参数,slab 分配器通过对象描述符自动获取大小。
// 正确示例
void *buf = kmalloc(256, GFP_KERNEL);
if (!buf) {
pr_err("kmalloc failed\n");
return -ENOMEM;
}
// ... 使用 buf ...
kfree(buf);
buf = NULL; // 防止悬空指针
// 错误示例:释放偏移后的指针
void *buf = kmalloc(256, GFP_KERNEL);
kfree(buf + 16); // 内核崩溃!
3.7 本章小结
kmalloc 是内核驱动最基础的内存分配接口,其核心要点包括:
- 函数原型为
void *kmalloc(size_t size, gfp_t flags),返回物理连续内存。 - 分配大小上限通常为 4 MB(64 位系统),但超过 128 KB 时失败风险增加。
- 底层依赖 slab 分配器,通过预缓存对象实现高效分配。
- gfp_t 标志决定分配行为,GFP_KERNEL 和 GFP_ATOMIC 是最常用的两个标志。
- 必须使用 kfree 释放,且遵循配对规则,避免 double free 或悬空指针。