4. 驱动中的内存管理:kmalloc/kfree、vmalloc、内存屏障、DMA一致性内存与流式映射

设备驱动运行在内核空间,其内存管理直接关系到系统的稳定性与性能。与用户空间不同,内核不能依赖虚拟内存的按需换页,且必须处理物理地址连续、DMA一致性、内存访问顺序等特殊需求。本章将系统讲解驱动开发中最核心的内存管理接口与工程实践。

核心认知: 用户空间内存管理关注“够用”,内核空间内存管理关注“正确”。物理连续性、DMA一致性、内存屏障是驱动开发者必须跨越的三道门槛。

4.1 内核内存分配:kmalloc vs vmalloc

内核提供两套主要的内存分配接口,其根本区别在于物理地址是否连续以及分配范围。

特性 kmalloc / kfree vmalloc / vfree
物理连续性 物理地址连续 物理地址不连续(虚拟地址连续)
分配速度 快(slab 缓存) 慢(页表操作)
最大分配量 通常 < 4 MB(受限于连续物理内存) 可达数百 MB
适用场景 DMA 缓冲区、小数据结构、中断上下文 大块软件缓冲区、模块加载
中断上下文可用 是(使用 GFP_ATOMIC) 否(可能睡眠)
// kmalloc 典型用法:分配 DMA 描述符
struct dma_desc *desc;
desc = kmalloc(sizeof(*desc), GFP_KERNEL | GFP_DMA);
if (!desc)
    return -ENOMEM;
// ... 使用 desc ...
kfree(desc);

// vmalloc 典型用法:分配大块软件缓冲区
void *large_buf = vmalloc(16 * 1024 * 1024);  // 16 MB
if (!large_buf)
    return -ENOMEM;
// ... 使用 large_buf ...
vfree(large_buf);
工程陷阱: 在中断处理函数或持有自旋锁的上下文中,严禁使用 vmalloc,因为它可能触发页错误并睡眠。此时必须使用 kmalloc(..., GFP_ATOMIC)。
kmalloc vs vmalloc 内存布局对比 kmalloc 分配 物理页 0x1000 物理页 0x1001 物理页 0x1002 物理地址连续 vmalloc 分配 虚拟页 0xC000 虚拟页 0xC001 虚拟页 0xC002 物理地址可能不连续

4.2 内存屏障:保证访问顺序

现代处理器和编译器会为了性能对内存访问进行重排序。在驱动中,当硬件设备与 CPU 共享内存时,这种重排序可能导致严重的同步错误。内存屏障(Memory Barrier)就是用来强制保证访问顺序的指令。

典型场景: 驱动向 DMA 描述符写入数据后,必须确保 CPU 的写操作在硬件读取描述符之前全部完成。此时需要写屏障(wmb())。

4.2.1 屏障类型速查

屏障宏 作用 典型使用位置
mb() 全内存屏障(读+写) CPU 与设备状态同步
rmb() 读内存屏障 读取设备状态寄存器前
wmb() 写内存屏障 提交 DMA 描述符后
dma_wmb() DMA 写屏障(轻量) DMA 描述符链更新
smp_mb() SMP 全屏障 多核间数据同步
// 典型 DMA 提交序列:必须使用 wmb()
struct dma_desc *desc = get_dma_desc();
desc->src_addr = cpu_to_le32(src_phys);
desc->dst_addr = cpu_to_le32(dst_phys);
desc->length   = cpu_to_le32(len);
// 关键:确保所有描述符写入完成,再通知硬件
wmb();
writel(1, &dma_regs->go);  // 启动 DMA
常见错误: 忘记在写描述符和写“启动寄存器”之间插入 wmb(),导致硬件读到不完整的描述符,引发 DMA 传输出错或数据损坏。
内存屏障在 DMA 提交中的作用 CPU 写操作 ① 写 desc->src ② 写 desc->dst ③ 写 desc->len ④ 写 GO 寄存器 wmb() 屏障 硬件 DMA 引擎 ⑤ 读取 desc ⑥ 执行传输 ⑦ 触发中断 无屏障时 硬件可能读到 不完整的 desc 导致传输错误

4.3 DMA 内存映射:一致性 vs 流式

DMA 传输要求 CPU 和设备能共同访问内存。但 CPU 的缓存(Cache)可能导致数据不一致:设备写入内存的数据可能还在 CPU 缓存中未被写回,或者 CPU 修改了缓存但设备看到的是过时的内存数据。Linux 内核提供两种 DMA 映射模型来解决这个问题。

4.3.1 一致性 DMA 映射(Consistent DMA)

在映射期间保证 CPU 和设备看到的内存视图始终一致。通常用于需要长期共享的控制结构,如 DMA 描述符环、状态寄存器。

// 分配一致性 DMA 缓冲区
dma_addr_t dma_handle;
void *cpu_addr;

cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr)
    return -ENOMEM;

// cpu_addr 和 dma_handle 指向同一块物理内存
// CPU 通过 cpu_addr 访问,设备通过 dma_handle 访问
// 无需手动刷新缓存

// 使用完毕后释放
dma_free_coherent(dev, size, cpu_addr, dma_handle);

