资讯详情

第 9 篇 DMA 方向与缓冲区所有权:dma_sync_* 的作用

📅 2026/10/17 13:42:50 | 华诺云谱 👁 阅读
第 9 篇 DMA 方向与缓冲区所有权:dma_sync_* 的作用
本文属于第三册“内核抽象dma-mapping 接口的设计原则”回答第 0 篇提出的问题 9dma_sync_*系列同步操作的作用是什么CPU 与设备之间如何交接缓冲区所有权第 8 篇指出流式映射不改变缓冲区的内存类型一致性只在 CPU 与设备交接缓冲区时成立。本文首先建立缓冲区所有权模型然后说明 DMA 方向如何决定交接时执行的操作接着分析dma_sync_*接口的实现最后以网卡驱动为例说明交接序列的组织方式。1. 问题回顾第 3 篇第 4.2 节给出了非一致性平台上 cache 维护的时机第 8 篇第 4.1 节将流式映射的约定概括为映射期间缓冲区归设备使用CPU 如需访问须调用dma_sync_*接口临时收回所有权。若缓冲区只用于一次传输映射与解除映射即完成全部交接。但为了降低 IOMMU 的开销网卡驱动倾向于让接收缓冲区长期保持映射、反复使用第 7 篇第 6 节。每次往返都需要与映射、解除映射等价的一致性操作却不应重新分配 DMA 地址dma_sync_*接口正是为此提供的。需要回答的问题有三个交接的规则是什么交接时执行哪些操作由什么决定驱动应如何组织交接的顺序与范围。2. 缓冲区所有权模型2.1 CPU 域与设备域内核以“所有权”描述流式映射缓冲区的状态缓冲区在任一时刻或属于 CPU 域或属于设备域。dma_sync_sgtable_for_cpu()的注释直接采用了这一表述include/linux/dma-mapping.h/** * dma_sync_sgtable_for_cpu - Synchronize the given buffer for CPU access * dev: The device for which to perform the DMA operation * sgt: The sg_table object describing the buffer * dir: DMA direction * * Performs the needed cache synchronization and moves the ownership of the * buffer back to the CPU domain, so it is safe to perform any access to it * by the CPU. Before doing any further DMA operations, one has to transfer * the ownership of the buffer back to the DMA domain by calling the * dma_sync_sgtable_for_device(). */缓冲区的所有权状态及其转换如图 1 所示。dma_map_*()将 CPU 拥有的普通内存交给设备dma_unmap_*()将其交还 CPU 并使 DMA 地址失效映射保持期间dma_sync_*_for_cpu()与dma_sync_*_for_device()在两个域之间来回转移所有权。图 1 缓冲区的所有权状态及其转换2.2 交接规则所有权模型对双方的约束如下设备拥有期间CPU 不得访问缓冲区。读取可能得到陈旧数据写入可能被丢弃或在 cache 行被替换时覆盖设备写入的数据第 3 篇第 3.3 节CPU 拥有期间不得让设备访问缓冲区。即不得将其 DMA 地址写入描述符也不得启动以其为目标的传输每次转移都须显式调用接口。映射与解除映射是第一次与最后一次交接其间的每次往返都须成对调用for_cpu与for_device转移的单位是字节范围。同一映射的不同部分可以分属不同的域前提是它们不共享 cache 行第 3 篇第 5.1 节。反之若从映射到解除映射期间 CPU 从不访问数据则无须调用任何同步接口Documentation/core-api/dma-api-howto.rst。2.3 以所有权描述接口的原因同步接口的名称描述的是所有权转移的方向而不是具体的 cache 操作。不同架构在同一时刻执行的维护操作并不相同第 3 篇第 4.3 节交接时还可能需要 SWIOTLB 的数据复制一致性设备上则可能无事可做。所有权模型将驱动的职责限定为在正确的时刻声明交接具体操作由映射层根据设备与平台的属性决定。驱动若以某一平台的实际行为为依据省略交接代码将只在部分平台上正确。3. DMA 方向3.1 方向的定义流式映射与同步接口都带有一个方向参数其取值定义如下include/linux/dma-direction.henumdma_data_direction{DMA_BIDIRECTIONAL0,DMA_TO_DEVICE1,DMA_FROM_DEVICE2,DMA_NONE3,};方向描述的是整个映射期间数据的流向DMA_TO_DEVICE表示从内存到设备DMA_FROM_DEVICE表示从设备到内存。方向在映射时确定此后的同步与解除映射须使用相同的方向。DMA_NONE仅用于调试一致性映射没有方向参数相当于隐含的DMA_BIDIRECTIONAL。3.2 方向决定交接时的操作方向参数使映射层能够只执行必要的操作其对三类操作的影响如图 2 所示。图 2 方向决定交接时的操作cache 维护。交给设备时内存中须是最新数据且不存在之后可能被写回的脏行因此各方向都需要 clean或对DMA_FROM_DEVICE直接 invalidate。交还 CPU 时只有设备写入过内存cache 中的副本才可能陈旧因此DMA_TO_DEVICE无须操作其余两个方向须执行 invalidate以清除推测预取产生的副本第 3 篇第 3.3 节。SWIOTLB 数据复制。缓冲区经中转时原缓冲区与中转缓冲区之间的复制方向同样由 DMA 方向决定kernel/dma/swiotlb.cvoid__swiotlb_sync_single_for_device(structdevice*dev,phys_addr_ttlb_addr,size_tsize,enumdma_data_directiondir,structio_tlb_pool*pool){if(dirDMA_TO_DEVICE||dirDMA_BIDIRECTIONAL)swiotlb_bounce(dev,tlb_addr,size,DMA_TO_DEVICE,pool);elseBUG_ON(dir!DMA_FROM_DEVICE);}void__swiotlb_sync_single_for_cpu(structdevice*dev,phys_addr_ttlb_addr,size_tsize,enumdma_data_directiondir,structio_tlb_pool*pool){if(dirDMA_FROM_DEVICE||dirDMA_BIDIRECTIONAL)swiotlb_bounce(dev,tlb_addr,size,DMA_FROM_DEVICE,pool);elseBUG_ON(dir!DMA_TO_DEVICE);}交给设备时只有设备需要读取数据的方向才复制到中转缓冲区交还 CPU 时只有设备可能写入数据的方向才复制回原缓冲区。映射时有一处例外即使方向为DMA_FROM_DEVICEswiotlb_tbl_map_single()也会将原缓冲区复制到中转缓冲区。原因是设备可能只写入一部分而解除映射会将整个中转缓冲区复制回去预先复制保证未被写入的部分保持原值使中转对驱动透明。访问权限。启用 IOMMU 时方向决定 I/O 页表项的读写权限DMA_TO_DEVICE只允许设备读取DMA_FROM_DEVICE只允许设备写入第 6 篇第 4.3 节。设备越权访问将引发 IOMMU 故障。3.3 方向的选择方向参数应尽可能准确。内核文档允许在无法确定方向时使用DMA_BIDIRECTIONAL同时指出这可能带来性能代价。方向过窄会导致错误以DMA_FROM_DEVICE映射需要设备读取的缓冲区经 SWIOTLB 中转时 CPU 写入的数据不会被复制启用 IOMMU 时设备的读取还会因权限不足而失败。方向过宽只带来额外开销多一次 invalidate 或数据复制并使设备获得不必要的写权限。网卡驱动的发送缓冲区使用DMA_TO_DEVICE接收缓冲区使用DMA_FROM_DEVICE。DMA_BIDIRECTIONAL适用于同一缓冲区先后被设备写入和读取的场合典型例子是 XDPXDP 程序可以修改接收到的数据包并以XDP_TX从同一缓冲区发送。stmmac 等驱动因此根据是否加载了 XDP 程序选择方向发送前再以同一方向交还设备drivers/net/ethernet/stmicro/stmmac/stmmac_main.c节选自两处pp_params.dma_dirxdp_prog?DMA_BIDIRECTIONAL:DMA_FROM_DEVICE;...dma_sync_single_for_device(priv-device,dma_addr,xdpf-len,DMA_BIDIRECTIONAL);4.dma_sync_*的实现4.1 接口族同步接口按描述缓冲区的方式与转移方向组合构成以下几组缓冲区的描述方式交还 CPU交给设备单个连续映射dma_sync_single_for_cpu()dma_sync_single_for_device()单个连续映射中的一段dma_sync_single_range_for_cpu()dma_sync_single_range_for_device()scatterlistdma_sync_sg_for_cpu()dma_sync_sg_for_device()sg_tabledma_sync_sgtable_for_cpu()dma_sync_sgtable_for_device()range版本只是将偏移加到 DMA 地址上。内核文档允许对单个映射进行部分同步但对 scatterlist 要求参数与映射时相同Documentation/core-api/dma-api.rst节选Synchronise a single contiguous or scatter/gather mapping for the CPU and device. With the sync_sg API, all the parameters must be the same as those passed into the sg mapping API. With the sync_single API, you can use dma_handle and size parameters that arent identical to those passed into the single mapping API to do a partial sync.其中dma_sync_sg_*()的nents参数须为传给dma_map_sg()的值而不是其返回的 DMA 段数原因将在第 10 篇说明。4.2 分发与执行顺序同步接口的分发方式与映射接口相同依次判断直接映射、IOMMU 与dma_map_ops三条路径。直接映射路径的两个方向实现如下kernel/dma/direct.h节选staticinlinevoiddma_direct_sync_single_for_device(structdevice*dev,dma_addr_taddr,size_tsize,enumdma_data_directiondir){phys_addr_tpaddrdma_to_phys(dev,addr);swiotlb_sync_single_for_device(dev,paddr,size,dir);if(!dev_is_dma_coherent(dev)){arch_sync_dma_for_device(paddr,size,dir);arch_sync_dma_flush();}}staticinlinevoiddma_direct_sync_single_for_cpu(structdevice*dev,dma_addr_taddr,size_tsize,enumdma_data_directiondir,bool flush){phys_addr_tpaddrdma_to_phys(dev,addr);if(!dev_is_dma_coherent(dev)){arch_sync_dma_for_cpu(paddr,size,dir);if(flush)arch_sync_dma_flush();arch_sync_dma_for_cpu_all();}swiotlb_sync_single_for_cpu(dev,paddr,size,dir);}swiotlb_sync_single_*()只在paddr属于中转缓冲区时执行复制。由于 DMA 地址指向设备实际访问的内存中转时 cache 维护也作用于中转缓冲区。两个方向中 SWIOTLB 复制与 cache 维护的先后顺序恰好相反如图 3 所示。交给设备时CPU 复制到中转缓冲区的数据首先停留在 cache 中须在复制之后 clean交还 CPU 时须先丢弃中转缓冲区在 cache 中的陈旧副本复制时才能读到设备写入的数据。IOMMU 路径采用相同的顺序只是先通过 I/O 页表将 IOVA 转换为物理地址。图 3 dma_sync 的流程4.3 无须同步时的快速路径在一致性设备上若缓冲区不经 SWIOTLB 中转同步接口无事可做。网卡驱动对每个数据包都调用同步接口内核为此提供了快速路径include/linux/dma-mapping.hstaticinlinebooldma_dev_need_sync(conststructdevice*dev){/* Always call DMA sync operations when debugging is enabled */return!dev-dma_skip_sync||IS_ENABLED(CONFIG_DMA_API_DEBUG);}staticinlinevoiddma_sync_single_for_cpu(structdevice*dev,dma_addr_taddr,size_tsize,enumdma_data_directiondir){if(dma_dev_need_sync(dev))__dma_sync_single_for_cpu(dev,addr,size,dir);}dma_skip_sync在设备的 DMA 配置阶段按是否一致设置一旦该设备的某个映射使用了 SWIOTLBswiotlb_tbl_map_single()即调用dma_reset_need_sync()清除这一标志。若架构不提供 cache 维护的实现且未启用 SWIOTLB、DMA 调试与dma_map_ops同步接口则编译为空函数。因此在常见的 x86 服务器上正确调用同步接口的代价通常只是一次标志检查。4.4 映射与解除映射中的交接映射与解除映射的实现中包含了同步操作。以直接映射路径的解除映射为例kernel/dma/direct.h节选physdma_to_phys(dev,addr);if(!(attrsDMA_ATTR_SKIP_CPU_SYNC))dma_direct_sync_single_for_cpu(dev,addr,size,dir,flush);swiotlb_tbl_unmap_single(dev,phys,size,dir,attrs|DMA_ATTR_SKIP_CPU_SYNC);可以认为映射等于“分配 DMA 地址”加上一次交给设备的同步解除映射等于一次交还 CPU 的同步加上“释放 DMA 地址”。DMA_ATTR_SKIP_CPU_SYNC属性用于省去其中的同步部分Documentation/core-api/dma-attributes.rst节选DMA_ATTR_SKIP_CPU_SYNC allows platform code to skip synchronization of the CPU cache for the given buffer assuming that it has been already transferred to device domain. This attribute can be also used for dma_unmap_{single,page,sg} functions family to force buffer to stay in device domain after releasing a mapping for it. Use this attribute with care!典型场景是同一缓冲区为多个设备分别建立映射后续映射无须重复 cache 维护另一类用法是将交接完全交给同步接口管理第 5.3 节的 igb 驱动即属此类。使用这一属性时驱动须自行保证每次交接都有对应的同步调用。5. 典型的交接序列5.1 发送一次映射对应一次传输第 8 篇第 1 节所示的 igb 发送路径是最简单的情形CPU 在映射之前已写好数据此后不再访问映射与解除映射完成了全部交接不需要调用同步接口。5.2 接收检查内容后决定去留内核文档给出了一个在接收后检查内容的示例Documentation/core-api/dma-api-howto.rst节选并简化if(read_card_status(cp)RX_BUF_TRANSFERRED){structmy_card_header*hp;dma_sync_single_for_cpu(cp-dev,cp-rx_dma,cp-rx_len,DMA_FROM_DEVICE);/* Now it is safe to examine the buffer. */hp(structmy_card_header*)cp-rx_buf;if(header_is_ok(hp)){dma_unmap_single(cp-dev,cp-rx_dma,cp-rx_len,DMA_FROM_DEVICE);pass_to_upper_layers(cp-rx_buf);make_and_setup_new_rx_buf(cp);}else{give_rx_buf_to_card(cp);}}驱动在读取报头之前收回所有权若接受数据则解除映射并交给上层若丢弃数据则直接将缓冲区交还设备。后一分支省略for_device的依据是 CPU 只读取而未写入缓冲区中不存在脏行SWIOTLB 也无须复制文档在此处的注释也指出了这一点。若 CPU 可能写入缓冲区则必须调用for_device否则脏行可能在设备写入之后被替换出 cache覆盖设备写入的数据。5.3 接收缓冲区的复用igb 驱动igb 驱动以页为单位管理接收缓冲区在 4 KB 页的系统上将一页分为两半交替使用并尽可能复用页而不重新映射其交接序列如图 4 所示。图 4 igb 驱动的交界序列分配新页时驱动以DMA_ATTR_SKIP_CPU_SYNC映射整页映射只负责取得 DMA 地址drivers/net/ethernet/intel/igb/igb.h与igb_main.c节选#defineIGB_RX_DMA_ATTR\(DMA_ATTR_SKIP_CPU_SYNC|DMA_ATTR_WEAK_ORDERING)/* map page for use */dmadma_map_page_attrs(rx_ring-dev,page,0,igb_rx_pg_size(rx_ring),DMA_FROM_DEVICE,IGB_RX_DMA_ATTR);将缓冲区填入接收环时驱动对其中实际交给设备的一段执行交给设备的同步drivers/net/ethernet/intel/igb/igb_main.c节选if(!igb_alloc_mapped_page(rx_ring,bi))break;/* sync the buffer for use by the device */dma_sync_single_range_for_device(rx_ring-dev,bi-dma,bi-page_offset,bufsz,DMA_FROM_DEVICE);新分配的页与复用的页都经过这段代码。对前者这是第一次交接对后者该段上一次曾交给协议栈处理驱动无法确认 CPU 未曾写入同样需要同步。收到数据后驱动只收回设备实际写入的部分同一文件节选/* we are reusing so sync this buffer for CPU use */dma_sync_single_range_for_cpu(rx_ring-dev,rx_buffer-dma,rx_buffer-page_offset,size,DMA_FROM_DEVICE);其中size取自描述符中设备写回的长度invalidate 与 SWIOTLB 复制的范围都随之缩小。数据交给协议栈之后驱动将页内偏移切换到另一半若该页的其他引用已经释放则将其放回接收环否则以DMA_ATTR_SKIP_CPU_SYNC解除映射因为设备写入的部分已经交还 CPU同一文件节选if(igb_can_reuse_rx_page(rx_buffer,rx_buf_pgcnt)){/* hand second half of page back to the ring */igb_reuse_rx_page(rx_ring,rx_buffer);}else{/* We are not reusing the buffer so unmap it and free * any references we are holding to it */dma_unmap_page_attrs(rx_ring-dev,rx_buffer-dma,igb_rx_pg_size(rx_ring),DMA_FROM_DEVICE,IGB_RX_DMA_ATTR);同一个页的映射中一半可能正由协议栈处理而属于 CPU另一半已经交给设备。两半各自通过范围同步交接且相隔 2 KB不会共享 cache 行这正是第 2.2 节最后一条规则的体现。5.4 page_pool 中的交接page_pool 将上述做法通用化。驱动创建 page_pool 时若指定PP_FLAG_DMA_SYNC_DEV则由 page_pool 在页回收时代为执行交给设备的同步net/core/page_pool.c节选staticvoid__page_pool_dma_sync_for_device(conststructpage_pool*pool,netmem_ref netmem,u32 dma_sync_size){#ifdefined(CONFIG_HAS_DMA)defined(CONFIG_DMA_NEED_SYNC)dma_addr_tdma_addrpage_pool_get_dma_addr_netmem(netmem);dma_sync_sizemin(dma_sync_size,pool-p.max_len);__dma_sync_single_for_device(pool-p.dev,dma_addrpool-p.offset,dma_sync_size,pool-p.dma_dir);#endif}offset与max_len在创建时指定界定设备可能写入的区域归还页时传入的dma_sync_size可以进一步缩小范围。交还 CPU 的一侧仍由驱动负责通常在收包时调用page_pool_dma_sync_for_cpu()完成。6. 常见误区6.1 认为 x86 平台上可以省略dma_sync_*x86 平台上的设备通常是一致性的但 SWIOTLB 中转仍可能发生设备寻址能力不足第 5 篇第 4.3 节、不可信设备的未对齐映射第 6 篇第 4.3 节以及内存加密的虚拟机。对于后者内核强制所有 DMA 经由中转arch/x86/kernel/pci-dma.c节选/* * Guest with guest memory encryption currently perform all DMA through * bounce buffers as the hypervisor cant access arbitrary VM memory * that is not explicitly shared with it. */if(cc_platform_has(CC_ATTR_GUEST_MEM_ENCRYPT)){x86_swiotlb_enabletrue;x86_swiotlb_flags|SWIOTLB_FORCE;}在这些情况下遗漏for_cpu会使 CPU 读到旧数据遗漏for_device会使设备读到旧数据。验证交接是否完整可以在启动参数中加入swiotlbforce强制所有映射经由中转并启用CONFIG_DMA_API_DEBUG。6.2 在设备拥有缓冲区期间读取其内容部分驱动在设备工作期间轮询流式缓冲区中的某个字段以判断传输是否完成。在非一致性平台上CPU 读到的可能是陈旧副本经 SWIOTLB 中转时原缓冲区在for_cpu之前根本不会更新。即使每次轮询前都调用for_cpu设备也可能在其后继续写入。CPU 与设备需要并发访问的状态字段应放在一致性映射的内存中第 8 篇第 2.1 节流式缓冲区只在设备报告完成之后才收回。6.3 同步的方向或范围与映射不一致同步须使用与映射相同的方向范围须位于映射之内。启用 DMA 调试时check_sync()对这些情况报告错误kernel/dma/debug.c节选并简化if(ref-sizeentry-size){err_printk(dev,entry,device driver syncs DMA memory outside allocated range ...}if(entry-directionDMA_BIDIRECTIONAL)gotoout;if(ref-direction!entry-direction){err_printk(dev,entry,device driver syncs DMA memory with different direction ...}以DMA_BIDIRECTIONAL映射的缓冲区可以以较窄的方向同步反之则不允许。另一类常见错误是对已经解除映射的地址调用同步此时 DMA 地址可能已被其他映射复用。6.4 以DMA_FROM_DEVICE映射 CPU 写入后由设备读取的缓冲区有的设备在接收前需要驱动在缓冲区中预先填写命令头等内容设备读取后再写回结果。若这类缓冲区以DMA_FROM_DEVICE映射在一致性且不经中转的平台上可以正常工作但经 SWIOTLB 中转时 CPU 写入的内容不会被复制启用 IOMMU 时设备的读取还会失败。此时应使用DMA_BIDIRECTIONAL或将读写两部分分为两个映射。7. 小结本文回答了问题 9要点如下dma_sync_*的作用是转移缓冲区所有权。缓冲区在任一时刻属于 CPU 域或设备域。映射与解除映射分别包含第一次与最后一次交接dma_sync_*用于映射保持期间的反复交接。驱动声明交接映射层决定操作。交接时可能执行 cache 维护、SWIOTLB 复制也可能什么都不执行同一份驱动代码因此可在各种平台上正确运行。方向决定交接时的操作。方向影响 cache 维护、SWIOTLB 复制方向与 IOMMU 权限过窄导致错误过宽只增加开销。交接的单位是字节范围。igb 与 page_pool 以范围同步只交接实际使用的部分减少维护与复制的开销。无须同步时开销接近于零。驱动在任何平台上都应完整地调用同步接口并可借助swiotlbforce与 DMA 调试功能验证。以上讨论的都是一段连续的 DMA 地址。实际的 I/O 缓冲区常常由多个物理上分散的页组成。这些分散的内存如何以连续地址的形式提供给设备dma_map_sg完成了哪些工作即为问题 10。系列导航上一篇《第 8 篇 一致性映射与流式映射两种映射契约》下一篇《第 10 篇 Scatter-Gather 与 sg_table分散内存的统一映射》将回答问题 10物理上分散的内存为何能以连续地址的形式提供给设备。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