ARM 架构下 cache 一致性问题整理
本篇文章主要整理 ARM 架构下,和 Cache 一致性相关的一些知识。
本文假设读者具备一定的计算机体系结构和 Cache 相关基础知识,适合有相关背景的读者阅读
1、引言
简单介绍一下 Cache 和内存之间的关系
在使能 Cache 的情况下,CPU 每次获取数据都会先访问 Cache,如果获取不到数据则把数据加载到 Cache 中进行访问
数据在 Cache 与 Memory 之间移动的最小单位通常在 32 - 128 字节之间。
- Memory 中对应的最小单位数据称为 Cache Block
- Cache 中与单个 Cache Block 对应的存储空间称为 Cache Line
在 Cache 中除了存储 Block 数据,还需要存储 Block 对应的唯一标识 (Tag),以及一个用于标记 Cache Line 是否有数据的有效位。完整对应关系如下图所示:

2、cache 的更新策略与写策略
Read-allocate cache:
A cache in which a cache miss on reading data causes a cache line to be allocated into the cache
Write-allocate cache:
A cache in which a cache miss on storing data causes a cache line to be allocated into the cache
Write-back cache:
A cache in which when a cache hit occurs on a store access, the data is only written to the cache. Data in the cache
can therefore be more up-to-date than data in main memory. Any such data is written back to main memory when
the cache line is cleaned or reallocated. Another common term for a write-back cache is a copy-back cache.
Write-through cache:
A 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 带来的一致性问题
背景:某些外设(如网卡、磁盘控制器)会使用 DMA(Direct 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 一致性相关问题。这里只讲结论:
- Step(2):因为那片内存可能有 dirty 位,当 dma 读的时候,可能会有 cache 淘汰,这样 dma 读的区域就会错,所以 dma 读的时候,如果读的内存可以被 cache 同时 cache 没有强一致性,那么就需要先 cache invalid 再进行 dma 读
- Step(4):有些 处理器在 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 行:
- M(modified),已修改位。状态就是我们前面提到的脏标记,代表该 Cache Block 上的数据已经被更新过,但是还没有写到内存里
- E(exclusive),独占位。独占状态的时候,数据只存储在一个 CPU 核心的 Cache 里,而其他 CPU 核心的 Cache 没有该数据
- S(shared),共享位。状态代表着相同的数据在多个 CPU 核心的 Cache 里都有,所以当我们要更新 Cache 里面的数据的时候,不能直接修改,而是要先向所有的其他 CPU 核心广播一个请求,要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态,然后再更新当前 Cache 里面的数据
- 独占和共享状态都代表 Cache Block 里的数据是干净的,也就是说,这个时候 Cache Block 里的数据和内存里面的数据是一致性的
- I(invalid),已失效位。表示的是这个 Cache Block 里的数据已经失效了,不可以读取该状态的数据
MESI 只针对多核之间的 Dcache,不包括 Icache。因为 Icache 是只读的,不存在一致性问题
随着缓存一致性协议的发展,涌现出了诸如 MOESI、MESIF、Dragon 、ACE 、AXI 等诸多类型的缓存一致性协议。例如,Cortex-A7 中使用的就是 MOESI
Cortex-A7 MPCore processor supports between one and four individual processors with L1 data
cache coherency maintained by the SCU. The SCU is clocked synchronously and at the same
frequency as the processors.
The SCU maintains coherency between the individual data caches in the processor using ACE
modified equivalents of MOESI state, as described in Data Cache Unit on page 2-4.
——《 Cortex™-A7 MPCore Technical Reference Manual 》![]()
注:
有些处理器,需要单独去使能、配置 Snnop 单元,例如 Cortex-A9

4.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按照正确的顺序执行内存访问:
- DMB(Data Memory Barrier):保证所有CPU核心看到的内存访问顺序一致
- DSB(Data Synchronization Barrier):确保之前的所有内存访问完成后,才执行后续指令
- ISB(Instruction Synchronization Barrier):用于指令流水线刷新,确保指令执行的可见性。
4.3 关于 DMA buffer 对齐问题
操作系统在使用 DMA 时,DMA buffer 如果带 cache,则 DMA buffer 一定要 cacheline 对齐。原因如下:
int temp = 5;
char buffer[64] = { 0 };
假设,cacheline 大小是 64 字节。那么 temp 变量和 buffer 位于同一个 cacheline,buffer 横跨两个 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 对齐
4.4 Icache 与 Dcache 之间的一致性
重定位时、debug 调试插入断点指令时,通常需要修改指令、修改数据。
这里需要知道两个前提:
- ARM 中,通常 iCache 是只读的,cpu 不会改写 icache 中的数据
- 修改指令时,会将指令 load 到 dcache 中,然后在 dcache 中去修改
这会面临 2 个问题:
- 如果旧指令已经缓存在 iCache 中。而修改后的指令在内存中。那么对于程序执行来说依然会命中 iCache。这不是我们想要的结果
- 如果 dCache 使用的是写回(write_back)策略,那么新指令数据依然缓存在 dCache 中。这种情况也不是我们想要的。
所以通常会这么做:
- 将需要修改的指令数据加载到 dCache 中
- 修改成新指令,写回 dCache
- clean dCache 中修改的指令对应的 cacheline,保证 dCache 中新指令写回主存(这里通常是 PoU)
- invalid iCache 中修改的指令对应的 cacheline,保证从主存中读取新指令
当然,对于多核来说,也需要通过像 IPI 核间中断这样的功能,去通知其它所有核去刷新 cache,即 clean dCache、invalid iCache
5、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 内容相斥,会访问到错误的物理地址,造成无法捕获的异常情况。
有些架构不需要通过 IPI,让其它核同步去刷 TLB,因为架构支持 maintiance broadcast 广播。例如 ARM:
不同处理器对 maintiance broadcast 操作的控制不同,例如,cortex-A9 需要手动设置 ACTLR.FW 和 ACTLR.SMP 寄存器位来使能 maintiance broadcast 功能。而 cortex-A7 不需要,默认支持 maintiance broadcast。
这里要注意,广播不是所有核都会去广播,必须是 Inner Shareable domain:
Maintenance 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 》