3. 内存分配API(上):kmalloc、kzalloc、kcalloc、krealloc的使用场景、GFP标志位详解

在Linux设备驱动开发中,内核态的内存分配与用户态有着本质区别。驱动无法使用malloc或free,必须通过内核提供的专用API来申请和释放内存。本章聚焦于最常用的四个分配函数——kmalloc、kzalloc、kcalloc、krealloc,并深入解析决定分配行为的GFP标志位。

3.1 核心分配函数族

这四个函数均分配物理连续的内存,适用于DMA缓冲区、设备I/O寄存器映射、协议控制块等场景。它们的原型定义在<linux/slab.h>中。

函数 原型 核心特性 典型场景
kmalloc void *kmalloc(size_t size, gfp_t flags) 分配size字节,内容未初始化 通用内核缓冲区、临时数据结构
kzalloc void *kzalloc(size_t size, gfp_t flags) 分配并清零 需要零初始化结构体、安全敏感数据
kcalloc void *kcalloc(size_t n, size_t size, gfp_t flags) 分配n*size字节,自动检测溢出,清零 数组分配、元素计数明确的批量结构
krealloc void *krealloc(const void *p, size_t new_size, gfp_t flags) 调整已分配内存大小,保留原内容 动态增长缓冲区、协议栈重配置
最佳实践:优先使用kzalloc而非kmalloc + memset,前者在单次调用中完成分配与清零,性能更优且不易遗漏初始化。对于数组分配,kcalloc比kzalloc(n * size, flags)更安全,因为它内置了乘法溢出检查。

3.1.1 代码示例:四种API的基本用法

#include <linux/slab.h>
#include <linux/device.h>

struct my_dev {
    u32 id;
    char name[32];
    void __iomem *regs;
};

/* 1. kmalloc:分配原始内存 */
struct my_dev *dev1 = kmalloc(sizeof(*dev1), GFP_KERNEL);
if (!dev1)
    return -ENOMEM;
/* 注意:dev1指向的内存内容未定义,必须手动初始化 */

/* 2. kzalloc:分配并清零 */
struct my_dev *dev2 = kzalloc(sizeof(*dev2), GFP_KERNEL);
if (!dev2)
    return -ENOMEM;
/* dev2->id == 0, dev2->name全零,安全可靠 */

/* 3. kcalloc:分配数组 */
struct my_dev **dev_array = kcalloc(10, sizeof(*dev_array), GFP_KERNEL);
if (!dev_array)
    return -ENOMEM;
/* 自动处理 10 * sizeof(指针) 的乘法溢出 */

/* 4. krealloc:扩展缓冲区 */
u8 *buf = kmalloc(128, GFP_KERNEL);
if (!buf)
    return -ENOMEM;
u8 *new_buf = krealloc(buf, 256, GFP_KERNEL);
if (!new_buf) {
    kfree(buf);  /* krealloc失败时原指针仍有效,需手动释放 */
    return -ENOMEM;
}
/* 成功后,buf指针失效,应使用new_buf */
buf = new_buf;
关键陷阱:krealloc失败时不会释放原内存,调用者必须检查返回值并手动kfree原指针。这与用户态realloc的行为一致,但容易被忽略。

3.2 GFP标志位详解

GFP(Get Free Page)标志位是内存分配行为的“控制寄存器”,它决定了分配器在内存紧张时的行为、允许使用的内存区域、以及是否允许阻塞。标志位通过位掩码组合使用,常见形式为GFP_KERNEL、GFP_ATOMIC等。

3.2.1 三大核心标志位族

标志族 标志位 含义 影响
行为修饰 __GFP_WAIT 允许分配器睡眠等待内存 仅在进程上下文可用,中断/自旋锁中禁止
__GFP_IO 允许分配器发起磁盘I/O以回收内存 文件系统、块设备驱动常用
__GFP_FS 允许分配器调用文件系统操作 与__GFP_IO配合,避免递归锁
区域修饰 __GFP_DMA 从ZONE_DMA(低16MB)分配 ISA设备、某些DMA控制器必须使用
__GFP_HIGHMEM 允许从高端内存分配 仅映射到内核虚拟地址空间,用户态不可见
__GFP_DMA32 从ZONE_DMA32(低4GB)分配 64位系统上32位DMA设备
紧急修饰 __GFP_HIGH 允许访问紧急保留内存 中断处理程序、原子上下文
__GFP_NOWARN 分配失败时不打印警告 试探性分配、非关键路径
__GFP_ZERO 分配后立即清零 kzalloc内部使用此标志

