4. Linux 内核 DMA API 概述

DMA(Direct Memory Access)是嵌入式 Linux 系统中实现高性能数据传输的核心机制。Linux 内核提供了一套标准化的 DMA API,用于管理设备与内存之间的数据传输,并确保缓存一致性。本章将系统介绍最常用的五个 DMA API:dma_alloc_coherent、dma_map_single、dma_map_page、dma_unmap_single 以及 dma_sync_single_for_cpu/device。

核心概念: DMA API 的设计目标是让驱动程序开发者无需关心底层硬件架构(如 ARM、x86、RISC-V)的缓存一致性细节,通过统一的接口完成内存分配、地址映射和同步操作。

4.1 DMA API 分类与适用场景

Linux 内核的 DMA API 主要分为两大类:一致性 DMA 映射(Coherent DMA Mapping) 和 流式 DMA 映射(Streaming DMA Mapping)。两者在内存管理、缓存策略和使用场景上有显著区别。

特性 一致性 DMA 映射 流式 DMA 映射
核心 API dma_alloc_coherent / dma_free_coherent dma_map_single / dma_unmap_single / dma_map_page
缓存策略 禁止 CPU 缓存(或硬件自动维护一致性) 临时维护缓存一致性,需要显式同步
分配开销 较高(分配时可能进行页表修改) 较低(复用已有内存)
适用场景 长期存在的 DMA 缓冲区(如网卡描述符环) 短时、频繁的 DMA 传输(如音频数据流)
同步要求 无需额外同步 必须调用 dma_sync_single_for_* 系列函数
选择指南: 如果 DMA 缓冲区在设备生命周期内几乎一直存在(如控制结构体),优先使用一致性映射;如果缓冲区是临时分配的、频繁创建和销毁的,使用流式映射可以获得更好的性能。

4.2 一致性 DMA 映射:dma_alloc_coherent

dma_alloc_coherent 用于分配一块在 CPU 和设备视角下地址连续、且缓存一致的内存区域。该函数返回两个地址:CPU 虚拟地址(供驱动使用)和 DMA 总线地址(供设备使用)。

#include <linux/dma-mapping.h>

dma_addr_t dma_handle;
void *cpu_addr;

cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
    dev_err(dev, "Failed to allocate coherent DMA memory\n");
    return -ENOMEM;
}

// ... 使用 cpu_addr 和 dma_handle ...

dma_free_coherent(dev, size, cpu_addr, dma_handle);
注意事项:
  • size 参数必须是 2 的幂次方对齐(通常为 PAGE_SIZE 的倍数)。
  • 在 SMP 系统上,该函数保证所有 CPU 核看到的缓存是一致的。
  • 分配的内存默认是物理连续的,适用于不支持 scatter-gather 的简单设备。
一致性 DMA 映射架构 CPU 虚拟地址空间 设备 DMA 地址空间 一致性 DMA 缓冲区 CPU 缓存已禁用 / 硬件自动同步 cpu_addr (虚拟地址) dma_handle (总线地址) CPU 和设备可以直接访问同一块物理内存,无需手动同步缓存

4.3 流式 DMA 映射:dma_map_single 与 dma_map_page

流式映射用于将已有的内核缓冲区(如通过 kmalloc 或 __get_free_pages 分配的内存)映射到 DMA 总线地址空间。映射过程中,内核会执行必要的缓存刷新操作,以确保设备看到的数据是完整的。

4.3.1 dma_map_single

用于映射一段连续的虚拟地址内存(通常由 kmalloc 分配)。

dma_addr_t dma_addr;

dma_addr = dma_map_single(dev, cpu_addr, size, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
    dev_err(dev, "dma_map_single failed\n");
    goto err_out;
}

// 启动 DMA 传输 ...

dma_unmap_single(dev, dma_addr, size, DMA_TO_DEVICE);

4.3.2 dma_map_page

用于映射物理页框中的一段区域(适用于基于 struct page 的缓冲区,如文件系统页缓存)。

dma_addr_t dma_addr;

dma_addr = dma_map_page(dev, page, offset, size, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
    dev_err(dev, "dma_map_page failed\n");
    goto err_out;
}

