深入学习缓存一致性问题和缓存一致性协议MESI(一)

先来梳理下关于高速缓存的一些知识

高速缓存是一种存取速率远比主内存大而容量远比主内存小的存储部件,每个处理器都有其高速缓存。引入高速缓存之后, 处理器在执行内存读、 写操作的时候并不直接与主内存打交道, 而是通过高速缓存进行的。变量名相当于内存地址, 而变量值则相当于相应内存空间所存储的数据。

从内部结构来看高速缓存相当于一个拉链散列表(ChainedHash Table). 它包含若干桶(Bucket, 硬件上称之为Set), 每个桶又可以包含若干缓存条目(CacheEntry) .

高速缓存内存结构示意图

缓存条目可被进一步划分为Tag、Data Block以及Flag这三个部分其中, Data Block也被称为缓存行(CacheLine) , 它是高速缓存与主内存之间的数据交换最小单元 用于存储从内存中读取的或者准备写往内存的数据。 Tag则包含了与缓存行中数据相应的内存地址的部分信息(内存地址的高位部分比特)。 Flag用于表示相应缓存行的状态信息。 缓存行的容量(也被称为缓存行宽度)通常是2的倍数, 其大小在16-256 字节(Byte)之间不等。一个缓存行可以存储若干变量的值, 而多个变量的值则可能被存储在同一个缓存行之中。

缓存条目
处理器在执行内存访问操作时会将相应的内存地址解码。内存地址的解码结果包括 tag、index以及offset这三部分数据。其中,index相当于桶编号,它可以用来定位内存地址对应的桶;

一个桶可能包含多个缓存条目. tag相当于缓存条目的相对编号, 其作用在于用来与同一个桶中的各个缓存条目中的Tag部分进行比较, 以定位一个具体的缓存条目; 一个缓存条目中的缓存行可以用来存储多个变量,offset是缓存行内的位置偏移, 其作用在于确定一个变量在一个缓存行中的存储起始位置。

缓存命中

根据这个内存地址的解码结果,如果高速缓存子系统能够找到相应的缓存行并且缓存行所在的缓存条目的Flag表示相应缓存条目是有效的, 那么我们就称相应的内存操作产生了缓存命中(CacheHit) 3; 否则,我们就称相应的内存操作产生了缓存未命中(CacheMiss)。

从性能角度来看,减少缓存未命中

缓存未命中包括读未命中(Read Miss)和写未命中(Write Miss), 分别对应内存读和写操作。当读未命中产生时, 处理器所需读取的数据会从主内存中加载并被存入相应的缓存行之中。 这个过程会导致处理器停顿(Stall)而不能执行其他指令,这不利于发挥处理器的处理能力。

缓存未命中不可避免

由于高速缓存的总容量远小于主内存的总容量,同一个缓存行在不同时刻存储的可能是不同的一段数据, 因此缓存未命中是不可避免的。

在Linux系统中,我们可以使用Linux内核工具perf来查看程序运行过程中的缓存未命中情况。

高速缓存通常被称为一级缓存(LI Cache)、 二级缓存(L2 Cache)、 三级缓存(L3 Cache)

image.png

image.png
高速缓存从下到上越接近CPU速度越快,同时容量也越小。现在大部分的处理器都有二级或者三级缓存,从下到上依次为 L3 cache, L2 cache, L1 cache. 缓存又可以分为指令缓存和数据缓存,指令缓存用来缓存程序的代码,数据缓存用来缓存程序的数据L1 Cache,一级缓存,本地core的缓存,分成32K的数据缓存L1d和32k指令缓存L1i,访问L1需要3cycles,耗时大约1ns; L2 Cache,二级缓存,本地core的缓存,被设计为L1缓存与共享的L3缓存之间的缓冲,大小为256K,访问L2需要12cycles,耗时大约3ns; L3 Cache,三级缓存,在同插槽的所有core共享L3缓存,分为多个2M的段,访问L3需要38cycles,耗时大约12ns;

一级缓存可能直接被集成在处理器的内核里,因此访问效率非常高。通常包括两部分,一部分用于存储指令,一部分用于存储数据,距离处理器越近的高速缓存,存储速率越快,制造成本越高,容量越小。

缓存一致性协议

缓存一致性问题

多个线程并发访问同一个共享变植的时候,这些线程的执行处理器上的高速缓存各自都会保留一份该共享变撒的副本,这就带来一个新问题一个处理器对其副本数据进行更新之后, 其他处理器如何 “察觉” 到该更新并做出适当反应, 以确保这些处理器后续读取该共享变扯时能够读取到这个更新。这就是缓存一致性问题。 例如: CPU-0读取主存的数据,缓存到CPU-0的高速缓存中,CPU-1也做了同样的事情,而CPU-1把count的值修改成了2,并且同步到CPU-1的高速缓存,但是这个修改以后的值并没有写入到主存中,CPU-0访问该字节,由于缓存没有更新,所以仍然是之前的值,就会导致数据不一致的问题.

