Fork me on GitHub
侧边栏

【ARM Cache 与 MMU 系列文章 5.1 -- Cache 缓存一致性协议】

1. cache的组织

image

L1 cache 分为单独的 instruction cache(ICache)和 data cache(DCache)。
L1 cache是CPU私有的,每个CPU都有一个L1 cache。

一个cluster 内的所有CPU共享一个L2 cache,L2 cache不区分指令和数据,都可以缓存。
所有cluster之间共享L3 cache。L3 cache通过总线和主存相连。

2 多级cache之间的配合工作

当CPU试图从某地址load数据时,首先从L1 cache中查询是否命中,如果命中则把数据返回给CPU。

如果L1 cache缺失,则继续从L2 cache中查找,当L2 cache命中时,数据会返回给L1 cache以及CPU。

如果L2 cache也缺失,很不幸,我们需要从主存中load数据,将数据返回给L2 cache、L1 cache及CPU,这种多级cache的工作方式称之为 inclusive cache ,即某一地址的数据可能存在多级缓存中。与 inclusive cache 对应的是 exclusive cache ,这种cache保证某一地址的数据缓存只会存在于多级cache其中一级。也就是说,任意地址的数据不可能同时在L1和L2 cache中缓存。

3 多核心cache的一致性

在多核系统(简称MP,Multi-Processor),我们知道每个CPU都有一个私有的L1 Cache(不细分iCache和dCache)。

假设一个双核的系统,我们将会有 2 个 L1 Cache。 这就引入了一个问题,不同CPU之间的 L1 Cache 如何保证一致性呢 ?首先看下什么是多核Cache一致性问题。

假设 2 个CPU的系统,并且 L1 Cache 的 cache line 大小是 64 Bytes 。两个CPU都读取 0x00000040 地址数据,导致 0x00000040 开始的 64 B 字节内容分别加载到 CPU0 和 CPU1 的私有的 cache line。

CPU0执行写操作,写入值 0x01 ,CPU0私有的 L1 Cache 更新 cache line 的值,然后,CPU1读取 0x40 数据,CPU1发现命中cache,然后返回 0x00 值,并不是CPU0写入的 0x01 。这就造成了 CPU0 和 CPU1 私有L1 Cache数据不一致现象。

  • 某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache,这个称为 写传播Wreite Propagation );
  • 某个 CPU 核心里对数据的操作顺序,必须在其他核心看起来顺序是一样的,这个称为 事务的串形化Transaction Serialization )。

第一点写传播很容易就理解,当某个核心在 Cache 更新了数据,就需要同步到其他核心的 Cache 里。而对于第二点事务事的串形化,我们举个例子来理解它。

假设我们有一个含有 4 个核心的 CPU,这 4 个核心都操作共同的变量 i(初始值为 0 )。A 号核心先把 i 值变为 100,而此时同一时间,B 号核心先把 i 值变为 200,这里两个修改,都会「传播」到 C 和 D 号核心。

image

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 Lock 指令

在早期的CPU当中,是通过在总线上加 LOCK 锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加 LOCK 锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU 能使用这个变量的内存。在总线上发出了LCOK 锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从其内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

但是由于在锁住总线期间,其他CPU无法访问内存,会导致效率低下。

关于LOCK 的内容请见:ARM AMBA AXI 入门 6 - AXI3 协议中的锁定访问之AxLOCK信号

1.1.5 Bus Snooping Protocol

写传播的原则就是当某个 CPU 核心更新了 Cache 中的数据,要把该事件广播通知到其他核心。最常见实现的方式是总线嗅探(Bus Snooping)。

我还是以前面的 i 变量 例子来说明总线嗅探的工作机制,当 A 号 CPU 核心修改了 L1 Cache 中 i 变量 的值,通过总线把这个事件广播通知给其他所有的核心,然后每个 CPU 核心都会监听总线上的广播事件,并检查是否有相同的数据在自己的 L1 Cache 里面,如果 B 号 CPU 核心的 L1 Cache 中有该数据,那么也需要把该数据更新到自己的 L1 Cache。

可以发现,总线嗅探方法很简单, CPU 需要每时每刻监听总线上的一切活动,但是不管别的核心的 Cache 是否缓存相同的数据,都需要发出一个广播事件,这无疑会加重总线的负载。

另外,总线嗅探只是保证了某个 CPU 核心的 Cache 更新数据这个事件能被其他 CPU 核心知道,但是并不能保证事务串形化。

