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。
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_* 系列函数 |
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 的简单设备。
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:双向传输,开销最大,非必要不使用。
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的缓冲区再次调用同步函数。
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可能触发跨核缓存一致性操作,频繁调用会影响性能。