本文严禁转载,仅供学习使用。参考资料来自中国科学院大学计算机体系结构课程PPT以及《Digital Design and Computer Architecture》、《超标量处理器设计》、同济大学张晨曦教授资料。如有侵权,联系本人修改。
本文衔接上文计算机体系结构----存储系统
写在前面:这里面几个概念一定要区分,不然会很混乱,这里仅仅简要介绍,具体定义看下文:
Flynn分类法:SISD、SIMD、MISD、MIMD。早期很多SIMD的计算机,现在MIMD被作为优选。
现有的MIMD计算机分为两类,每一类代表了一种存储器的结构和互联策略。
第一类机器称为集中式共享存储器结构(Centralized Shared-Memory Architecture)。这类多处理机在目前最多是由几十个处理器构成的。由于处理器个数较少,各处理器可共享一个集中式的物理存储器。因为只有单一的主存,而且这个主存相对于各处理器的关系是对称的,所以这类机器经常被称为对称式共享存储器多处理机(Symmetric sharedmemory Multi Processor,SMP)。这种系统结构也称为 **UMA(Uniform Memory Access)**结构,这是因为从各处理器访问存储器所花的时间相同。SMP 结构是目前最流行的结构,1.2 节将详细讨论这种系统结构。图 10.1 是对称式共享存储器多处理机结构的示意图。图中多个“处理器-Cache”模块共享同一个物理存储器,其连接一般采用一条或多条总线,或者采用交叉开关。(总的来说,对称式共享存储器UMA多处理机就是共享一个存储器)
第二类是分布式存储器多处理机,如图10.2所示。在这类机器中,存储器在物理上是分布的。它支持构建规模较大的多处理机系统。为了支持较多的处理器,存储器必须分布到各个处理器上,不能采用集中式的存储器,否则存储器将不能满足因处理器个数较多而带来的带宽要求。处理器个数较多还要求有高带宽的互连网络。图 10.2 给出了这类多处理机的结构示意图。系统中的每个节点由处理器及其 Cache、存储器、I/O 以及互连网络接口组成。近几年,随着处理器性能的迅速提高和处理器对存储器带宽要求的不断增加,越来越多中小规模的多处理机系统也逐渐开始采用分布式存储器结构。(总的来说,分布式存储器多处理机就是每个CPU都有自己的存储器,这种处理机结构是当今主流)
将存储器分布到各节点有两个优点:
分布式存储器系统结构最主要的缺点是处理器之间的通信较为复杂(因为有了多个分布的存储器),且各处理器之间的访问延迟较大。
通常情况下,I/O 和存储器一样也分布于多处理机的各节点当中。每个节点内还可能包含个数较少(例如 2~8)的处理器,这些处理器之间可采用另一种互连技术(例如总线)相互连接形成簇,这样形成的节点叫做超级节点。由于节点是否为超级节点对机器的基本运行原理没有影响,采用分布式存储器结构的计算机之间的主要差别在于通信方法和分布式存储器的逻辑结构方面,所以我们只讨论每个节点只有一个处理器的情况。1.3 节将对分布式存储器多处理机展开详细讨论。
需要注意的是,由于现代处理机的演变,多级缓存的引入,上面两种处理机的结构图演变成了下面这种。对称式共享存储器多处理机是第二幅图,拥有共享的缓存(Shared cache),分布式存储器多处理机是第一幅图,拥有私有的缓存(Private cache)
如上所述,在分布式存储器多处理机(注意是上文提到的第二种处理机)中,存储器在物理上是分布于各个处理节点中的。但在逻辑地址空间的组织方式以及处理器之间通信的实现方法上,有以下两种方案。第一种方案把物理上分离的所有存储器作为一个统一的共享逻辑空间进行编址,这样任何一个处理器就都可以访问该共享空间中的任何一个单元(如果它具有访问权)了,不同处理器上的同一个物理地址指向的是同一个存储单元。这类计算机被称为分布式共享存储器系统(Distributed Shared-Memory,DSM)。注意,这里的“共享”指的是地址空间上是共享的,并不意味着具有一个集中的存储器。与 UMA 相反,DSM 计算机被称为 NUMA(Non-Uniform Memory Access)计算机,这是因为其访存时间取决于数据在存储器中的存放位置。
==另一种方案是把每个节点中的存储器编址为一个独立的地址空间,不同节点的地址空间之间是相互独立的。也就是说,整个系统的地址空间是由多个独立的地址空间构成的每个节点中的存储器只能由本地的处理器进行访问,远程的处理器不能直接对其进行访问显然,与DSM 不同,不同处理器上的同一个物理地址指向的是不同的存储单元,因为它们指向的是不同的存储器。==在这里,每一个“处理器-存储器”模块实际上是一台单独的计算机。以前,这种计算机系统是由不同的处理节点经专门的互连网络形成的,而现在的这种计算机系统多以机群的形式存在。
对于上述两种地址空间的组织方案,分别有相应的通信机制。对于共享地址空间的计算机系统来说,是采用共享存储器通信机制的。处理器之间的通信是通过用 load 和 store 指令对相同存储器地址进行读/写操作来实现的。而对于采用多个独立地址空间的计算机系统来说数据通信要通过在处理器之间显式地传递消息来完成,这称为消息传递通信机制。
在消息传递多处理机中,处理器之间是通过发送消息来进行通信的,这些消息请求进行某些操作或者传送数据。当某个处理器(设为 A)要对远程存储器上的数据进行访问(或操作)时,它就通过给相应的远程处理器(设为 B)发送一个消息来请求数据(或对该数据进行操作)。在这种情况下,可以把该消息看成一个远程进程调用(Remote Process Call,RPC)当目的处理器 B 接收到消息以后,就代替远程处理器 A 对相应的数据进行访问(或执行相应的操作),然后发送一个应答消息给处理器 A,将结果返回。
如果请求方处理器在发送一个请求消息后,要一直等到收到应答后才能继续运行,则这种消息传递称为是同步的。现有许多计算机的软件系统已对发送和接收消息的具体细节进行了封装,为编程人员提供了有力的支持,使他们能很容易地进行消息通信,包括传送复杂的参数和结果。
不同多处理机所提供的消息传递机制可能差别很大,为了便于程序移植,人们提出了标准的消息传递库(例如 MPI),这为编程人员实现消息传递提供了有力的支持。
分布式共享存储器(即共享存储空间的计算机系统)通信的主要优点是:
(1)与常用的对称式多处理机使用的通信机制兼容。
(2)当处理器之间通信方式复杂或在执行过程中动态变化时,采用共享存储器通信,编程容易,同时在简化编译器设计方面也占有优势。
(3) 采用大家所熟悉的共享存储器模型开发应用程序,而把重点放到解决对性能影响较大的数据访问上。
(4)当通信数据量较小时,通信开销较小,带宽利用较好。
(5) 可以通过采用 Cache 技术来减少远程通信的频度。这是通过对所有数据(包括共享的和私有的)进行 Cache 缓冲来实现的。在后面将看到,Cache 不仅能减少访问共享数据的延迟,而且能减少对共享数据的访问冲突
消息传递通信机制(即采用多个独立地址空间的计算机系统的通信方式)的主要优点是:
(1) 硬件更简单。特别是在与可扩放共享存储器实现方案相比时更是如此
(2) 通信是显式的,因此更容易搞清楚何时发生通信以及通信开销是多少
(3) 显式通信可以让编程者重点注意并行计算的主要通信开销,使之有可能开发出结构更好、性能更高的并行程序。
(4)同步很自然地与发送消息相关联。能减少不当的同步带来错误的可能性。在上述两种通信模式中,可以在任何一种通信机制硬件的基础上建立另外一种通信模式。在共享存储器上实现消息传递非常简单,因为发送一条消息可通过将一部分地址空间的内容复制到另一部分地址空间来实现。而在实现消息传递的硬件上支持共享存储器则困难得多,所有对共享存储器的访问都要依靠操作系统来进行地址转换和存储保护,并将存储器访问转换为消息的发送和接收。此外,load 和 store 一般只访问少量的数据,这种用软件方法实现的共享存储器开销很大,以至于到无法接收的程度,所以其应用范围是非常有限的。
多级 Cache 可以降低处理器对存储器带宽的要求。如果每个处理器对存储器带宽的要求都降低了,那么多个处理器就可以共享一个存储器。自20世纪80年代以来,随着微处理器逐渐成为主流,人们设计出了许多通过总线共享一个单独物理存储器的小规模多处理机。由于大容量 Cache 很大程度地降低了对总线带宽的要求,当处理机规模较小时,这种计算机十分经济。以往的这种计算机一般是将 CPU 和 Cache 做在一块板上,然后插人底板总线。后来,每块板上的处理器数目达到了 4 个,而近些年,则能在一个单独的芯片上实现 2~8 个处理器核。
对称式共享存储器系统结构一般都支持对共享数据和私有数据的 Cache 缓存。私有数据是指只供一个处理器使用的数据,而共享数据则是指供多个处理器共同使用的数据。处理器之间可以通过读/写共享数据来实现通信。
私有数据进入 Cache,使得处理器对它们的访问可以在 Cache 中完成,从而减少平均访存时间和减少对存储器带宽的要求。当允许共享数据进入 Cache 时,共享数据可能会在多个 Cache 中被复制,这样相应的处理器就可以在自己的 Cache 中找到这些数据。这样做不仅可以减少访存时间和对存储器带宽的要求,而且还可以减少多个处理器同时读取共享数据所产生的冲突。不过,共享数据进人 Cache 也带来了一个新的问题,即 Cache 的一致性问题。
请注意,对称式共享存储器的系统结构如下图所示
如果允许共享数据进入 Cache,就可能出现多个处理器的 Cache 中都有同一存储块的副本的情况,当其中某个处理器对其 Cache 中的数据进行修改后,就会使得其 Cache 中的数据与其他 Cache 中的数据不一致。这就是多处理机的 Cache 一致性(Cache coherence)问题。
图 10.3 通过一个例子来说明这个问题。假设初始状况是 CPU A 和 CPU B的 Cache中都有存储单元X的副本,其值都是 m,如图 10.3(a)所示。并假设这两个 Cache 都采用写直达法。当CPU A对X 进行写入后,Cache A 和存储器中相应单元的值都变成了 p,但Cache B 中的值仍为 m。如果 CPU B 读取 X,则它得到的仍是旧值 m。
对于一致性,我们可以有这样的说法,如果对某个数据项的任何读操作均可得到其最新写入的值,则认为这个存储系统是一致的。这个定义尽管很直观,但却不够清楚和全面现实中的情况要复杂得多。这个简单的定义包括了存储系统行为的两个不同方面: 第一个方面是指读操作得到的是什么值(what),第二个方面是指什么时候读操作才能得到新写入的值(when)。
如果一个存储器满足以下三点,则称该存储器是一致的。
(1) 处理器 P在对存储单元 X 进行一次写之后又对 X 进行读,在这读和写之间没有其他处理器对 X 进行写,则 P 读到的值总是刚写进去的值。(自己改写后读到的值是新值)
(2) 处理器 P 对存储单元 X 进行写之后,另一处理器 Q 对X 进行读,在这读和写之间没有其他对 X 的写.则 Q 读到的值应为 P 写进去的值。(别人改写后读到的也是新值)
(3) 对同一存储单元的写是串行化的。即任意两个处理器对同一存储单元的两次写从各个处理器的角度来看顺序都是相同的。例如,对同一地址先写 1,再写 2,则任何处理器都不会先读到 2,然后再读到 1。(所有人看到的改写顺序都一致)
从上面三点可以看出,一致性可以简单概括成:无论哪个处理器修改了值,所有处理器看到的值应都是修改后的值,另外就是修改值的顺序对所有处理器来说都是一样的顺序。
第一条属性保证了程序顺序,即使在单处理机中也要求如此。第二条属性给出了存储器一致性的概念。如果一个处理器不断地读取到旧的数据,就可以肯定地说这个存储器是不一致的。
(插入注释:
存储一致性(Consistency)是指:不同处理器发出的所有存储器操作的顺序问题(即针对不同存储单元或相同存储单元)
Cache一致性(Coherence)是指:不同处理器访问相同存储单元时的访问顺序问题,访问每个Cache块的局部序问题)
写操作的串行化难理解一些,但也同样重要。假设处理器 P1 对存储单元 X 进行一次写,接着处理器 P2 对X 也进行一次写,如果不保证写操作串行化,就可能出现这样的情况某个处理器先看到 P2 写的值而后看到 P1 写的值。解决这个问题最简单的方法是把写操作串行化,使得对同一存储器单元所进行的写操作顺序在所有处理器看来都是相同的,这种属性称为写串行化(Write Serialization)。
尽管上面三条已充分地保证了一致性,但什么时候才能获得写进去的值仍是一个重要的问题。通常不可能要求在一个处理器对 X 写后马上就能在另外的处理器上读出这一值.因为此时写入的值有可能在这一时刻还没离开进行写的处理器,所以总是有延迟的。为了简化起见,在后面的讨论中作以下假设:①直到所有的处理器均看到了写的结果,这个写操作才算完成:②处理器的任何访存均不能改变写的顺序。就是说,允许处理器对读进行重排序。但必须以程序规定的顺序进行写。
在支持 Cache 一致性的多处理机中,Cache 实现了共享数据的迁移(migration)和复制(replication)功能。共享数据的迁移是把远程的共享数据拷贝一份,迁入本地 Cache 供本处理器使用,从而减少对远程共享数据的访问延迟,也减少了对共享存储器带宽的要求。共享数据的复制则是把多个处理器需要同时读取的共享数据在这些处理器的本地 Cache 中各存放一个副本。复制不仅减少了访问共享数据的延迟,而且还减少了访问共享数据所产生的冲突。共享数据的迁移和复制对于提高访问共享数据的性能来说是非常重要的。一般情况下,小规模多处理机是采用硬件的方法来实现 Cache 的一致性的。
在多个处理器中用来维护一致性的协议称为 Cache 一致性协议(Cache coherent protocol)。实现 Cache 一致性协议的关键是跟踪共享数据块的状态。目前有两类协议,它们采用了不同技术来跟踪共享数据的状态。
本节后面着重讨论监听式协议。目录式协议将在 1.3 节中详细论述。
在使用多个微处理器且每个 Cache 都与单一共享存储器相连组成的多处理机中,一般都采用监听协议,因为这种协议可直接利用已有的物理连接(连接到存储器的总线)。
可以采用两种方法来解决上述的 Cache 一致性问题。一种方法是保证在处理器对某个数据项进行写人之前,它拥有对该数据项的唯一访问权。做法是在处理器(设为 P)进行写入操作之前,把所有其他 Cache 中的副本全部作废,这称为写作废协议(Write Invalidate)。它是目前最常用的协议,无论是采用监听协议还是采用目录协议都是如此。唯一的访问权保证了在进行写入操作时其他处理器上不存在任何副本。如果其他处理器接着要访问该数据,就会产生不命中,从而从存储器取出新的数据副本(写直达法),或者从P的 Cache 中获得新的数据(写回法)。
要保证进行写的处理器具有唯一的访问权,就必须禁止其他处理器和它同时进行写操作。但如果两个处理器要同时进行写操作,该如何处理?这可以通过竞争来解决,它们中只有一个会在竞争中获胜----获得访问权,而另一个处理器中的副本以及其他处理器中的副本(如果有的话)就会作废。竞争失败的处理器要完成写操作,就必须先获得一份新的数据副本。该副本已经包含了更新后的数据。显然,这种协议保证了写操作的串行化。(就是说如果有两个处理器要写,那么竞争失败的处理器等竞争成功的处理器先写,拿着写完的新数据再去做操作)
下面通过一个例子来看看写作废协议如何保持数据一致性,如图 10.4 所示。初始状态是:CPU A,CPU B、CPU C都有X的副本。在 CPU A 要对X 进行写入时,需先作废 CPUB和CPU C 中的副本,然后再将 p 写入 Cache A 的副本中,同时用该数据更新主存单元 X.
另外一种协议是写更新协议(Write Update)。在这种协议中,当一个处理器对某数据项进行写入时,它把该新数据广播给其他所有 Cache。这些 Cache 用该新数据对其中的副本(如果有的话)进行更新。当然,如果知道其他 Cache 中都没有相应的副本,就不必进行广播和更新。这样处理能够减少实现该协议所需的带宽。
图 10.5 给出了写更新协议操作过程的一个例子。这里假设三个 Cache 都有 X 的副本。当CPU A将数据写入 Cache A 中的副本时,将广播(在这个例子中是通过总线)给所有的 Cache 的,这些 Cache 用力更新其中的副本。显然,此后 CPU A和CPU B读取X时,都将在其 Cache 中命中,得到最新的值。由于这里采用写直达法,所以 CPU A 还要将力写入存储器中的 X。如果采用写回法,则不需要写人存储器。
在这两种协议中,写作废协议的应用比较广泛。大多数的计算机都采用写作废协议写更新和写作废协议在性能上的差别主要来自以下三个方面:
(1) 在对同一个数据进行多次写操作而中间无读操作的情况下,写更新协议需进行多次写广播操作.而写作废协议只需一次作废操作。
(2) 在对同一 Cache 块的多个字进行写操作的情况下,写更新协议对于每一个写操作都要进行一次广播,而写作废协议仅在对该块的第一次写时进行作废操作即可。写作废是针对 Cache 块进行操作的。而写更新则是针对字(或字节)进行的。
(3) 考虑从一个处理器 A 进行写操作后到另一个处理器 B 能读到该写入数据之间的延迟时间。在写更新协议中,这个延迟时间比较小,因为它在进行写操作时,立即更新了所有其他 Cache 中的副本,包括 Cache B 中的副本(假设有此副本)。而在写作废协议中,由于在处理器 A 进行写操作时已经作废了 Cache B 中的副本,所以当处理器 B 进行读操作时需要等待,直到新的副本被调入 Cache。
在基于总线的多处理机中,总线和存储器带宽是最紧缺的资源,而写作废协议所耗费的总线和存储器带宽比较少,因此写作废协议成为绝大多数多处理机系统的选择。当然,在设计处理器个数不多(2~4)的多处理机时,处理器之间紧密耦合,写更新法所要求的带宽还可以接受。尽管如此,考虑到处理器性能不断提高的趋势以及相关带宽需求的增长,写更新模式很少被采用,因此本篇博文的剩余部分只关注写作废协议(这就意味着远程Cache要拿自己没有的块必须从存储器获取,而不能从其他Cache直接复制)。
监听协议适合多个处理器通过总线相连的集中式共享存储系统,但是监听协议可扩展性有限,总线是一种独占性资源,延迟随处理器数目的增加而增加。同时我们要知道,每个cache都在监听,如下图所示。
实现监听协议的关键有三个方面:
(1) 处理器之间通过一个可以实现广播的互连机制相连,通常采用的是总线。(处理器之间通过总线广播消息)
(2) 当一个处理器的 Cache 响应本地 CPU 的访问时,如果它涉及全局操作,例如需要访问共享的存储器或需要其他处理器中的 Cache 进行相应的操作(例如作废等),其 Cache控制器就要在获得总线的控制权后,在总线上发出相应的消息(如图 10.4 中的 Cache A所示)。(处理器涉及全局操作时,要获得总线控制权才能发消息)
(3) 所有处理器都一直在监听总线,它们检测总线上的地址在它们的 Cache 中是否有副本。若有,则响应该消息,并进行相应的操作(如图 10.4 中的 Cache B 所示)。
获取总线控制权的顺序性保证了写操作的串行化,因为当两个处理器要同时对同一数据块进行写操作时,必然是只有其中一个处理器先获得总线控制权,并作废所有其他处理器上的相关副本。(本博文默认写作废协议)另一处理器要等待前一个处理器的写操作完成后,再排队竞争总线控制权这保证了写操作严格地按顺序进行。所有的一致性协议都要采用某种方法来保证对同一个Cache 块的写访问的串行化。
虽然不同的监听协议在具体实现上有些差别,但在许多方面是相同的。Cache 发送到总线上的消息主要有以下两种:
RdMiss 和 WtMiss 分别表示本地 CPU 对 Cache 进行读访问和写访问时不命中,这时都需要通过总线找到相应数据块的最新副本,然后调入本地 Cache 中。尽管这个副本不一定在存储器中,但为了尽快获得这个副本,一般是马上启动对存储器相关块的访问。对于写直达 Cache 来说,由于所有写入的数据都同时被写回存储器,所以其最新值总可以从存储器中找到。而对于写回法 Cache 来说,难度就大一些了,因为这个最新副本有可能是在其他某个处理器的 Cache 中(尚未写回存储器)。在这种情况下,将由该 Cache 向请求方处理器提供该块,并终止由 RdMiss 或 WtMiss 所引发的对存储器的访问。当然,RdMiss 和 WtMiss还将使得相关 Cache 块的状态发生改变。(上面这一段对写不命中和读不命中进行了详细介绍,应当细品,下面我简单概括下这段说的啥,假如自己的Cache读或写未命中,应该立马向存储器(写直达)或其他Cache(写回法)请求新的值,得到值后自己的Cache终止访问其他存储器或Cache,同时改变新拿到的块的状态)
有的监听协议还增设了一条 Invalidate 消息,用来通知其他各处理器作废其 Cache 中相应的副本。Invalidate 和 WtMiss 的区别在于 Invalidate 不引起调块。
我之前写过的博文中介绍过,单 Cache 情况下写回法 Cache 对于每一个 Cache 块都设置了一个修改位,用于记录该块是否被修改过。在采用写回法的多 Cache 中,我们可以直接利用这个标志位来实现一致性。将一个数据进行写入时,只写入 Cache(如果不命中,就要先从存储器调块),而不直接写回存储器。这时这个块的修改位被置位,表示该块中保存的是整个系统中唯一的最新副本,存储器中的副本是过时了的,而且所有其他 Cache 中也没有其副本。每个处理器(实际上是 Cache 控制器)都监听其他处理器放到总线上的地址,如果某个处理器发现它拥有被请求数据块的一个最新副本,它就把这个数据块送给发出请求的处理器。与写直达法相比。尽管写回法在实现的复杂度上有所增加。但由于写回法 Cache 所需的存储器带宽较低,它在多处理机实现上仍很受欢迎。在后面的讨论中,只考虑写回法 Cache。(从本文开头到现在,我们约束了两个条件,一个是使用写作废协议,一个是使用写回法)
Cache 本来就有的**标识(tag)**可直接用来实现监听。通过把总线上的地址和 Cache 内的标识进行比较,就能找到相应的 Cache 块(如果有的话),然后对其进行相应的处理。每个块的有效位使得我们能很容易地实现作废机制。当要作废一个块时,只需将其有效位置为无效即可。对于 CPU 读不命中的情况,处理比较简单,Cache 控制器向总线发 RdMiss 消息,并启动从主存的读块操作,准备调入 Cache。当然,如果存储器中的块不是最新的,最新的副本是在某个 Cache 中,就要由该 Cache 提供数据,并终止对存储器的访问。
对于写操作来说,希望能够知道其他处理器中是否有该写入数据的副本,因为如果没有,就不用把这个写操作放到总线上,从而减少所需要的带宽以及这个写操作所花的时间这可以通过给每个 Cache 块增设一个共享位来实现。该共享位用来表示该块是被多个处理器所共享(共享位为“1”),还是仅被某个处理器所独占(共享位为“0”)。拥有该数据块的唯一副本的处理器通常被称为该块的拥有者(Owner)。(写操作时其他处理器没副本就不要把写操作放到总线,通过共享位来判断这个块有没有被共享)
当一个块处于独占状态时,其他处理器中没有该块的副本,因此不必向总线发Invalidate 消息。否则就是处于共享状态,这时要向总线发 Invalidate 消息,作废所有其他Cache 中的副本,同时将本地 Cache 中该块的共享标志位置零。如果后面又有另一处理器再读这个块,则其状态将再次转化为共享。由于每个 Cache 都在监听总线上的消息,所以它们知道什么时候另一个处理器请求访问该块,从而把其状态改为共享。
每一次总线操作都要检查 Cache 中的地址标识,这会打扰处理器对 Cache 的访问。必须设法减少这种打扰。一种方法是设置两套标识,分别用于处理来自 CPU 的访问和来自总线的访问。当然,Cache 不命中时,处理器要对两套标识进行操作。类似地,如果监听到了一个相匹配的地址,也要对两套 Cache 的标识进行操作。
在多级 Cache 中,还可以采用另一种方法,即把监听的操作请求交给第二级 Cache 来处理。由于处理器只在第一级 Cache 不命中时才会访问第二级 Cache,而第一级 Cache 的命中率往往都很高,所以这种方法是可行的。不过,当监听机制在第二级 Cache 中发现相匹配的项目时,就会与处理器争用第一级 Cache。在监听机制获得使用权后,要修改状态且可能需要访问第一级 Cache 中的数据。此外,这两级 Cache 必须满足包容关系,即第一级 Cache中的内容是第二级 Cache 中内容的一个子集。
实现监听协议通常是在每个节点内嵌入一个有限状态控制器。该控制器根据来自处理器或总线的请求以及 Cache 块的状态,做出相应的响应,包括改变所选择的 Cache 块的状态,通过总线访问存储器,或者作废 Cache 块等。
根据前面我写的博文,我们知道,在单 Cache 中,每次 CPU 进行读操作时,Cache 最后都要把所访问的数据送给 CPU。而在每次进行写操作时,最后 CPU 都要把数据写入 Cache。在多Cache 中也是如此。在后面的讨论中,为简洁起见,我们省略这些操作,而把重点放在实现一致性的操作上。
下面要介绍的监听协议实例比较简单。每个数据块的状态(不是每个Cache的状态)只能取以下三种状态中的一种。
(1) 无效(Invalid,I):表示 Cache 中该块的内容为无效。显然,所要访问的块尚未进入Cache。
(2) 共享(Shared,S): 表示该块可能处于共享状态,即在多个(>2)处理器中都有副本。这些副本都相同,且与存储器中相应的块相同。之所以说可能,是因为它包含了这种特殊情况:在整个系统中,该块只在一个 Cache 中有副本,而且该副本与存储器中相应的块相同(这种情况发生在块本来就处于共享状态,读未命中后从主存中调入块到Cache中的时候)。对处于共享状态的块只能进行读操作。如果要进行写操作,就要先把其状态改为“已修改”。
(3) 已修改(Modified,M):表示该块已经被修改过,并且还没写入存储器。这时该块中的内容是最新的,而且是整个系统中唯一的最新副本。处于已修改状态的块由本地处理器独占。该处理器不仅可以对它进行读操作,而且可以对它进行写操作。
下面来讨论在各种情况下MSI监听协议所进行的操作。
1) Cache块响应来自处理器的请求
对不发生替换和发生替换的两种情况分别进行讨论
当CPU 要进行写访问时,由于所要访问的块尚未调入 Cache,所以发生写不命中,需要向总线发 WtMiss 消息。调入该块后,将其状态改为已修改(M)。这时该数据块在 Cache中有唯一的一个副本(最新),且该副本与存储器中的相应内容不同。存储器中的内容已过时。
状态为S
当 CPU 要进行读访问时,如果命中,则状态不变。否则就需要进行替换,这种情况后面再讨论。
当 CPU 要进行写访问时,需先把 Cache 中相应块的状态改为已修改(M),然后再把数据写人,同时要作废所有其他 Cache 中的副本。在命中的情况下,无需调块,只要向总线发Invalidate 消息即可。如果不命中,就需要进行替换。这种情况后面再讨论。
状态为M
在这种状态下,当 Cache 读命中或写命中时,状态不变。但当不命中时,就需要进行替换,这种情况后面再讨论。
2) Cache块响应来自总线的请求
每个处理器都在监视总线上的消息和地址,当发现有与总线上的地址相匹配的 Cache块时,就要根据该块的状态以及总线上的消息,进行相应的处理,见图 10.7。
状态为S
这种状态表示该块是一个只读副本。当远程处理器(相对于本地处理器而言)因进行读访问不命中而在总线上发 RdMiss 时,由于调块后不对该块进行写操作,所以本地 Cache 中该块的状态不变。但如果远程节点是因为要进行写操作而往总线上发 WtMiss 或Invalidate 消息的,则需要作废本地 Cache 中的该块,将其状态改为 I
状态为M
这种状态表示该块是整个系统中唯一的最新副本。不管远程处理器发的是 RdMiss 还是 WtMiss,本 Cache 都需要将这个唯一的副本写回存储器,并终止 RdMiss 或 WtMiss引发的对存储器的访问,改由本 Cache 提供该块。在状态方面,RdMiss 将导致本地 Cache 中该块的状态变为 S,即该块也变成一个只读的共享块;而 WtMiss 则将其状态修改为无效状态I ,将之作废。这是因为远程处理器需要的是一个独占的块。
将Cache响应总线和处理器的响应汇总的状态转移图汇总如下。下图中实线表示来自处理器的请求,虚线表示来自总线的请求。斜杠左边表示Cache接受的请求,斜杠右边表示Cache发出的请求。
注意以下示例都才用写回法(Write Back)
目录协议适用于分布式共享存储系统。本节专注于讨论目录协议
在前面介绍的监听协议中,每当 Cache 不命中时,就要与所有其他的 Cache 进行通信,这是通过总线的广播以及其他 Cache 的监听来实现的。之所以要广播和监听,是因为系统中没有一个集中的数据结构来记录 Cache 的状态,数据块的状态是保存在各自的 Cache 中的。所以当一个 Cache 发生不命中时,它不知道其他 Cache 中哪些拥有相应的副本,就只好在总线上广播相应的信息,让所有其他的 Cache 都来协助完成相关的操作。这种协议直接利用了系统中已经存在的总线和 Cache 中的状态位。因而它具有实现容易、成本较低的优点。然而,当系统的规模变大时,它又是个致命的弱点。大量的总线广播操作会使得总线很快就成为系统的瓶颈。广播和监听的机制使得监听一致性协议的可扩放性很差。
为了实现较大规模的可扩放的共享存储器多处理机系统,需要寻找新的一致性协议来代替监听协议,这就是前面提过的目录协议。另外,总线的可扩放性不好,可以改用可扩放性更好的互连网络。互连网络能很高效地实现点到点的通信。目录协议采用了一个集中的数据结构–目录。对于存储器中的每一个可以调入 Cache 的数据块,在目录中设置了一条目录项,用于记录该块的状态以及哪些 Cache 中有副本等相关信息。这样,对于任何一个数据块,都可以快速地在唯一的一个位置(根据该存储块的地址来确定)中找到相关的信息这使得目录协议避免了广播操作。
目录法常采用位向量的方法来记录哪些 Cache 中有副本,该位向量中的每一位对应于一个处理器。例如可以用“1”表示相应的处理器的 Cache 有副本,用“0”表示没有副本。这个位向量的长度与处理器的个数成正比。为便于讨论,后面我们将把由位向量指定的处理机的集合称为共享集 S。如下图所示,这就是一个简单的相量法记录方式。
目录协议根据该项目中的信息以及当前要进行的访问操作,依次对相应的 Cache 发送控制消息,并完成对目录项信息的修改。此外,还要向请求处理器发送响应信息。为了提高可扩放性,可以把存储器及相应的目录信息分布到各节点中,如图 10.8 所示每个节点的目录中的信息是对应于该节点存储器中的数据块的。这使得对于不同目录项的访问可以在不同的节点中并行进行。当处理器进行访存操作时,如果该地址落在本地存储器的地址范围中,就是本地的,否则就是远程的,这是由节点内的控制器根据访问地址来判定的。对于图10.8我们还要注意,每个节点都有一个目录结构。
对于目录法来说,最简单的实现方案是对于存储器中的每一块都在目录中设置一项。在这种情况下,目录中的信息量与 M X N 成正比(M X N表示每个处理器都要有所有存储器块目录,一个处理器有所有存储器M个块的项,N个处理器就是M X N个项)。其中 M 表示存储器中存储块的总数量,N 表示处理器的个数。由于 M=K X N,K 是每个处理机中存储块的数量,所以如果 K 保持不变,则目录中的信息量就与
N
2
N^2
N2成正比。显然这种方法的可扩放性不好,只有在处理器个数比较少的情况下才是可行的。
当处理器数量较多时,需要采用扩放性更好的方法,例如只给那些已经进入 Cache 的块(而不是所有的块)设置目录项,或者让每个目录项的位数固定。后面将进一步介绍这些方法。在目录协议中,存储块(目录)的状态有以下三种:
(1)未缓冲(UnCached)一-该块尚未被调入 Cache。所有处理器的 Cache 中都没有这个块的副本。
(2) 共享(Shared)—该块在一个或多个处理机上有这个块的副本,且这些副本与存储器中的该块相同
(3) 独占(Exclusive)—仅有一个处理机有这个块的副本,且该处理机已经对其进行了写操作,所以其内容是最新的,而存储器中该块的数据已过时。这个处理机称为该块的拥有者(Owner)
为了提高实现效率,在每个 Cache 中还跟踪记录每个 Cache 块的状态,如下图所示。下图Cache的state可以包含本文讲的三种目录状态标记,还可以包含Cache的有效位(Valid bit),方便在CPU调用Cache时进行cache hit的比对,脏位(dirty bit),在Cache写回法中使用。
可以发现,在目录协议中,每个 Cache 中的 Cache 块的状态及其转换与前面监听协议的情况相同。只是在状态转换时所进行的操作有些不同。
下面我们来介绍几种Cache state 和Directory state
由于处理器访问启动的Cache状态转移(注意这里是本地处理器访问,不是总线请求,总线请求可以看成外地处理器访问)
注意斜杠左边是代表处理器给cache的请求,斜杠右边表示这个cache给总线发的请求。比如PrWr/ExReq表示处理器想对Cache进行写操作,Cache由I状态变成M状态,同时向总线发出ExReq独占请求。
由于目录请求启动的状态转移
由于数据置换(evictions)启动的状态转移
由于数据请求启动的状态转移
由于写回启动的状态转移
注意,下面的示例采用的是写回法(Write back)。
上图表示,Core0要读Load 0xA处的数据,Cache和Directory state发生的变化、状态转移图及响应。注意虽然三个core中只有core0有0xA的数据,但是core0不是独占状态,而是共享状态,因为core0并没有写这个地址的数据。
上图表示,Core2要读Load 0xA处的数据,Cache和Directory state发生的变化、状态转移图及响应。
上图左上角MSI图表示core1的,右下角MSI图表示core0和core2的。左下角表示目录状态转移图。
本文1.2和1.3节讲了基于目录和监听的MSI协议,它们存在下面这个问题:如果缓存块一开始就没有缓存数据 ,在读取时,块会立即进入“共享”状态,尽管它可能是唯一要缓存的副本(即,没有其他处理器会缓存它) ,假设读取块的缓存想要在某个时候写入它,它需要广播“无效”,即使它有唯一的缓存副本!如果缓存知道它有系统中唯一的缓存副本,它可以在不通知任何其他缓存的情况下写入块,避免不必要的失效广播。因此我们在MSI协议中引入E状态:独占,没有其他处理器缓存了该数据备份,可以直接修改,不必马上同步到主存中。
MESI协议状态转移图如下:
该协议可以通过更多的状态和预测机制进行优化,以减少不必要的失效和缓存块转移,但是,需要更多的状态和优化 ,更难设计和验证(导致更多案例需要处理,竞争条件)。
Cache一致性是在块级别实现,一个Cache块中包含多个数据字,当多个处理器并发访问同一数据块的不同数据字时,假设处理器P1写字i,处理器P2写字k,且两个字有相同的块地址,由于地址在同一块中,该块可能会产生多次不必要的失效( ping-pong问题),因为每次写这个块中的不同的两个字就要进行独占等操作。
解决方案:原子操作(但是原子操作代价高、耗费大,严重影响并行计算的性能)
用于实现原子操作的一种机制
性能VS原子操作
假设目录一致性协议,一个向量编码(vector)目录负责 block address 0xA和 0xB,3 个处理器核:core 0,core 1 和 core 2,根据以下的内存操作请求,详细描述目录的状态变化过程。假设:初始时,三个 core 的私有 cache 都没有缓存以上两个 block,且 cache 的容量大于 2 blocks
core 1 store 0xB
core 0 load 0xA
core 2 load 0xA
core 1 load 0xB
core 1 store 0xB
core 0 store 0xB
core 2 store 0xB
core 1 store 0xA
core 2 load 0xA
core 1 load 0xB
解:
考虑一个对称的共享内存多处理器系统(4个处理器共享一个总线),实现类似Snooping(MSI)缓存一致性协议。对于以下的每个事件,请解释一致性协议的步骤(缓存是否命中/未命中,总线上发送了什么请求,谁会响应,是否需要写回等),并提及每个处理器缓存中数据块的最终状态。假设在序列开始时,X 不在任何缓存中。已经给出第一步,请参考第一步完成以下表格。 P 表示 Processor,C 表示 Cache。
解:
假设有 4 个处理器通过点对点互连相连,并使用基于目录的缓存一致性协议实现分布式共享内存。对于以下指令序列,对于每条指令,列出必须发送到网络的消息以及缓存和目录中线的状态。假设消息可以包括一些控制信息以及地址和缓存行。假设在序列开始时,X 不在任何缓存中,P 表示 Processor,C 表示 Cache。
解: