ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

第 8 篇 一致性映射与流式映射:两种映射契约

第 8 篇 一致性映射与流式映射:两种映射契约

本文属于第三册“内核抽象:dma-mapping 接口的设计原则”,回答第 0 篇提出的问题 8:dma_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->size=tx_ring->count*sizeof(unione1000_adv_tx_desc);tx_ring->size=ALIGN(tx_ring->size,4096);tx_ring->desc=dma_alloc_coherent(dev,tx_ring->size,&tx_ring->dma,GFP_KERNEL);

发送每个数据包时,则以dma_map_single()映射协议栈交来的数据,并把 DMA 地址写入描述符(同一文件,节选并简化):

dma=dma_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_addr=cpu_to_le64(dma);

两个接口都返回 DMA 地址,区别在于调用者与内核之间就“这块内存如何被访问、一致性由谁保证”所作的约定,本文称之为映射契约。图 1 对比了两类缓冲区的访问模式。

图 1: 两类缓冲区的访问模式

2. 两类缓冲区的访问模式

2.1 描述符环:CPU 与设备并发访问

描述符环在驱动初始化时分配、设备关闭时释放,其间 CPU 与设备持续访问它。以接收方向为例,CPU 把空闲缓冲区的地址填入描述符,设备收到数据后写回状态与长度,CPU 以长度字段判断描述符是否完成(drivers/net/ethernet/intel/igb/igb_main.c,节选):

rx_desc=IGB_RX_DESC(rx_ring,rx_ring->next_to_clean);size=le16_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 行,可能以旧值覆盖设备刚写回的同一行中的描述符 i+1(第 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_addr=dma_direct_alloc(dev,size,dma_handle,flag,attrs);}elseif(use_dma_iommu(dev)){cpu_addr=iommu_dma_alloc(dev,size,dma_handle,flag,attrs);}elseif(ops->alloc){cpu_addr=ops->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.c):

pgprot_tdma_pgprot(structdevice*dev,pgprot_tprot,unsignedlongattrs){if(dev_is_dma_coherent(dev))returnprot;#ifdefCONFIG_ARCH_HAS_DMA_WRITE_COMBINEif(attrs&DMA_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.h):

staticinlinedma_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)||...)addr=dma_direct_map_phys(dev,phys,size,dir,attrs,true);elseif(use_dma_iommu(dev))addr=iommu_dma_map_phys(dev,phys,size,dir,attrs);elseif(ops->map_phys)addr=ops->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_*的作用》将回答问题 9:dma_sync_*系列同步操作的作用是什么。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进