4. DMA地址映射机制

DMA 地址映射是 PCIe 驱动调试中最核心、最容易出错的环节之一。设备看到的“总线地址”(Bus Address, BA)与 CPU 看到的“物理地址”(Physical Address, PA)通常不一致,必须通过 IOMMU(I/O Memory Management Unit)或 SMMU(System MMU)进行转换。本章深入分析映射原理、两种映射方式的差异及调试要点。

核心概念: DMA 地址映射的本质是建立“设备可见地址 → 系统物理地址”的转换规则。IOMMU/SMMU 硬件负责实时转换,驱动通过内核 API 管理映射表。

4.1 IOMMU/SMMU 的作用与地址转换流程

IOMMU(Intel/AMD 平台)和 SMMU(ARM 平台)是位于 PCIe Root Complex 与系统内存之间的硬件单元。它们为每个 PCIe 设备维护独立的 I/O 页表,实现设备地址空间到物理地址空间的隔离与映射。

IOMMU/SMMU 地址转换架构 PCIe 设备 DMA 引擎 设备地址 (BA) DMA 请求 IOMMU / SMMU I/O 页表查找 I/O 页表 (映射规则) 物理地址 系统内存 物理地址 (PA) 转换流程: 1. 设备发起 DMA,携带设备地址 (BA) 2. IOMMU 根据设备 BDF 查找对应 I/O 页表,将 BA 转换为 PA 3. 内存控制器使用 PA 访问物理内存 IOMMU/SMMU 关键特性: • 地址翻译:设备地址 → 物理地址 • 内存保护:设备只能访问映射给它的物理内存 • 大页支持:减少页表占用,提升 TLB 命中率
调试提示: 在 /sys/kernel/debug/iommu/ 下可以查看当前 IOMMU 的映射状态。使用 dmesg | grep -i iommu 确认 IOMMU 是否启用。如果 DMA 出现 Unhandled context fault 或 IOMMU page fault,通常是映射未建立或地址错误。

4.2 流式映射(Streaming DMA Mapping)

流式映射是最常用的 DMA 映射方式,适用于数据在设备和内存之间一次性或短周期传输的场景。映射在每次 DMA 操作前建立,操作完成后立即解除。

4.2.1 核心 API

// 映射单个缓冲区(返回设备可用的 DMA 地址)
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
                          size_t size, enum dma_data_direction dir);

// 解除映射
void dma_unmap_single(struct device *dev, dma_addr_t dma_handle,
                      size_t size, enum dma_data_direction dir);

// 映射 SG 列表(用于分散/聚集 DMA)
int dma_map_sg(struct device *dev, struct scatterlist *sg,
               int nents, enum dma_data_direction dir);

// 解除 SG 映射
void dma_unmap_sg(struct device *dev, struct scatterlist *sg,
                  int nents, enum dma_data_direction dir);

4.2.2 流式映射的典型流程

  1. 分配 CPU 缓冲区:使用 kmalloc 或 dma_alloc_coherent(注意:流式映射通常用 kmalloc 分配的缓冲区)。
  2. 调用 dma_map_single:内核执行 cache 一致性操作(flush/invalidate),并返回 DMA 地址。
  3. 将 DMA 地址写入设备寄存器:启动 DMA 传输。
  4. 等待传输完成:通过中断或轮询。
  5. 调用 dma_unmap_single:解除映射,确保 cache 数据同步。
常见错误: 忘记调用 dma_unmap_single 会导致 IOMMU 页表泄漏,最终耗尽 IOVA 空间。流式映射必须“映射-使用-解除”成对出现,不可长期持有。

4.3 一致性映射(Consistent DMA Mapping)

一致性映射适用于长期存在的 DMA 缓冲区,例如描述符环、控制寄存器、状态队列等。映射建立后,CPU 和设备看到的地址始终有效,无需每次传输都重新映射。

4.3.1 核心 API

// 分配并映射一致性缓冲区
dma_addr_t dma_alloc_coherent(struct device *dev, size_t size,
                              dma_addr_t *dma_handle, gfp_t flag);

// 释放一致性缓冲区
void dma_free_coherent(struct device *dev, size_t size,
                       void *cpu_addr, dma_addr_t dma_handle);

4.3.2 一致性映射的关键特性

特性 说明
映射持久性 映射在驱动生命周期内一直有效,直到显式释放
Cache 一致性 硬件保证 CPU 和设备看到的缓冲区内容一致,无需软件维护 cache
内存属性 通常映射为 non-cacheable 或 write-combine,避免 cache 一致性问题
典型用途 描述符环(Descriptor Ring)、DMA 控制块、统计计数器

