本文属于第三册“内核抽象:dma-mapping 接口的设计原则”,回答第 0 篇提出的问题 11:
- 驱动为何需要声明 DMA 掩码?
- 掩码设置不当会引入何种性能代价?
本文首先说明掩码的声明接口及内核判断掩码能否满足的依据;然后分析掩码在直接映射、一致性分配与 IOMMU 三条路径上的作用;接着介绍 SWIOTLB 的中转过程及其强制使用的场景;最后讨论掩码过窄与过宽两种设置错误的后果。
1. 问题回顾
第 5 篇 §3.2 介绍了struct device中的dma_mask、coherent_dma_mask与bus_dma_limit,以及dma_capable()以掩码与总线限制中较小的非零值判断设备能否访问某一地址;§4.3 介绍了 SWIOTLB 的基本参数。第 6 篇 §6.1 指出,启用 IOMMU 后掩码仍然限制 IOVA 的分配。本文讨论尚未展开的部分:掩码由谁声明,内核如何判断能否满足,地址超出范围时映射层如何处理。
这一问题容易被忽视,原因在于掩码过窄通常不会导致功能错误。地址超出范围时,映射层自动经 SWIOTLB 中转,数据仍然正确,代价只体现为 CPU 复制与吞吐下降。
2. 掩码的声明与协商
2.1 默认值与声明接口
PCI 设备在枚举时获得 32 位的默认掩码:
dev->dma_mask=0xffffffff;...dev->dev.dma_mask=&dev->dma_mask;dev->dev.coherent_dma_mask=0xffffffffull;32 位是早期 PCI 设备的寻址宽度,也是绝大多数设备能够满足的下限,因此作为默认值。支持更宽地址的设备需要驱动主动提高掩码;寻址能力不足 32 位的设备必须由驱动降低掩码,否则映射层可能交给设备其无法访问的地址。dev->dma_mask是指针,指向总线设备结构中的存储,该指针为空的设备不能进行流式映射。
驱动通过三个接口声明掩码:dma_set_mask()设置流式映射的掩码,dma_set_coherent_mask()设置一致性分配的掩码,dma_set_mask_and_coherent()同时设置两者。两个掩码分开的原因是部分设备的描述符只支持 32 位地址,数据缓冲区却支持 64 位地址;描述符通常由一致性分配获得,数据缓冲区通常经流式映射,两者应分别声明。
2.2 设置过程
intdma_set_mask(structdevice*dev,u64 mask){mask=(dma_addr_t)mask;if(!dev->dma_mask||!dma_supported(dev,mask))return-EIO;arch_dma_set_mask(dev,mask);*dev->dma_mask=mask;dma_setup_need_sync(dev);return0;}掩码首先被截断为dma_addr_t的宽度,避免在 32 位dma_addr_t的平台上产生无法表示的地址。dma_supported()按设备所在的路径判断:使用 IOMMU 的设备总是返回真,因为 IOVA 由内核分配,总能落在掩码之内;存在dma_map_ops时交给其回调;直接映射时调用dma_direct_supported():
intdma_direct_supported(structdevice*dev,u64 mask){u64 min_mask=((u64)max_pfn<<PAGE_SHIFT)-1;if(mask>=DMA_BIT_MASK(32))return1;if(IS_ENABLED(CONFIG_ZONE_DMA))min_mask=min_t(u64,min_mask,zone_dma_limit);returnmask>=phys_to_dma_unencrypted(dev,min_mask);}不小于 32 位的掩码总被接受:内核要求每种体系结构都能满足 32 位掩码,或者不支持更多物理内存,或者提供 ZONE_DMA32,否则必须使用 IOMMU。小于 32 位的掩码须覆盖 ZONE_DMA 的上限或全部内存,否则内核无法为设备提供可访问的内存,设置失败。
由此可见,所谓“协商”是指:驱动报告设备的硬件能力,内核判断能否以某种手段满足,手段包括从低地址区域分配、经 SWIOTLB 中转与使用 IOMMU。设置成功并不表示设备能直接访问全部内存,只表示内核承诺此后交给设备的地址都在掩码之内。
2.3 正确的声明方式
既然不小于 32 位的掩码不会失败,“先尝试 64 位、失败后回退到 32 位”的写法就没有意义,内核文档将其列为错误示例。驱动应按设备的实际能力声明一次。例如 NVMe 驱动对存在缺陷的控制器声明 48 位,其余声明 64 位;amdgpu 按 GPU 代次声明:
dma_addr_bits=amdgpu_ip_version(adev,GC_HWIP,0)>=IP_VERSION(9,4,2)?48:44;r=dma_set_mask_and_coherent(adev->dev,DMA_BIT_MASK(dma_addr_bits));if(r){drm_warn(adev_to_drm(adev),"No suitable DMA available.\n");returnr;}设置失败时,设备在该平台上无法正确进行 DMA,驱动应打印警告并放弃使用 DMA。
3. 掩码在各路径上的作用
掩码在直接映射、一致性分配与 IOMMU 三条路径上所起的作用各不相同,如图 1 所示。
3.1 直接映射
第 2 篇已经给出dma_direct_map_phys()的主体,与掩码相关的部分如下:
if(is_swiotlb_force_bounce(dev)){...returnswiotlb_map(dev,phys,size,dir,attrs);}...dma_addr=phys_to_dma(dev,phys);if(unlikely(!dma_capable(dev,dma_addr,size,true))||dma_kmalloc_needs_bounce(dev,size,dir)){if(is_swiotlb_active(dev)&&!(attrs&DMA_ATTR_REQUIRE_COHERENT))returnswiotlb_map(dev,phys,size,dir,attrs);gotoerr_overflow;}(kernel/dma/direct.h,节选并简化)
地址超出掩码时,若 SWIOTLB 可用则经其中转,否则映射失败并打印“DMA addr … overflow”。直接映射路径无法改变缓冲区所在的物理地址,掩码不足只能以复制弥补。
3.2 一致性分配
一致性分配由内核选择内存,因而可以按coherent_dma_mask直接从合适的区域分配(第 5 篇 §3.3),不需要复制。掩码过窄的代价是分配集中于 ZONE_DMA32 等低地址区域,这些区域容量有限,且由所有受限设备共享。
3.3 IOMMU 路径
启用 IOMMU 后,掩码限制的是 IOVA 的分配范围(第 6 篇 §4.1),物理页可以位于任意地址,掩码不会引起复制。例外是标记为不受信任的设备(如经外部接口接入的 PCIe 设备):其缓冲区不足一个 IOMMU 页时,iommu-dma先将数据复制到 SWIOTLB,避免同页的其他数据暴露给设备。
3.4 查询接口
驱动可以通过以下接口了解寻址能力是否构成限制:
dma_get_required_mask():覆盖系统全部内存所需的掩码,描述的是系统而非设备。dma_addressing_limited():设备掩码是否小于上述掩码,即是否可能发生中转。dma_max_mapping_size():单次映射的最大长度。可能经 SWIOTLB 中转时,该值受槽位数限制,为 256 KB。
dma_addressing_limited()的典型用途是从低地址区域分配缓冲区,在源头避免中转。etnaviv 驱动的做法如下:
if(dma_addressing_limited(dev)){priv->shm_gfp_mask|=GFP_DMA32;priv->shm_gfp_mask&=~__GFP_HIGHMEM;}amdgpu 在初始化 TTM 时也据此决定页池是否从 DMA32 区域分配。块设备驱动则以dma_max_mapping_size()或dma_opt_mapping_size()限制单个请求的长度,例如 NVMe 驱动以后者计算max_hw_sectors,避免请求超出中转所能处理的长度。
4. SWIOTLB 的中转过程
4.1 缓冲池的组织
SWIOTLB 在启动阶段从低地址预留一块内存,默认 64 MB,划分为 2 KB 的槽位,单次映射至多使用 128 个连续槽位。为减少多处理器间的锁竞争,缓冲池又划分为若干 area,每个 area 有独立的自旋锁,数量默认按 CPU 数取整到 2 的幂。查找槽位时从当前 CPU 对应的 area 开始,失败后依次尝试其余 area。
启用CONFIG_SWIOTLB_DYNAMIC时,所有 area 都找不到空闲槽位后,内核以GFP_NOWAIT按设备掩码分配一个临时缓冲池,并在后台扩充缓冲池;否则映射失败。
4.2 映射与解除映射
一次中转从分配槽位、复制数据到复制回原缓冲区的完整过程如图 2 所示。
index=swiotlb_find_slots(dev,orig_addr,size,alloc_align_mask,&pool);if(index==-1){if(!(attrs&DMA_ATTR_NO_WARN))dev_warn_ratelimited(dev,"swiotlb buffer is full (sz: %zd bytes), total %lu (slots), used %lu (slots)\n",size,mem->nslabs,mem_used(mem));return(phys_addr_t)DMA_MAPPING_ERROR;}...for(i=0;i<(nr_slots(size)-pad_slots);i++)pool->slots[index+i].orig_addr=slot_addr(orig_addr,i);tlb_addr=slot_addr(pool->start,index)+offset;swiotlb_bounce(dev,tlb_addr,mapping_size,DMA_TO_DEVICE,pool);returntlb_addr;映射时,每个槽位记录对应的原地址,随后将原缓冲区复制到槽位。复制不区分方向:即使是DMA_FROM_DEVICE,设备也可能只写入部分内容,预先复制可以保证未写入的部分与原缓冲区一致。设备获得的是槽位的地址。解除映射或调用dma_sync_*_for_cpu()时,方向为DMA_FROM_DEVICE或DMA_BIDIRECTIONAL则将槽位内容复制回原缓冲区;dma_sync_*_for_device()在方向含DMA_TO_DEVICE时再次复制到槽位。第 9 篇所述的同步接口在这里成为数据复制,因此一旦设备使用过 SWIOTLB,同步就不能再被跳过(第 9 篇 §4.3)。
4.3 初始化与强制使用
x86 在启动时决定是否初始化 SWIOTLB:
staticvoid__initpci_swiotlb_detect(void){if(!no_iommu&&max_possible_pfn>MAX_DMA32_PFN)x86_swiotlb_enable=true;if(cc_platform_has(CC_ATTR_HOST_MEM_ENCRYPT))x86_swiotlb_enable=true;if(cc_platform_has(CC_ATTR_GUEST_MEM_ENCRYPT)){x86_swiotlb_enable=true;x86_swiotlb_flags|=SWIOTLB_FORCE;}}物理内存可能超过 4 GB 时启用 SWIOTLB,以备 32 位设备使用。此外,SWIOTLB 还承担与寻址能力无关的职责:
- 机密计算虚拟机。客户机内存加密后,宿主机无法访问未显式共享的内存。所有 DMA 都经共享的 SWIOTLB 中转,与掩码无关(第 9 篇 §6.1)。
- 受限 DMA 区域。设备树以
restricted-dma-pool为特定设备指定专用缓冲池,使设备只能访问该区域。 - 不受信任的设备。见 §3.3。
- 调试。启动参数
swiotlb=force使所有直接映射都经中转,可用于验证驱动的同步调用是否完整。
5. 掩码设置不当的代价
掩码与设备实际寻址能力不一致时,过窄与过宽两种情形的后果不同,如图 3 所示。
5.1 掩码过窄
最常见的情形是 64 位设备的驱动未声明掩码,保持 32 位默认值。在内存超过 4 GB 且未启用 IOMMU 的系统上,位于 4 GB 以上的缓冲区都经 SWIOTLB 中转,代价包括:
- CPU 复制。每次映射、解除映射或同步都复制数据,占用 CPU 时间与内存带宽,DMA 卸载 CPU 的意义大为削弱。
- 槽位耗尽。缓冲池默认只有 64 MB,高吞吐设备的在途映射可能将其占满,映射失败并打印“swiotlb buffer is full”,驱动进入错误处理路径。
- 映射长度受限。单次映射不超过 256 KB,块设备的请求被拆分为更多更小的请求。
- 锁竞争。并发映射在 area 锁上竞争。
- 同步无法跳过。原本一致的设备也必须执行同步。
这些代价都不会表现为功能错误,因而常在性能分析时才被发现。排查时可查看内核日志中的上述警告,读取 debugfs 中swiotlb/io_tlb_used与io_tlb_used_hiwater的值,或启用swiotlb:swiotlb_bounced跟踪点确认中转的设备与长度。
5.2 掩码过宽
若设备实际只支持 N 位地址,驱动却声明了 64 位,映射层会认为所有地址都可访问,交给设备超出 2^N 的地址。设备丢弃高位后访问的是另一处物理内存,读取错误的数据或破坏其他内存,映射层无法察觉。启用 IOMMU 时,IOVA 优先从 32 位以内分配,问题只在 32 位 IOVA 空间耗尽后才出现,表现为负载升高后偶发的数据错误,更难定位。
两种错误的后果并不对称:过窄只损失性能,过宽则破坏数据。掩码应准确反映硬件能力;无法确定硬件的实际宽度时,应声明较窄的掩码。
6. 常见误区
6.1 设置 64 位掩码失败后回退到 32 位
不小于 32 位的掩码不会被拒绝,回退分支永远不会执行。设备不支持 64 位时,应直接声明其实际宽度。
6.2 认为设置成功即表示设备可以直接访问全部内存
设置成功只表示内核能够以某种手段满足该掩码,手段可能是 SWIOTLB 中转。是否会发生中转,应以dma_addressing_limited()判断。
6.3 以dma_get_required_mask()的结果作为设备的掩码
该接口描述覆盖系统内存所需的宽度,与设备能力无关。以此设置掩码,在内存较少的机器上会使掩码偏窄,在内存较多的机器上可能超出设备的实际能力。
6.4 认为 SWIOTLB 只在设备寻址能力不足时使用
机密计算虚拟机、受限 DMA 区域与不受信任的设备都会使用 SWIOTLB,与掩码无关。驱动不应假设映射得到的地址对应原缓冲区,也不应在映射期间由 CPU 直接读写原缓冲区来与设备交换数据。
7. 小结
本文回答了问题 11,要点如下:
- 掩码是驱动对硬件能力的声明。PCI 设备默认为 32 位;驱动以
dma_set_mask_and_coherent()等接口按实际能力声明,流式映射与一致性分配可以分别设置。 - 协商的结果是内核的承诺。不小于 32 位的掩码总被接受,更窄的掩码须覆盖 ZONE_DMA;设置成功表示此后交给设备的地址都在掩码之内,而非设备能直接访问全部内存。
- 掩码在三条路径上的作用不同。直接映射以 SWIOTLB 中转处理超出范围的地址;一致性分配从低地址区域分配;IOMMU 路径限制 IOVA 的范围。只有第一种引入复制。
- SWIOTLB 以复制保证正确性。映射时复制到槽位,解除映射或同步时按方向复制回原缓冲区;缓冲池容量与单次映射长度有限,并承担机密计算等与寻址无关的职责。
- 过窄损失性能,过宽破坏数据。前者表现为 CPU 复制、槽位耗尽与请求拆分,后者表现为难以定位的内存错误。
本文多次涉及同一次映射调用在不同情况下进入直接映射、IOMMU 或 SWIOTLB。映射接口如何在这些后端之间分发,各后端以何种结构接入,即为问题 12。
系列导航:
- 上一篇《第 10 篇 Scatter-Gather 与 sg_table:分散内存的统一映射》
- 下一篇《第 12 篇 dma-mapping 的分层架构:从映射接口到
dma_map_ops》