CPU 一纳秒也等不了外设,大批量数据搬运靠 DMA:设备自己读写内存,CPU 只管下命令.听起来简单,难在"设备眼中的内存"和"CPU 眼中的内存"不是一回事.这篇把 dma 地址和物理地址的关系,两种映射 API,swiotlb 和 CMA 这些救火队员,还有 IOMMU 的翻译和隔离,全部摆到实机上验证.
这篇回答几件事:
- dma_map_single 返回的地址到底是什么,什么时候不等于物理地址
- coherent 和 streaming 两种映射的区别,为什么不能混着用
- swiotlb 和 CMA 各救什么场
- IOMMU 除了翻译还能干什么,QEMU 里的组隔离长什么样
先破一个常见误解:DMA 地址是设备总线视角的地址,物理地址是内存阵列视角的地址,两者恰好相等的情形有,但那是运气不是定律.实测板上的对照,自建 platform 设备,64... 40 位掩码,dma_map_single 一把:
dma_map_single: kbuf=ffff000000d0d000 dma_addr=0x0000000000d0d000
kbuf_phys=0x0d0d000
kbuf 物理地址 0xd0d000,dma_map_single 返回 0x0d0d000,逐字节相等 — 因为 RK3399 没有通用 IOMMU,设备直接看物理总线,这种平台叫 direct DMA.dma API 存在的第一个理由就是把这层关系抽象掉:驱动拿到 dma_addr 给设备填寄存器,映射怎么来的是架构的事,同一份驱动在有 IOMMU 的机器上跑,dma_addr 就变成 IOVA(I/O 虚拟地址),由 IOMMU 翻译回物理,驱动无感.
两种映射 API 对应两种使用姿势.coherent 一致性映射,dma_alloc_coherent 一次分配两边可用,CPU 写完设备立即可见,不需要任何同步动作,代价是这内存在有些架构上要禁缓存,贵且有限,适合描述符环,控制块这类双方高频共读共写的结构.实测输出:
dma_alloc_coherent: cpu_va=ffff800081c5d000 dma_addr=0x0000000006601000
注意 CPU 侧地址在 0xffff8000 开头的区域 — arm64 专门为一致性内存留的映射区,和普通 kmalloc 的线性映射(0xffff0000 开头)分区而治.streaming 流式映射,dma_map_single 把普通 kmalloc 的缓冲临时借给设备,方向敏感(DMA_TO_DEVICE/DMA_FROM_DEVICE),借出期间 CPU 别碰,还回来时缓存一致性由 unmap 或 sync 动作处理.大块数据传输用它,别用 coherent 硬扛.
然后是两位救火队员.swiotlb,software IO TLB:设备的寻址能力比内存小时(经典是 32 位 PCI 卡摸不到 4GB 以上),内核把你的缓冲复制进一块低位弹跳区,设备实际搬的是弹跳区.开销是内存拷贝,能不用就不用,但有些硬件组合没它根本跑不起来.QEMU 内核的启动日志明明白白:
software IO TLB: mapped [mem 0x7c980000-0x7cb80000] (2MB)
板上虽然编了 CONFIG_SWIOTLB,启动日志一行没有 — 40 位总线和 40 位 DMA 能力,根本用不上弹跳,这就是"编了但闲置"的样板.CMA,contiguous memory allocator:大块物理连续内存的预留池,摄像头,视频编解码这类要几百 KB 到几兆连续缓冲的设备靠它.QEMU 内核留了 32MiB("cma: Reserved 32 MiB at 0x7cc00000"),板上的 Memory 行写着 0K cma-reserved — 没有这类设备,一页不占.它和 buddy 的关系是:CMA 区域空闲时正常当普通页用,有设备来要连续大块时把散住户迁走腾地方.
重头戏 IOMMU.QEMU 的 virt 机器能仿真 ARM SMMUv3,启动参数 iommu=smmuv3 加一块 virtio 网卡,内核日志:
arm-smmu-v3 9050000.smmuv3: ias 44-bit, oas 44-bit (features 0x01088705)
arm-smmu-v3 9050000.smmuv3: allocated 65536 entries for cmdq
pci 0000:00:00.0: Adding to iommu group 0
pci 0000:00:01.0: Adding to iommu group 1
SMMU 初始化,命令队列,事件队列,然后每个设备被分进一个 iommu group — sysfs 里 /sys/kernel/iommu_groups/ 下每个组一个目录,组里挂设备.这个组是隔离的单位:同组设备共享地址空间,跨组完全看不见.它带来三个改变.一是地址翻译,设备的 IOVA 随便给,物理页可以不连续,散射列表映射后设备看到一片连续;二是权限,IOMMU 页表能限制设备只能碰授权过的页,野指针 DMA 直接 fault;三是安全隔离,虚拟机直通设备(pass-through)就是靠每组一张翻译表,VM A 的网卡永远打不进 VM B 的内存.板上那颗 RK3399 没有通用 SMMU,只有 VOP/VPU 等专用 IOMMU,所以前面才是直通式 DMA — 两套硬件,一个 dma API,这就是抽象层的价值.
最后留几个容易忽略的细节:
- dma_map_single 的方向参数是承诺不是建议:写 DMA_TO_DEVICE 却又读回来,拿到的是未同步的旧数据,这类 bug 在有 IOMMU 的机器上更容易暴露.
- dma_mapping_error 必须查,映射失败返回的不是 NULL 是个哨兵值,直接填进设备寄存器就是玄学故障.
- scatterlist 不是可有可无的抽象:用户态一次 read 可能给你一串不连续页,dma_map_sg 一条链全映射,设备靠 DMA 逐段搬.
- IOMMU 开着时 IOVA 空间可以比物理内存大得多,别拿 dma_addr 的位数推断物理布局.
- "Adding to iommu group" 的组号在 VFIO 直通配置里天天用,一个组只能给一个 VM,想直通的设备要和同组邻居一起处理.
- coherent 分配在 x86 上几乎免费(一致性由硬件协议保证),在 arm 上要特殊映射区,跨平台驱动别假设它的成本.
- CMA 的迁移依赖页面可迁移,内存碎片化加上不可迁移页扎堆,CMA 分配照样失败,启动早期就锁定大块是稳妥习惯.