MelonBlog

缓存一致性解决方案:MESI

我们知道,缓存如果不保证一致性,可能会导致程序发生预期之外的结果,正确性得不到保障,那如何保证缓存的一致性呢?

MESI协议就是用来解决缓存一致性问题的,下面来一步一步聊聊什么的MESI。

总线窥探

了解MESI之前,先了解一下什么是总线窥探

image

一般我们的电脑和服务器都是基于总线的架构(Bus-based)。例如上图有3个cpu核心和一个共享的主存,每一个cpu通过同一个总线向主存读写数据。

总线窥探从通俗的话来讲,就是每一个cpu都可以监测到某一个内存地址的更新操作,因为所有cpu都是共用同一个bus,所以cpu可以关注自己的cache里有那些地址,并且关注这些地址的数据是否有更新。

窥探协议

那么是怎么实现这个窥探的呢?

有两种协议,写失效(Write Invalidate)写更新(Write Update)

写失效:cpu更新缓存里某一条数据时,先发送一个写无效消息,使其他同样缓存了此数据的副本失效,然后再更新缓存并且同步到内存,其他cpu操作这条数据的时候需要重新从内存里获取最新的数据。
写更新:cpu在更新缓存的同时,通过一个广播,通知其他缓存了相同数据的cpu也一起更新缓存。

无论是Write Invalidate 还是 Write Update 操作,都需要通过总线仲裁来完成,总线仲裁就是同一时刻,只有一个cpu能够使用总线,类似给总线加了一把锁。

到底使用哪种协议呢?

写更新协议看上去最简单,但是存在性能问题,例如多次更新同一条数据,需要占用多次总线进行同步更新,而占用总线的成本是非常高的。

写失效协议虽然使用起来相对复杂,但是是最节省总线资源的方案,因为写失效协议不需要每次更新其他cpu缓存,仅仅是发送缓存失效消息。

如果你再多思考一下, 你会发现写失效不一定总是性能上强于写更新的,例如当一个cpu核心更新了一条数据之后,其他的cpu核心马上需要读取数据, 那总线资源一样会占用的非常大,但是实际情况不会这么极端。从经验上来看,写失效在性能上是好于写更新,但是不是绝对的。

MESI

在了解什么是写失效(Write Invalidate)之后,来看看什么是MESI。

MESI相当于是Write Invalidate的一个拓展,MESI试图让使用Write Invalidate协议的过程中,尽可能的节省总线资源。


MESI协议定义了所有的缓存行都有一个状态,状态分为4种:

1.Modified:缓存行已被修改,和主存内的数据不一致,是唯一有效的备份。
2.Exclusive:缓存行和主存内的数据一致,并且是唯一的一个备份。
3.Shared:缓存行和主存内数据一致,但是不唯一,存在其他相同的备份。
4.Invalid :缓存行已经失效了。

从上面的名字可以看出来,MESI其实就是这4种操作的首字母


MESI还对缓存行的操作,进行了抽象,所有的操作都可以抽象成4种:

Read Hit:读缓存时,命中缓存,缓存行的状态必须为M|E|S状态。
不做任何状态更新,仅仅读取数据。
Read Miss:读缓存时,未命中缓存
当读取的数据未被任何cpu缓存时,从主存内读取数据,并将缓存行的状态设置为E。
当其他某一个cpu的缓存有此数据的副本时(状态为E的情况),更新当前缓存行的所有副本的状态为S(两份)。
当读取的数据存在在其他cpu的缓存里,并且存在多份时,读取主存内的数据,并且将缓存行的状态设置为S。
Write Hit:写入缓存,并且写入成功,缓存行的状态必须为M|E|S状态。
如果状态为M,说明缓存已经是独占的,不需要做任何更新状态操作。
如果状态为E,说明缓存也是独占的,只用将缓存行的状态升级为M。
如果状态时S,需要利用总线广播,通知其他cpu这个缓存行已经是脏(Dirty)了,并且在本地更新这条缓存行的状态为M。
Write Miss:写入缓存,但是写入失败
当写入的数据未被任何cpu缓存时,读取主存内的数据,并将缓存行的状态设置为M。
当写入的数据已经被其他cpu缓存时(无论缓存行的状态是E还是S),读取主存内的数据,并且通过总线发送失效广播,其他cpu设置各自的缓存行的状态为I,当前cpu设置缓存行的状态为M。
当写入的数据已经被另外一个cpu(称为Snooping cpu)缓存时,并且Snooping cpu的缓存行的状态为M时,Snooping cpu会执行一个 copy back 操作,将数据回写到内存,并将缓存行的状态设置为I,而当前操作的cpu则更新当前缓存行,并且更新缓存行的状态为M。


用一张图来展示这4种操作对应的状态变化:

image