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标志位的决策路径,帮助你在不同上下文中快速选择正确的标志位。
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缓冲区。