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 时,内核按以下流程分配内存:

  1. 大小对齐与缓存查找: 将请求的 size 向上对齐到 2 的幂次或预定义尺寸,查找对应的 kmem_cache(如 kmalloc-64、kmalloc-128)。
  2. 从 slab 缓存分配: 从对应缓存的空闲对象链表(slab 中的空闲列表)中取出一个对象。
  3. 缓存未命中处理: 如果当前 slab 已耗尽,slab 分配器调用页面分配器(alloc_pages)分配新的页面,并将其分割成多个对象加入缓存。
  4. 返回虚拟地址: 将对象的内核虚拟地址返回给调用者。
性能优势: slab 分配器通过对象复用、着色(coloring)和每 CPU 缓存(per-CPU cache)大幅减少了内存碎片和分配延迟。kmalloc 在大多数情况下的分配时间在纳秒级别。

3.4 kmalloc 与 slab 分配器关系图

下图展示了 kmalloc 从调用到最终从 slab 分配器获取内存的完整路径:

kmalloc 与 slab 分配器关系图 kmalloc 调用 → slab 分配器 → 页面分配器 驱动调用 kmalloc(size, flags) 大小对齐 → 查找 kmem_cache slab 有空闲对象? 是 从空闲链表取对象 否 调用 alloc_pages 分配新页面 分割页面 → 加入 slab 缓存 返回虚拟地址给驱动 说明 • kmalloc 不直接操作页面分配器 • 大部分分配命中 slab 缓存 • 只有缓存耗尽才触发 alloc_pages

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 或悬空指针。