实质

实质就是如何防止读脏数据和丢失更新的问题。 各个厂家提供了不少解决方案,最后选择了缓存一致性协议。

总线锁 当一个CPU对其缓存中的数据进行操作的时候,往总线中发送一个Lock信号。其他处理器的请求将会被阻塞,那么该处理器可以独占共享内存。总线锁相当于把CPU和内存之间的通信锁住了,所以这种方式会导致CPU的性能下降,所以P6系列以后的处理器,出现了另外一种方式,就是缓存锁。

缓存锁 如果缓存在处理器缓存行中的内存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理不在总线上声明LOCK信号,而是修改内部的缓存地址,然后通过缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域的数据,当其他处理器回写已经被锁定的缓存行的数据时会导致该缓存行无效。所以如果声明了CPU的锁机制,会生成一个LOCK指令,会产生两个作用。

  1. Lock前缀指令会引起引起处理器缓存回写到内存,在P6以后的处理器中,LOCK信号一般不锁总线,而是锁缓存。
  2. 一个处理器的缓存回写到内存会导致其他处理器的缓存无效

X86基于MESI协议的问题。

MESI (Modified-Exclusive-Shared-Invalid)协议是一种广为使用的缓存一致性协议, x86处理器所使用的缓存一致性协议就是基于MESI协议的。

MESI协议对内存数据访问的控制类似读写锁,它使得针对同一地址的读内存操作是并发的,而针对同一地址的写内存操作是独占的,即针对同一内存地址进行的写操作在任意一个时刻只能够由一个处理器执行。 在MESI协议中, 一个处理器往内存中写数据时必须持有该数据的所有权。

为了保障数据的一致性. MESI将缓存条目的状态划分为Modified(更新的)、Exclusive(排外的)、Shared(共享的)和Invalid(无效的)这4种, 并在此基础上定义了一组消息(Message)用于协调各个处理器的读、 写内存操作。

MESI协议中一个缓存条目的Flag值有以下4种可能: ​ •Invalid (无效的.记为I)。 该状态表示相应缓存行中不包含任何内存地址对应的有效副本数据。 该状态是缓存条目的初始状态。 ​ •Shared (共享的,记为s)。 该状态表示相应缓存行包含相应内存地址所对应的 副本数据。 并且, 其他处理器上的高速缓存中也可能包含相同内存地址对应的副本数据。因此,一个缓存条目的状态如果为 Shared, 并且其他处理器上也存在 Tag 值与该缓存条目的 Tag 值相同的缓存条目,那么这些缓存条目的状态也为 Shared。 处于该状态的缓存条目, 其缓存行中包含的数据与主内存中包含的数据一致。 ​ •Exclusive (独占的,记为 E) 。该状态表示相应缓存行包含相应内存地址所对应 的副本数据。 并且, 该缓存行以独占的方式保留了相应内存地址的副本数据, 即 其他所有处理器上的高速缓存当前都不保留该数据的有效副本。 处千该状态的缓 存条目, 其缓存行中包含的数据与主内存中包含的数据一致。 ​ •Modified (更改过的,记为 M) 。该状态表示相应缓存行包含对相应内存地址所做的更新结果数据。 由于 MESI 协议中的任意一个时刻只能够有一个处理器对同一内存地址对应的数据进行更新, 因此在多个处理器上的高速缓存中 Tag 值相同 的缓存条目中, 任意一个时刻只能够有一个缓存条目处于该状态。 处于该状态的 缓存条目, 其缓存行中包含的数据与主内存中包含的数据不一致。

MESI 协议定义了一组消息 (Message) 用于协调各个处理器的读、 写内存操作,如表11-1 所示。 比照 HTTP 协议, 我们可以将 MESI 协议中的消息分为请求消息和响应消息。处理器在执行内存读、写操作时在必要的情况下会往总线 (Bus) 中发送特定的请求消息,同时每个处理器还嗅探 (Snoop, 也称拦截)总线中由其他处理器发出的请求消息并在一 定条件下往总线中回复相应的响应消息。

image.png

MESI协议的处理器读写操作?

下面讨论在Processor 0上读取数据S的实现。

设内存地址A上 的数据S是处理器Processor 0和处理器Processor l可能共享的数据。

下面讨论在Processor 0上读取数据S的实现。Processor 0会根据地址A找到对应的 缓存条目, 并读取该缓存条目的Tag和Flag值(缓存条目状态)。 为讨论方便, 这里我们不讨论Tag值的匹配问题。Processor0找到的缓存条目的状态如果为M、 E或者s. 那么 该处理器可以直接从相应的缓存行 中读取地址 A所对应的数据, 而无须往总线中发送任何消息。Processor 0找到的缓存条目的状态如果为I. 则说明该处理器的高速缓存中并不 包含S的有效副本数据,此时Processor 0需要往总线发送Read消息以读取地址A对应的 数据, 而其他处理器Processor l (或者主内存)则需要回复ReadResponse以提供相应的 数据

