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 页表,实现设备地址空间到物理地址空间的隔离与映射。
调试提示: 在 /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 流式映射的典型流程
- 分配 CPU 缓冲区:使用
kmalloc或dma_alloc_coherent(注意:流式映射通常用kmalloc分配的缓冲区)。 - 调用 dma_map_single:内核执行 cache 一致性操作(flush/invalidate),并返回 DMA 地址。
- 将 DMA 地址写入设备寄存器:启动 DMA 传输。
- 等待传输完成:通过中断或轮询。
- 调用 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 日志。
4.7 实际调试案例:流式映射导致 IOMMU 页错误
问题现象: 网卡驱动加载后,
dmesg 持续输出 DMAR: [DMA Read] Request device [00:1f.6] fault addr 0x12345000,网络无法收发。
排查步骤:
- 确认 IOMMU 启用:
dmesg | grep DMAR显示DMAR: IOMMU enabled。 - 检查驱动代码,发现 RX 缓冲区使用
dma_alloc_coherent分配,但 TX 缓冲区使用kmalloc+dma_map_single。 - 打印 TX 映射后的
dma_handle,发现地址为0x12345000,与 fault 地址一致。 - 进一步检查发现,TX 完成中断处理中调用了
dma_unmap_single,但传入的size参数比实际映射时小(映射 2048 字节,unmap 只传了 256 字节)。 - 根因: unmap 时 size 不匹配导致 IOMMU 页表未完全解除,后续 DMA 访问到残留映射区域触发 fault。
- 修复: 确保 map/unmap 的 size 完全一致,并使用
dma_unmap_single的 size 参数与 map 时相同。
最佳实践: 在驱动中封装 map/unmap 辅助函数,自动记录 size 和方向,避免人为错误。使用
DMA_API_DEBUG 内核选项(CONFIG_DMA_API_DEBUG=y)可以在 map/unmap 不匹配时自动打印警告。