3. DMA映射API详解:从dma_map_single到一致性内存分配
在PCIe驱动移植过程中,DMA映射是连接设备与系统内存的核心桥梁。本章将深入剖析Linux内核中DMA映射API的完整体系,包括流式映射与一致性映射两大分支,以及同步操作的关键细节。理解这些API的语义差异和正确使用方式,是避免数据损坏、内存泄漏和硬件故障的前提。
3.1 DMA映射的两大分类
Linux内核将DMA映射分为流式映射(Streaming DMA)和一致性映射(Consistent DMA)两类。流式映射适用于短时、频繁的数据传输,每次传输后需要显式同步;一致性映射适用于长期、共享的数据缓冲区,硬件和CPU可随时访问而无需同步。
| 特性 | 流式映射(Streaming DMA) | 一致性映射(Consistent DMA) |
|---|---|---|
| 典型API | dma_map_single / dma_map_page | dma_alloc_coherent |
| 同步要求 | 需要 dma_sync_single_for_* 显式同步 | 无需同步,硬件与CPU缓存一致性由硬件保证 |
| 映射开销 | 较低,可动态建立/销毁 | 较高,分配时建立页表映射 |
| 适用场景 | 网络包收发、块设备I/O、一次性传输 | 描述符环、控制寄存器、状态队列 |
| 生命周期 | 短,每次传输前后建立/解除 | 长,驱动加载时分配,卸载时释放 |
移植要点:在PCIe驱动移植中,流式映射常用于数据路径(data path),一致性映射常用于控制路径(control path)。错误地将流式映射用于长期共享缓冲区,会导致缓存一致性问题,表现为随机数据损坏。
3.2 dma_map_single / dma_unmap_single
dma_map_single 将内核虚拟地址(通常是 kmalloc 或栈缓冲区)映射为DMA总线地址,供设备直接访问。其原型如下:
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
size_t size, enum dma_data_direction dir);
参数说明:
- dev:指向PCIe设备的struct device指针,通常通过
&pdev->dev获取。 - cpu_addr:CPU侧的虚拟地址,必须对齐到缓存行大小(通常64字节)。
- size:映射的字节数。
- dir:数据传输方向,可选值见下表。
| 方向枚举 | 含义 | 典型场景 |
|---|---|---|
| DMA_TO_DEVICE | CPU写入,设备读取 | 发送网络包、写入命令描述符 |
| DMA_FROM_DEVICE | 设备写入,CPU读取 | 接收网络包、读取状态寄存器 |
| DMA_BIDIRECTIONAL | 双向传输 | 共享内存、双工通信 |
| DMA_NONE | 调试用途,无实际传输 | 初始化占位 |
对应的解除映射API:
void dma_unmap_single(struct device *dev, dma_addr_t dma_handle,
size_t size, enum dma_data_direction dir);
常见错误:忘记调用 dma_unmap_single 会导致DMA地址泄漏,在IOMMU开启的系统中会耗尽IOVA空间。另外,
dma_map_single 返回的 dma_addr_t 必须由设备使用,CPU侧应继续使用原始虚拟地址,两者不可混用。
3.3 dma_map_page / dma_unmap_page
dma_map_page 用于映射 struct page 结构体指向的物理页面,适用于高端内存或从块层/网络层获取的页缓存。其原型:
dma_addr_t dma_map_page(struct device *dev, struct page *page,
unsigned long offset, size_t size,
enum dma_data_direction dir);
与 dma_map_single 的核心区别:
- 输入不同:
dma_map_single接受虚拟地址,dma_map_page接受struct page *+ 页内偏移。 - 适用内存类型:
dma_map_page可映射__get_free_pages分配的页面、块设备bio中的页面、网络skb中的页面。 - 性能:
dma_map_page在IOMMU环境下可能更高效,因为IOMMU以页为单位建立映射。
// 典型用法:映射网络接收缓冲区
struct page *rx_page = alloc_page(GFP_KERNEL);
dma_addr_t dma_addr = dma_map_page(dev, rx_page, 0, PAGE_SIZE,
DMA_FROM_DEVICE);
if (dma_mapping_error(dev, dma_addr)) {
__free_page(rx_page);
return -ENOMEM;
}
// 将 dma_addr 写入设备描述符寄存器...
// 传输完成后:
dma_unmap_page(dev, dma_addr, PAGE_SIZE, DMA_FROM_DEVICE);
__free_page(rx_page);
移植调试技巧:在移植过程中,如果遇到设备无法访问内存或数据内容错误,首先检查
dma_mapping_error() 的返回值。许多驱动移植失败的根本原因是未检查映射错误,导致设备使用无效的DMA地址。
3.4 dma_sync_single_for_cpu / dma_sync_single_for_device
流式映射在传输完成后,CPU和设备之间可能存在缓存一致性问题。这两个API用于在CPU和设备之间同步数据:
void dma_sync_single_for_cpu(struct device *dev, dma_addr_t dma_handle,
size_t size, enum dma_data_direction dir);
void dma_sync_single_for_device(struct device *dev, dma_addr_t dma_handle,
size_t size, enum dma_data_direction dir);
使用规则:
- CPU准备写入,设备准备读取:CPU写入数据前调用
dma_sync_single_for_cpu(方向为DMA_TO_DEVICE),写入完成后调用dma_sync_single_for_device将数据刷出到设备可见的内存。 - 设备写入,CPU准备读取:设备完成写入后,CPU读取前调用
dma_sync_single_for_cpu(方向为DMA_FROM_DEVICE),使CPU缓存失效,读取到最新数据。 - 双向缓冲区:每次切换访问方时都需要同步,方向参数与当前操作方向一致。
性能提示:在ARM/ARM64等弱一致性架构上,省略同步操作会导致CPU读到过时数据或设备看到未刷新的写入。x86架构虽然硬件上对PCIe设备有较强的缓存一致性,但为了跨平台兼容,仍应正确调用同步API。
3.5 DMA映射与同步的完整流程
下图展示了PCIe驱动中使用流式DMA进行数据传输的完整生命周期:
3.6 一致性DMA分配:dma_alloc_coherent
对于需要长期共享的缓冲区(如描述符环、状态队列),使用一致性DMA分配:
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t flag);
该API返回两个地址:
- 返回值:CPU侧的虚拟地址,可直接解引用。
- dma_handle:设备侧的DMA总线地址,写入设备寄存器。
释放时调用:
void dma_free_coherent(struct device *dev, size_t size,
void *cpu_addr, dma_addr_t dma_handle);
移植注意事项:
- 一致性内存分配在底层可能使用
__get_free_pages或dma_alloc_attrs,分配大小必须是2的幂次页倍数。 - 在IOMMU开启的系统中,
dma_alloc_coherent会建立IOMMU页表映射,确保设备可见。 - 一致性缓冲区不需要调用
dma_sync_*系列函数,硬件保证缓存一致性。
3.7 常见调试问题与排查方法
| 症状 | 可能原因 | 排查方法 |
|---|---|---|
| 设备DMA超时 | dma_map返回错误地址,或未检查dma_mapping_error | 添加dma_mapping_error检查,打印dma_addr_t值 |
| 数据内容全零或全FF | 方向参数错误(DMA_TO_DEVICE与DMA_FROM_DEVICE颠倒) | 核对驱动中所有dma_map和sync的方向参数 |
| 随机数据损坏 | 缺少sync_for_cpu/sync_for_device调用 | 在每次CPU/设备切换访问时添加同步 |
| IOMMU页错误 | dma_unmap未调用或调用时机错误 | 使用kmemleak或iommu调试跟踪DMA地址泄漏 |
| 系统挂死 | 一致性缓冲区越界访问 | 启用CONFIG_DMA_API_DEBUG内核选项 |
调试利器:内核提供了
CONFIG_DMA_API_DEBUG 和 CONFIG_DMA_API_DEBUG_SG 调试选项,开启后会在dma_map/unmap时进行完整性检查,检测方向错误、双重释放、泄漏等问题。在移植初期务必开启此选项。
3.8 总结:API选择决策树
在PCIe驱动移植中,根据缓冲区生命周期和访问模式选择合适的DMA映射API: