CPU的缓存一致性协议MESI
在多核CPU中,内存中的数据会在多个核心中存在数据副本,某一个核心发生修改操作,就产生了数据不一致的问题,而一致性协议正是用于保证多个CPU cache之间缓存共享数据的一致性。
cache的写操作
write through 写通
每次CPU修改cache中的内容会立即更新到内存,也就意味着每次CPU写共享数据,会导致总线事务,因此这种方式常常会引起总线事务的竞争,虽然后高的一致性但是效率非常低。
write back 写回
每次CPU修改了cache中的数据,不会立即更新到内存,而是等到cache line在某一个必须或合适的实际才会更新到内存。
写失效
当一个CPU修改了数据,如果其他CPU有该数据,则通知其为无效;
写更新
当一个CPU修改了数据,如果其他CPU有该数据,则通知其更新;
cache line
cache line是cache与内存数据交换的最小单位,根据操作系统一般是32或64byte,在MESI协议中
状态
可以是M、E、S、I,
地址
则是cache line中映射的内存地址,
数据
则是从内存中读取的数据。
工作方式
当CPU从cache中读取数据时候,会比较地址是否相同,如果相同则检查cache line的状态,再决定该数据是否有效,无效则从主存中获取数据,或根据一致性协议发生一次cache-to-cache的数据推送。
工作效率
当CPU能够从cache中拿到有效数据的时候,消耗几个CPU cycle,如果发生cache miss也就是缓存中没有数据需要从主存中读取,则会消耗几十上百个CPU cycle。
状态介绍
MESI协议将cache line的状态分为modify、exclusive、shared、invalid分别是修改、独占、共享、失效
状态 | 描述 |
---|---|
M(modify) |
当前CPU刚修改完数据的状态,当前CPU拥有最新数据,其他CPU拥有失效数据,而且和主存数据不一致 |
E(exclusive) |
只有当前CPU中有数据,其他CPU中没有改数据,当前CPU的数据和主存的数据是一致的 |
S(shared) |
当前CPU和其他CPU中都有共同的数据,并且和主存中的数据一致; |
I(invalid) |
当前CPU中的数据失效,数据应该从主存中获取,其他CPU中可能有数据也可能无数据;当前CPU中的数据和主存中的数据被认为不一致。 |
M和E状态下的Cache Line数据是独有的,不同点在于M状态的数据时dirty和内存的不一致,E状态下数据和内存是一致的;
MESI协议状态迁移
在MESI协议中,每个Cache控制器不仅知道自己的读写操作,而且也监听其他Cache的读写操作,每个Cache line所处的状态根据本核和其他核的读写操作在4个状态之间进行迁移。
分为以下四个操作:
- 读本cache LocalRead;
- 写本cache LocalWrite;
- 读其他cache Remote Read;
-
写其他cache Remote Write;
内存屏障
编译器和CPU可以保证输出结果一样的情况下对指令重排序,使性能得到优化,插入一个内存屏障,相当于告诉CPU和编译器限于这个命令的必须先执行,后于这个命令的必须后执行。
内存屏障的另一个作用是强制更新一次不同CPU的缓存,这意味着如果你对一个volatile字段进行写操作,你必须知道:
- 一旦你完成写入,任何访问这个字段的线程将会得到最新的值;
- 在你写入之前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。
Volatile是如何保证可见性的?
加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个
内存屏障
,它有三个功能:
- 确保指令重排序时不会把其后面的指令重排到内存屏障之前的位置,也不会把前面的指令排到内存屏障后面,即在执行到内存屏障这句指令时,前面的操作已经全部完成;
- 将当前处理器缓存行的数据立即写回系统内存(由volatile先行发生原则保证);
- 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。写回操作时要经过总线传播数据,而每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器要对这个值进行修改的时候,会强制重新从系统内存里把数据读到处理器缓存(也是由volatile先行发生原则保证);
缓存一致性协议有多种,但是日常处理的大多数计算机设备都属于”嗅探(snooping)”协议,它的基本思想是:
所有内存的传输都发生在一条共享的总线上,而所有的处理器都能看到这条总线:缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(同一个指令周期中,只有一个CPU缓存可以读写内存)。
CPU缓存不仅仅在做内存传输的时候才与总线打交道,而是不停在嗅探总线上发生的数据交换,跟踪其他缓存在做什么。所以当一个缓存代表它所属的处理器去读写内存时,其它处理器都会得到通知,它们以此来使自己的缓存保持同步。只要某个处理器一写内存,其它处理器马上知道这块内存在它们的缓存段中已失效。
** 反复思考IA-32手册对lock指令作用的这几段描述,可以得出lock指令的几个作用:
1、锁总线,其它CPU对内存的读写请求都会被阻塞,直到锁释放,不过实际后来的处理器都采用锁缓存替代锁总线,因为锁总线的开销比较大,锁总线期间其他CPU没法访问内存
2、lock后的写操作会回写已修改的数据,同时让其它CPU相关缓存行失效,从而重新从主存中加载最新的数据
3、不是内存屏障却能完成类似内存屏障的功能,阻止屏障两遍的指令重排序
由于效率问题,实际后来的处理器都采用锁缓存来替代锁总线,这种场景下多缓存的数据一致是通过缓存一致性协议来保证的 **
问题
既然CPU有了MESI协议可以保证cache的一致性,那么为什么还需要volatile这个关键词来保证可见性(内存屏障)?或者是只有加了volatile的变量在多核cpu执行的时候才会触发缓存一致性协议?
两个解释结论:
- 多核情况下,所有的cpu操作都会涉及缓存一致性的校验,只不过该协议是弱一致性,不能保证一个线程修改变量后,其他线程立马可见,也就是说虽然其他CPU状态已经置为无效,但是当前CPU可能将数据修改之后又去做其他事情,没有来得及将修改后的变量刷新回主存,而如果此时其他CPU需要使用该变量,则又会从主存中读取到旧的值。而volatile则可以保证可见性,即立即刷新回主存,修改操作和写回操作必须是一个原子操作;
- 正常情况下,系统操作并不会进行缓存一致性的校验,只有变量被volatile修饰了,该变量所在的缓存行才被赋予缓存一致性的校验功能。
volatile的使用场景
- 状态标志(开关模式)
- 双重检查锁定
- 需要利用顺序性
volatile和synchronized的区别
使用上的区别
volatile只能修饰变量,synchronized只能修饰方法和语句块;
对原子性的保证
synchronized可以保证原子性,volatile不能保证原子性;
对可见性的保证
都可以保证可见性,但实现原理不同,volatile对变量加了lock,synchronized使用monitorEnter和monitorExit;
对有序性的保证
都可以保证有序性,但是synchronized并发退化到串行;
其他
synchronized引起阻塞;
volatile不会引起阻塞;