缓存一致性协议MESI
高速缓冲存储器一致性 ( $Cache\ \ coherence$ ),也称缓存一致性,是指在采用层次结构存储系统的计算机系统中,保证告诉缓冲存储器中数据与主存储器中数据相同的机制。在有多个CPU
的多处理机系统中特别容易出现高速缓存中数据不一致的问题。
在CPU
缓存设计中,L1
高速缓存包含指令缓存和数据缓存,位于CPU
芯片上,访问速度几乎和寄存器一样快。L2
高速缓存在L1
和主存之间,连接到存储器总线或者高速缓存总线上。有些高性能系统还会在存储器总线上设置L3
高速缓存。L1
和L2
是每个CPU
内核间独立的,L3
是所有CPU
内核间共享的。
对于单核CPU
来说,数据更新时缓存更新只用考虑自己的就行了,主要有两种处理方法。写回法 ( $write\ \ back$ ),是当处理器执行写操作时,信息只写入cache
,当cache
中的数据被替换出去时写回主存。为了减少内存写操作,cache
中通常还会设置一个脏位 ( $dirty\ \ bit$ ),标识该块在被载入后是否发生了更新。直写法 ( $write\ \ through$ ) 是当处理器执行写操作时,既向cache
中写入也向主存中写入。直写法会造成大量写内存操作,需要设置一个缓冲来减少硬件冲突,称为写缓冲器 ( $write\ \ buffer$ ),通常不超过 $4$ 个缓存块的大小,也适用于写回法。
相比于单核CPU
,多核CPU
除了要保证L1
和L2
最新外还要考虑到其他核中L1
和L2
的实时性和有效性。MESI
协议是一个基于失效的缓存一致性协议,是支持写回缓存的最常用协议。该协议对总线上的操作进行监听,即核 $A$ 可以窥探到核 $B$ 对过期值的读操作,并更新主存中的过期值。MESI
把cache
中的数据分为几个状态:
状态 | 描述 | 监听 |
---|---|---|
$Invalid$ | 该cache 字段失效 |
无 |
$Shared$ | 字段数据一致并且多核cache 共享该字段 |
监听其他缓存使该字段无效或者变为 $Exclusive$ 的请求,监听到对应事件后会将该字段设为 $Invalid$ |
$Exclusive$ | 字段数据一致并且只在当前核cache 中独有 |
监听其他缓存读主存中该字段的操作,监听到对应事件后将该字段变为 $Shared$ |
$Modified$ | 该字段有效但是与主存不一致,只存在于当前核cache 中 |
监听所有试图读该字段对应主存字段的操作,该操作会被延迟到当前缓存字段写回主存并将状态设为 $Shared$ 之后执行 |
事件 | 描述 |
---|---|
$Local\ \ Read$ | 读取本地cache 字段 |
$Local\ \ Write$ | 写入本地cache 字段 |
$Remote\ \ Read$ | 其他cache 读取字段 |
$Remote\ \ Write$ | 其他cache 写入字段 |
对于 $Modified$ 和 $Exclusive$
状态,数据是精确的,而 $Shared$ 状态可能是非一致的。如果一个处于 $Shared$ 的缓存字段作废了,另一个缓存实际上可能已经独享了该缓存字段,但是该缓存不会转为 $Exclusive$ ,因为其他缓存并不会广播他们作废该缓存字段的通知。如果一个CPU
想修改一个处于 $Shared$ 状态的缓存字段,总线事务需要将所有该缓存字段的副本变为 $Invalid$ 状态,而修改 $Exclusive$ 状态的缓存字段不需要总线事务。
缓存的一致性消息传递是需要时间的,这就使其切换时产生延迟。当一个缓存被切换状态时其他缓存收到消息完成各自的切换并且发出回应消息这么长一段时间中CPU
都会等待所有缓存响应完成。为了避免这种CPU
运算能力的浪费,$Store\ \ Buffer$ ( 写缓存 ) 被引入。处理器会将想要写入主存的值写到 $Store\ \ Buffer$ 中再去处理其他事情。
同样的,CPU
在收到无效化通知后,也不会立即将数据无效化,因为发出通知的CPU
在等待响应,而无效化需要时间。所以CPU
会将数据存入无效化队列,在存入之后就返回 $Ack$ ,以免阻塞CPU
。
写缓冲器和无效化队列也会带来新的问题:
CPU 0
更新写缓存还没有写入内存时,CPU 1
再次更新,但是无法无效写缓存器中的记录,从而CPU 0
认为写缓存数据是最新的,导致数据错误;CPU 0
先对 $Invalid$ 记录写入,再对 $Exclusive$ / $Modify$ 记录写入时,会将 $Invalid$ 记录写入写缓冲器,对 $Exclusive$ / $Modify$ 记录直接写入缓存,由于写缓冲器有延迟,从而导致变相的指令重排序;CPU 0
收到无效化信息后存入无效化队列,然后立即对无效数据进行读取,导致数据错误。
对于上述问题,需要通过内存屏障解决:
- 存储屏障:清空写缓冲器,全部写入缓存;
- 加载屏障:清空无效化队列,设置对应记录为 $Invalid$ 。