4. 设备驱动中的内存管理:kmalloc/kfree、vmalloc、DMA内存分配与一致性
在Linux设备驱动开发中,内存管理是决定系统稳定性与性能的核心环节。驱动代码运行在内核空间,无法直接使用用户态的malloc/free,必须通过内核提供的内存分配API来获取和释放内存。本章将系统讲解三种最常用的内核内存分配机制:kmalloc/kfree(物理连续、小内存)、vmalloc(虚拟连续、大内存)以及DMA内存分配与一致性管理(外设直接访问)。
核心原则:驱动开发者必须清楚每种分配方式的物理连续性、内存大小限制、执行上下文(是否可睡眠)以及适用场景。错误的内存分配策略会导致系统性能下降、内存碎片甚至内核崩溃。
4.1 内核内存分配全景图
下图展示了内核中不同内存分配API的层次关系与适用区间,帮助开发者快速定位所需接口。
4.2 kmalloc / kfree:物理连续内存分配
kmalloc 是驱动开发中最常用的内存分配函数,它保证分配的内存物理地址连续,这对于需要直接与硬件交互(如寄存器访问、DMA描述符)的场景至关重要。
4.2.1 函数原型与标志位
#include <linux/slab.h>
void *kmalloc(size_t size, gfp_t flags);
void kfree(const void *objp);
| 参数/标志 | 说明 | 典型值 |
|---|---|---|
| size | 请求分配的字节数,最大通常不超过 4MB(受限于SLUB分配器) |
32, 64, 128, 256, 1024 ... |
| GFP_KERNEL | 标准分配,可能睡眠,只能在进程上下文使用 | 最常用 |
| GFP_ATOMIC | 原子分配,不会睡眠,可在中断上下文使用 | 中断处理、自旋锁内 |
| GFP_DMA | 分配可用于DMA的内存(低端内存,ZONE_DMA) | DMA缓冲区 |
| __GFP_ZERO | 返回零初始化内存,常与上述标志组合 | GFP_KERNEL | __GFP_ZERO |
常见错误:在中断处理函数或持有自旋锁的代码路径中使用
GFP_KERNEL 会导致内核调度,引发死锁。此时必须使用 GFP_ATOMIC。
4.2.2 典型使用模式
#include <linux/slab.h>
#include <linux/device.h>
struct my_device_data {
int id;
char name[64];
unsigned long flags;
};
static int __init my_driver_init(void)
{
struct my_device_data *data;
/* 分配并零初始化结构体 */
data = kmalloc(sizeof(*data), GFP_KERNEL | __GFP_ZERO);
if (!data) {
pr_err("Failed to allocate memory\n");
return -ENOMEM;
}
data->id = 42;
snprintf(data->name, sizeof(data->name), "my_device_0");
/* 使用 data ... */
/* 释放内存 */
kfree(data);
return 0;
}
4.2.3 kmalloc 与 kzalloc / kcalloc 的关系
| 函数 | 等价形式 | 适用场景 |
|---|---|---|
kzalloc(size, flags) |
kmalloc(size, flags | __GFP_ZERO) |
需要零初始化内存 |
kcalloc(n, size, flags) |
kmalloc(n * size, flags | __GFP_ZERO) |
分配数组,自动处理乘法溢出 |
krealloc(ptr, size, flags) |
重新调整已分配内存大小 | 动态增长缓冲区 |
4.3 vmalloc / vfree:虚拟连续内存分配
vmalloc 分配的内存在虚拟地址空间连续,但物理页面可能不连续。它适用于需要大块内存(超过kmalloc限制)但不需要物理连续性的场景,如大型软件缓冲区、模块加载等。
4.3.1 函数原型与特点
#include <linux/vmalloc.h>
void *vmalloc(unsigned long size);
void vfree(const void *addr);
/* 变体 */
void *vzalloc(unsigned long size); /* 零初始化 */
void *__vmalloc(unsigned long size, gfp_t gfp_mask, pgprot_t prot);
| 特性 | kmalloc | vmalloc |
|---|---|---|
| 物理连续性 | 物理地址连续 | 物理地址不连续 |
| 最大大小 | 通常 < 4MB | 可达数百MB(受限于虚拟地址空间) |
| 执行上下文 | GFP_KERNEL可睡眠;GFP_ATOMIC不可睡眠 | 总是可能睡眠(分配页表) |
| 性能 | 快速(SLUB缓存) | 较慢(需要建立页表映射) |
| 适用场景 | 小数据结构、DMA缓冲区、寄存器映射 | 大块软件缓冲区、模块加载 |
4.3.2 使用示例
#include <linux/vmalloc.h>
#define BUF_SIZE (16 * 1024 * 1024) /* 16MB */
static char *large_buffer;
static int __init my_vmalloc_init(void)
{
large_buffer = vmalloc(BUF_SIZE);
if (!large_buffer) {
pr_err("vmalloc failed for %d bytes\n", BUF_SIZE);
return -ENOMEM;
}
/* 使用缓冲区,注意:不能直接用于DMA! */
memset(large_buffer, 0, BUF_SIZE);
return 0;
}
static void __exit my_vmalloc_exit(void)
{
if (large_buffer)
vfree(large_buffer);
}
性能提示:vmalloc 分配的内存不能直接用于 DMA 操作,因为 DMA 需要物理连续地址。如果需要大块 DMA 缓冲区,应使用
dma_alloc_coherent 或 __get_free_pages。
4.4 DMA内存分配与一致性管理
DMA(Direct Memory Access)允许外设直接读写系统内存,无需CPU逐字节搬运。驱动开发者必须使用专门的DMA API来分配和管理内存,以确保缓存一致性和物理连续性。
4.4.1 DMA内存分配核心API
#include <linux/dma-mapping.h>
/* 分配一致性DMA缓冲区(推荐) */
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag);
void dma_free_coherent(struct device *dev, size_t size,
void *cpu_addr, dma_addr_t dma_handle);
/* 分配流式DMA缓冲区(需手动同步) */
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
size_t size, enum dma_data_direction dir);
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr,
size_t size, enum dma_data_direction dir);
4.4.2 一致性DMA vs 流式DMA
| 特性 | 一致性DMA (Coherent) | 流式DMA (Streaming) |
|---|---|---|
| 缓存同步 | 硬件自动维护一致性(或禁用缓存) | 需要手动调用 dma_sync_single_for_cpu/device |
| 分配开销 | 较大(建立一致性映射) | 较小(复用已有内存) |
| 使用场景 | DMA描述符环、控制寄存器、长期共享缓冲区 | 网络数据包、块设备IO、一次性传输 |
| API | dma_alloc_coherent |
dma_map_single / dma_map_sg |
4.4.3 DMA数据传输方向
enum dma_data_direction {
DMA_BIDIRECTIONAL = 0, /* 双向传输 */
DMA_TO_DEVICE = 1, /* CPU -> 设备 */
DMA_FROM_DEVICE = 2, /* 设备 -> CPU */
DMA_NONE = 3, /* 无传输 */
};
4.4.4 完整DMA驱动示例
#include <linux/dma-mapping.h>
#include <linux/slab.h>
struct dma_example_dev {
struct device *dev;
void *dma_buf; /* CPU虚拟地址 */
dma_addr_t dma_handle; /* 设备可见的DMA地址 */
size_t buf_size;
};
static int dma_example_setup(struct dma_example_dev *priv)
{
priv->buf_size = 4096; /* 4KB DMA缓冲区 */
/* 分配一致性DMA缓冲区 */
priv->dma_buf = dma_alloc_coherent(priv->dev, priv->buf_size,
&priv->dma_handle, GFP_KERNEL);
if (!priv->dma_buf) {
dev_err(priv->dev, "Failed to allocate DMA coherent buffer\n");
return -ENOMEM;
}
/* 初始化缓冲区内容 */
memset(priv->dma_buf, 0, priv->buf_size);
dev_info(priv->dev, "DMA buf: CPU=%p, DMA=0x%llx, size=%zu\n",
priv->dma_buf, (unsigned long long)priv->dma_handle,
priv->buf_size);
/* 将DMA地址写入硬件寄存器,启动传输 */
// writel(priv->dma_handle, priv->regs + DMA_ADDR_REG);
// writel(priv->buf_size, priv->regs + DMA_SIZE_REG);
// writel(1, priv->regs + DMA_START_REG);
return 0;
}
static void dma_example_cleanup(struct dma_example_dev *priv)
{
if (priv->dma_buf) {
dma_free_coherent(priv->dev, priv->buf_size,
priv->dma_buf, priv->dma_handle);
priv->dma_buf = NULL;
}
}
4.4.5 DMA一致性管理流程图
4.5 三种内存分配方式对比总结
| 对比维度 | kmalloc | vmalloc | dma_alloc_coherent |
|---|---|---|---|
| 物理连续 | ✅ 是 | ❌ 否 | ✅ 是 |
| 虚拟连续 | ✅ 是 | ✅ 是 | ✅ 是 |
| 最大大小 | ~4MB | ~VMALLOC_SIZE | 取决于ZONE_DMA大小 |
| 可睡眠 | 取决于flags | 总是 | 取决于flags |
| 缓存一致性 | 需手动处理 | 需手动处理 | 自动维护 |
| DMA可用 | ✅ 是(GFP_DMA) | ❌ 否 | ✅ 是(专用) |
| 释放函数 | kfree | vfree | dma_free_coherent |
选型建议:
- 小数据结构(< 1MB)且需要物理连续 → kmalloc
- 大块软件缓冲区(> 4MB)且不需要物理连续 → vmalloc
- 外设DMA传输缓冲区 → dma_alloc_coherent
- 中断上下文分配 → kmalloc(GFP_ATOMIC)
4.6 常见陷阱与调试技巧
- 内存泄漏:每次
kmalloc/vmalloc/dma_alloc_coherent必须有对应的kfree/vfree/dma_free_coherent,建议在错误路径和模块卸载路径中统一释放。 - 越界访问:内核没有像用户态那样的段错误保护,越界写会破坏内核内存,导致随机崩溃。使用
kasan(Kernel Address Sanitizer)在调试时开启检测。 - DMA地址截断:某些32位设备只能访问32位DMA地址,必须使用
dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))限制地址范围。 - 缓存一致性问题:使用流式DMA时,必须在CPU访问前调用
dma_sync_single_for_cpu,在设备访问前调用dma_sync_single_for_device。
/* 流式DMA同步示例 */
/* 设备写入完成后,CPU读取前 */
dma_sync_single_for_cpu(dev, dma_handle, size, DMA_FROM_DEVICE);
/* 读取数据 */
process_data(cpu_buf, size);
/* CPU修改完成后,设备再次读取前 */
dma_sync_single_for_device(dev, dma_handle, size, DMA_FROM_DEVICE);
/* 通知硬件继续传输 */
关键提醒:在设备驱动中,
dma_alloc_coherent 分配的内存默认是non-cacheable(或使用硬件一致性协议),因此CPU访问速度可能比普通kmalloc内存慢。对于高频访问的DMA缓冲区,可以考虑使用流式DMA + 手动同步来获得更好的性能。