4. 设备驱动中的内存管理:kmalloc/kfree、vmalloc、DMA内存分配与一致性

在Linux设备驱动开发中,内存管理是决定系统稳定性与性能的核心环节。驱动代码运行在内核空间,无法直接使用用户态的malloc/free,必须通过内核提供的内存分配API来获取和释放内存。本章将系统讲解三种最常用的内核内存分配机制:kmalloc/kfree(物理连续、小内存)、vmalloc(虚拟连续、大内存)以及DMA内存分配与一致性管理(外设直接访问)。

核心原则:驱动开发者必须清楚每种分配方式的物理连续性、内存大小限制、执行上下文(是否可睡眠)以及适用场景。错误的内存分配策略会导致系统性能下降、内存碎片甚至内核崩溃。

4.1 内核内存分配全景图

下图展示了内核中不同内存分配API的层次关系与适用区间,帮助开发者快速定位所需接口。

Linux内核内存分配API全景图 用户态:malloc / calloc / realloc / free 系统调用:brk / mmap / sbrk 内核态内存分配器 kmalloc / kfree 物理连续,< 4MB vmalloc / vfree 虚拟连续,可大块 DMA分配API dma_alloc_coherent 选择依据:物理连续性需求、内存大小、是否允许睡眠、DMA外设要求

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一致性管理流程图

DMA一致性管理流程 dma_alloc_coherent() 返回 CPU虚拟地址 + DMA总线地址 物理连续,缓存一致性已处理 将DMA地址写入硬件寄存器 启动DMA传输(硬件自动执行) DMA完成中断 → CPU直接读取数据 dma_free_coherent() 释放

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 + 手动同步来获得更好的性能。