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进行数据传输的完整生命周期:

流式DMA映射与同步完整流程 流式DMA映射与同步完整流程 1. 分配CPU缓冲区 2. dma_map_single/page 3. sync_for_device 4. 设备执行DMA 5. sync_for_cpu 6. CPU读取数据 7. dma_unmap ⚠ 关键规则: 每次CPU与设备交替访问缓冲区前,必须调用对应的sync API。方向参数必须与实际数据传输方向一致,否则会导致数据损坏。

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:

DMA映射API选择决策树 DMA映射API选择决策树 缓冲区生命周期? 长期共享 短期传输 dma_alloc_coherent 内存来源? dma_map_single dma_map_page 所有流式映射必须配合 dma_sync_* 使用,且必须检查 dma_mapping_error