一 CPU Cache的数据写入
1.1 CPU Cache的结构
是由很多个Cache Line组成的,CPU Line是CPU从内存读取的基本单位,CPU Line是由多个标志+数据块组成。
1.2 CPU Cache数据的写入
数据不仅仅只有读取,还有数据的写入,写入数据也是先将数据写入到CPU 的L1Cache中,然后再由 CPU 控制器将数据写入到内存。怎么知道数据是否要写入到内存,很简单,内存中和Cache相对应的内容不同时,就i该把Cache的数据同步到内存。
什么时机把Cache中的数据写回到内存?
1)写直达
2)写回
1.2.1 写直达
写直达: 最简单的保持Cache和内存一致性的方法,就是在数据写入到Cache时候,同时也写入到内存一份。这个方法,写入到L1Cache之前先判断是否已经在Cache中了,如果在Cache里面,先把Cache内的数据更新,并把数据也写到内存。 如果Cache没有该数据,就直接把该数据写入到内存,不用写入到Cache了。 缺点:无论数据在不在L1Cache,都会写入到内存,其实会花费很多时间,降低效率。
1.2.2 写回
写回: 新写入的数据只会被写入到Cache Block中,只有当修改过的Cache Block被替换时候才写到内存中,减少了数据写回到内存的频率。
具体做法:
发生写操作时,数据就已经在CPU Cache中的话,则把数据更新到CPU Cache中,同时标记CPU Cache中的这个Cache Line为脏,代表内存和CPU Cache中的数据不一致,这种情况不用将数据写入到内存。
如果当发生写操作时,数据所对应的 Cache Block 里存放的是「别的内存地址的数据」的话,就要检查这个 Cache Line 里的数据有没有被标记为脏的,如果是脏的话,我们就要把这个 Cache Line 里的数据写回到内存,然后再把当前要写入的数据,写入到这个 Cache Line 里,同时也把它标记为脏的;如果 Cache Line 里面的数据没有被标记为脏,则就直接将数据写入到这个 Cache Line里,然后再把这个 Cache Line 标记为脏的就好了。 可以发现写回这个方法,在把数据写入到 Cache 的时候,只有在缓存不命中,同时数据对应的 Cache 中的 Cache Line 为脏标记的情况下,才会将数据写到内存中,而在缓存命中的情况下,则在写入后 Cache 后,只需把该数据对应的 Cache Line 标记为脏即可,而不用写到内存里。 这样的好处是,如果我们大量的操作都能够命中缓存,那么大部分时间里 CPU 都不需要读写内存,自然性能相比写直达会高很多。
二 缓存一致性问题
多核心CPU是造成缓存一致性问题的主要原因。
以一个二核心CPU为例,假设 A 号核心和 B 号核心同时运行两个线程,都操作共同的变量 i(初始值为 0 )。这时如果 A 号核心执行了 i++ 语句的时候,为了考虑性能,使用了我们前面所说的写回策略,先把值为 1 的执行结果写入到 L1/L2 Cache 中,然后把 L1/L2 Cache 中对应的 Block 标记为脏的,这个时候数据其实没有被同步到内存中的,因为写回策略,只有在 A 号核心中的这个 Cache Block 要被替换的时候,数据才会写入到内存里。 如果这时旁边的 B 核心尝试从内存读取 i 变量的值,则读到的将会是错误的值,因为刚才 A 核心更新 i 值还没写入到内存中,内存中的值还依然是 0。这个就是所谓的缓存一致性问题,A 核心和 B 核心的缓存,在这个时候是不一致,从而会导致错误。
那么,要解决这一问题,就需要一种机制,来同步两个不同核心里面的缓存数据。要实现的这个机制的话,要保证做到下面这两点:
●第一点,某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache,这个称为写传播;
●第二点,某个 CPU 核心里对数据的操作顺序,必须在其他核心看起来顺序是一样的,这个称为事务的串形化。
第一点写传播很容易就理解,当某个核心在 Cache 更新了数据,就需要同步到其他核心的 Cache 里。而对于第二点事务事的串形化,我们举个例子来理解它:
假设我们有一个含有 4 个核心的 CPU,这 4 个核心都操作共同的变量 i(初始值为 0 )。A 号核心先把 i 值变为 100,而此时同一时间,B 号核心先把 i 值变为 200,这里两个修改,都会「传播」到 C 和 D 号核心。那么问题就来了,C 号核心先收到了 A 号核心更新数据的事件,再收到 B 号核心更新数据的事件,因此 C 号核心看到的变量 i 是先变成 100,后变成 200。 而如果 D 号核心收到的事件是反过来的,则 D 号核心看到的是变量 i 先变成 200,再变成 100,虽然是做到了写传播,但是各个 Cache 里面的数据还是不一致的。 所以,我们要保证 C 号核心和 D 号核心都能看到相同顺序的数据变化,比如变量 i 都是先变成 100,再变成 200,这样的过程就是事务的串形化。 要实现事务串形化,要做到两点:
●CPU 核心对于 Cache 中数据的操作,需要同步给其他 CPU 核心。
●要引入「锁」的概念,如果两个 CPU 核心里有相同数据的 Cache,那么对于这个 Cache 数据的更新,只有拿到了「锁」,才能进行对应的数据更新。
三 总线嗅探
写传播的原则就是当某个 CPU 核心更新了 Cache 中的数据,要把该事件广播通知到其他核心。最常见实现的方式是总线嗅探。 还是以前面的 i 变量例子来说明总线嗅探的工作机制:
当 A 号 CPU 核心修改了 L1 Cache 中 i 变量的值,A核心通过总线把这个事件广播通知给其他所有的核心,然后每个CPU 核心都会监听总线上的广播事件,并检查是否有相同的数据在自己的 L1 Cache 里面,如果 B 号 CPU 核心的 L1 Cache 中有该数据,那么也需要把该数据更新到自己的 L1 Cache。
总线嗅探方法很简单,不过 CPU 需要每时每刻监听总线上的一切活动,但是不管别的核心的 Cache 是否缓存相同的数据,都需要发出一个广播事件,这无疑会加重总线的负载。 另外,总线嗅探只是保证了某个 CPU 核心的 Cache 更新数据这个事件能被其他 CPU于是,有一个协议基于总线嗅探机制实现了事务串形化,用状态机机制降低了总线带宽压力,这个协议就是 MESI 协议,这个协议就做到了 CPU 缓存一致性。
四 MESI协议
MESI 协议其实是 4 个状态单词的开头字母写,分别是:
●Modified,已修改
●Exclusive,独占
●Shared,共享
●Invalidated,已失效
这四个状态来标记 Cache Line 四个不同的状态。
●「已修改」状态就是我们前面提到的脏标记,代表该 Cache Block 上的数据已经被更新过,但是还没有写到内存里。而「已失效」状态,表示的是这个 Cache Block 里的数据已经失效了,不可以读取该状态的数据。
●「独占」和「共享」状态都代表 Cache Block 里的数据是干净的,也就是说,这个时候 Cache Block 里的数据和内存里面的数据是一致性的。
●「独占」和「共享」的差别在于,独占状态的时候,数据只存储在一个 CPU 核心的 Cache 里,而其他 CPU 核心的 Cache 没有该数据。这个时候,如果要向独占的 Cache 写数据,就可以直接自由地写入,而不需要广播给其他 CPU 核心,因为只有你这有这个数据,就不存在缓存一致性的问题了,于是就可以随便操作该数据。
●在「独占」状态下的数据,如果有其他核心从内存读取了相同的数据到各自的 Cache ,那么这个时候,独占状态下的数据就会变成共享状态。
●「共享」状态代表着相同的数据在多个 CPU 核心的 Cache 里都有,所以当我们要更新 Cache 里面的数据的时候,不能直接修改,而是要先向所有的其他 CPU 核心广播一个请求,要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态,然后再更新当前 Cache 里面的数据。
4.1 举例
举例说明: 我们举个具体的例子来看看这四个状态的转换:
1.当 A 号 CPU 核心从内存读取变量 i 的值,数据被缓存在 A 号 CPU 核心自己的 Cache 里面,此时其他 CPU 核心的 Cache 没有缓存该数据,于是标记 Cache Line 状态为「独占」,此时其 Cache 中的数据与内存是一致的; 1.然后 B 号 CPU 核心也从内存读取了变量 i 的值,此时会发送消息给其他 CPU 核心,由于 A 号 CPU 核心已经缓存了该数据,所以会把数据返回给 B 号 CPU 核心。在这个时候, A 和 B 核心缓存了相同的数据,Cache Line 的状态就会变成「共享」,并且其 Cache 中的数据与内存也是一致的; 1.当 A 号 CPU 核心要修改 Cache 中 i 变量的值,发现数据对应的 Cache Line 的状态是共享状态,则要向所有的其他 CPU 核心广播一个请求,要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态,然后 A 号 CPU 核心才更新 Cache 里面的数据,同时标记 Cache Line 为「已修改」状态,此时 Cache 中的数据就与内存不一致了。 1.如果 A 号 CPU 核心「继续」修改 Cache 中 i 变量的值,由于此时的 Cache Line 是「已修改」状态,因此不需要给其他 CPU 核心发送消息,直接更新数据即可。 1.如果 A 号 CPU 核心的 Cache 里的 i 变量对应的 Cache Line 要被「替换」,发现Cache Line 状态是「已修改」状态,就会在替换前先把数据同步到内存。 1.所以,可以发现当 Cache Line 状态是「已修改」或者「独占」状态时,修改更新其数据不需要发送广播给其他 CPU 核心,这在一定程度上减少了总线带宽压力。