// 启动 DMA 传输 ...

dma_unmap_page(dev, dma_addr, size, DMA_FROM_DEVICE);
API 输入参数 典型用途
dma_map_single 虚拟地址指针 cpu_addr 驱动内部 kmalloc 缓冲区
dma_map_page struct page * + 页内偏移 文件系统缓存、网络套接字缓冲区
方向参数:
  • DMA_TO_DEVICE:CPU 写入,设备读取(如发送网络数据)。
  • DMA_FROM_DEVICE:设备写入,CPU 读取(如接收网络数据)。
  • DMA_BIDIRECTIONAL:双向传输,开销最大,非必要不使用。
流式 DMA 映射与取消映射流程 1. 分配/获取缓冲区 2. dma_map_single/page 3. 启动 DMA 传输 4. DMA 传输完成 5. dma_unmap_single 6. 访问/释放缓冲区 关键操作说明 映射时:内核执行缓存刷新(Cache Flush),确保 CPU 写入的数据被设备可见 取消映射时:内核执行缓存失效(Cache Invalidate),确保 CPU 读取到设备写入的最新数据

4.4 缓存同步操作:dma_sync_single_for_cpu / for_device

在流式 DMA 映射的生命周期内,如果 CPU 和设备需要交替访问同一个缓冲区,必须使用 dma_sync_single_for_cpu 和 dma_sync_single_for_device 来维护缓存一致性。这两个函数是流式映射的核心同步原语。

函数 调用时机 执行操作
dma_sync_single_for_cpu 设备完成 DMA 写入后,CPU 读取数据前 使 CPU 缓存行失效(Invalidate),确保 CPU 看到设备写入的最新数据
dma_sync_single_for_device CPU 完成数据写入后,设备启动 DMA 读取前 刷新 CPU 缓存行(Flush),确保设备看到 CPU 写入的最新数据
// 典型使用模式:CPU 准备数据,设备读取
memcpy(cpu_addr, data, len);
dma_sync_single_for_device(dev, dma_addr, len, DMA_TO_DEVICE);
// 启动 DMA 传输(设备从缓冲区读取)

// 设备写入完成后,CPU 读取数据
// 等待 DMA 完成中断 ...
dma_sync_single_for_cpu(dev, dma_addr, len, DMA_FROM_DEVICE);
process_data(cpu_addr);
常见错误:
  • 忘记在 CPU 访问前调用 dma_sync_single_for_cpu,导致读取到过时的缓存数据。
  • 在设备正在 DMA 时调用同步函数,造成数据竞争。
  • 对已经 dma_unmap_single 的缓冲区再次调用同步函数。
dma_sync_single 缓存同步时序 时间 CPU 写入数据 sync_for_device CPU 读取数据 DMA 读取 DMA 写入 缓存刷新 缓存失效 sync_for_device 确保设备看到 CPU 最新写入;sync_for_cpu 确保 CPU 看到设备最新写入

4.5 API 使用对比总结

下表从分配方式、同步要求和典型场景三个维度,对五个核心 API 进行横向对比,帮助你在实际驱动开发中快速做出选择。

API 内存来源 同步方式 典型场景
dma_alloc_coherent 内核自动分配 无需同步 描述符环、控制寄存器映射
dma_map_single 已有 kmalloc 缓冲区 映射/取消映射时自动同步 音频缓冲区、小数据块传输
dma_map_page 已有 struct page 映射/取消映射时自动同步 网络 SKB、文件系统页缓存
dma_sync_single_for_cpu 已映射的流式缓冲区 手动调用,缓存失效 设备写入完成后,CPU 读取前
dma_sync_single_for_device 已映射的流式缓冲区 手动调用,缓存刷新 CPU 写入完成后,设备读取前
性能建议:
  • 对于高频小数据包(如音频流),优先使用流式映射 + 手动同步,避免一致性映射的分配开销。
  • 对于低频大数据块(如视频帧),一致性映射可以简化代码逻辑,降低出错概率。
  • 在 SMP 系统上,dma_sync_single_for_cpu 可能触发跨核缓存一致性操作,频繁调用会影响性能。