于是,有一个协议基于总线嗅探机制实现了事务串形化,也用状态机机制降低了总线带宽压力,这个协议就是 MESI 协议,这个协议就做到了 CPU 缓存一致性。

image

5 MESI Protocol

MESI是现在一种使用广泛的协议,用来维护多核Cache一致性。可以将MESI看做是状态机,将每一个cache line标记状态,并且维护状态的切换。其大概思想就是:
一个Cache加载一个变量的时候,是Exclusive状态,当这个变量被第二个Cache加载,更改状态为Shared;这时候一个CPU要修改变量, 就把状态改为Modified,并且Invalidate其他的Cache,其他的Cache再去读这个变量,达到一致。

  • MESI 协议中的 cache line 的四种状态
消息名称 定义
Read 如果CPU需要读取某个地址的数据
Read Response 可以由主存或者其他CPU发出,包含"Read"消息请求的主存地址的数据,当发出"Read"消息的CPU收到该消息后,将"Read Response"中的数据 放入自身的 cache line
Invalidate 其中包含待失效的主存地址,其他CPU收到消息后将某地址对应的 cache line 移除,并将返回"Invalidate Acknowledge"
Invalidate Acknowledge 在收到 Invalidate 消息后,必须移除对应的cache line,并返回"Invalidate Acknowledge"
Read Invalidate Read + Invalidate消息的结合
Writeback 包含主存和待写回数据,该消息仅在cache line 为 modified 状态发出,该消息之后,对应的 cache line 如需要可以被移除,从而使其可以被用于存储别的主存块

image

状态之间的相互转换关系可以使用下表来表示:

M E S I
M x x x
E x x x
S x x
I

继续上面的例子:

  • CPU0 读取 0x40 数据,数据被缓存到 CPU0 私有Cache ,此时 CPU1 没有缓存 0x40 数据,所以我们标记 cache line 状态为 Exclusive
    Exclusive 代表 cache line 对应的数据仅在数据只在一个CPU的私有Cache中缓存,并且其在缓存中的内容与主存的内容一致。
  • 然后 CPU1 读取 0x40 数据,发送消息给其他CPU,发现数据被缓存到CPU0私有Cache,数据从CPU0 Cache返回给CPU1。此时CPU0和CPU1同时缓存 0x40 数据,此时cache line状态从 Exclusive 切换到 Shared 状态。
    Shared代表 cache line 对应的数据在 “多” 个CPU私有Cache中被缓存,并且其在缓存中的内容与主存的内容一致。
  • 继续 CPU0 修改 0x40 地址数据,发现 0x40 内容所在cache line状态是Shared。CPU0发出 invalid 消息传递到其他CPU,这里是CPU1。CPU1接收到invalid消息。将0x40所在的cache line置为Invalid状态。
    Invalid状态 表示表明当前cache line无效。
  • 然后 CPU0 收到 CPU1 已经 invalid 的消息,修改 0x40 所在的cache line中数据。并更新 cache line 状态为 Modified
    Modified表明 cache line 对应的数据仅在一个CPU私有Cache中被缓存,并且其在缓存中的内容与主存的内容不一致,代表数据被修改。
  • 如果 CPU0 继续修改 0x40 数据,此时发现其对应的cache line的状态是 Modified 。因此CPU0不需要向其他CPU发送消息,直接更新数据即可。
  • 如果 0x40 所在的cache line需要替换,发现cache line状态是Modified。所以数据应该先写回主存。

Cache 之间数据和状态同步沟通,是通过发送message 来完成的。MESI主要涉及一下几种message。

  • Read: 如果CPU需要读取某个地址的数据。
  • Read Response: 答复一个Read消息,并且返回需要读取的数据。
  • Invalidate: 请求其他CPU将某地址对应的 cache line 移除。
  • Invalidate Acknowledge: 回复 invalidate 消息,表明对应的 cache line已经被移除。
  • Read Invalidate: Read + Invalidate消息的组合。
  • Writeback: 该消息包含要回写到内存的地址和数据。

继续以上的例子,我们有5个步骤。现在加上这些message,看看消息是怎么传递的。

  • CPU0 发出 Read 消息。主存返回 Read Response 消息,消息包含地址 0x40 的数据。
  • CPU1 发出 Read 消息,CPU0返回 Read Response 消息,消息包含地址 0x40 数据。
  • CPU0 发出 Invalidate 消息,CPU1接到消息后,返回Invalidate Acknowledge 消息。
  • 不需要发送任何消息。
  • 发送Writeback消息。
posted @ 2025-07-21 17:08  yooooooo  阅读(200)  评论(0)    收藏  举报