第 8 篇 一致性映射与流式映射:两种映射契约
本文属于第三册“内核抽象dma-mapping 接口的设计原则”回答第 0 篇提出的问题 8dma_alloc_coherent与dma_map_single的本质区别是什么内核为何提供两种映射契约前两册讨论了 DMA 的硬件约束cache 一致性第 3、4 篇、物理连续性与寻址位宽第 5 篇、IOMMU 的地址翻译及其开销第 6、7 篇。驱动通过 DMA 映射接口间接应对这些约束。本文从网卡驱动中两类缓冲区的访问模式出发说明两种映射契约的约定与实现再介绍介于两者之间的接口并给出选择依据。1. 问题回顾第 0 篇第 2.8 节指出内核提供两类 DMA 映射一类分配后 CPU 与设备可随时访问另一类效率更高但须由驱动管理同步时机。这两类需求在网卡驱动中同时存在。igb 驱动在初始化发送队列时以dma_alloc_coherent()分配描述符环drivers/net/ethernet/intel/igb/igb_main.c节选/* round up to nearest 4K */tx_ring-sizetx_ring-count*sizeof(unione1000_adv_tx_desc);tx_ring-sizeALIGN(tx_ring-size,4096);tx_ring-descdma_alloc_coherent(dev,tx_ring-size,tx_ring-dma,GFP_KERNEL);发送每个数据包时则以dma_map_single()映射协议栈交来的数据并把 DMA 地址写入描述符同一文件节选并简化dmadma_map_single(tx_ring-dev,skb-data,size,DMA_TO_DEVICE);if(dma_mapping_error(tx_ring-dev,dma))gotodma_error;...tx_desc-read.buffer_addrcpu_to_le64(dma);两个接口都返回 DMA 地址区别在于调用者与内核之间就“这块内存如何被访问、一致性由谁保证”所作的约定本文称之为映射契约。图 1 对比了两类缓冲区的访问模式。图 1 两类缓冲区的访问模式2. 两类缓冲区的访问模式2.1 描述符环CPU 与设备并发访问描述符环在驱动初始化时分配、设备关闭时释放其间 CPU 与设备持续访问它。以接收方向为例CPU 把空闲缓冲区的地址填入描述符设备收到数据后写回状态与长度CPU 以长度字段判断描述符是否完成drivers/net/ethernet/intel/igb/igb_main.c节选rx_descIGB_RX_DESC(rx_ring,rx_ring-next_to_clean);sizele16_to_cpu(rx_desc-wb.upper.length);if(!size)break;/* This memory barrier is needed to keep us from reading * any other fields out of the rx_desc until we know the * descriptor has been written back */dma_rmb();这类访问有两个特点双方的访问在时间上交错没有可以划分的边界访问的粒度小于 cache 行一个 64 字节的 cache 行容纳 4 个 16 字节的接收描述符。在非一致性平台上若以可缓存方式访问描述符环CPU 回填描述符 i 后 clean 所在的 cache 行可能以旧值覆盖设备刚写回的同一行中的描述符 i1第 3 篇第 5.1 节。这类缓冲区只能由硬件保证一致性或以不可缓存方式访问。2.2 数据缓冲区CPU 与设备交替访问数据缓冲区则由双方交替访问发送时设备读取协议栈准备好的 skb接收时设备写入后再交给协议栈以映射与解除映射为界。这类内存来自上层子系统映射接口无法选择其位置与类型CPU 解析报头、复制数据时又依赖 cache因此只需在边界上执行 cache 维护第 3 篇第 4.2 节。2.3 两种访问模式的不同要求特点描述符环等控制结构数据缓冲区生命周期与设备工作期间相同一次或若干次传输访问方式并发访问粒度可能小于 cache 行交替访问以映射与解除映射为界CPU 访问量少量字段大量数据内存来源驱动为 DMA 专门分配上层子系统已分配的内存非一致性平台上适宜的内存类型不可缓存可缓存在边界上维护在非一致性平台上两者对内存类型的要求恰好相反控制结构须放弃 cache 以保证正确数据缓冲区须保留 cache 以保证性能这是内核提供两种映射契约的根本原因。3. 一致性映射的契约3.1 接口与约定一致性映射以dma_alloc_coherent()同时完成分配与映射返回值是 CPU 使用的虚拟地址dma_handle返回设备使用的 DMA 地址。内核文档对其约定的描述如下Documentation/core-api/dma-api-howto.rst节选The invariant these examples all require is that any CPU store to memory is immediately visible to the device, and vice versa. Coherent mappings guarantee this.即在分配之后、dma_free_coherent()之前双方可随时访问驱动无需同步。兑现这一约定的前提是内存由接口自己分配接口因而可以选择满足寻址限制的物理位置第 5 篇第 3.3 节与 CPU 访问时的内存类型。3.2 不同平台上的实现dma_alloc_attrs()根据设备所使用的后端选择实现kernel/dma/mapping.c节选if(dma_alloc_from_dev_coherent(dev,size,dma_handle,cpu_addr)){...returncpu_addr;}/* let the implementation decide on the zone to allocate from: */flag~(__GFP_DMA|__GFP_DMA32|__GFP_HIGHMEM);if(dma_alloc_direct(dev,ops)||arch_dma_alloc_direct(dev)){cpu_addrdma_direct_alloc(dev,size,dma_handle,flag,attrs);}elseif(use_dma_iommu(dev)){cpu_addriommu_dma_alloc(dev,size,dma_handle,flag,attrs);}elseif(ops-alloc){cpu_addrops-alloc(dev,size,dma_handle,flag,attrs);}else{...returnNULL;}dma_alloc_from_dev_coherent()处理设备专用的预留区域后端分发机制见第 12 篇调用者传入的区域标志被清除改由实现根据coherent_dma_mask决定。其余三种典型情形如图 2 所示。图 2 一致性分配在三种典型情形下的实现一致性设备直接映射。x86 平台上的设备以及 ARM 平台上接入一致性互连的设备由硬件侦听保持一致第 4 篇。dma_direct_alloc()直接返回线性映射中的可缓存地址CPU 访问没有额外代价。非一致性设备直接映射。dma_direct_alloc()先清除线性映射上的脏行再把同一组物理页重新映射为不可缓存类型或直接修改其内存属性第 4 篇第 5.2 节。页属性由dma_pgprot()决定kernel/dma/mapping.cpgprot_tdma_pgprot(structdevice*dev,pgprot_tprot,unsignedlongattrs){if(dev_is_dma_coherent(dev))returnprot;#ifdefCONFIG_ARCH_HAS_DMA_WRITE_COMBINEif(attrsDMA_ATTR_WRITE_COMBINE)returnpgprot_writecombine(prot);#endifreturnpgprot_dmacoherent(prot);}ARM64 的pgprot_dmacoherent()选用 Normal Non-cacheable 类型第 3 篇第 4.4 节。此时约定由内存类型兑现CPU 的读写不经过 cache代价是每次访问都到达内存。启用 IOMMU。允许睡眠时iommu_dma_alloc()分配不要求物理连续的页映射到一段连续的 IOVA再以dma_common_pages_remap()为 CPU 建立连续的内核虚拟地址第 6 篇第 4.2 节。同一个调用在不同平台上得到的内存可能连续或不连续、可缓存或不可缓存约定则相同。3.3 约定的附加条件访问顺序须由驱动保证。“立即可见”只表示 cache 中没有陈旧副本不保证写入按程序顺序到达内存Documentation/core-api/dma-api-howto.rst节选Coherent DMA memory does not preclude the usage of proper memory barriers. The CPU may reorder stores to coherent memory just as it may normal memory. Example: if it is important for the device to see the first word of a descriptor updated before the second, you must do something like:: desc-word0 address; wmb(); desc-word1 DESC_VALID;内核为此提供dma_wmb()、dma_rmb()与dma_mb()第 2.1 节 igb 代码中的dma_rmb()即为一例。分配的粒度为页。长度向上取整到页并清零地址按 2 的幂次页数对齐。大量小块一致性内存应使用dma_pool第 6.2 节。分配与释放的上下文。可以GFP_ATOMIC在原子上下文中分配此时在需要重映射的平台上由容量有限的原子内存池提供释放则不能在关闭中断时进行因为可能调用vunmap()。分配结果不能转换回页。返回的 CPU 地址可能位于 vmalloc 区不能对其调用virt_to_page()或dma_map_single()映射到用户态用dma_mmap_coherent()以 scatterlist 描述用dma_get_sgtable()。寻址范围由coherent_dma_mask决定。它与dma_mask的区别见第 5 篇第 3.2 节设置方法见第 11 篇。3.4 一致性映射的代价在非一致性平台上CPU 每次访问都不经过 cache用于少量字段尚可用于大量数据则性能明显下降。每次分配至少占一页映射在整个生命周期内占用内存与 IOVA。更根本的是内存必须由接口分配无法用于上层子系统已有的数据这决定了一致性映射不可能替代流式映射。4. 流式映射的契约4.1 接口与约定流式映射为调用者已有的内存建立映射include/linux/dma-mapping.hstaticinlinedma_addr_tdma_map_single_attrs(structdevice*dev,void*ptr,size_tsize,enumdma_data_directiondir,unsignedlongattrs){/* DMA must never operate on areas that might be remapped. */if(dev_WARN_ONCE(dev,is_vmalloc_addr(ptr),rejecting DMA map of vmalloc memory\n))returnDMA_MAPPING_ERROR;debug_dma_map_single(dev,ptr,size);returndma_map_page_attrs(dev,virt_to_page(ptr),offset_in_page(ptr),size,dir,attrs);}dma_map_single()把虚拟地址转换为页与页内偏移最终进入dma_map_phys()。传入的地址因此必须位于内核线性映射中且整段物理连续例如kmalloc()所得的内存vmalloc 区的地址被直接拒绝第 5 篇第 7.2 节。流式映射的约定如下映射时声明传输方向DMA_TO_DEVICE、DMA_FROM_DEVICE或DMA_BIDIRECTIONAL映射之后、解除映射之前缓冲区归设备使用CPU 如需访问须调用dma_sync_*接口临时收回所有权解除映射之后缓冲区归还 CPU设备写入的数据此时可见映射可能失败须以dma_mapping_error()检查。前三点构成所有权交接规则是第 9 篇的主题。4.2 不同平台上的实现流式映射的各个接口最终进入dma_map_phys()由其根据设备选择后端kernel/dma/mapping.c节选并简化if(dma_map_direct(dev,ops)||...)addrdma_direct_map_phys(dev,phys,size,dir,attrs,true);elseif(use_dma_iommu(dev))addriommu_dma_map_phys(dev,phys,size,dir,attrs);elseif(ops-map_phys)addrops-map_phys(dev,phys,size,dir,attrs);内存不由接口分配接口无法改变其位置与内存类型只能在所有权交接的时刻采取措施。视设备与平台的不同映射与解除映射时可能执行三类操作如图 3 所示cache 维护对非一致性设备映射时 clean解除映射时按方向 invalidate第 3 篇第 4.3 节反弹缓冲缓冲区超出设备的寻址范围或属于不可信设备且未按页对齐时经 SWIOTLB 中转按方向复制数据第 5 篇第 4.3 节、第 6 篇第 4.3 节地址翻译启用 IOMMU 时映射时分配 IOVA 并写入 I/O 页表解除映射时清除页表项并使 IOTLB 失效第 6、7 篇。图 3 映射与解除映射时可能执行三类操作对于地址在寻址范围内、未启用 IOMMU 的一致性设备三类操作均不需要映射仅完成第 2 篇第 4 节所述的地址转换。这正是流式映射在 x86 上常被误认为“只是取得 DMA 地址”的原因。4.3 约定的附加条件缓冲区须独占所在的 cache 行否则在设备写入方向上出现第 3 篇第 5.1 节所述的问题kmalloc()的对齐保证见第 3 篇第 5.2 节。失败以特殊地址表示。SWIOTLB 槽位或 IOVA 空间耗尽时返回DMA_MAPPING_ERROR必须以dma_mapping_error()判断不能与 0 比较。映射路径不能睡眠。流式映射多在软中断等快速路径上调用第 6 篇第 4.1 节的 IOMMU 路径以GFP_ATOMIC调用iommu_map()即出于这一要求。CPU 以可缓存方式访问。流式映射不改变内存类型CPU 在拥有缓冲区期间以正常速度访问这是它相对一致性映射的主要优势。4.4 流式映射的代价流式映射的代价集中在每次交接cache 维护的开销与长度成正比反弹缓冲多一次复制IOMMU 路径带来 IOVA 分配与 IOTLB 失效第 7 篇。反复使用的缓冲区可以保留映射仅在交接时调用dma_sync_*如 page_pool第 7 篇第 6 节第 9 篇将详细讨论。5. 两种契约的本质区别项目一致性映射流式映射典型接口dma_alloc_coherent()dma_map_single()、dma_map_page()、dma_map_sg()内存来源由接口分配由调用者提供生命周期通常与设备工作期间相同通常为一次传输一致性的保证方式硬件侦听或内存类型始终成立交接时执行维护仅在交接之后成立驱动的责任以内存屏障保证访问顺序声明方向遵守交接检查错误非一致性平台上的 CPU 访问不可缓存可缓存寻址范围分配时满足coherent_dma_mask映射时检查必要时经 SWIOTLB 或 IOMMU失败的表现返回NULL返回DMA_MAPPING_ERROR典型用途描述符环、命令队列、完成队列网络数据包、块设备数据页两者的本质区别在于一致性由谁、在何时保证一致性映射由接口分配内存并选定类型一致性始终成立代价在 CPU 的每次访问上流式映射只能在驱动声明的交接时刻执行维护代价在每次交接上。在一致性平台上二者行为差异很小但驱动须按契约而非平台的实际行为选择接口。6. 介于两者之间的接口6.1 非一致性分配有些缓冲区由驱动专门为 DMA 分配访问方式却与数据缓冲区相同CPU 与设备交替访问且 CPU 需要高速访问其内容。对此内核提供了dma_alloc_pages()及其封装dma_alloc_noncoherent()内存由接口分配因而保证寻址范围但以普通的可缓存方式访问驱动须像使用流式映射那样在交接时调用dma_sync_single_for_device()与dma_sync_single_for_cpu()。需要较大且物理上可以不连续的缓冲区时可以使用dma_alloc_noncontiguous()它返回描述所分配内存的sg_table并保证其在设备侧只有一个连续的 DMA 段。6.2 小块一致性内存与分配属性dma_pool在一致性分配所得的页中划分固定大小的块可指定对齐与不得跨越的边界用于大量小块一致性内存。dma_alloc_attrs()还接受若干属性常用的有DMA_ATTR_WRITE_COMBINE在非一致性平台上以写合并类型建立映射适用于 CPU 只写、设备只读的缓冲区DMA_ATTR_NO_KERNEL_MAPPING不建立内核映射返回值只能作为句柄传给dma_mmap_attrs()与dma_free_attrs()适用于只由用户态与设备访问的缓冲区。平台不支持时这两个属性均被忽略。另有dmam_alloc_coherent()所分配的内存在驱动与设备解除绑定时自动释放。6.3 接口的分类图 4 按“内存由谁分配”与“一致性在何时成立”两个维度对以上接口进行分类。图 4“内存由谁分配”与“一致性在何时成立”两维度对比图中右上角不存在通用的接口调用者的内存已经以可缓存方式映射并可能与其他数据共享同一页内核无法为一次传输改变其内存类型。DMA_ATTR_REQUIRE_COHERENT属性与此相关带有该属性的映射在需要 SWIOTLB 或 cache 维护的系统上直接失败。它并不使内存变得一致只是要求平台本身已具备一致性内核文档规定其仅用于 RDMA、DRM 等需要与用户态持续保持硬件一致性的场合。6.4 选择依据长期存在、被并发访问的控制结构如描述符环与完成队列dma_alloc_coherent()大量小块时用dma_pool。来自上层子系统、每次传输都不同的缓冲区dma_map_single()、dma_map_page()或dma_map_sg()。驱动分配并反复使用、CPU 需高速访问的缓冲区dma_alloc_noncoherent()或长期保留流式映射并在交接时调用dma_sync_*。只由设备与用户态访问的缓冲区带DMA_ATTR_NO_KERNEL_MAPPING分配以dma_mmap_attrs()映射到用户态。7. 常见误区7.1 认为一致性映射无须内存屏障一致性映射只消除了 cache 中的陈旧副本不约束 CPU 读写的顺序。驱动须在写入所有权标志之前使用dma_wmb()在读取所有权标志之后、读取其他字段之前使用dma_rmb()。x86 不会重排这类访问其dma_wmb()与dma_rmb()仅为编译器屏障因此遗漏屏障的代码往往在移植到 ARM 等弱序平台后才表现为偶发的数据错误。7.2 认为对大块数据使用一致性映射可以省去同步开销在非一致性平台上一致性内存以不可缓存方式访问由此增加的访问开销往往远大于省去的 cache 维护开销即使在一致性平台上为每次传输分配与释放一致性内存也有分配、清零与 IOMMU 映射的开销。需要 CPU 处理的大块数据应使用流式映射或非一致性分配。7.3 将一致性分配的内存再交给流式接口在一致性设备上返回地址通常位于线性映射中对其调用virt_to_page()或dma_map_single()的代码碰巧可以运行换到需要重映射或启用 IOMMU 的平台后前者得到错误的页后者直接拒绝。正确做法见第 3.3 节跨设备共享缓冲区的规范做法见第 13 篇。7.4 认为一致性分配的内存总是物理连续启用 IOMMU 时iommu_dma_alloc()分配的是不连续的页只有 IOVA 与内核虚拟地址连续。由 DMA 地址推算物理地址或把缓冲区交给不经过 IOMMU 的另一个 DMA 引擎都会访问到错误的内存。确实需要物理连续时应指定DMA_ATTR_FORCE_CONTIGUOUS或以连续内存分配的方式获取第 5 篇第 2.4 节。8. 小结本文回答了问题 8要点如下两类缓冲区的访问模式相反。控制结构被并发、细粒度地访问在非一致性平台上只能不可缓存数据缓冲区被交替访问需保留 cache 并在边界上维护。一致性映射由接口分配内存一致性始终成立。驱动仍须以dma_wmb()、dma_rmb()保证访问顺序。流式映射为已有内存建立映射一致性仅在交接时成立。驱动须声明方向、遵守所有权约定并检查映射错误。本质区别在于一致性由谁、在何时保证。前者的代价在 CPU 的每次访问上后者的代价在每次交接上。两者之间存在折中的接口。如dma_alloc_noncoherent()、dma_pool与DMA_ATTR_*属性。流式映射的约定以所有权交接为核心反复使用的缓冲区通过dma_sync_*完成交接。这些同步操作的作用是什么CPU 与设备之间如何交接缓冲区所有权即为问题 9。系列导航上一篇《第 7 篇 IOMMU 的内部机制I/O 页表、IOVA 分配与 IOTLB》下一篇《第 9 篇 DMA 方向与缓冲区所有权dma_sync_*的作用》将回答问题 9dma_sync_*系列同步操作的作用是什么。