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()等接口获取。
二、地址转换链路与IOMMU/SMMU的作用
下图展示了从CPU虚拟地址到设备DMA地址的完整转换路径,以及IOMMU/SMMU在其中的位置。
当系统存在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等非睡眠接口 |
%pad格式的dma_addr_t,并对比/proc/iomem中的物理地址范围。若使用IOMMU,可查看/sys/kernel/debug/iommu下的映射信息。
iommus属性正确,且驱动调用了dma_set_mask_and_coherent()设置合适的DMA掩码。
总结来说,物理地址、虚拟地址、总线地址和DMA地址分属不同视角:CPU看虚拟地址,内存看物理地址,总线看总线地址,设备看DMA地址。IOMMU/SMMU在设备与物理内存之间插入了一层I/O页表翻译,既提升了灵活性也增加了调试复杂度。驱动开发者应始终通过DMA API获取和使用DMA地址,避免任何形式的地址假设,这样才能写出跨平台稳定可靠的DMA代码。