
本文属于第一册“硬件原理”回答第 0 篇提出的问题 3数据已写入内存另一方为何仍可能读到旧值cache 一致性问题的根源是什么第 2 篇说明了设备如何获得正确的地址但地址正确仅保证访问到达正确的位置并不保证访问双方观察到的数据相同。本文从 CPU 与设备访问内存的路径差异出发分析 DMA 场景下两类不一致的形成过程说明软件维护一致性所依赖的 cache 操作及其时机并讨论 cache 行粒度带来的额外约束。1. 问题回顾第 1 篇的接收流程中网卡将帧数据写入接收缓冲区并回写描述符CPU 随后读取缓冲区将数据交给协议栈发送流程则方向相反CPU 先将待发送的数据写入缓冲区再由网卡读取。两个流程中CPU 与设备通过同一段内存交换数据并且都假定一方写入的数据能被另一方读到。在部分平台上这一假定并不成立网卡已将新数据写入内存CPU 读到的却是旧值或者 CPU 已写入待发送的数据网卡读出的却是旧值。内存本身并无错误问题源于 CPU 与设备访问内存的方式不同。2. CPU 与设备访问内存的路径2.1 CPU 经由 cache 访问内存CPU 访问 DRAM 的延迟通常为 80 至 100 纳秒而访问一级 cache 仅需数个时钟周期。为弥补这一差距CPU 与内存之间设有多级 cache通常包括每个核心私有的 L1、L2 cache以及多个核心共享的末级 cacheLLC。CPU 执行 load/store 指令时首先查询 cache命中时直接在 cache 中完成读写未命中时才访问内存并将数据所在的整块区域载入 cache。cache 与内存之间的数据交换以 cache 行cache line为单位。x86 与多数 ARM 处理器的 cache 行为 64 字节部分 ARM 处理器为 128 字节。由此可知同一地址的数据可能同时存在两个副本一个位于 cache 中一个位于内存中。2.2 写策略写回与写直通CPU 写入一个已载入 cache 的地址时存在两种处理方式写策略行为内存中的数据写直通write-through同时更新 cache 与内存始终为最新值写回write-back仅更新 cache并将该 cache 行标记为脏dirty该行被替换出 cache 时才写回内存在写回之前为旧值写回策略显著减少了内存写流量因此现代处理器对普通内存普遍采用写回策略。其代价在于脏行写回之前内存中保存的是旧值最新值仅存在于 cache 中。多个 CPU 核心之间同样存在多份 cache 副本这一问题由硬件一致性协议如 MESI 及其变体维护一个核心写入某一 cache 行时其他核心持有的副本将被置为无效读取时可从持有最新值的核心获得数据。因此CPU 之间共享内存时软件无需关心 cache 的存在。2.3 设备直接访问内存设备发出的 DMA 请求经 Root Complex 到达内存控制器直接读写 DRAM。在本文讨论的非一致性平台上DMA 请求不经过 CPU cache也不参与 CPU 之间的一致性协议设备读取时不会从 cache 中取得脏数据设备写入时不会使 cache 中的副本失效。部分平台的硬件会使 DMA 请求参与一致性协议从而消除这一差异该机制及其平台差异将在第 4 篇讨论。本文分析的是硬件不提供该机制时所产生的问题这也是理解一致性问题的出发点。3. 两类不一致CPU 经由 cache 访问内存设备直接访问内存二者所观察到的是同一地址的不同副本。按数据流向由此产生两类不一致如图 1 所示。图 1 CPU Cache 导致的数据不一致问题3.1 设备读到旧值脏数据尚未写回发送方向上CPU 将待发送的数据写入缓冲区。在写回策略下新数据保留在 cache 的脏行中内存中仍为旧值。若此时网卡通过 DMA 读取该缓冲区取得的是内存中的旧值发送出去的是错误的数据。解决方法是在设备读取之前将缓冲区所覆盖的脏行写回内存该操作称为 clean或 write back。3.2 CPU 读到旧值cache 中保留陈旧副本接收方向上网卡将帧数据写入内存。若 CPU 此前访问过该缓冲区例如上一次接收后处理过其中的数据cache 中仍保留着对应 cache 行的有效副本。设备的写入不会使这些副本失效CPU 读取时命中 cache得到的是旧数据。解决方法是在 CPU 读取之前使缓冲区所覆盖的 cache 行失效迫使 CPU 从内存重新读取该操作称为 invalidate。3.3 接收方向的两个附加问题接收方向在上述问题之外还存在两个容易忽略的问题。脏行覆盖 DMA 数据。若设备写入之前缓冲区所在的某一 cache 行处于脏状态该行可能在设备写入之后才被替换出 cache 并写回内存从而覆盖设备刚刚写入的数据。因此在设备写入之前须确保该区域不存在脏行或者先执行 clean或者直接 invalidate丢弃脏数据前提是该区域中不存在仍需保留的 CPU 数据。推测预取产生新的副本。现代处理器会依据访存模式推测性地预取数据在程序尚未访问某一地址时即可能将其载入 cache。若在设备写入期间发生预取cache 中将重新出现该区域的旧副本。因此仅在设备写入之前执行 invalidate 并不充分在设备写入完成之后、CPU 读取之前还须再执行一次 invalidate。4. 软件维护一致性的方法4.1 cache 维护操作处理器提供按地址操作 cache 的指令基本操作有三种操作作用ARM64 指令x86 指令clean将脏行写回内存cache 中的副本保留DC CVACCLWBinvalidate丢弃 cache 中的副本脏数据不写回DC IVAC无按地址操作的对应指令clean invalidate先写回脏行再丢弃副本DC CIVACCLFLUSH、CLFLUSHOPTARM 架构中上述指令以 PoCPoint of Coherency为作用点即保证所有观察者包括 DMA 设备在该点看到相同的数据。在 x86 平台上DMA 一致性通常由硬件保证驱动一般无需执行此类操作其原因将在第 4 篇讨论。cache 维护操作逐行进行其开销与缓冲区大小成正比。对于大缓冲区这一开销在非一致性平台上相当可观也是后续篇目中讨论映射方式选择时需要考虑的因素。4.2 维护操作的时机结合第 3 节的分析一次 DMA 传输所需的维护操作取决于数据流向与所处阶段如图 2 所示。图 2 一次 DMA 传输所需的维护操作内核 ARC 架构的实现arch/arc/mm/dma.c以注释的形式给出了这一对应关系/* * | map for_device | unmap for_cpu * |---------------------------------------------------------------- * TO_DEV | writeback writeback | none none * FROM_DEV | invalidate invalidate | invalidate* invalidate* * BIDIR | writebackinv writebackinv | invalidate invalidate * * [*] needed for CPU speculative prefetches */表中的 map 与 for_device 表示缓冲区交给设备之前unmap 与 for_cpu 表示缓冲区交还 CPU 之后。DMA_FROM_DEVICE在交还 CPU 时仍需 invalidate原因即 3.3 节所述的推测预取。4.3 Linux 中的实现非一致性平台通过两个架构接口实现上述操作arch_sync_dma_for_device()在缓冲区交给设备之前调用arch_sync_dma_for_cpu()在缓冲区交还 CPU 之后调用。ARM64 的实现如下arch/arm64/mm/dma-mapping.cvoidarch_sync_dma_for_device(phys_addr_tpaddr,size_tsize,enumdma_data_directiondir){unsignedlongstart(unsignedlong)phys_to_virt(paddr);dcache_clean_poc_nosync(start,startsize);}voidarch_sync_dma_for_cpu(phys_addr_tpaddr,size_tsize,enumdma_data_directiondir){unsignedlongstart(unsignedlong)phys_to_virt(paddr);if(dirDMA_TO_DEVICE)return;dcache_inval_poc_nosync(start,startsize);}与 ARC 的实现相比ARM64 在交给设备之前对所有方向统一执行 clean而非对DMA_FROM_DEVICE执行 invalidate。clean 同样能够保证该区域不存在脏行且不会丢弃 CPU 写入的数据而交还 CPU 之后的 invalidate 已负责清除旧副本。RISC-V 的实现arch/riscv/mm/dma-noncoherent.c则根据平台特性在两种做法之间选择并在交还 CPU 时注明caseDMA_FROM_DEVICE:caseDMA_BIDIRECTIONAL:/* FROM_DEVICE invalidate needed if speculative CPU prefetch only */if(arch_sync_dma_cpu_needs_post_dma_flush())arch_dma_cache_inv(paddr,size);break;第 2 篇节选的dma_direct_map_phys()中if (!dev_is_dma_coherent(dev)) arch_sync_dma_for_device(...)一句即为映射阶段的 cache 维护。驱动并不直接调用这些架构接口而是通过 DMA 映射接口与dma_sync_*系列接口间接触发其约定将在第 8、9 篇讨论。4.4 改变内存类型除 cache 维护之外另一种做法是改变缓冲区的内存类型使其不经过 cache 或不产生脏数据内存类型设备读到旧值CPU 读到旧值代价写回WB存在存在需要 cache 维护操作写直通WT不存在存在写操作直达内存读方向仍需 invalidate不可缓存UC、Normal-NC不存在不存在CPU 的每次访问均到达内存访问延迟高不可缓存映射从根本上消除了两类不一致代价是 CPU 访问失去 cache 的加速。因此该方式适用于描述符等小而频繁被双方访问的数据结构不适用于大块数据。ARM64 上非一致性设备通过dma_alloc_coherent()分配的内存即以 Normal Non-cacheable 类型映射给 CPUpgprot_dmacoherent()这是第 8 篇所述一致性映射在此类平台上的实现方式。5. cache 行粒度带来的约束5.1 共享 cache 行的问题cache 维护操作以 cache 行为最小单位。若 DMA 缓冲区的起止地址未与 cache 行对齐缓冲区首尾所在的 cache 行将同时包含其他数据。考虑如下结构/* 错误示例DMA 缓冲区与 CPU 频繁更新的字段位于同一 cache 行 */structfoo_priv{u8 status[16];/* 设备通过 DMA 写入 */u32 irq_count;/* CPU 在中断处理中更新 */...};设备写入status期间若 CPU 更新了irq_count将出现以下两种结果之一交还 CPU 时执行 invalidate该 cache 行被整体丢弃CPU 对irq_count的更新随之丢失。该 cache 行因irq_count的更新而成为脏行随后被替换出 cache 并写回内存覆盖设备写入的status。无论哪种结果均无法通过调整维护操作的时机加以避免。唯一的解决方法是保证 DMA 缓冲区独占其所在的 cache 行。5.2 内核的对齐保证内核以ARCH_DMA_MINALIGN表示 DMA 缓冲区所需的最小对齐其值为平台可能的最大 cache 行长度。例如 ARM64 定义为 128 字节arch/arm64/include/asm/cache.h#defineARCH_DMA_MINALIGN(128)#defineARCH_KMALLOC_MINALIGN(8)嵌入结构体中的 DMA 缓冲区应以__aligned(ARCH_DMA_MINALIGN)标注并保证其后不紧跟其他字段或者单独分配。对于kmalloc()分配的缓冲区早期内核令kmalloc()的最小对齐等于ARCH_DMA_MINALIGN代价是小对象浪费大量内存。当前的 ARM64 内核在 SWIOTLB 可用时允许将kmalloc()的最小对齐降至 8 字节ARCH_KMALLOC_MINALIGN对于非一致性设备上的未对齐小缓冲区改由 SWIOTLB 中转。该判断位于dma_direct_map_phys()中的dma_kmalloc_needs_bounce()第 2 篇的节选将其省略。其判断条件中有一处值得注意include/linux/dma-map-ops.h/* * kmalloc() buffers are DMA-safe irrespective of size if the device * is coherent or the direction is DMA_TO_DEVICE (non-desctructive * cache maintenance and benign cache line evictions). */if(dev_is_dma_coherent(dev)||dirDMA_TO_DEVICE)returntrue;DMA_TO_DEVICE方向只需执行 clean不会丢弃数据设备也不写入内存因此共享 cache 行不会造成破坏。这一条件从反面印证了 5.1 节的分析共享 cache 行的问题仅出现在设备写入内存的方向上。6. 常见误区6.1 认为内存屏障能够解决 cache 一致性问题编译器屏障、内存屏障与 cache 维护操作作用于不同的层次机制作用对象所解决的问题编译器屏障barrier()、READ_ONCE()、volatile编译器禁止编译器重排、合并或省略访存指令内存屏障wmb()、rmb()、dma_wmb()、dma_rmb()CPU 的访存顺序保证多个访问之间的先后关系对其他观察者成立cache 维护操作cache 与内存中的数据副本保证双方观察到的副本内容相同内存屏障只约束访问的顺序不会将 cache 中的脏数据写回内存也不会使 cache 中的副本失效。Documentation/memory-barriers.txt在介绍dma_wmb()与dma_rmb()时明确指出它们用于一致性内存consistent memory即以硬件或映射方式已经保证一致性为前提These are for use with consistent memory to guarantee the ordering of writes or reads of shared memory accessible to both the CPU and a DMA capable device.二者是相互补充的关系。一致性保证设备能够看到 CPU 写入的数据屏障保证设备看到这些数据的顺序例如在设置描述符的所有权标志之前描述符的其他字段已经写入。6.2 认为在设备写入之前执行一次 invalidate 即可如 3.3 节所述CPU 的推测预取可能在设备写入期间将该区域重新载入 cache。因此接收方向须在设备写入完成之后再执行一次 invalidate。驱动遵循 DMA 映射接口的约定在读取数据之前调用dma_unmap_*()或dma_sync_*_for_cpu()即可保证这一操作被执行。6.3 认为在 x86 平台上测试通过即说明一致性处理正确x86 平台的 DMA 一致性通常由硬件保证arch_sync_dma_for_device()等接口在该平台上不执行任何 cache 操作。遗漏dma_sync_*调用、DMA 缓冲区与其他数据共享 cache 行等错误在 x86 平台上均不会表现出来移植至非一致性平台后才会出现偶发的数据损坏。其背后的硬件差异即为下一篇的主题。7. 小结对问题 3 的回答可归纳如下一致性问题的根源在于访问路径不同。CPU 经由 cache 访问内存设备直接访问内存同一地址的数据在 cache 与内存中可能存在两个不同的副本。在非一致性平台上设备的访问不参与 CPU 的 cache 一致性协议。不一致分为两类。设备读取时CPU 写入的新数据可能仍在 cache 的脏行中需要 cleanCPU 读取时cache 中可能保留着陈旧副本需要 invalidate。接收方向还需防止脏行覆盖 DMA 数据并在设备写入完成后再次 invalidate以清除推测预取产生的副本。软件维护有两种方式按传输方向与阶段执行 cache 维护操作或将缓冲区映射为不可缓存类型。前者适用于大块数据后者适用于小而频繁访问的数据结构。cache 维护以 cache 行为单位DMA 缓冲区须独占其所在的 cache 行否则在设备写入的方向上将破坏相邻数据。内存屏障不能替代 cache 维护二者分别解决顺序问题与副本一致问题。本文的分析建立在“设备访问不经过 CPU cache”这一前提之上。在 x86 等平台上硬件使 DMA 访问参与一致性协议上述问题不会出现。硬件一致性机制具体承担了哪些职责各平台之间的差异从何而来即为问题 4也是下一篇讨论的内容。系列导航上一篇《第 2 篇 总线主控与设备地址DMA 事务中地址的来源》下一篇《第 4 篇 硬件一致性总线侦听与一致性互连》将回答问题 4同一代码为何在 x86 平台上运行正常在其他平台上却出现数据错误。关联阅读《内存序与 host-device 一致性》:分析了内存序的原理