1、CPU没有管脚直接连到内存。相反,CPU和一级缓存(L1 Cache)通讯,而一级缓存才能和内存通讯。大约二十年前,一级缓存可以直接和内存传输数据。如今,更多级别的缓存加入到设计中,一级缓存已经不能直接和内存通讯了,它和二级缓存通讯——而二级缓存才能和内存通讯。或者还可能有三级缓存。总结CPU访问主存的规律:
—
CPU从来都不直接访问主存,都是通过cache间接访问主存
。
— 每次需要访问主存时,遍历所有的cache line,查找主存的地址是否在某个cache line中。
— 如果cache中没有找到,则分配一个新的cache entry,把主存对应地址的内容copy到cache line中,再从cache line中读取。
2、缓存是分“段”(cache line)的,一个段对应一块存储空间,大小是32(较早的ARM、90年代/2000年代早期的x86和PowerPC)、64(较新的ARM和x86)或128(较新的Power ISA机器)字节。每个缓存段知道自己对应什么范围的物理内存地址。
—- Cache entry包含如下部分:
—- cache line:从主存一次copy的数据大小
—- tag : 标记cache line对应的主存的地址。
—- flag:标记当前cache line
是否invalid
,如果是数据cache,还有
是否dirty
。
3、
当CPU看到一条读内存的指令时,它会把内存地址传递给一级数据缓存(L1)。一级数据缓存会检查它是否有这个内存
地址对应的缓存段。如果没有,它会把整个缓存段从内存中加载进来。是的,
一次加载整个缓存段
,这是基于这样一个假设:内存访问倾向于本地化(localized),如果我们当前需要某个地址的数据,那么很可能我们马上要访问它的邻近地址。一旦缓存段被加载到缓存中,读指令就可以正常进行读取。
如果我们只处理读操作,那么事情会很简单,因为所有级别的缓存都遵守以下规律
:
在任意时刻,任意级别缓存中的缓存段的内容,等同于它对应的内存中的内容。
4、
一旦我们允许写操作,事情就变得复杂一点了。
这里有两种基本的写模式:
直写
(write-through)和
回写
(write-back)。
—- 直写:我们透过本级缓存,直接把数据写到下一级缓存(或直接到内存)中,如果对应的段被缓存了,我们同时
更新缓存中的内容(甚至直接丢弃),就这么简单。这也遵守前面的定律:
缓存中的段永远和它对应的内存内容匹配
。
—- 回写:有点复杂,缓存不会立即把写操作传递到下一级,而是仅修改本级缓存中的数据,并且把对应的缓存段标记为
“脏”(dirty)段。
脏段会触发回写,也就是把里面的内容写到对应的内存或下一级缓存中。回写后,脏段又变“干净”了。
当一个脏段被丢弃的时候(前),总是先要进行一次回写
。回写所遵循的规律有点不同。
回写定律
:
当所有的脏段被回写后,任意级别缓存中的缓存段的内容,等同于它对应的内存中的内容。
—- 写策略总结->Cache中的数据更新后,需要回写到主存,回写的时机有多种,
1)每次更新都写回内存,即write-through cache
2)更新后不写回内存,先标记为dirty,仅当cache entry被evict时才写回内存。
3)更新后把cache entry送入回写队列,待队列收集到多个entry时批量写回内存。
5、在
回写模式的定律中,我们去掉了“在任意时刻”这个修饰语,代之以弱化一点的条件:要么
缓存段的内容和内存一致
(如果
缓存段是干净的话),要么
缓存段中的内容最终要回写到内存中
(对于脏缓存段来说)。
直接模式更简单,但是回写模式有它的优势:它能
过滤掉对同一地址的反复写操作
,并且,如果大多数缓存段都在回写模式
下工作,
那么系统经常可以一下子写一大片内存,而不是分成小块来写,后者的效率更高。
有些(大多数是比较老的)CPU只使用直写模式,有些只使用回写模式,还有一些,一级缓存使用直写而二级缓存使用回写。这样做虽然在一级和二级缓存之间产生了不必要的数据流量,但二级缓存和更低级缓存或内存之间依然保留了回写的优势。
6、一致性协议(Coherency protocols)
有两种情况可能导致cache中的数据过期/无效(invalid):
1)DMA等其他外部设备直接更新主存中的数据。
2)SMP,多处理器的情况,同一个cache line存在
多个CPU
各自的
cache中,其中一个CPU对其进行了更新。
如果系统只有一个CPU Core在工作,一切都没问题。如果
有多个核,每个核又都有自己的缓存
,并且发生:某个CPU缓存段中
对应的内存内容被另外一个CPU偷偷改了,会发生什么?
答
:什么都不会发生。这很糟糕,因为如果一个CPU缓存了某块内存,那么在其他CPU修改这块内存的时候,我们希望得到通知。
当我们拥有多组缓存的时候,真的需要它们保持同步,但是系统的内存在各个CPU之间无法做到与生俱来的同步,我们需要一个大家都能遵守的方法来达到同步的目的。
—- 这个问题的根源是我们拥有
多组缓存
,而不是多个CPU核。我们可以让
多个CPU核共用一组缓存
,即只有一块一级缓存,所有
处理器都必须共用它。
在每一个指令周期,只有一个CPU能通过一级缓存做内存操作,运行它的指令。
问题就是
太慢了
,因为处理器的时间都花在排队等待使用一级缓存了(处理器会做大量的这种操作,至少每个读写指令都要做一次)。解决方案是使用多组缓存,但使他们的行为看起来就像只有一组缓存那样。
缓存一致性协议就是为了做到这一点而设计的。使多组缓存的内容保持一致。缓存一致性协议有多种,大多数计算机设备使用的
都属于“
窥探
”即snooping协议。还有一种叫“
基于目录
”的协议,这种协议的延迟较大,但是在拥有多个处理器的系统中,它有更好的可扩展性。“窥探”背后的基本思想是:所有内存传输都发生在一条共享的总线上,而
所有的处理器都能看到这条总线
,缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(arbitrate),同一个指令周期中,只有一个缓存可以读写内存。窥探协议的思想是:缓存不仅仅是在做内存传输的时候才和总线打交道,而是不停的在窥探总线上发生的数据交换,
跟踪其他缓存在做什么
。所以当一个缓存代表它所属的处理器去读写内存时,其他处理器都会得到通知,它们以此来使自己的缓存保持同步。只要某个处理器一写内存,其他处理器马上就知道这块内存在它们自己的缓存中对应的段已经失效。
—- 在直写模式下,这是很直接的,因为写操作一旦发生,它的效果马上会被“公布”出去,即一旦一个缓存发生写内存操作,其他缓存
会进行更新和同步(耗时,低效)。
但是如果混着回写模式,就有问题了。因为有可能在写指令执行过后很久,数据才会被真正回写到物理内存中,在这段时间内,其他处理器的缓存也可能去写同一块内存地址,导致冲突。