4. DMA映射与地址空间

在Linux驱动开发中,DMA传输失败往往不是控制器配置错误,而是地址空间转换链条中某一环被误解。CPU看到的指针、设备发出的地址、内存控制器实际访问的物理单元,三者并不等价。理解物理地址、虚拟地址、总线地址、DMA地址以及IOMMU/SMMU的角色,是写出可移植、可调试DMA代码的前提。

一、四类地址的本质区别

虚拟地址是CPU执行指令时使用的地址,经过MMU页表映射到物理地址。驱动中kmalloc()、vmalloc()返回的都是虚拟地址,CPU可以直接解引用,但设备无法直接使用。

物理地址是内存控制器在DRAM颗粒上实际寻址的地址。CPU通过MMU把虚拟地址翻译为物理地址后访问内存。设备若绕过IOMMU,其发出的地址也必须是物理地址。

总线地址是设备在总线上看到的地址。在x86等平台,总线地址通常等于物理地址;但在ARM、PowerPC等平台,由于存在地址窗口映射(如AXI到DRAM的偏移),总线地址与物理地址可能不同。

DMA地址是驱动通过DMA API交给设备使用的地址。它可能是物理地址,也可能是IOMMU映射后的I/O虚拟地址(IOVA)。驱动不应假设DMA地址等于物理地址,而应使用dma_map_single()等接口获取。

核心原则:CPU用虚拟地址访问内存,设备用DMA地址访问内存,两者之间的转换由DMA API和IOMMU/SMMU共同完成。驱动代码中混用这两类地址,是DMA故障的高发原因。

二、地址转换链路与IOMMU/SMMU的作用

下图展示了从CPU虚拟地址到设备DMA地址的完整转换路径,以及IOMMU/SMMU在其中的位置。

CPU / 驱动 虚拟地址 VA kmalloc / vmalloc MMU 页表翻译 VA → PA 物理内存 DRAM 物理地址 PA 内存控制器寻址 外设设备 DMA地址 DA dma_map_single 返回 IOMMU / SMMU I/O页表翻译 DA → PA 总线地址 BA 设备在总线上看到的地址 可能等于PA,也可能有偏移 CPU路径:VA → MMU → PA → DRAM  设备路径:DA → IOMMU/SMMU → PA → DRAM 无IOMMU时:DA 必须等于 PA(或经平台总线偏移转换)

当系统存在IOMMU(x86)或SMMU(ARM)时,设备发出的DMA地址先经过I/O页表翻译为物理地址,再访问内存。这带来两个关键好处:一是设备可以使用不连续的物理页,驱动只需映射连续的IOVA;二是提供隔离与保护,防止设备越界访问。

三、DMA映射API与地址获取流程

Linux提供了一套统一的DMA API,驱动应始终使用这些接口,而不是手动做virt_to_phys之类的转换。

/* 流式映射:一次性传输 */
dma_addr_t dma_handle;
void *cpu_addr = kmalloc(size, GFP_KERNEL);
dma_handle = dma_map_single(dev, cpu_addr, size, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma_handle)) {
    /* 映射失败处理 */
}
/* 将 dma_handle 写入设备寄存器,启动DMA */
...
dma_unmap_single(dev, dma_handle, size, DMA_TO_DEVICE);

/* 一致性映射:长期共享 */
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
/* cpu_addr 和 dma_handle 同时可用,无需手动同步 */
...
dma_free_coherent(dev, size, cpu_addr, dma_handle);

关键点:dma_map_single()返回的dma_handle才是设备应使用的地址。在无IOMMU的平台上,它通常等于物理地址;在有IOMMU的平台上,它是IOVA。驱动代码不应假设两者相等。

四、常见误区与调试要点

误区 后果 正确做法
把虚拟地址直接写入设备DMA寄存器 设备访问错误地址,数据损坏或总线异常 使用dma_map_single返回的dma_addr_t
假设DMA地址等于物理地址 在IOMMU/SMMU平台传输失败 始终通过DMA API获取地址
映射后忘记unmap IOVA泄漏,长时间运行后映射失败 传输完成后立即dma_unmap_single
方向参数写错(TO_DEVICE/FROM_DEVICE) 缓存同步方向错误,数据不一致 根据实际传输方向选择DMA_TO_DEVICE等
在中断上下文使用可能睡眠的映射API 内核崩溃或死锁 中断中使用dma_map_single等非睡眠接口
调试DMA地址问题时,可在驱动中打印%pad格式的dma_addr_t,并对比/proc/iomem中的物理地址范围。若使用IOMMU,可查看/sys/kernel/debug/iommu下的映射信息。
在ARM64平台上,SMMU的配置错误会导致设备DMA直接失败且无明显报错。务必确认设备树中iommus属性正确,且驱动调用了dma_set_mask_and_coherent()设置合适的DMA掩码。

总结来说,物理地址、虚拟地址、总线地址和DMA地址分属不同视角:CPU看虚拟地址,内存看物理地址,总线看总线地址,设备看DMA地址。IOMMU/SMMU在设备与物理内存之间插入了一层I/O页表翻译,既提升了灵活性也增加了调试复杂度。驱动开发者应始终通过DMA API获取和使用DMA地址,避免任何形式的地址假设,这样才能写出跨平台稳定可靠的DMA代码。