Processor 0接收到ReadResponse消息时, 会将其中携带的数据(包含数据S的数据块) 存入相应的缓存行 并将相应缓存条目的状态更新为S。 Processor 0 接收到的Read Response消息可能来自主内存也可能来自其他处理器(Processor I)。

Processor I会嗅探总线中由其他处理器发送的消息。Processor I嗅探到Read消息的时候, 会从该消息中取 出待读取的内存地址.并根据该地址在其高速缓存中查找对应的缓存条目。如果Processor I 找到的缓存条目的状态不为I (表11-2所示的情况). 则说明该处理器的高速缓存中有待 读取数据的副本,此时Processor l会构造相应的ReadResponse消息并将相应缓存行所存储的整块数据(而不仅仅是Processor0所请求的数据s) ,. 塞入 “ 该消息。如果Processor1 找到的相应缓存条目的状态为M, 那么Processor1可能在往总线发送ReadResponse消息 前将相应缓存行中的数据写入主内存。Processor1往总线发送ReadResponse之后,相应缓存条目的状态会被更新为 S。 如果 Processor I 找到的高速缓存条目的状态为I, 那么 Processor 0所接收到的ReadResponse消息就来自主内存。

可见,在Processor0读取内存 的时候,即便Processor I对相应的内存数据进行了更新且这种更新还停留在Processor I 的高速缓存中而造成高速缓存与主内存中的数据不一致,在MESI消息的协调下这种不一 致也并不会导致Processor0读取到一个过时的旧值。

讨论Processor 0往地址A写数据的实现

任何一个处理器执行内存写操作时必须拥有相应数据的所有权。在执行内存写操作时,Processor0会先根据内存地址A找到相应的缓存条目。Processor0所找到的缓存条目的状态若为E或者M, 则说明该处理器已经拥有相应数据的所有权,此时该处理器可以直接将数据写入相应的缓存行并将相应缓存条目的状态更新为M 。Processor0所找到的缓存条目的状态如果不为E、M, 则该处理器需要往总线发送Invalidate消息以获得数据的所有权。其他处理器接收到Invalidate消息后会 将其高速缓存中相应的缓存条目状态更新为I (相当于删除相应的副本数据)并回复 Invalidate Acknowledge消息。发送Invalidate消息的处理器(即内存写操作的执行处理器),必须在接收到其他所有处理器所回复的所有I nvalidate Acknowledge消息之后再将数据更 新到相应的缓存行之中.

image.png

Processor 0所找到的缓存条目的状态若为s. 则说明Processor l上的高速缓存可能也保留了地址A对应的数据副本(场景I). 此时Processor 0需要往总线发送Invalidate消息。Processor 0在接收到其他所有处理器所回复的InvalidateAcknowledge消息之后会将相应的缓存条目的状态更新为 E, 此时 Processor 0 获得了地址 A 上数据的所有权。 接着,Processor 0 便可以将数据写入相应的缓存行, 并将相应缓存条目的状态更新为 M 。Processor 0 所找到的缓存条目的状态若为 I, 则表示该处理器不包含地址 A 对应的有效副本数据(场景 2), 此时 Processor 0 需要往总线发送 Read Invalidate 消息。Processor 0 在接收到 Read Response 消息以及其他所有处理器所回复的 Invalidate Acknowledge 消息之后, 会将相应缓存条目的状态更新为E, 这表示该处理器已经获得相应数据的所有权。接 着, Processor 0 便可以往相应的缓存行中写入数据了并将相应缓存条目的状态更新为 M 。 其他处理器在接收到 Invalidate 消息或者 Read Invalidate 消息之后, 必须根据消息中包含的内存地址在该处理器的高速缓存中查找相应的高速缓存条目。若 Processor I 所找到的高速缓存条目的状态不为 I (场景 2), 那么 Processor I 必须将相应缓存条目的状态更新为I, 以删除相应的副本数据并给总线回复 Invalidate Acknowledge 消息。可见. Invalidate 消息和 Invalidate Acknowledge 消息使得针对同一个内存地址的写操作在任意一个时刻只能由一个处理器执行, 从而避免了多个处理器同时更新同一数据可能导致的数据不一致问题。

从上述例子来看. 在多个线程共享变抵的情况下, MESI 协议已经能够保障一个线程 对共享变量的更新对其他处理器上运行的线程来说是可见的;既然如此,可见性又何以存在呢?这就要从写缓冲器和无效化队列的角度来解释了。

整理不易,喜欢请点个赞。

猜你喜欢

转载自juejin.im/post/5d67e75a5188256db0644778