一、CPU高速缓存简单介绍
CPU高速缓存机制的引入,主要是为了解决CPU越来越快的运行速度与相对较慢的主存访问速度的矛盾。CPU中的寄存器数量有限,在执行内存寻址指令时,经常需要从内存中读取指令所需的数据或是将寄存器中的数据写回内存。而CPU对内存的存取相对CPU自身的速度而言过于缓慢,在内存存取的过程中CPU只能等待,机器效率太低。
为此,设计者在CPU与内存之间引入了高速缓存。CPU中寄存器的存储容量小,访问速度极快;内存存储容量很大,但相对寄存器而言访问速度很慢。而高速缓存的存储大小和访问速度都介于二者之间,作为一个缓冲桥梁来填补寄存器与主存间访问速度过大的差异。
引入高速缓存后,CPU在需要访问主存中某一地址空间时,高速缓存会拦截所有对于内存的访问,并判断所需数据是否已经存在于高速缓存中。如果缓存命中,则直接将高速缓存中的数据交给CPU;如果缓存未命中,则进行常规的主存访问,获取数据交给CPU的同时也将数据存入高速缓存。但由于高速缓存容量远小于内存,因此在高速缓存已满而又需要存入新的内存映射数据时,需要通过某种算法选出一个缓存单元调度出高速缓存,进行替换。
由于对内存中数据的访问具有局部性,使用高速缓存能够极大的提高CPU访问存储器的效率。
二、高速缓存一致性问题
高速缓存与内存的一致性问题
高速缓存在命中时,意味着内存和高速缓存中拥有了同一份数据的两份拷贝。CPU在执行修改内存数据的指令时如果高速缓存命中,只会修改高速缓存中的数据,此时便出现了高速缓存与内存中数据不一致的问题。
这个不一致问题在早期单核CPU环境下似乎不是什么大问题,因为所有的内存操作都来自唯一的CPU。但即使是单核环境下,为了减轻CPU在I/O时的负载、提高I/O效率,先进的硬件设计都引入了DMA机制。DMA芯片在工作时会直接访问内存,如果高速缓存首先被CPU修改和内存不一致,就会出现DMA实际写回磁盘的内容和程序所需要写入的内容不一致的问题。
为了更好的理解多核CPU环境下工作的MESI协议,这里先简单介绍单核环境下高速缓存被首先改写而导致cache与主存不一致问题的解决方案,简单来说有两种方法:通写法和回写法。
通写法(Write Through):
即CPU在对cache写入数据时,同时也直接写入主存,这样就能使得主存和cache中的数据始终保存一致。
通写法的优点是简单,硬件上容易实现,同时在调度缓存单元时不会有脏数据,调度速度快;缺点是每次cache写入操作时都增加了写主存的等待时间,效率较低。
回写法(Write Back):
回写法和通写法的主要区别在于,回写法在CPU写cache时并不实时同步写主存,而是在进行调度被覆盖前整体的写回主存。如果被调度出的cache单元并没有被写入过,则直接被覆盖无需写回主存。
回写法的优点是写入cache时无需同步主存,总体效率比通写法高。缺点是硬件实现较为复杂。
多核CPU高速缓存间的一致性问题
随着单核主频速度的增长受到制约,CPU的发展由单核逐渐过度到了多核,目前主流的CPU都是多核心的。但随着多核CPU提高计算机并发性能的同时,也带来了一系列的问题,这其中就包括了多核CPU下的高速缓存一致性问题。
在多核CPU的架构下,通常每一个核心都拥有着自己独有的高速缓存,每个核心能并发的读写自己的高速缓存。高速缓存可以有多个,但其对应的内存数据逻辑上却只有一份,多核并发的修改其高速缓存中同一内存的映射数据就会出现高速缓存中的数据不一致的问题。如果不对多核CPU下的高速缓存并发访问施加一定的约束,那么并发程序中对共享内存数据的存取就会出现问题,并发程序的正确性将无法得到有效保障。
高速缓存一致的存储系统定义
在讨论解决高速缓存一致性问题的方法前,我们需要更精确的定义什么是高速缓存一致性。
内存系统的一个本质特征是:一个内存系统应该能提供一组保存值的存储单元,当对一个存储单元执行读操作时,应该能返回“最近”一个对该存储单元的写操作所写入的值。在串行程序中,程序员利用内存来将程序中某一点计算出来的值,传递到该值的使用点,实际上就是利用了上述基本性质。同样,运行在单处理器上的多个进程或线程利用共享地址空间进行通信,实际上也是利用了内存系统的这个性质。
一个读操作应返回最近的向那个位置的写操作所写的值,而不管是哪个线程写的。当所有的线程运行在同一个物理处理器上时,它们通过相同的高速缓存层次来看内存,因此在这种情况下,高速缓存不会引起问题。当在共享存储的多处理器系统上运行一个具有多个线程的程序时,希望不管这些线程是运行在同一个处理器上,还是位于不同的处理器上,程序的运行结果都是相同的。
上面摘抄自书上的概念描述有些晦涩,我个人的理解是:按照程序指令的运行顺序,对于同一内存单元内容(变量值)能够令后面的读操作读取到之前最近写操作后的结果,保证程序逻辑序的正确性。这一顺序性的保证在单核环境下不是问题,因为所有的指令顺序都使用同一个高速缓存,但在多核多高速缓存副本的情况下运行某一程序的多个并发任务时就会出现问题,因为并没有约定多个处理器核心对同一存储单元并发操作时的全局顺序,即“最近”这一概念是模糊、不明确的。
因此,一个高速缓存一致的存储系统其首先要满足的一个条件便是:根据一个程序的任意一次执行结果,都能够对每个内存单元的存取操作构造出逻辑上的全局串行序列(即使是多核体系下,对内存的存取逻辑上也要强制串行化)。这一全局逻辑串行序列还需要满足额外的两个条件:一是同一处理器所发出的程序内存存取指令顺序(程序逻辑序),与在全局逻辑串行序列中的先后顺序保持一致;二是每个读操作的值,返回的是在全局逻辑串行序列中最近的写操作之后的值。
上述高速缓存一致性的定义隐含了在多核环境下的两个重要性质:一是写传播,二是写串行化。
写传播(Write Propagation):一个处理器对一个位置的所写入的值,最终对其它处理器是可见的。
写串行化(Write Serialization):对同一内存单元的所有写操作(无论是来自一个处理器还是多个处理器)都能串行化。换句话说,所有的处理器能以相同的次序看到这些写操作。
三、MESI高速缓存一致性协议
通常多核并行架构的CPU,每个核虽然都独自工作,但与外部存储器的交互依然是共用同一总线进行的。通过总线,每个核心都能够监听、接收到来自其它核心的消息通知,这一机制被称为总线侦听或是总线嗅探。
基于总线侦听的写传播:
每个核心在对自己独有的高速缓存行进行修改时,需要将修改通知送至总线进行广播。其它核心在监听到总线上来自其它核心的远程写通知时,需要查询本地高速缓存中是否存在同样内存位置的数据。如果存在,需要选择将其设置为失效状态或是更新为最新的值。
基于总线侦听的写串行化:
总线上任意时间只能出现一个核的一个写通知消息。多个核心并发的写事件会通过总线仲裁机制将其转换为串行化的写事件序列(可以简单理解为逻辑上的一个FIFO事件队列),在每个写事件广播时,必须得到每个核心对事件的响应后,才进行下一个事件的处理,这一机制被称作总线事务。
而本文的主角MESI协议便是基于总线侦听机制,采用回写法、写传播失效策略的高速缓存一致性协议,其另一个更精确的名称是四态缓存写回无效协议。
下面介绍MESI协议是如何工作以实现多核间高速缓存一致性的。
MESI协议缓存行状态介绍
MESI四态缓存写回无效协议,为高速缓存中的每个存储单元行cache line赋予了一个状态属性,状态类型共有4种:Modified(已被修改)、Exclusive(被独占)、Shared(被共享)、Invalid(无效),这也是MESI这一名称的由来。
任意时刻每个缓存行都处于上述四种状态的其中一种,并且可能会因为发生的缓存事件而迁移至另一种状态。
Modified:
缓存数据有效,在读入缓存后曾经被当前CPU修改过却没有写回,导致与内存中的对应数据不一致。
内存中对应的数据只在本地核心的高速缓存中存在,其它核的高速缓存中并没有缓存这一内存数据。
有效,本地cache独占,与内存数据不一致(被修改 Modified)。
Exclusive:
缓存数据有效,在读入缓存后没有被当前CPU修改过,与内存中的对应数据保持一致。
内存中对应的数据只在本地核心的高速缓存中存在,其它核的高速缓存中并没有缓存这一内存数据。
有效,本地cache独占,与内存数据一致(被独占 Exclusive)。
Shared:
缓存数据有效,在读入缓存后没有被当前CPU修改过,与内存中的对应数据保持一致。
内存中对应的数据除了本地核心的高速缓存中存在,其它核的高速缓存中也缓存了这一内存数据。
有效,与其它cache共享,与内存数据一致(被共享 Shared)。
Invalid:
缓存数据无效。无效的含义既代表着之前缓存行有效,却因为某些事件变为无效;也代表着对应缓存行不存在。
上述缓存行的共享/独占状态,指的是本地高速缓存中存在有效的对应存储单元缓存行,而其它核的高速缓存中不存在对应单元的内容或是对应的缓存行是Invalid无效状态。
在MESI协议中不存在与Invalid无效可以视作是等价的。
内存缓存行的稳定态与非稳定态
了解了MESI的四种内存缓存行状态后,下面引入缓存内存行稳定态与非稳定态的概念。缓存内存行的稳定态指的是多核下高速缓存中的对应内存中在所有高速缓存中数据是一致的;非稳定态是稳定态的反面,指的是多核高速缓存中对应内存中在所有的高速缓存中数据出现了不一致,有不同的副本值。
当处于稳定态的缓存行由于某些事件的发生转向不稳定态时,MESI协议能够采取一些措施令整个多核存储系统重新回到稳定态(可以类比自平衡二叉树在发生插入/删除事件时,引起失衡后进行的重平衡操作)。
内存缓存行处于稳定态的几种情况:
1、对应内存行在所有核的高速缓存中都不存在。
2、对应内存行有且仅在一个核的高速缓存中存在,其状态可以是Exclusive也可以是Modified。
3、对应内存行在一个以上核的高速缓存中存在,每个缓存行中的存储的数据都和内存一致,其状态都为Shared。
MESI协议缓存事件
随着多核CPU中并发程序的不断运行,高速缓存被反复的读写,缓存内存行的状态也会在MESI这四种状态间反复变化。
在MESI协议中,抽象出了四种会导致缓存内存行状态的变化缓存事件:本地读、本地写、远程读以及远程写。缓存事件针对的是某一内存缓存行的事件。
本地读(Local Read):
本地读事件指的是本地核心对自己的缓存行进行读取。
本地写(Local Write):
本地写事件指的是本地核心对自己的缓存行进行写入。
远程读(Remote Read):
远程读事件指的是总线上的其它核心对某一内存缓存行进行了读取,当前核心监听到的事件。
某一个核心的本地读事件,对于其他核心就是针对其对应内存缓存行的远程读事件。
远程写(Remote Write):
远程写事件指的是总线上的其它核心对某一内存缓存行进行了写入,当前核心监听到的事件。
某一个核心的本地写事件,对于其他核心就是针对其对应内存缓存行的远程写事件。
MESI协议缓存行状态迁移
缓存事件的发生可能会使得本地/远程的对应缓存行状态发生变化。比如当原本处于独占状态的本地缓存行监听到远程读事件时,需要将其由Exclusive被独占转变为Shared被共享;当监听到远程写事件时,如果发现对应的缓存行存在(此时必定是Shared被共享状态),此时的内存缓存行便失去了稳定,MESI协议是采取的是写无效策略,需要将本地对应的缓存行设置为Invalid无效。
MESI协议中的四种状态在发生上述四种缓存事件时都可能发生对应的状态迁移,两两组合之后共有4*4=16种情况,下面进行详细讨论。
本地读/写事件和其它核的远程读/写事件是互相对应的,相互之间对照能更好的理解MESI协议的工作机制。
当前缓存行处于Invalid状态时:
发生local read本地读事件:
Invalid无效的缓存行,在发生本地读事件时,必须从内存或是处于Exclusive状态的远程高速缓存中获取对应的最新数据写入本地缓存。
1.当其它核心中都不存在对应缓存行数据时,从内存中获取。加载后全局有且只有本地缓存中有此缓存行记录,本地缓存行的状态由Invalid变成Exclusive。
2.当其它的某一核心中恰好也存在对应缓存行数据,且状态为Exclusive或Shared时,从内存或是存在缓存行的核心中获取。此时由于本地和其它核心都保存了对应缓存行数据,但不独占缓存行,本地缓存行的状态由Invalid变成Shared。
3.当其它的某一核心中恰好也存在对应缓存行数据,且状态为Modified时,触发其远程读事件,将修改过的最新值写入内存后,由本地核心从内存中读取最新的值。此时由于本地和之前状态为modified的核心都保存了对应缓存行数据,本地缓存行的状态由Invalid变成Shared。
发生local write本地写事件:
Invalid无效的缓存行,在发生本地写事件时,必须从内存或是处于Exclusive状态的远程高速缓存中获取对应的最新数据写入本地缓存,再进行修改。
1.当其它核心中都不存在对应缓存行数据时,从内存获取并修改。加载后全局有且只有本地缓存中有此缓存行记录,由于修改过和内存中数据不同,本地缓存行的状态由Invalid变成Modified。
2.当其它的某一核心中恰好也存在对应缓存行数据时,且状态为Exclusive或Shared时,从内存或是存在缓存行的核心中获取并修改。为了保持高速缓存一致性的稳定,远端的其它核心中的数据不能与本地核心本地写的最新数据产生冲突,远端的其它核触发远程写事件,状态都设置为Invalid(写失效协议)。此时全局有且只有本地缓存中有此缓存行记录,本地缓存行的状态由Invalid变成Modified。
2.当其它的某一核心中恰好也存在对应缓存行数据时,且状态为Modified,触发其远程写事件,将修改过最新值写入内存后,由本地核心从内存中读取最新的值并修改。远端的核心(之前为modified)中的数据不能与本地核心本地写的最新数据产生冲突,状态设置为Invalid。此时全局有且只有本地缓存中有此缓存行记录,本地缓存行的状态由Invalid变成Modified。
发生remote read远程读事件:
Invalid无效状态等价于缓存行不存在,不对远程读事件进行任何处理,状态依然为Invalid。
发生remote write远程写事件:
Invalid无效状态等价于缓存行不存在,不对远程写事件进行任何处理,状态依然为Invalid。
当前缓存行处于Exclusive状态时:
发生local read本地读事件:
Exclucive代表本地核独占当前缓存行。
本地读时直接从自己的高速缓存中获取数据即可,状态不变,依然为Exclusive。
发生local write本地写事件:
Exclucive代表本地核独占当前缓存行,且数据和内存中一致。
本地写会使得缓存中的数据与内存不一致,但依然独占,状态由Exclusive变为Modified。
发生remote read远程读事件:
当监听到远程读事件时,意味着当前缓存行已经不再是独占状态,而是共享状态了,状态由Exclusive变为Shared。
发生remote write远程写事件:
当监听到远程写事件时,意味着当前缓存行的数据已经和本地缓存中的数据不一致了,需要废弃本地的缓存行,状态由Exclusive变为Invalid。
当前缓存行处于Shared状态时:
发生local read本地读事件:
Shared代表本地核心和远程核心共享了缓存行,且数据是最新的。本地读时直接从自己的高速缓存中获取数据即可,状态不变,依然为Shared。
发生local write本地写事件:
本地写会触发其它核的(处于Shared状态)远程写事件,远程核的状态会被统一设置为无效,本地核心将独占这一缓存行。由于本地写使得缓存行数据和内存不一致,状态由Shared变为Modified。
发生remote read远程读事件:
当监听到远程读事件时,其并没有改变全局状态下缓存行的数据,状态不变依然为Shared。
发生remote write远程写事件:
当监听到远程写事件时,意味着当前缓存行的数据已经和本地缓存中的数据不一致了,需要废弃本地的缓存行,状态由Shared变为Invalid。
当前缓存行处于Modified状态时:
发生local read本地读事件:
Modified代表本地核心独占当前缓存行,在全局串行写序列中,本地读事件读取的依然是最新的值。本地读时直接从自己的高速缓存中获取数据即可,状态不变,依然为Modified。
发生local write本地写事件:
本地写时依然独占缓存行,且和内存中数据不一致,状态不变,依然为Modified。
发生remote read远程读事件:
当监听到远程写事件时,为了使得其获取到的值是全局串行序列中最近的值,需要先将Modified之后的最新值写回内存,令最新值对其它核心可见。
远程读事件意味着有其它核心共享了对应的缓存行,本地不再独占,但数据依然和本地缓存中的值保持一致,状态由Modified变为Shared。
发生remote write远程写事件:
当监听到远程写事件时,为了使得其获取到的值是全局串行序列中最近的值,需要先将Modified之后的最新值写回内存,令最新值对其它核心可见。
远程读事件意味着有其它核心共享了对应的缓存行,本地不再独占,且本地缓存的数据不再是最新的,需要令其失效,状态由Modified变为Invalid。
四、MESI协议的优化与内存屏障
通过MESI协议,在强制串行化的总线事务帮助下能够始终保持一个全局高速缓存一致的稳定状态。
MESI协议依赖总线侦听机制,在某个核心发生本地写事件时,为了保证全局只能有一份缓存数据,要求其它核对应的缓存行统统设置为Invalid无效状态。为了确保总线写事务的强一致性,发生本地写的高速缓存需要等到远端的所有核心都处理完对应的失效缓存行,返回Ack确认消息后才能继续执行下面的内存寻址指令(阻塞)。
原始MESI协议实现时的性能问题:
1.对于进行本地写事件的核心,远端核心处理失效并进行响应确认相对处理器自身的指令执行速度来说是相当耗时的,在等待所有核心响应的过程中令处理器空转效率并不高。
2.对于响应远程写事件的核心,在其高速缓存压力很大时,要求实时的处理失效事件也存在一定的困难,会有一定的延迟。
不进行优化的MESI协议在实际工作中效率会非常的低下,因此CPU的设计者在实现时对MESI协议进行了一定的改良。
存储缓存(Store Bufferes)
针对上述本地写事件需要等待远端核心ACK确认,阻塞本地处理器的问题,引入了存储缓存机制。
存储缓存是属于每个CPU核心的。当使用了存储缓存后,每当发生本地写事件时,本地核心不再阻塞的等待远程核的确认响应,而是将写入的新值放入存储缓存中,继续执行后面的指令。存储缓存会替处理器接受远端核心的ACK确认,当对应本地写事件广播得到了全部远程核心的确认后,再提交事务,将其新值写入本地高速缓存中。存储缓存的大小是十分有限的,当堆积的事务满了之后,依然会阻塞CPU,直到有事务提交释放出新的空间。
存储缓存的引入将本地写事件--->等待远程写通知确认消息并提交这一事务,从同步、强一致性变成了异步、最终一致性,提高了本地写事件的处理效率。
本地处理器在进行本地读事件时,由于可能存储缓存中新修改的数据还未提交到本地缓存中,这就会造成一个核心内,对于同一缓存行其后续指令的读操作无法读取到之前写操作的最新值。为此,在进行本地读操作时,处理器会先在存储缓存中查询对应记录是否存在,如果存在则会从存储缓存中直接获取,这一机制被称为Store Fowarding。
失效队列(Invalid Queue)
针对上述远端核心响应远程写事件,实时的将对应缓存行设置为Invalid无效状态延迟高的问题,引入了失效队列机制。
失效队列同样是属于每个CPU核心的。当使用了失效队列后,每当监听到远程写事件时,对应的高速缓存不再同步的处理失效缓存行后返回ACK确认信息,而是将失效通知存入失效队列,立即返回ACK确认消息。对于失效队列中的写失效通知,会在空闲时逐步的进行处理,将对应的高速缓存中的缓存行设置为无效。失效队列的引入在很大程度上缓解了存储缓存空间有限,容易阻塞的问题。
失效队列的引入将监听到远程写事件处理失效缓存行--->返回ACK确认消息这一事务,从同步、强一致性变成了异步、最终一致性,提高了远程写事件的处理效率。
内存屏障(Memory Barrier)
存储缓存和失效队列的引入在提升MESI协议实现的性能同时,也带来了一些问题。由于MESI的高速缓存一致性是建立在强一致性的总线串行事务上的,而存储缓存和失效队列将事务的强一致性弱化为了最终一致性,使得在一些临界点上全局的高速缓存中的数据并不是完全一致的。
对于一般的缓存数据,基于异步最终一致的缓存间数据同步不是大问题。但对于并发程序,多核高速缓存间短暂的不一致将会影响共享数据的可见性,使得并发程序的正确性无法得到可靠保证,这是十分致命的。但CPU在执行指令时,缺失了太多的上下文信息,无法识别出缓存中的内存数据是否是并发程序的共享变量,是否需要舍弃性能进行强一致性的同步。
CPU的设计者提供了内存屏障机制将对共享变量读写的高速缓存的强一致性控制权交给了程序的编写者或者编译器。
内存屏障分为读屏障和写屏障两种,内存屏障以机器指令的形式进行工作。
写屏障
写屏障用于保证高速缓存间写事务的强一致性。当CPU执行写屏障指令时,必须强制等待存储缓存中的写事务全部处理完再继续执行后面的指令。相当于将存储缓存中异步处理的本地写事务做了强一致的同步。
写屏障指令执行完后,当前核心位于写屏障执行前的本地写事务全部处理完毕,其它的核心都已经接收到了当前所有的远程写事件的写无效通知。
读屏障
读屏障用于保证高速缓存间读事务的强一致性。当CPU执行读屏障指令时,必须先将当前处于失效队列中的写无效事务全部处理完,再继续的执行读屏障后面的指令。相当于将异步队列中异步处理的远程写事务做了强一致的同步。
读屏障指令执行完后,当前核心位于读屏障执行前的远程写无效事务全部处理完毕,对于读屏障之后的共享数据读取会得到最新的值。
在进行并发程序的开发时,针对关键的任务间共享变量的读写需要使用内存屏障保证其在多核间高速缓存的一致性。在对共享变量的写入指令后,加入写屏障,令新的数据立即对其它核心可见;在对共享变量的读取指令前,加入读屏障,令其能获取最新的共享变量值。
通过在指令中的适当位置加入读/写内存屏障,虽然一定程度上降低了效率,但保证了并发程序在多核高速缓存条件下对于共享变量的可见性,是一个很好的折中解决方案。
五、总结
由于最近在学习有关硬件和操作系统相关的知识,在看到高速缓存相关的内容时,便想把一直以来都一知半解的MESI协议弄懂。通过广泛的阅读有关博客和书籍等资料,理解并整理后写下了这篇博客,希望能帮到对MESI协议等相关内容感兴趣的人。
MESI协议和内存屏障的知识,在其基础之上有高级语言中c、java的volatile关键字的工作原理,在其下有多核并行处理器、高速缓存、总线等硬件电路工作原理等相关的内容。在学习的过程中,一方面使我更好的理解了更上层的知识,另一方面黑盒子下还有黑盒子,令我感叹吾生也有涯,而知也无涯。但在学习的过程中,满足了对底层工作机制的好奇心,有着理解通透之后的快乐,还是挺有意思的。
本篇博客还存在很多不足之处,请多多指教。
主要参考书籍与博客:
https://www.cnblogs.com/hello-shf/p/12091591.html
https://cloud.tencent.com/developer/article/1152642
https://www.cnblogs.com/yanlong300/p/8986041.html
https://blog.csdn.net/u013546788/article/details/105829283
https://www.jianshu.com/p/0e036fa7af2a
https://blog.csdn.net/weixin_44936828/article/details/89430358
http://www.wowotech.net/kernel_synchronization/memory-barrier.html