4.3.2 流式 DMA 映射(Streaming DMA)

适用于一次性或短时传输。映射时需指定数据方向,并在传输前后显式同步缓存。性能优于一致性映射,但使用更复杂。

方向枚举 含义 同步方向
DMA_TO_DEVICE CPU 写数据给设备 映射前:CPU 写入完成 → 同步缓存
DMA_FROM_DEVICE 设备写数据给 CPU 映射后:设备写入完成 → 无效化缓存
DMA_BIDIRECTIONAL 双向传输 每次访问前后都需要同步
// 流式 DMA 映射典型流程
void *buffer = kmalloc(size, GFP_KERNEL);
dma_addr_t dma_handle;

// 1. 映射缓冲区
dma_handle = dma_map_single(dev, buffer, size, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_handle))
    goto err_map;

// 2. 填充数据到 buffer(CPU 写入)
memcpy(buffer, data, len);

// 3. 同步:确保 CPU 写入对设备可见
dma_sync_single_for_device(dev, dma_handle, size, DMA_TO_DEVICE);

// 4. 启动 DMA 传输
writel(dma_handle, &dma_regs->src);
writel(size, &dma_regs->len);
wmb();
writel(1, &dma_regs->go);

// 5. 传输完成后,同步:使 CPU 缓存失效
dma_sync_single_for_cpu(dev, dma_handle, size, DMA_TO_DEVICE);

// 6. 解除映射
dma_unmap_single(dev, dma_handle, size, DMA_TO_DEVICE);
工程建议: 对于控制结构(描述符环、状态块)使用一致性映射;对于数据缓冲区(网络包、音频采样)使用流式映射以获得更好性能。流式映射的同步开销远小于一致性映射的缓存禁用代价。
DMA 映射模型对比 一致性 DMA 映射 CPU 视角 cpu_addr 设备视角 dma_handle 同一块物理内存 缓存一致性保证 适用:描述符环、状态寄存器 特点:简单但性能略低 分配:dma_alloc_coherent() 流式 DMA 映射 CPU 缓存 可能包含脏数据 物理内存 设备直接访问 需要显式同步 dma_sync_single_for_*() 适用:数据缓冲区(网络、音频) 特点:高性能,需手动管理 映射:dma_map_single()

4.4 工程实践:选择正确的内存管理策略

在驱动开发中,根据场景选择合适的内存管理方式,是保证系统稳定性和性能的关键。以下决策树可以帮助你快速做出选择。

  1. 判断是否在中断上下文: 是 → 只能使用 kmalloc(GFP_ATOMIC) 或预分配内存;否 → 可以进入下一步。
  2. 判断是否需要 DMA: 是 → 进入 DMA 映射选择;否 → 小数据用 kmalloc,大数据用 vmalloc。
  3. 判断 DMA 缓冲区用途: 控制结构(描述符、状态)→ 一致性映射;数据缓冲区 → 流式映射。
  4. 判断是否需要内存屏障: 只要 CPU 和硬件共享内存且存在顺序依赖,就必须插入适当的屏障。
调试技巧: 使用 CONFIG_DEBUG_ATOMIC_SLEEP 内核选项可以检测在原子上下文中是否错误地调用了可能睡眠的内存分配函数。这是驱动开发中最常用的调试手段之一。
// 综合示例:网络驱动中的 RX 缓冲区管理
struct rx_ring {
    void *cpu_buf;          // CPU 虚拟地址
    dma_addr_t dma_buf;     // DMA 总线地址
    unsigned int size;
};

// 初始化:使用流式映射
int rx_ring_init(struct device *dev, struct rx_ring *ring, size_t size)
{
    ring->cpu_buf = kmalloc(size, GFP_KERNEL | GFP_DMA);
    if (!ring->cpu_buf)
        return -ENOMEM;

    ring->dma_buf = dma_map_single(dev, ring->cpu_buf, size, DMA_FROM_DEVICE);
    if (dma_mapping_error(dev, ring->dma_buf)) {
        kfree(ring->cpu_buf);
        return -ENOMEM;
    }

    ring->size = size;
    return 0;
}

// 接收完成:同步并处理数据
void rx_ring_complete(struct device *dev, struct rx_ring *ring)
{
    // 使 CPU 缓存失效,确保看到设备写入的数据
    dma_sync_single_for_cpu(dev, ring->dma_buf, ring->size, DMA_FROM_DEVICE);

    // 处理数据
    process_packet(ring->cpu_buf, ring->size);

    // 重新同步给设备,准备下一次接收
    dma_sync_single_for_device(dev, ring->dma_buf, ring->size, DMA_FROM_DEVICE);
}

// 清理
void rx_ring_cleanup(struct device *dev, struct rx_ring *ring)
{
    dma_unmap_single(dev, ring->dma_buf, ring->size, DMA_FROM_DEVICE);
    kfree(ring->cpu_buf);
}
性能关键点: 流式映射的 dma_sync_single_for_cpu() 和 dma_sync_single_for_device() 调用开销不可忽视。在高速网络驱动中,应尽量合并同步操作,避免每个小包都触发一次完整的缓存刷新。