3.2.2 常用组合标志

内核预定义了若干组合标志,覆盖绝大多数使用场景:

组合标志 等价位掩码 允许睡眠 典型上下文
GFP_KERNEL __GFP_WAIT | __GFP_IO | __GFP_FS 是 进程上下文、系统调用、文件操作
GFP_ATOMIC __GFP_HIGH 否 中断处理、tasklet、自旋锁内
GFP_DMA __GFP_DMA 是(可组合) ISA DMA缓冲区、旧式硬件
GFP_KERNEL | GFP_DMA 组合 是 需要DMA区域的进程上下文分配
GFP_NOWAIT __GFP_NOWARN 否 快速试探、非关键路径
黄金法则:在中断上下文、软中断、tasklet、持有自旋锁时,必须使用GFP_ATOMIC。在进程上下文且允许睡眠时,始终优先使用GFP_KERNEL,它提供最积极的回收策略,成功率最高。

3.3 GFP标志位选择流程图

下面的SVG流程图展示了驱动开发中GFP标志位的决策路径,帮助你在不同上下文中快速选择正确的标志位。

GFP标志位选择决策流程 开始分配内存 原子上下文? (中断/自旋锁/tasklet) GFP_ATOMIC 需要DMA区域? (ISA/旧式DMA) GFP_DMA 允许睡眠? (进程上下文) GFP_KERNEL GFP_NOWAIT 说明: • GFP_KERNEL 是默认首选,支持睡眠和文件系统回收,成功率最高。 • GFP_ATOMIC 用于不可睡眠上下文,只能使用紧急保留内存,失败率较高。

3.4 实际驱动场景中的GFP选择

不同的驱动模块和上下文对GFP标志位有严格要求。下表总结了常见场景的推荐选择:

驱动场景 上下文 推荐GFP标志 原因
字符设备open()回调 进程上下文 GFP_KERNEL 允许睡眠,分配大块内存
中断处理程序 原子上下文 GFP_ATOMIC 不可睡眠,使用紧急池
tasklet/软中断 原子上下文 GFP_ATOMIC 与中断相同限制
DMA缓冲区(ISA设备) 进程上下文 GFP_KERNEL | GFP_DMA 需要低16MB物理内存
DMA缓冲区(64位设备) 进程上下文 GFP_KERNEL | GFP_DMA32 需要低4GB物理内存
热插拔探测 进程上下文 GFP_KERNEL 标准分配,可睡眠
定时器回调 原子上下文 GFP_ATOMIC 定时器运行在软中断中
系统调用快速路径 进程上下文 GFP_KERNEL 性能关键,但允许睡眠

3.5 内存释放与调试

所有通过上述API分配的内存,都必须使用kfree释放:

void kfree(const void *ptr);
重要规则:
  • 必须使用与分配时相同的指针值调用kfree,不能偏移。
  • 释放后应将指针置为NULL,防止悬空指针。
  • 在错误处理路径中,确保所有已分配的内存都被释放,避免泄漏。
  • 可以使用kmemleak内核调试工具检测未释放的内存。

3.6 本章小结

  • kmalloc:基础分配,内容未初始化,适合需要手动填充的场景。
  • kzalloc:分配并清零,推荐用于结构体和安全敏感数据。
  • kcalloc:数组分配,内置溢出保护,适合批量对象。
  • krealloc:动态调整大小,注意失败时原指针仍有效。
  • GFP_KERNEL:进程上下文首选,支持睡眠和回收。
  • GFP_ATOMIC:原子上下文唯一选择,成功率较低。
  • __GFP_DMA / __GFP_DMA32:用于特定物理地址范围的DMA缓冲区。