操作系统中的存储器构成了一个金字塔,越往上的存储器速度越快,但是价格也越贵,所以也就越小。为了解决高速的处理器和低速的存储器之间的矛盾,上一层的存储器作为下一层存储器的缓存。

在现代的CPU(大多数)上,所有的内存访问都需要通过层层的缓存来进行。CPU的读/写(以及取指令)单元正常情况下甚至都不能直接访问内存——这是物理结构决定的;CPU都没有管脚直接连到内存。相反,CPU和一级缓存(L1 Cache)通讯,而一级缓存才能和内存通讯。大约二十年前,一级缓存可以直接和内存传输数据。如今,更多级别的缓存加入到设计中,一级缓存已经不能直接和内存通讯了,它和二级缓存通讯——而二级缓存才能和内存通讯。或者还可能有三级缓存。你明白这个意思就行。

比如要需要操作内存的某个区域时,处理器不会直接去内存读取,而是会去高速缓存中查看该区域是不是被调进来了,如果没有,则把该区域调入高速缓冲区中。那么接下来处理器直接在高速缓存中进行读写操作。

同样的,对于读取磁盘数据,处理器把内存中的某块区域作为磁盘的缓存。那么便可以直接在内存中进行读写。

处理器在缓存中对数据进行读写操作,但是还是需要把该数据写会到原来的区域中去,这接涉及到了一定的策略。

 

写缓存策略

(1)第一种策略称为不缓存(nowrite),也就是说高速缓存不去缓存任何写操作。当对缓存中的数据进行写时,将直接跳过缓存,直接写到磁盘,同时标记缓存的数据失效。如果后续需要进行读操作,需要重新从磁盘读取数据。

(2)第二种策略称为写透缓存(write-through cache),即写操作将自动更新缓存,同时也更新磁盘文件。这种操作对保持缓存一致性很有好处,所以不需要将缓存标记为失效,同时实现也比较简单。

(3)第三种策略,也是Linux所采用的,称为回写(write-back)。在这种策略下,程序执行写操作直接写到缓存中,但是不会直接更新磁盘,而是将高速缓存中被写入的页面标记成“脏”,并加入脏页链表中。然后由一个进程(回写进程)周期性将脏页链表的页写会到磁盘,从而让磁盘中的数据和缓存中的数据一致,最后清理缓存的“脏”页标志。“脏”的意思不是说数据不干净,而是说数据没有同步到磁盘。

 

缓存一致性策略

现在的的多处理器的计算机,每个CPU都有自己的寄存器和缓存。那么一个多线程的程序就会出现这个问题,线程A更改了缓存A中的数据,但是缓存B中的数据还是原来的数据,那么线程B去缓存B中读取的数据就是错误的数据。这个就是缓存一致性的问题了。(注:问题的产生是因为多缓存引起的)

 

既然问题的产生是因为多缓存引起的,那么为什么不让所有的处理器共享一个缓存呢?那么在一个指令周期内只有一个处理器能够通过一级缓存运行它的指令。这样效率实在是太低了。所以就有了缓存一致性协议。

缓存一致性协议有多种,但是你日常处理的大多数计算机设备使用的都属于“窥探(snooping)”协议,窥探”背后的基本思想是,所有内存传输都发生在一条共享的总线上,而所有的处理器都能看到这条总线:缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(arbitrate):同一个指令周期中,只有一个缓存可以读写内存。窥探协议的思想是,缓存不仅仅在做内存传输的时候才和总线打交道,而是不停地在窥探总线上发生的数据交换,跟踪其他缓存在做什么。所以当一个缓存代表它所属的处理器去读写内存时,其他处理器都会得到通知,它们以此来使自己的缓存保持同步。只要某个处理器一写内存,其他处理器马上就知道这块内存在它们自己的缓存中对应的段已经失效。

在直写模式下,这是很直接的,因为写操作一旦发生,它的效果马上会被“公布”出去。但是如果混着回写模式,就有问题了。因为有可能在写指令执行过后很久,数据才会被真正回写到物理内存中——在这段时间内,其他处理器的缓存也可能会傻乎乎地去写同一块内存地址,导致冲突。在回写模型中,简单把内存写操作的信息广播给其他处理器是不够的,我们需要做的是,在修改本地缓存之前,就要告知其他处理器。搞懂了细节,就找到了处理回写模式这个问题的最简单方案,我们通常叫做MESI协议(译者注:MESI是Modified、Exclusive、Shared、Invalid的首字母缩写,代表四种缓存状态,下面的译文中可能会以单个字母指代相应的状态)。

其中的Exclusive表示独占缓存段,当处理器想写某个缓存段时,如果它没有独占权,它必须先发送一条“我要独占权”的请求给总线,这会通知其他处理器,把它们拥有的同一缓存段的拷贝失效(如果它们有的话)。只有在获得独占权后,处理器才能开始修改数据——并且此时,这个处理器知道,这个缓存段只有一份拷贝,在我自己的缓存里,所以不会有任何冲突。