4.4 流式映射 vs 一致性映射:对比与选型

对比维度 流式映射(Streaming) 一致性映射(Consistent)
映射生命周期 短周期,每次 DMA 前后建立/解除 长周期,驱动加载时建立,卸载时释放
Cache 维护 由驱动在 map/unmap 时显式维护 硬件自动保证一致性(non-cacheable 映射)
性能开销 每次 DMA 有 map/unmap 开销,但 cache 利用率高 无 map/unmap 开销,但 non-cacheable 访问较慢
适用场景 大数据块传输(网络包、存储 I/O) 控制结构、描述符环、小数据频繁交互
API 函数 dma_map_single / dma_unmap_single dma_alloc_coherent / dma_free_coherent
IOMMU 页表操作 频繁建立/拆除页表映射 一次性建立,长期保留
选型原则: 对于数据面的大块数据传输(如网络 RX/TX 缓冲区),优先使用流式映射以利用 cache 加速;对于控制面的描述符环、doorbell 寄存器等,使用一致性映射避免 cache 同步开销和复杂度。

4.5 DMA 数据方向与 Cache 一致性操作

流式映射的 dma_data_direction 参数决定了内核在 map/unmap 时执行的 cache 操作类型。

方向枚举 含义 map 时 cache 操作 unmap 时 cache 操作
DMA_TO_DEVICE CPU → 设备(内存写) flush(写回+失效) 无操作(设备只读)
DMA_FROM_DEVICE 设备 → CPU(内存读) invalidate(失效) invalidate(确保 CPU 看到最新数据)
DMA_BIDIRECTIONAL 双向传输 flush+invalidate flush+invalidate
DMA_NONE 调试/占位 无操作 无操作
调试陷阱: 方向指定错误会导致数据损坏。例如,DMA_FROM_DEVICE 时如果使用 DMA_TO_DEVICE,CPU 可能读到 cache 中的旧数据。调试时可以在 map/unmap 前后打印缓冲区内容对比。

4.6 调试 IOMMU 映射问题的常用方法

  • 检查 IOMMU 是否启用:dmesg | grep -E "IOMMU|DMAR|SMMU",确认 iommu=pt 或 iommu.passthrough=1 是否设置。
  • 查看 IOVA 使用情况:cat /sys/kernel/debug/iommu/intel/iova_domain/allocated(Intel 平台)。
  • 捕获 IOMMU 错误:echo 1 > /sys/kernel/debug/iommu/intel/irq_remapping/enable 并监控 dmesg 中的 DMAR: [INTR-REMAP] 或 IOMMU: page fault。
  • 验证 DMA 地址有效性:在 map 后打印 dma_handle,使用 iommu-dma 工具或 devmem2 检查该地址是否可访问。
  • 使用 ftrace 跟踪 DMA API:echo dma:* > /sys/kernel/debug/tracing/set_event,然后查看 trace 日志。
DMA 映射调试决策流程 DMA 传输失败 检查 IOMMU 是否启用 dma_map_single 返回值 dmesg IOMMU fault 检查方向/大小/对齐 检查 IOVA 空间/页表

4.7 实际调试案例:流式映射导致 IOMMU 页错误

问题现象: 网卡驱动加载后,dmesg 持续输出 DMAR: [DMA Read] Request device [00:1f.6] fault addr 0x12345000,网络无法收发。

排查步骤:

  1. 确认 IOMMU 启用:dmesg | grep DMAR 显示 DMAR: IOMMU enabled。
  2. 检查驱动代码,发现 RX 缓冲区使用 dma_alloc_coherent 分配,但 TX 缓冲区使用 kmalloc + dma_map_single。
  3. 打印 TX 映射后的 dma_handle,发现地址为 0x12345000,与 fault 地址一致。
  4. 进一步检查发现,TX 完成中断处理中调用了 dma_unmap_single,但传入的 size 参数比实际映射时小(映射 2048 字节,unmap 只传了 256 字节)。
  5. 根因: unmap 时 size 不匹配导致 IOMMU 页表未完全解除,后续 DMA 访问到残留映射区域触发 fault。
  6. 修复: 确保 map/unmap 的 size 完全一致,并使用 dma_unmap_single 的 size 参数与 map 时相同。
最佳实践: 在驱动中封装 map/unmap 辅助函数,自动记录 size 和方向,避免人为错误。使用 DMA_API_DEBUG 内核选项(CONFIG_DMA_API_DEBUG=y)可以在 map/unmap 不匹配时自动打印警告。