3. DMA内存映射基础:物理地址与虚拟地址、IOMMU/SMMU的作用、DMA地址空间、dma_addr_t类型

在Linux驱动开发中,DMA(Direct Memory Access)是设备绕过CPU直接访问内存的关键技术。然而,现代操作系统普遍采用虚拟内存管理,CPU看到的是虚拟地址,而DMA设备看到的是物理地址。这种地址空间的差异,加上Cache一致性问题,构成了DMA调试的核心难点。本章将系统梳理DMA内存映射的底层概念,为后续的API实战打下坚实基础。

3.1 物理地址与虚拟地址:CPU视角 vs DMA视角

CPU通过MMU(Memory Management Unit)将虚拟地址转换为物理地址。进程访问的地址空间是连续的虚拟地址,但对应的物理页帧可能分散在内存的不同位置。DMA设备则直接操作物理地址,它不经过MMU转换。因此,驱动必须为DMA操作提供正确的物理地址。

关键认知: CPU使用虚拟地址(VA),DMA设备使用物理地址(PA)。驱动开发者的核心任务之一,就是在两者之间建立正确的映射关系,并确保数据的一致性。
CPU与DMA的地址空间对比 CPU 虚拟地址空间 (VA) 0x7f00_0000 ~ 0x7f00_ffff MMU 地址转换 页表映射 (VA → PA) 物理地址空间 (PA) 0x1f00_0000 (可能不连续) DMA 设备地址空间 直接使用物理地址 DMA 引擎 无MMU,直接寻址物理内存 物理地址空间 (PA) 0x1f00_0000 (与CPU共享) 共享同一物理内存 注意:DMA设备无法直接访问CPU的虚拟地址,必须经过物理地址转换

3.2 IOMMU/SMMU:DMA地址转换的硬件桥梁

IOMMU(I/O Memory Management Unit)或SMMU(System Memory Management Unit)是专门为DMA设备设计的地址转换硬件。它允许DMA设备使用设备虚拟地址(DMA地址),由IOMMU将其转换为物理地址。这带来了两大好处:地址空间隔离和大块连续DMA缓冲区。

特性 无IOMMU 有IOMMU
DMA地址 直接使用物理地址 使用设备虚拟地址(IOVA)
内存连续性 需要物理连续内存 物理不连续,IOMMU映射为连续
安全性 设备可访问任意物理内存 IOMMU限制设备访问范围
典型场景 嵌入式低端SoC x86 VT-d、ARM SMMU
IOMMU/SMMU 在DMA路径中的位置 DMA设备 发出DMA请求 IOMMU / SMMU IOVA → PA 转换 物理内存 页帧0, 页帧1, ... IOVA PA IOMMU 页表映射示例 设备看到的DMA地址 (IOVA) 物理地址 (PA) 0x1000_0000 0x1f00_0000 (页帧A) 0x1000_1000 0x2a00_0000 (页帧B) 0x1000_2000 0x3b00_0000 (页帧C) IOMMU使物理上不连续的页帧对DMA设备呈现为连续的地址空间

3.3 DMA地址空间与dma_addr_t类型

DMA地址空间是指DMA设备能够寻址的地址范围。在无IOMMU的系统上,DMA地址空间就是物理地址空间;在有IOMMU的系统上,DMA地址空间是IOMMU管理的设备虚拟地址空间(IOVA)。Linux内核使用dma_addr_t类型来存储DMA地址,该类型定义在<linux/types.h>中。

编程提示: dma_addr_t在32位系统上通常是u32,在64位系统上通常是u64。驱动开发者应始终使用dma_addr_t而非直接使用unsigned long或phys_addr_t,以保证跨平台兼容性。

3.3.1 dma_addr_t 与 phys_addr_t 的区别

类型 含义 使用场景 转换函数
phys_addr_t 物理地址 CPU访问物理内存、ioremap __pa(), virt_to_phys()
dma_addr_t DMA总线地址 DMA设备编程、dma_map_single dma_map_*() 系列函数

3.3.2 DMA地址空间的范围

DMA设备可能只能访问有限的地址范围。例如,一个32位DMA设备只能访问0~4GB的地址空间。如果系统物理内存超过4GB,驱动必须确保DMA缓冲区落在设备可寻址范围内。Linux内核通过struct device的coherent_dma_mask和dma_mask字段来描述设备的能力。

/* 设置DMA掩码示例 */
if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))) {
    dev_err(dev, "No suitable DMA available\n");
    return -ENXIO;
}

3.4 DMA内存映射的三种典型场景

  1. 一致性映射(Coherent Mapping):使用dma_alloc_coherent()分配,保证CPU和DMA看到的数据始终一致(硬件自动维护Cache一致性)。适用于DMA描述符环、控制寄存器等。
  2. 流式映射(Streaming Mapping):使用dma_map_single()或dma_map_sg(),需要驱动手动管理Cache一致性(通过dma_sync_*())。适用于大数据块传输。
  3. 分散/聚集映射(SG Mapping):使用dma_map_sg()将多个不连续的物理缓冲区映射为连续的DMA地址列表,配合IOMMU实现高效传输。
DMA内存映射三种场景对比 一致性映射 dma_alloc_coherent() 硬件自动维护一致性 适合:描述符环、控制块 流式映射 dma_map_single() 手动Cache同步 适合:大数据块传输 SG映射 dma_map_sg() 分散物理页→连续DMA地址 适合:网络、存储IO 选择原则 • 小数据、频繁访问 → 一致性映射(避免手动同步开销) • 大数据、一次传输 → 流式映射(减少一致性维护成本)

3.5 常见陷阱与调试建议

调试陷阱: 不要将dma_addr_t直接当作物理地址传递给ioremap或phys_to_virt。在有IOMMU的系统上,dma_addr_t是IOVA,不是物理地址。正确的做法是使用dma_alloc_coherent返回的dma_handle和cpu_addr,或者使用dma_map_*系列函数。
  • 检查DMA掩码:确保dma_set_mask_and_coherent调用成功,否则DMA分配可能失败或使用错误的地址范围。
  • 区分物理地址和DMA地址:在打印调试信息时,使用%pad格式符打印dma_addr_t,使用%pa打印phys_addr_t。
  • IOMMU调试:通过/sys/kernel/debug/iommu/查看IOMMU映射状态,确认DMA地址转换是否正确。
  • Cache一致性:流式映射后,必须调用dma_sync_single_for_device()或dma_sync_single_for_cpu(),否则会导致数据损坏。
/* 调试打印示例 */
dma_addr_t dma_handle;
void *cpu_addr;

cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
dev_dbg(dev, "CPU addr: %p, DMA addr: %pad, size: %zu\n",
        cpu_addr, &dma_handle, size);

3.6 本章小结