现在的CPU都是多核的CPU,每个CPU内核都有着自己的L1、L2级缓存,多个CPU内核共享L3级缓存和计算机的组主内存。CPU在加载数据的时候,首先会尽可能的从 Cache 中取加载,并不是一开始就从主内存中取加载数据。当 Cache 中没有数据的时候,才会从内存中去加载数据。
但是对于CPU处理的数据来说,肯定也会有数据的写入操作,Cache 的速度比内存快很多,那应该是直接写入 Cache 中还是内存中呢?如果直接写入内存中,Cache中的内存是否会失效?(与redis作为数据库缓存类似,当发送数据更新写入缓存还是直接写入库)。
两种写入策略
写直达
写直达是最简单直接的一种策略,在写入之前先判断 Cache 中是否已经存在这个数据(通过地址映射关系判断这个地址的数据是否在 Cache 上),如果存在则直接更新 Cache 在写入到主内存中,如果不存在 Cache 就只更新主内存。
这个策略存在一个很明显的问题,就是不管缓存命中与否,都要写入到主内存中。
写回(Write-Back)
写回这个策略的想法是:既然CPU加载数据首先会从 Cache 中查找,那么是否可以直接将数据写入 Cache,而不把所有的数据同步到主内存呢?这样当缓存命中率高的时候可以减少很多I/O操作。
所以写回这个策略不再需要每次都同步数据到主内存中,而只是写到 Cache中,只有当 Cache 中的数据要被替换(应该是由于缓存淘汰策略)的时候才同步到主内存中。
过程是这样子的:当有要写入的数据的时候,直接更新 Cache 中的数据(如果存在),同时这个这个数据所在的块(cpu是按块加载数据)会被标记成脏的,就是这个块中的数据和主内存中的是不一致的。如果要写入的块存放着别的内存地址的数据(这个时候这个 Cache 中的数据要被淘汰),再查看这个块是否标记了脏块,如果标记了就先把这个 Cache 块的数据同步到主内存中,如果没有标记成脏块,就直接更新 Cache 块,再把这个块标记成脏块!
由于有了一个脏块标记,在从内存中加载数据写入到 Cache 中也需要多加一个同步操作,如果缓存到的 Cache 块是脏块的话,就需要先将块中的数据同步到主内存之后再进行缓存操作,让后把脏标记去掉。
写直达和写回都是针对单个CPU核心数为前提的,如果是多个CPU核心同时工作多线程的前提下,如果保证各个核心的高速缓存一致性呢?(数据还没有同步到主内存)
如果有这样一种传播机制,可以在cpu的一个核心更新完 Cache 之后,主动传播到其他的核心上。这样就可以解决同步的问题,但是仅仅是这个还是不够的,如果有四个cpu核心,内核1对缓存做了更改,内核2也对缓存做了更改,如果内核3和内核4接收到的更改顺序不一样,那也是不行的,所有也需要有一种串行化的机制。
总线嗅探
就是前面说的那种传播机制,本质上就是把所有的读写请求都通过总线(Bus)广播给所有的 CPU 核心,然后让各个核心去“嗅探”这些请求,再根据本地的情况进行响应。
MESI 协议
MESI表示对 Cache Line(CPU高速缓存块)的四种标记:
它是一种写失效的协议,在写失效协议里面,只有一个CPU核心是负责写入数据(关键,解决串行的问题),其他核心只是同步读取到这个写入,但是不做任何操作。在负责写入数据的核心操作完之后,发送一个"失效"请求到其他的CPU核心(那个块失效了),其他的核心只通过这个请求判断自己是否有这个失效的块,有的话就标记为失效。
MESI协议的核心是E和S,在共享(s)状态下,因为同样的数据在多个 CPU 核心的 Cache 里都有。所以,当我们想要更新 Cache 里面的数据的时候,不能直接修改,而是要先向所有的其他 CPU 核心广播一个请求,要求先把其他 cpu 核心里面的 Cache,都变成无效的状态,然后再更新当前 Cache 里面的数据。
在独占(e)状态下,对 Cache 块的更新操作就只能在这个核心下操作,其他的核心是没有操作权限的,相当于加了一把写锁,任何数据操作都只会在这个核心下进行。在独占状态下的数据,如果收到了一个来自于总线的读取对应缓存的请求,它就会变成共享状态。这个共享状态是因为,这个时候,另外一个 CPU 核心,也把对应的 Cache Block,从内存里面加载到了自己的 Cache 里来。
CPU,高速缓存,内存。在数据同步方面就像我们在编程过程中使用redis作为mysql的缓存层,同样也是需要面临和计算机组成一样的问题。以及多核cpu换从同步和应用程序的缓存集群同步问题。不管做什么都是一个发现问题,解决问题,优化解决问题的过程。站在巨人的肩膀看世界固然不错,但是也同样希望能有所突破。