ARM 架构下 cache 一致性问题整理

发布时间:2026/7/13 7:05:01

ARM 架构下 cache 一致性问题整理 本篇文章主要整理 ARM 架构下和 Cache 一致性相关的一些知识。本文假设读者具备一定的计算机体系结构和 Cache 相关基础知识适合有相关背景的读者阅读1、引言简单介绍一下 Cache 和内存之间的关系在使能 Cache 的情况下CPU 每次获取数据都会先访问 Cache如果获取不到数据则把数据加载到 Cache 中进行访问数据在 Cache 与 Memory 之间移动的最小单位通常在 32 - 128 字节之间。Memory 中对应的最小单位数据称为 Cache BlockCache 中与单个 Cache Block 对应的存储空间称为 Cache Line在 Cache 中除了存储 Block 数据还需要存储 Block 对应的唯一标识 (Tag)以及一个用于标记 Cache Line 是否有数据的有效位。完整对应关系如下图所示2、cache 的更新策略与写策略Read-allocate cacheA cache in which a cache miss on reading data causes a cache line to be allocated into the cacheWrite-allocate cacheA cache in which a cache miss on storing data causes a cache line to be allocated into the cacheWrite-back cacheA cache in which when a cache hit occurs on a store access, the data is only written to the cache. Data in the cachecan therefore be more up-to-date than data in main memory. Any such data is written back to main memory whenthe cache line is cleaned or reallocated. Another common term for a write-back cache is a copy-back cache.Write-through cacheA cache in which when a cache hit occurs on a store access, the data is written both to the cache and to main memory. This is normally done via a write buffer, to avoid slowing down the processor———《ARM Architecture Reference Manual ARMv7-A and ARMv7-R edition》因为后面的讲解会用到这些个概念所以这里简单介绍下。详细的内容可以去研究 ARM 官方手册Write-through定义在 Write-through 策略中每当数据被写入缓存时系统会立即将数据同步写入主存。这意味着缓存和主存中的数据始终保持一致。优点数据的一致性较好因为主存与缓存始终同步。即使系统崩溃或掉电主存中的数据不会丢失。缺点性能较低因为每次写入操作都要同时写入缓存和主存。这会导致更高的延迟和更多的存储带宽消耗。Write-back定义在 Write-back 策略中数据只有在从缓存中被替换或被其他操作所驱逐时才会写回到主存。也就是说数据在缓存中进行修改但只有在必要时才会同步到主存。优点性能较高因为数据写入缓存时无需立刻写入主存这减少了写操作对主存的访问频率。缺点数据的一致性较差因为主存中的数据可能与缓存中的数据不同步。若系统崩溃缓存中的数据可能丢失。3、单核 cache 和 内存之间一致性单核处理器下的 Cache Policy 要解决的问题可以被概括为CPU 从 Cache 中读到的数据必须是最近写入的数据要满足定义最简单的方式就是 Write-Through即每次写入 Cache 时也将数据写到 Memory 中。但是这样带来的问题就是更高的延迟和更多的存储带宽消耗。现代 CPU 几乎都是采取 Write-back 策略来管理 Cache可以提升系统性能但是该策略带来了数据一致性问题需要软件开发人员去注意、维护3.1 DMA 带来的一致性问题背景某些外设如网卡、磁盘控制器会使用 DMADirect Memory Access直接访问内存而不会经过 CPU cache。场景CPU 先读取某块数据到 cache并在 cache 里修改该数据DMA 设备直接从内存中读取数据而此时的内存数据仍然是旧的未更新的最终CPU 看到的是缓存中的新数据而 DMA 设备读取的却是旧数据导致数据不一致解决方案Cache Flush缓存刷新在数据同步前手动刷新 cache如 clflush 指令Memory Barrier内存屏障强制 CPU 在特定时间点刷新内存访问这里推荐一篇文章《Stale data, or how we (mis-)manage modern caches》讲的很详细关于 ARM 架构中Cache 和 DMA 一致性相关问题。这里只讲结论Step2因为那片内存可能有 dirty 位当 dma 读的时候可能会有 cache 淘汰这样 dma 读的区域就会错所以 dma 读的时候如果读的内存可以被 cache 同时 cache 没有强一致性那么就需要先 cache invalid 再进行 dma 读Step4有些 处理器在 DMA 传输过程中会随机预取因为是传输过程中buffer 肯定有部分没有数据、或是不稳定状态这时如果发生了预取将内存预取到 cache就会预取到脏数据所以DMA read 之后也要 invalid 一下4、多核 cache 之间的一致性在多核 CPU 模式下对缓存数据的写入还可能带来缓存一致性的问题。譬如核心 A 和 B 都同时运行两个线程都操作相同的变量全局变量那么必然会带来一致性的问题为了保证一致性需要保证做到以下两点某个 CPU 核心里的 Cache 数据更新时必须要传播到其他核心的 Cache这个称为写传播Write Propagation某个 CPU 核心对数据的操作顺序必须在其他核心看起来顺序是一致的这个称为事务串行化Transaction Serialization对于第一点写传播可以这样理解。假设我们有一个含有 4 个核心的 CPU这 4 个核心都需要依赖共同的变量 i全局变量。假设某时刻CPU 0 将 i 修改成值 5那么必须确保其余几个核也能收到变量 i 被修改成值 5 这个事件。而对于第二点事务的串形化我们举个例子来理解它。假设我们有一个含有 4 个核心的 CPU这 4 个核心都操作共同的变量 i初始值为 0 。A 号核心先把 i 值变为 100而此时同一时间B 号核心先把 i 值变为 200这里两个修改都会 “传播” 到 C 和 D 号核心。那么问题就来了C 号核心先收到了 A 号核心更新数据的事件再收到 B 号核心更新数据的事件因此 C 号核心看到的变量 i 是先变成 100后变成 200。而如果 D 号核心收到的事件是反过来的则 D 号核心看到的是变量 i 先变成 200再变成 100虽然是做到了写传播但是各个 Cache 里面的数据还是不一致的。所以我们要保证 C 号核心和 D 号核心都能看到相同顺序的数据变化比如变量 i 都是先变成 100再变成 200这样的过程就是事务的串形化。要实现事务串形化要做到 2 点CPU 核心对于 Cache 中数据的操作需要同步给其他 CPU 核心其实就是写传播要引入锁 的概念如果两个 CPU 核心里有相同数据的 Cache那么对于这个 Cache 数据的更新只有拿到了「锁」才能进行对应的数据更新。4.1 MESI 协议“写传播”主要依赖于 MESI、MOESI 这些缓存一致性协议。MESI 协议是处理多个 CPU 之间 cache 一致性常用的协议基于 snooping 实现该协议中有四个状态位来描述每一个 cache 行Mmodified已修改位。状态就是我们前面提到的脏标记代表该 Cache Block 上的数据已经被更新过但是还没有写到内存里Eexclusive独占位。独占状态的时候数据只存储在一个 CPU 核心的 Cache 里而其他 CPU 核心的 Cache 没有该数据Sshared共享位。状态代表着相同的数据在多个 CPU 核心的 Cache 里都有所以当我们要更新 Cache 里面的数据的时候不能直接修改而是要先向所有的其他 CPU 核心广播一个请求要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态然后再更新当前 Cache 里面的数据独占和共享状态都代表 Cache Block 里的数据是干净的也就是说这个时候 Cache Block 里的数据和内存里面的数据是一致性的Iinvalid已失效位。表示的是这个 Cache Block 里的数据已经失效了不可以读取该状态的数据MESI 只针对多核之间的 Dcache不包括 Icache。因为 Icache 是只读的不存在一致性问题随着缓存一致性协议的发展涌现出了诸如 MOESI、MESIF、Dragon 、ACE 、AXI 等诸多类型的缓存一致性协议。例如Cortex-A7 中使用的就是 MOESICortex-A7 MPCore processor supports between one and four individual processors with L1 datacache coherency maintained by the SCU. The SCU is clocked synchronously and at the samefrequency as the processors.The SCU maintains coherency between the individual data caches in the processor using ACEmodified equivalents of MOESI state, as described in Data Cache Unit on page 2-4.——《 Cortex™-A7 MPCore Technical Reference Manual 》注有些处理器需要单独去使能、配置 Snnop 单元例如 Cortex-A94.2 事务串行化回忆一下上面提到的事务串行化的一个必要条件要引入锁 的概念如果两个 CPU 核心里有相同数据的 Cache那么对于这个 Cache 数据的更新只有拿到了「锁」才能进行对应的数据更新。这里的“锁” 主要指的是对共享资源的访问控制机制可以通过硬件和软件两方面实现1硬件层面的锁基于原子指令在 ARM 架构中提供了一组特殊的原子操作指令来支持锁的实现最常见的是LDREX (Load Exclusive) / STREX (Store Exclusive)LDREX 读取某个内存地址并标记该地址为“独占访问”STREX 试图将新值写回该地址但如果在此期间该地址被其他核修改过则 STREX 会失败写入不生效需要重新尝试。这对指令确保了多核环境下的互斥访问避免多个线程同时修改同一块内存时导致数据不一致。例如Linux 下的自旋锁使用的就是 LDREX/STREX 指令Linux 内核 spinlock 的实现2软件层面的锁基于互斥量、信号量等在操作系统层面我们可以使用互斥锁Mutex、自旋锁Spinlock、读写锁RWLock等机制来管理并发访问自旋锁Spinlock线程在获取锁时会一直循环检查不会主动让出CPU适用于短时间的临界区互斥锁Mutex如果锁被占用线程会进入睡眠状态等待锁释放适用于长时间的临界区信号量Semaphore允许多个线程同时访问一定数量的资源适用于资源共享场景读写锁RWLock允许多个线程同时读取数据但写入时必须互斥适用于读多写少的场景3内存屏障Memory Barrier确保事务的顺序ARM 还提供了一些内存屏障指令Memory Barrier用于确保CPU按照正确的顺序执行内存访问DMBData Memory Barrier保证所有CPU核心看到的内存访问顺序一致DSBData Synchronization Barrier确保之前的所有内存访问完成后才执行后续指令ISBInstruction Synchronization Barrier用于指令流水线刷新确保指令执行的可见性。4.3 关于 DMA buffer 对齐问题操作系统在使用 DMA 时DMA buffer 如果带 cache则 DMA buffer 起始地址一定要 cacheline 对齐并且长度必须是 cache line 的整数倍。原因如下inttemp5;charbuffer[64]{0};假设cacheline 大小是 64 字节。那么 temp 变量和 buffer 位于同一个 cachelinebuffer 横跨两个 cacheline。假设现在想要启动 DMA 从外设读取数据到buffer中。我们进行如下操作我们先 invalid buffer 对应的 2 行 cacheline启动 DMA 传输当 DMA 传输到 buff[3] 时程序改写 temp 的值为 6。temp 的值和 buffer[0]-buffer[60] 的值会被缓存到 cache 中并且标记 dirty bit。DMA 传输还在继续当传输到 buff[50] 的时候其他程序可能读取数据导致 temp 变量所在的 cacheline 需要替换由于cacheline 是 dirty 的。所以cacheline的数据需要写回。此时将temp数据写回顺便也会将 buffer[0]-buffer[60] 的值写回。看到这里就会发现第三步中DMA 传输数据的过程中内存中的脏值会被缓存到 buffer 对应的 cache 中。而后又因为其他程序对 temp 变量的读取根据 MESI 一致性协议temp 所在的 cacheline 会被写回内存会造成脏数据又会被写到内存中的问题。对于上面的操作从另一个角度来看如果 temp 的值是被当前 CPU 标记为已修改状态cache 中的 temp 是新数据而内存中是脏值。DMA 传输结束后会去 invalid 一整个 cacheline而当 CPU 再次访问 temp 时会出现 invalid、从内存拿脏值的问题。而对于该问题的解决通常操作系统内核在 cache invalid 操作中会判断 start 和 end 是否 cacheline align如果不对齐会执行 civac先clean 再invalidate如果对齐就执行 ivac只执行invalidate操作。如果无法确定当前操作系统关于 cache invalid 的实现是否解决了上述问题所以实际开发中建议申请 DMA buffer 时还是要 cache line 对齐且大小是 cache line 的整数倍4.4 Icache 与 Dcache 之间的一致性重定位时、debug 调试插入断点指令时通常需要修改指令、修改数据。这里需要知道两个前提ARM 中通常 iCache 是只读的cpu 不会改写 icache 中的数据修改指令时会将指令 load 到 dcache 中然后在 dcache 中去修改这会面临 2 个问题如果旧指令已经缓存在 iCache 中。而修改后的指令在内存中。那么对于程序执行来说依然会命中 iCache。这不是我们想要的结果如果 dCache 使用的是写回write_back策略那么新指令数据依然缓存在 dCache 中。这种情况也不是我们想要的。所以通常会这么做将需要修改的指令数据加载到 dCache 中修改成新指令写回 dCacheclean dCache 中修改的指令对应的 cacheline保证 dCache 中新指令写回主存这里通常是 PoUinvalid iCache 中修改的指令对应的 cacheline保证从主存中读取新指令当然对于多核来说也需要通过像 IPI 核间中断这样的功能去通知其它所有核去刷新 cache即 clean dCache、invalid iCache拓展PoC 是由硬件 cache coherency 协议如 MESI/MOESI实现的数据一致性点而 PoU 是架构定义的指令与数据统一点依赖软件显式的 cache 维护指令来实现由于 I-cache 不参与 coherency 协议因此无法通过 PoC 自动同步必须通过 IPI 中断让各核自行失效 I-cache5、cache 伪共享问题因为多个线程同时读写同一个 Cache Line 的不同变量时而导致 CPU Cache 失效的现象称为伪共享False Sharing。Cache Line 伪共享问题就是由多个 CPU 上的多个线程同时修改自己的变量引发的。这些变量表面上是不同的变量但是实际上却存储在同一条 Cache Line 里。在这种情况下由于 Cache 一致性协议MESI两个处理器都存储有相同的 Cache Line 拷贝的前提下本地 CPU 变量的修改会导致本地 Cache Line 变成 Modified 状态。然后在其它共享此 Cache Line 的 CPU 上引发 Cache Line 的 Invaidate 操作导致 Cache Line 变为 Invalidate 状态从而使 Cache Line 再次被访问时发生本地 Cache Miss从而伤害到应用的性能。False sharing occurs when threads on different processors modify variables that reside on the same cache line. This invalidates the cache line and forces a memory update to maintain cache coherency. Threads 0 and 1 require variables that are adjacent in memory and reside on the same cache line. The cache line is loaded into the caches of CPU 0 and CPU 1 (gray arrows). Even though the threads modify different variables (red and blue arrows), the cache line is invalidated, forcing a memory update to maintain cache coherency.6、TLB 一致性TLB 本质上也是 Cache那么它自然也会存在一致性问题。即对于多核来说设置虚拟地址对应物理地址页面 flag 标志时需要同步刷新所有 CPU TLB将物理页面重新映射时需要同步刷新所有 CPU TLB设置指定逻辑地址的访问权限需要同步刷新所有 CPU TLB同步刷新所有 CPU TLB一般会通过 IPI 核间中断来完成如果不是上面这么操作会存在1 核 和 2 核 的 TLB 内容相斥会访问到错误的物理地址造成无法捕获的异常情况。When the operating system changes translation table entries, it is possible that the TLB could contain stale translation information. The OS must take steps to invalidate TLB entries.————————《ARM Cortex-A Series (Armv7-A) Programmer’s Guide》我们需要厘清一个关键概念尽管 TLB 常被称为‘页表缓存’但其工作机制与 Data Cache 或 I-Cache 有本质区别。软件无法直接修改或写入 TLB 条目。当操作系统更新内存中的页表时TLB 中的旧映射不会自动同步更新。唯一的同步机制是软件必须显式失效Invalidate相关的 TLB 条目迫使硬件在下一次发生 TLB Miss 时重新从内存中读取最新的页表项并填充 TLB。有些架构不需要通过 IPI让其它核同步去刷 TLB因为架构支持 maintiance broadcast 广播。例如 ARM不同处理器对 maintiance broadcast 操作的控制不同例如cortex-A9 需要手动设置 ACTLR.FW 和 ACTLR.SMP 寄存器位来使能 maintiance broadcast 功能。而 cortex-A7 不需要默认支持 maintiance broadcast。这里要注意广播不是所有核都会去广播必须是 Inner Shareable domainMaintenance operations can only be broadcast and received when the processor is configured to participate in the Inner Shareable domain, using the SMP bit in ACTLR. Only Inner Shareable operations are broadcast, for example ● To invalidate TLB entry by virtual address.● To clean or invalidate data cache line by virtual address● To invalidate instruction cache line by virtual address————《 ARM® Cortex™-A Series Programmer’s Guide 》

相关新闻