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操作提供正确的物理地址。
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 |
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内存映射的三种典型场景
- 一致性映射(Coherent Mapping):使用
dma_alloc_coherent()分配,保证CPU和DMA看到的数据始终一致(硬件自动维护Cache一致性)。适用于DMA描述符环、控制寄存器等。 - 流式映射(Streaming Mapping):使用
dma_map_single()或dma_map_sg(),需要驱动手动管理Cache一致性(通过dma_sync_*())。适用于大数据块传输。 - 分散/聚集映射(SG Mapping):使用
dma_map_sg()将多个不连续的物理缓冲区映射为连续的DMA地址列表,配合IOMMU实现高效传输。
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);