Chapter4 MAINTAINING COHERENCY IN CACHED SYSTEMS

经常引用的一项统计数据表明,夫妻间最常见的争吵原因是金钱问题。为什么这样的统计数据会出现在一本关于高速缓存设计的书中呢?因为某些金钱争论和缓存一致性问题之间存在着深刻而又务实的相似之处。
让我们假设大多数金钱争议都以类似以下例子中的一种开始:
“'你为什么不在支票登记册上记录当你从自动提款机取钱时的支出?现在我们反弹了三张支票!”
“你在Visa账户上刷了900美元,却忘了告诉我?难怪在餐厅里当着我老板的面我的信用被拒绝了!”
“那是我们准备去夏威夷的钱!”
“那100美元现金到底发生了什么事情?”
这些例子展示了金钱的管理和沟通问题,而同样的问题也存在于缓存一致性的议题中。无论是金钱还是缓存,都需要在不同的操作之间保持一致性和准确性,否则就会导致混乱和冲突。
在所有这些情况中,都涉及到由婚姻中的双方共同拥有的金钱(或信用),而其中一方在未告知另一方的情况下进行了支出。如果我们从夫妻转移到计算机,并将共同财产转换为内存空间,我们可以看到,如果两个不同的设备在没有相互通知的情况下操作相同的数据,可能会发生潜在的灾难。夫妻中的每个成员都认为自己对银行账户中的内容或信用卡的已消费金额有一个心理画面,就像缓存内存应该包含主存储器中相应内容的准确副本一样。当夫妻中的一方修改主存储器中的内容时,必须通知另一方。同样地,当系统总线上的任何设备更新主存储器或稍后将被复制到主存储器的缓存位置时,它必须保证没有其他设备会对主存储器中数据的新鲜度产生误解。
这些问题已经在许多软件数据库中得到了解决。想象一下订购航班座位的问题。联合航空公司拥有一个数据库,仅在美国的三个办事处,大约有2100名电话订票代理人可以同时访问该数据库。同样,美国的数百名旅行社也可以在线预订航班座位。在最糟糕的情况下,从纽约飞往芝加哥的航班可能只剩下一个座位,在同一时间内,每个使用系统的人都试图将该座位出售给客户。如果问题没有得到解决,可能会有大约3000人被分配到同一个飞机上的同一个座位!
缓存一致性是确保多个缓存系统中缓存内存的内容与主存储器的内容要么相同,要么受到严格控制,以便不会混淆过期和当前数据的问题。就像任何其他缓存术语一样,与一致性这个词相比,还有很少使用的替代词,即一致性和实时性。过期数据这个术语用来描述那些不再反映所代表的内存位置的当前值的数据位置。当我输入这本书时,新版本的本章节存储在计算机的主存储器中,而旧版本则存储在计算机的硬盘上。下次我保存文件时,磁盘和主存储器将包含相同位置的相同数据,并且在我再次开始输入之前它们将是一致的。
4.1 SINGLE-PROCESSOR SYSTEMS
初看起来,大多数设计师会认为一致性只是多处理器系统中的问题,或者可能是具有写回缓存的系统中的问题。似乎在单个CPU系统中使用写通方式的缓存永远不会有一致性问题。但这是不正确的,因为存在着处理器无法控制的活动,换句话说,就是输入和输出活动。最简单的例子是内存映射的轮询式I/O设备,处理器会不断读取这个设备以确定一个位的状态。我们在2.2.7节中使用了相同的例子来说明非缓存区域的使用。如果缓存中包含内存映射的I/O位置的副本,并且CPU引用的是缓存中的副本而不是I/O设备,那么处理器将永远无法看到I/O位状态的任何变化,因为它将读取缓存中陈旧或不一致的值,而不是从内存映射的I/O位置中输入的真实值。
通过将I/O位置映射到不可缓存的地址,可以很容易地解决这个问题。当另一个总线主设备可以在没有CPU干预的情况下向主存储器写入时,问题就变得棘手了。主设备是指可以命令主存储器执行读写周期的任何设备,而不需要CPU为该设备执行这些周期。在单处理器系统中,最典型的例子是像磁盘控制器或视频接口这样的DMA设备。
以磁盘控制器为例,想象一下,当整个缓存已经存满了主存储器地址的副本时,CPU启动了一个DMA传输,将程序的一部分从硬盘传输到主存储器。其中一些缓存位置无疑是将被传入的DMA数据覆盖的主存储器位置的副本。如果这些缓存位置与主存储器的内容不同步,新提取的代码将在所有未缓存的地址上执行,并夹杂旧程序的缓存副本。更糟糕的是,在写回缓存中,从主存储器到DMA设备的输出也可能会引发问题,因为CPU认为它发送给DMA输出设备的数据是最新的,但如果某个数据块的一部分仍然存在于缓存中,并且是"脏"状态,那么某些陈旧的数据将最终进入永久存储器中。这两个问题将在本节的后续部分进行讨论。
也许这是一个区分缓存策略和缓存/总线协议的好地方。"协议"是缓存架构师用来表达缓存、处理器、主存储器和另一个总线主设备之间通信方式的术语。缓存策略确定了缓存与CPU和软件的交互方式,并设置了缓存的命中率。协议是系统中所有子系统保证一致性并避免总线冲突的手段。在本章中,所有的一致性机制都必须围绕缓存策略和总线协议进行设计,或者总线协议必须围绕缓存策略和一致性机制进行设计。
4.1.1 DMA Activity and Stale Cache Data
在DMA传输过程中,当前总线主设备(DMA设备)可以写入可能也存在于缓存中的主存储器位置。缓存控制器必须能够确保缓存内存的内容与复制的主存储器位置保持一致,而不是包含之前主存储器的副本。最简单的方法称为缓存清除,每次进行DMA写入周期时,就会使整个目录无效。完成这一操作的三种最常见方法如下:1)使用特殊的无效化硬件,在每个缓存行的有效位中写入无效状态;2)使用具有硬件复位功能的特殊缓存标签RAM执行相同操作;3)重置主要的有效标志,该标志将缓存标签比较器的输出传递给CPU(假设设计中没有使用有效位)。关于这种方法,唯一可以说的好处是它很有效且易于实现,只要缓存是简单的写通实现即可。但最糟糕的是,它需要在每次DMA写入周期后重新填充缓存。在某些情况下,这并不那么糟糕。在运行DOS的PC中,CPU在DMA块传输期间总是停止工作,因此在几个后续的缓存行填充周期中额外的延迟并不明显。而在使用类似UNIX的多任务操作系统的系统中,刷新操作可能会造成灾难性后果,因为当第一个任务需要DMA活动时,操作系统会尝试将另一个任务调度出来。理想情况下,另一个任务应该在缓存中执行,以使DMA活动和处理器活动彼此不干扰。
另一种更高效的确保DMA传输一致性的方法是提供硬件,监视所有系统总线周期并检查其地址,以便在其中一个地址影响到缓存时警告缓存。
总线观察或窥探机制是一种简单的方法,可确保任何与主内存更新或清除相关的系统总线周期更新或清除相应的缓存位置。在主内存写入周期中,如果被寻址位置在缓存中有副本,或者在一些设计中,如果该位置仅可能被包含在缓存中,它将被覆盖或无效,具体取决于设计。用于处理窥探写击时失效线的术语是反向失效,这意味着失效正在发生的方向与缓存更新的正常方向不同。大多数设计人员称其为缓存观察系统总线,并通常使用窥探术语,而其他人则认为系统总线正在查看缓存,称该过程为询问或说系统总线询问缓存的内容。另一个不那么广泛使用的术语是交叉询问,听起来像律师描述当前总线主控寻找其他缓存中可能存在的一致性问题的方式。查询和询问术语的问题在于它们暗示请求设备在查看主内存之前首先从任何现有缓存中请求数据。在真实的设计中,缓存通常观察总线,并在可能出现一致性问题时采取行动。就像里面有人在外面看,而另一个人在外面看里面。在一个回写缓存中,窥探逻辑还必须注意从缓存中的脏单词而不是主内存中的脏单词满足的主内存读取。DMA设备,通常是磁盘,将需要发送最新的内存地址副本,而不是实际包含在主内存中的数据。在窥探缓存设计中,缓存标记RAM持续监视主内存总线活动。这可以通过保持相同副本或超集的缓存实际目录(稍后详述)的独立缓存标记RAM执行,或通过缓存的实际目录执行。使用缓存实际目录的设计分为两类。最常见的类别是缓存在主内存总线和处理器之间进行复用。这通常称为双端口目录或双端口标记。在某些设计中,窥探周期通过停止CPU开始,从而禁用其对缓存的访问。然后将DMA地址路由到缓存标记RAM,并在DMA写入主存储器的情况下,如果有DMA命中,则可以比较并写入无效位置,比较并保留有效位置,并将新数据写入缓存数据RAM(如果再次命中),或仅使其失效(无论是否存在命中)。选择这些替代方案是系统相关权衡的另一个方面,因为前两个需要读写周期,这会导致CPU被禁用更长时间,但是最后一个最快的替代方案肯定会在许多无辜的非匹配缓存位置上出现问题,然后需要在随后的访问周期中更新,再次消耗CPU速度。就像每个其他缓存决策一样,对于这三种哪个最适合您的系统,没有简单的答案。所有这些都取决于缓存的大小和联想度,可能在很大程度上取决于运行在机器上的软件结构。其他方法涉及以使窥探周期对CPU不可见的方式复用标记RAM,或将窥探地址交给CPU并请求CPU本身通过专用硬件执行后勤,如大多数处理器所做的那样。复用方法不会减慢CPU速度,但只能在处理器在总运行时间的显着百分比内放弃总线时使用。基于CPU的失效机制可能会导致处理时间显著延迟,因为每个失效通常会消耗几个CPU周期。


一种离散缓存实现中的复用缓存标记RAM方法可以节省一些开销,并且不会减少芯片数,因为它需要在系统总线和本地总线之间的缓存标记RAM的地址输入上添加多路复用器(如图4.1)。由此带来的额外逻辑延迟可能会导致设计无法跟上快速CPU的步伐。在大多数处理器芯片上的综合缓存实现中,这种方法确实是有意义的,因为多路复用器可能已经在关键路径上,并且只需要扩宽,即使没有,它也可能以不到一个时钟周期的速度惩罚实现缓存标记RAM的访问时间。 这种方法的变化称为DMA通过缓存,其中DMA设备基本上与CPU /缓存总线连接,而不是与主存储器总线连接(如图4.2)。在DMA期间,处理器与CPU /缓存总线隔离,而DMA设备读取和写入缓存和主存储器的方法与CPU使用的方法完全相同。即使缓存控制器也不知道正在进行什么样的DMA。虽然这会关闭CPU在DMA期间的持续时间,但当DMA结束时,缓存是一致的,并且可能包含至少几个有用的位置,具体取决于选择的写入未命中策略。在这种类型的写通缓存中可能使用的最佳写入策略是,在写入未命中时不要替换行,因为1)该行可能存在有用的代码或数据,而处理器在DMA之后需要它们,2)在DMA转移期间,通常会传输很多暂时不使用的数据。如果DMA将地址的经常使用的缓存副本覆盖为不太可能使用但恰好位于某些有用信息的相同扇区的内容,则会出现问题。 很容易看出为什么一些设计人员将这种方法称为读取通缓存,尽管此术语实际上只讲了一半故事。 DMA通过缓存方法的一个有趣的附带效益是,可以完全避免有效位。 如果DMA数据是唯一缓存的数据(即ROM,EPROM,I/O位置等映射到不可缓存空间中),则缓存仅在自上电以来没有接收到DMA数据的那些地址处包含随机数据。 哪一个更糟糕:从DRAM还是从缓存中执行随机位?其实没有什么区别。 只要代码调试到只访问合法数据(源自硬盘而不是随机上电位),那么这些数据可以由缓存或主存储器提供,唯一的区别是执行速度。
第二类嗅探机制使用重复标记来嗅探主存储总线。根据缓存设计的不同,这些机制可以非常简单、非常昂贵,或者完全免费但很难理解。让我们从最简单的开始,逐渐过渡到最复杂的机制。
双缓存标记或双目录系统在相似的芯片数量下提供了更高的操作速度、异步操作和更大程度的系统设计灵活性,但需要稍微更昂贵的组件。

在双缓存标记RAM系统中,有两个相同的目录:一个监视CPU的地址总线,另一个监视系统地址总线。本地缓存标记RAM的内容的副本会复制到系统缓存标记RAM中。初步猜测会认为需要做出特殊的努力来确保嗅探标记RAM包含与缓存标记RAM相同的信息。实际上,这是一个微不足道的问题。在任何缓存行更新(图4.3)期间,缓存地址总线连接到主存储器总线,因此在更新缓存标记RAM时,嗅探标记RAM和缓存标记RAM将同时看到相同的地址。然后,只需使用相同的写脉冲来更新嗅探标记RAM和更新缓存标记RAM。
当CPU正在从缓存中操作,而另一个主写操作发生时,系统总线地址将与系统总线缓存标记RAM中的地址进行比较。如果存在DMA写命中(意味着正在访问的主存储器地址已经被复制到缓存中),则嗅探标记RAM会注意到匹配,并导致缓存控制器停止CPU并使匹配位置无效或更新。此时应同时使嗅探标记RAM和缓存标记RAM条目无效,以确保后续对先前无效位置的嗅探命中不会发生。甚至有些系统在完整的缓存之外还使用了一个嗅探标记RAM,但在嗅探命中时通过其复位输入引脚冲刷嗅探标记RAM和目录的缓存标记RAM。虽然这是一种激进的方法,但与每个DMA写周期都冲刷目录的系统相比,该系统经历的缓存冲刷次数更少。令人惊讶的是,在嗅探命中时进行冲刷的方法被使用了,考虑到对于这两种一致性机制,缓存控制器的复杂性几乎是相同的。
在进一步讨论更复杂的一致性机制之前,这可能是另一个好地方,来谈谈我在本书中最喜欢的话题之一:选择缓存策略时需要考虑系统上运行的软件。一致性方法的复杂性不应该超出正在使用的操作系统所需的范围。再次以基于Intel的单处理器个人电脑为例,并且只考虑那些仅使用DMA进行磁盘I/O而不做其他事情的系统。如果设计仅针对MS-DOS或Windows 3.1应用程序,那么在缓存设计中,嗅探可能不是一个重要的特性。在较简单的操作系统中,CPU在DMA访问期间不允许操作,因此在CPU空闲时间内减少向缓存的失效周期不会改善系统性能。另一方面,在使用UNIX或其他更复杂的操作系统和多处理器系统中,通过减少不必要的失效周期可以实现显著的性能提升,因为在这些情况下,处理器与其他总线主设备同时操作。
在升序复杂度方案中的下一个一致性机制涉及包含原则。这种设计用于嗅探标记较大,不一定与缓存目录的关联性相同的情况。首先,让我们看看为什么在理性的人会尝试这样做,然后让我们探讨它是如何工作的。
假设您正在构建一个用于处理器内部缓存的外部嗅探机制,该缓存具有8KB、四路、集合关联的统一物理缓存,并且每行大小为四个字(16字节)。似乎最容易的事情是复制内部缓存的目录。需要多少芯片?带上我们的数学帽子,我们可以发现需要四个单独的缓存标记RAM,每个都对应四路缓存之一,而每个缓存标记RAM则需要具有[8K字节/(4路·每行16字节)] = 128个位置的深度。假设有30个地址位,其中28个用于寻址缓存行,并且由于7个位用作集合位以进入128个标记位置(128 = 27),其他21个位将是标记位。加上一个有效位,总体缓存标记RAM需求将是四个128 X 22位的嗅探标记RAM。如果我们使用目前市场上最广泛的集成缓存标记RAM,它采用8K x 8位的组织方式,则需要12个设备来实现标记(22位/8 = 3,乘以四路缓存)!通过使用包含机制,可以将其简化为两个8Kx 8集成缓存标记RAM,在一个简单的直接映射配置中连接起来。我们来看看这是怎么实现的。
如果嗅探标记RAM始终可以包含缓存目录的超集,则嗅探标记RAM可以用于在这些周期传递到实际缓存之前验证所有失效周期。这样,嗅探标记RAM可以从缓存中筛选出大量无效的失效尝试(以及相应数量的不必要CPU等待状态)。"包含"一词表示整个缓存目录的内容包含在嗅探标记RAM的内容中。
将snoop-tag RAM强制成为更复杂缓存目录的一致超集可能一开始看起来很困难,有几个非常好的原因。首先,具有比snoop-tag RAM更高关联度的缓存能将主内存位置的副本放入多个缓存位置,而关联度较低的snoop-tag RAM将受到限制,在某些情况下无法同时包含易于适应更高关联度缓存目录的一组地址。其次,缓存可能不向外界披露足够信息,以使snoop-tag RAM确定正在被更新的哪一行。大多数处理器不会告诉外界正在替换哪些内部缓存行。如果缓存目录和snoop-tag RAM彼此难以理解,它们如何保持子集和超集的状态呢?答案出奇地简单:包含。包含有两个基本原则:
1. 写入缓存目录的任何标记项同时也写入snoop-tag RAM。
2. 从snoop-tag RAM中移除(无论是通过总线监听还是替换)的任何标记项同时也被强制从缓存中移除。
细心的读者会立即注意到,在某些处理器设计中,可以在不更新snoop-tag RAM的情况下从缓存中删除数据。此后,snoop-tag RAM将可能在缓存中不存在的地址上经历监听命中。这就是snoop-tag RAM成为缓存目录超集的原因。
上述两条规则没有提及的另一个问题涉及将地址标记放入snoop-tag RAM但不放入缓存的情况。如果snoop-tag RAM的线大小大于主缓存,这可能会发生。同样,此时snoop-tag RAM仅仅成为缓存目录的超集。
即使缓存是四路设计而snoop-tag RAM是直接映射的,四路缓存也不能包含snoop-tag RAM中未复制的地址,因为在将地址添加到缓存的同时,其地址被放入snoop-tag RAM。如果在snoop-tag RAM中使无效的每个地址也在缓存自身中使无效(无论它是否仍然存在于缓存中),那么在snoop-tag RAM中突然缺失的缓存中就不会剩下任何东西。
现在我们已经确定了包含确实可以保证缓存目录的内容是snoop-tag RAM的子集,我们应该考虑一下其缺点。
可能最重要的一个缺点是,在使用包含的系统中,缓存永远无法保持满。不可避免地,由于无法在关联度较低的snoop-tag RAM内放置它们,本来可以继续驻留在缓存中的行将被强制退出。这导致缓存的命中率稍微降低,并且如果需要再次获取使行无效的线,则会导致从主内存中重新获取此行的时间损失。因此,确保snoop-tag RAM比缓存目录更大,以减少它们自身和随后被强制从缓存中移除的行数是一个很好的原因。另一方面,如果选择了过大的snoop-tag RAM,将会传递更多错误的使行无效周期给缓存,从而降低系统速度。没有免费的午餐!(顺便说一句,我们还没有考虑snoop-tag RAM可能有多小。信不信由你,snoop-tag RAM可以比缓存标记RAM更小,并且包含仍然可以工作。如果只允许驻留在缓存中的项具有在snoop-tag RAM中复制的地址,那么在这种系统中使用的缓存标记RAM的大小只会比snoop-tag RAM的一小部分被使用。这样会工作,但会对未使用的缓存部分造成可耻的浪费。)
第二个明显的缺点是包含可能会大幅增加传递给缓存的使行无效周期的数量。每当一个地址从snoop-tag RAM中移除时,就会发生缓存使行无效周期。无论被移除的地址实际上是否仍然存在于缓存中,以及snoop-tag RAM条目是由总线监听周期还是由导致snoop线被覆盖的缓存行更新使之无效。尽管看起来不好,但这实际上是包含的一个强大优势。
大多数缓存使行无效周期将由snoop-tag RAM中有效条目的替换触发。这只会在缓存未命中时发生,并且在缓存未命中周期中,CPU被停止并且无法继续执行,直到接收到新数据为止。由于snoop-tag RAM中正在替换一个条目,所以在新数据对CPU可用之前会经过几个周期,这些浪费的时间可以用来在缓存内部使一行无效。通过这种方式,大多数缓存使行无效周期被迫与CPU停止的时间重合。只有那些通过snoop-tag RAM筛选的总线监听周期才被允许停止CPU,以引起性能受威胁的使行无效周期。
如果系统中使用二级缓存,则可以使用包含(inclusion)来允许二级缓存对总线流量进行预筛选,并限制传递给一级缓存的错误使行无效周期的数量。在非常小的成本下,二级缓存的标记可以得到第二个用途作为窥探标记。通常只需要微不足道的额外逻辑来支持使用现有二级缓存的包含。从主缓存/CPU子系统输出的集合位将包含相同的值,该值将驱动进入主缓存以使主缓存行无效,因此需要一个集合地址锁存器。这个锁存器可以吸收到现有地址缓冲器的低位中,因此在芯片计数方面的额外成本通常为零。包含所需的标记位从二级缓存的目录中提供。通常只需要添加很少的附加条款到缓存控制器状态机中,因此缓存控制器的复杂性并没有显著增加。
脏包含与上游和下游缓存均使用复制回写策略的缓存层次结构中的包含类似。使用脏包含时,一个二级复制回写缓存会包含对应于主缓存中每个标记脏的行的标记脏行;但是,并非所有标记脏的二级行都必须在主缓存中标记为脏。这使得可以将一行从主缓存中驱逐而不会导致在二级缓存和主内存之间的接口上发生任何写流量。直到二级缓存需要该行来接收新数据,二级行才会被驱逐出去,并且二级行更新次数比主行更新次数少,因此流量保持在低水平。使用脏包含的另一个优点是,复制回写缓存中对标记脏行的窥探读命中必须从缓存中而不是从主内存中支持。如果二级缓存要在允许使行无效周期通过传递到一级缓存之前预先筛选总线写周期,则也有意义允许二级缓存预先筛选读周期。如果每个二级行的状态与主线的状态相同,则可能会发生这种情况。这并不意味着二级缓存中的脏行将始终包含与主缓存中的脏行相同的数据。要实现这一点,主缓存必须是写通关的,并且必须每次CPU更新脏行时更新二级缓存。相反,将二级行的状态更新为脏状态,当主行首次变为脏行时,并且在窥探周期中保持该状态,即使在窥探读命中事件中,也无法保证来自二级缓存的数据是一致的。因此,所有对脏二级缓存行的窥探读命中都必须发送到主缓存。尽管如此,这仍然比将所有总线读周期发送到主缓存更好。
4.1.2 Problems Unique to Logical Caches
逻辑缓存中遇到的最大难题可能是地址别名问题,这在第2.2.1节中首次提到过。这个问题源于两个单独的虚拟地址可能被映射到同一个物理地址的事实。换句话说,操作系统可能会将具有相同偏移地址但具有不同页地址的两个虚拟地址映射到同一页内的同一个物理页,因此这两个虚拟地址将共享同一物理页内的同一偏移(即同一物理地址)。这是多任务系统中任务之间通信的一种方式(例如应用程序中的系统调用)。
在任何DMA写入主存储器时,也必须更新或使缓存中与DMA设备写入的地址副本无效。如果两个虚拟地址映射到同一物理地址,则可能会存在两个缓存中的同一主存储器位置的副本。当另一个总线主机写入主存储器时,这两个副本都将变得陈旧。
当然,有很多方法可以解决这个问题,其中最不成熟的方法是在每个DMA写周期上刷新缓存。更优雅的解决方案涉及不允许缓存使用任何CPU输出地址作为MMU页面号位中的集合地址位。虽然这在许多情况下对缓存的大小产生了严格限制,但它完全消除了非统一缓存设计中仅表示两个不同逻辑地址的同一物理地址的两个并发缓存副本的可能性。思考一下。只有当两个单独的逻辑地址具有相同的偏移位但具有不同的虚拟页数时,它们才能映射到同一个物理地址。一个使用某些页面号位作为集合位的大型缓存可能能够维护同一个物理地址的两个不同副本,位于两个不同的集合地址中,但其集合位被限制为偏移地址的子集的缓存永远不会有两个相同的物理地址的副本,因为它们都将最终被映射到同一集合地址。

应用该方法的另一个规则是在窥探逻辑缓存设计中仅使用统一的直接映射缓存。在DMA期间,DMA地址的集合位可以输入到缓存标记RAM的窥探机制中,并且标记输出该集合地址的虚拟页数(见图4.4)。这只能在MMU两侧的集合位相同时才能工作,符合要求需要集合位是页面偏移地址位的子集。然后调用MMU将标记的虚拟页数输出转换为物理页号,然后将其与DMA的标记地址位进行比较。如果发生命中,那么该缓存行将被使无效。如果听起来很慢,那是因为它确实很慢,但想想如果在非统一的缓存或多路设计中,两个窥探标记必须一次一个地通过MMU处理它们的标记位需要多慢!另一种选择是使所有具有匹配集合地址位的项无效,但这可能难以实现,并且会不必要地使不成问题的缓存项过多失效。几乎不可能以CPU不停顿进行干净的窥探失效过程,因此逻辑窥探缓存设计通常保持简单,以避免在窥探周期中产生大量速度惩罚。
虽然我从未见过这样的机器,但我不会怀疑有些机器采用了反向转换方案,将物理地址映射回所有相关的虚拟地址,然后允许高度关联的缓存的多个路通过窥探新创建的虚拟地址。这将使逻辑缓存可以是非统一的或更具关联性,并且还将消除由要求集位是页偏移位的子集而引起的缓存大小限制。我只是希望我认识的人没有被负责设计这样的怪物。
关于地址别名问题的讨论通常集中在上下文切换上,上下文切换是描述DMA活动可能发生的时候的软件方式。对于别名的大多数关注都集中在缓存和非缓存系统中的上下文切换上,因为MMU对主存储器和磁盘的映射与缓存与主存储器之间使用的映射非常相似。如果你有机会与构思你的操作系统的软件设计者讨论你的缓存设计,不妨去做!你们两个都会惊讶地发现你们有很多共同的问题,而且你们可能会扩大彼此的视角。
4.2 MULTIPLE-PROCESSOR SYSTEMS
在紧密耦合的多处理器系统中,特别是那些任何处理器都可以在内存的任何部分执行任何任务的系统中,缓存一致性的问题非常重要。为了复习一下,我们将再次解释紧密耦合和松散耦合的词汇,因为它们最初是在第1章中定义的。紧密耦合的多处理器系统被设计为允许每个处理器均能同样地访问主存储器。而松散耦合的系统由两个或更多的处理器组成,每个处理器都有私有的内存空间。图4.5展示了这两种系统的示例。在松散耦合系统中,处理器之间的连接可以是共享的主存储器部分、先进先出队列(FIFO)、双端口内存,甚至是类似于局域网的串行链路,或者像Inmos Transputer上使用的专用互处理通信通道。超立方体使用松散耦合结构。由于松散耦合系统中的处理器都有自己的主存储器,因此没有任何理由需要使得两个独立主存储器中的任何内存位置匹配,因此除了为了避免处理器的输入/输出活动产生的不一致缓存副本而需要维护的步骤之外,不需要采取其他措施来保证缓存的一致性。
在紧密耦合系统中,确保缓存一致性的一个非常简单的方法是在内存中设置一个称为一致性域(coherency domain)的不可缓存空间。由于一致性域中的数据不会被缓存,因此该数据始终是最新的。这是一个在处理器之间进行仲裁的通信空间。一致性域方法被像Honeywell Series 66、早期版本的Intel i860和Elxsi 6400等系统使用。在某些微处理器中,也可以通过MMU页面描述符中的状态位来实现这种方法。使用这种方案的主要缺点是对程序员来说比较限制,因为所有的程序都必须在一致性域中处理不希望共享的任何数据或代码。不可避免地,专门用于一致性域的内存部分总是会发现对某些应用来说太小,而对其他应用来说则占用了过大的内存空间。当然,一致性域方法只能在软件与缓存架构同时定义的系统中使用,而不是缓存被设计用于加速现有的CPU/软件组合的性能。这种情况并不常见。
如果硬件和软件同时定义,还有其他更灵活的方法可供选择。其中之一是使用复杂的协议,通过定制编译器生成和存储与不同主存储器地址相对应的位,告诉缓存哪些主存储器空间是可缓存的,哪些是共享的。使用特殊编译器会使这些缓存无法在软件中透明地使用,因此它们不适用于代码源不可用的系统。不依赖编译器支持的协议称为朴素协议,在本章末尾的示例中将给出所有的例子。朴素协议在最广泛的应用中是比较有用的,但也比复杂协议更难理解。
4.2.1 Two Caches with Different Data
很明显,一个具有两个或多个带缓存的紧密耦合系统可能会遇到这样的情况:两个CPU的缓存都保存了相同的主存储器位置的副本。使用紧密耦合架构而不是松散耦合架构的主要原因之一是允许不同处理器之间共享任意的内存位置,如果这些共享位置不被允许进行缓存,它们将执行得更慢。

在4.1.1节中,我们讨论了确保DMA写周期更新其对应的缓存位置的问题,并探讨了实现一致性所使用的几种方式。在紧密耦合的多处理器系统中,必须采取类似的方法,以确保来自一个处理器的任何主存储器写周期更新其他处理器的适当缓存。一种已经有些不受青睐的方法是使用单个缓存来支持多个CPU,如图4.6所示。Univac 1100/82就使用了这种方法来支持两个处理器,Futurebus+也支持这样的配置。如果CPU可以彼此交错运行,这种方法可以实现;然而,如今和未来的CPU常常以如此高的速度运行,以至于没有离散的主缓存能够设计成足够快,以满足交错CPU的带宽需求。另一个问题是,使用相同缓存的双处理器出现抖动的可能性是单处理器使用相同缓存时的两倍。最后,在使用片上主缓存的系统中,有时会使用共享的二级缓存来减少总线流量。如果系统中的所有CPU都连接到同一个缓存,带宽限制就仅仅从CPU-主存储器接口转移到了缓存-CPU接口,而没有被减少。
4.2.2 Maintaining Coherency in Multiple logical Caches
在单处理器系统中,具有一致逻辑缓存的有很多限制,这些限制在4.1.2节中进行了概述。这些限制同样适用于多处理器系统中的逻辑缓存。其中最重要的是,缓存集地址位必须由内存映射方案的MMU页偏移位的子集组成,除非设计者想要采取一些非常极端的措施来保持一致性。
让我们以mips R4000为例,看看这些措施可以变得多么极端。请仔细阅读,因为R4000中使用的方案非常复杂。R4000是为多处理器系统设计的,使用了逻辑主缓存和物理次级缓存。次级缓存通过包含进行了主缓存的无效化筛选。主缓存的大小是主存储器页面大小的8倍,因此MMU中的三个页面号映射到了集位。这意味着主缓存中可能存在别名,必须进行次级缓存的监视。
解决这个问题的一种激进的方式是使任何可能与更新后的主存储器位置匹配的主缓存位置无效化。在次级监视命中时,所有与页面偏移地址位匹配的主缓存位置将被无效化。这种方法可以解决问题,但会通过引发不必要的无效化来降低缓存的性能。
mips解决这个问题的方式是将物理次级缓存作为系统的必要部分,并使用包含性,确保每个主缓存位置在次级缓存中都有副本。每个次级缓存位置都包含三个颜色位,指示次级缓存对应的主缓存位置。这三个位存储在次级缓存行的集地址中,就像标记位和行状态位一样。
当次级监视命中时,次级缓存将其三个颜色位与页面偏移位合并,以创建匹配的主缓存地址的逻辑集地址。然后,该逻辑集地址上的主缓存位置被无效化。在使用这种包含方式的系统中,同一主存储器位置的多个缓存副本不能存在,因为两个逻辑副本都会映射到相同的物理次级缓存地址,并且主缓存位置与其次级缓存副本之间有一对一的映射。如果没有这种一对一的映射,每个次级线路将需要存储多个共存的颜色位集,以应对所有可能的别名情况。
让我们看一下如果CPU试图将内存位置的别名放入其主缓存中会发生什么。假设存在主内存位置的主副本和次级缓存副本,并且处理器试图读取(并缓存)一个不同的逻辑地址,该地址将映射到相同的物理地址。这将被主缓存视为未命中/填充周期,而次级缓存则视为命中。颜色位将与三位较低的逻辑页位进行比较,并不匹配,因此次级缓存会将该周期视为类似未命中周期的处理,通过无效化具有匹配偏移和颜色位的主缓存位置来执行包含清理操作,然而次级缓存的副本不会被无效化或者逐出。次级缓存数据的新副本随后被复制到新的主缓存地址中,次级缓存行的唯一修改部分是颜色位,以反映该缓存行所代表的新逻辑地址。即使次级缓存的行包含已修改的数据,也不会进行主内存访问。正如我之前所说,这样做非常复杂,并且可能不值得投入这个努力。
4.2.3 Write-through Caches as a Solution
确保系统中两个或多个高速缓存互相协调的问题实际上归结为如何处理写周期的问题。DMA写入主存必须由各个高速缓存进行适配,而由另一个处理器执行的写操作不能与同一内存位置的另一个高速缓存中随后持有的数据产生冲突。确保这一点的一个非常直接的方法是使所有的写周期(无论来自DMA设备还是缓存处理器)都放在主内存总线上。写穿透高速缓存将它们的所有写操作发送到主内存,直接或通过写缓冲区,因此它们是这种系统的明显选择。这种方法是IBM 3033处理器以及Sequent Balance 8000系统中使用的方法。总线上的所有写周期都被所有高速缓存查询,如果写查询命中,所有具有匹配地址的高速缓存都将使其更新数据的副本失效或替换。正如之前提到的,某些系统在写查询命中时清空整个高速缓存。
在多处理器中,这种最后一种选择甚至比DMA系统中的选择更加不可取,因为其他处理器的写周期通常会很小、频繁且随机,而不是DMA的典型偶尔的大块数据传输。如果每个总线写操作都导致系统中的所有高速缓存都被清空,那么任何处理器的高速缓存所满足的周期数量将几乎为零。
一个问题围绕着写缓冲区的使用。假设一个处理器将某个地址的数据写入写缓冲区,而另一个处理器正在同时从主存读取相同地址的数据。当写缓冲区最终获得总线访问权限时,第二个处理器应该首先从主存获得现在过时的数据,并在使用它后立即使其无效或更新吗?在大多数设计中,这并不是一个真正的问题,因为处理器本来就不是同步的。然而,其他设计通过配置写缓冲区来查询总线(就像缓存一样)并要么中止其他处理器的读周期,直到写缓冲区被清空到主存,要么直接从写缓冲区向请求的处理器提供数据,从而缩短了一个处理器的写操作到另一个处理器的读操作的响应时间。第三种选择是在总线仲裁期间将写缓冲区赋予更高的优先级,而不是赋予任何读设备的优先级。这将始终允许写缓冲区在可能出现任何读冲突之前完成清空。
4.2.4 Bus Traffic Problems
既然我们从多处理器一致性问题的角度看到了写穿透高速缓存所提供的优势,现在让我们从总线流量的角度来看待相同的高速缓存。总线流量是一个真正的问题,通常是将高速缓存添加到任何系统中的原因,尤其是多处理器系统。自然地,总线流量越少越好,但设计者绝不希望系统的总线接近饱和状态。
首先,让我们假设所有总线周期都没有等待状态。这只是为了准确性而做出的暂时假设。稍后我们将回归现实。与以前一样,假设CPU周期中有10%的写周期,由于使用的高速缓存是写穿透的,所有这些写操作都会传播到主内存总线,既会被写入主内存,也会被系统中所有其他高速缓存进行嗅探。假设高速缓存的读未命中率为5%,由于读周期占CPU I/O周期的90%,因此读未命中将占所有CPU周期的4.5%。
写周期和读未命中将相当随机地发生。如果它们是完全可预测的,那么系统将迅速同步,在出现任何问题的迹象之前,高达六个处理器(100%/[10%+4.5%])将在这样的总线上尽可能快地运行。然而,现实世界中并非如此,每增加一个处理器都会导致仲裁和相关开销,因此在安装六个处理器之前,总线将导致新增处理器产生递减的回报。(请记住,我们基于一些假定的数字进行分析。这些数字极大程度上取决于缓存设计以及正在执行的软件。Sequent的Balance 8000据说能够在饱和之前维持多达12个CPU,而它使用的是写穿透高速缓存。)
现在,让我们看看当添加等待状态时会发生什么。假设这意味着每个总线周期都比前面的情况要长两倍,那么总线将变得饱和,并且只能支持一半数量的处理器。即使添加第二个处理器,递减的回报也很明显。
更让人不爽的是,现在考虑到所有这些主内存总线周期将争夺总线使用权,进一步堆积等待状态。大多数多处理器总线仲裁算法至少会增加一个等待状态,即使总线此时并没有使用。
我相信您正在看这个例子,并且说“通过增加读命中率和确保主内存系统尽可能快,有很多可以得到的收益”,您是正确的,但是高速系统几乎不可能使用零等待总线设计,并且读命中率只能被推到一定程度。下一个选项是什么?由于写周期是总线上最常见的用户(在这个例子中为10%对4.5%),所以追求它们最有意义,对吧?
假设您的写穿透高速缓存,其未命中率为5%,可以转换为具有相似写未命中率的回写高速缓存。突然之间,总线仅需要处理所有CPU I/O周期的5%,而不是所有写操作加上5%的读操作,这看起来非常值得投入精力,但在多处理器设计中可能会引起许多困难问题。下一节将解决这些问题。
4.2.5 Copy-back Caches and Problems Unique to Copy-back Caches
我们现在已经决定,如果使用回写高速缓存将总线流量减少,那么在系统中总线流量的影响将大大降低,因此我们想知道在回写多处理器高速缓存设计中是否有任何特殊考虑因素会导致困难。确实有!
回写高速缓存的本质决定了它维护的数据可以与关联的主内存位置不一致。但是,高速缓存之间的缓存位置如何保持一致,并与主内存不一致呢?也许它并不总是必须一致!有可能,写入的位置仅属于单个处理器,例如循环计数器或私有指针。然而,在处理器之间共享的位置必须始终在各个高速缓存之间保持一致。尽管这似乎是需要编写支持协议的代码的地方,但实际情况并非如此,我们很快就会看到,确保共享的写周期适当地通信,而私有写操作则不需要,其实并不像看起来那么困难。
为了暂时从多处理器的复杂讨论中解脱出来,让我们看一下在具有回写高速缓存的单处理器系统中,用于确保DMA读周期使用最新数据副本的简单软件控制机制。与多处理器系统中遇到的问题相比,这个问题很简单,因为CPU启动了所有DMA活动,并且保证一致性的过程可以附加到允许DMA活动开始的例程上。在多处理器系统中,处理器之间的通信控制较少,可能会更加随机地发生。
为了支持单处理器系统中的DMA一致性,一些处理器指令集提供了一条指令,该指令启动所有脏(cache)位置的回写周期。这通常被称为缓存清除周期,因为该过程将所有脏位置从高速缓存中清除出去。如果控制从内存开始的DMA活动的程序(例如写入磁盘)通过执行清除指令来开始,那么当允许执行下一条指令时,将不会有脏位置存在。英特尔的Intel架构具有内部写穿透高速缓存,提供了一个写回使数据缓存失效(WBINVD)指令来支持外部回写高速缓存,但此指令只是引发一个标志,并期望外部高速缓存控制器阻止处理器执行进一步的I/O活动,直到所有脏的外部回写高速缓存行都已在主内存中得到更新。
缓存设计人员有时会对必须检查每个缓存行的状态并可能需要执行四倍或八倍于外部缓存中行数的写周期的指令的持续时间感到沮丧!更糟糕的是,一些处理器支持导出指令,该指令旨在将清除过程转化为一个软件循环,而不是硬件清除机制。在清除循环中使用导出指令将一个脏行的副本发送回主内存,以使该特定主内存位置在主内存中变得一致。当然,通过软件控制清除缓存对解决多处理器系统中备选处理器的随机嗅探读命中没有任何帮助,所以让我们继续向前看,面对更具挑战性的任务,通过硬件来确保任意一致性冲突。
在4.2.3节中提出的写透设计中,主内存被视为“真相”的持有者。任何需要数据的高速缓存都可以直接访问主内存,以获取最新的数据副本。在单处理器的回写高速缓存设计中,主内存地址的最新副本可能存储在主内存中或者缓存中,最新版本的位置由高速缓存行的Dirty位来指示。显然,在多处理器系统中,最新版本的位置变得更加模糊不清。

你可能立即想到的是,如果所有缓存的所有标签和Dirty位的副本都在一个集中的位置可用,那么每个进行读取周期的高速缓存都可以将其读取周期定向到主内存或者持有最新数据副本的高速缓存中。CDC computers和Chips and Technologies的M/PAX多处理器芯片组就是以这种方式操作的,这种方式有各种各样的名称:基于目录的、基于内存的、全局目录的或者内存标记。在基于目录的系统中(图4.7),每个与高速缓存行(有时称为块,有时不称为块)长度相同的内存位置或者内存位置组也会包含每个处理器的额外1或2个位。这些位指示该内存块作为一个或多个高速缓存中的缓存行的状态。该状态可能指示处理器A、B和F包含数据副本,并且尚未修改。在另一个块中,状态位可能指示只有处理器D有一个缓存副本,并且该副本是系统中唯一有效的副本。在一些更复杂的系统中,状态位可能指示D、E和F包含有效副本,但是主内存中的副本与这些副本不一致。所有这些状态位在系统初始化时都必须被重置。
无论如何,主内存充当裁判,必须不断了解每个处理器的高速缓存的状态。这在某种程度上简化了设计,因为所有的监控功能都由单个单元完成。另一方面,使用的处理器数量并不是任意的,而是受到分配给状态保持的主内存位数的限制。也许会有诱惑将这些位放在处理器板上,这样就可以随时添加处理器,但是扩展和减少主内存大小将需要同时修改所有处理器板。
这种方法并不像前文所示那么简单,因为必须选择某种方法,在一个处理器想要写入其高速缓存中缓存副本的内存位置时,不允许同时存在两个或多个相同主内存位置的副本。如果所有的高速缓存写入都必须先检查其他高速缓存中是否存在相同的行副本,那么主内存总线将被用于查询各个主内存行状态的周期而饱和。因此,许多协议不允许存在多个脏行的缓存副本。
一种更普遍的确保多个写回缓存一致性的方法是利用每个缓存内置的监听机制,这些系统被称为基于缓存的。可以采用几种方法来确保数据的一致性,并且在任何特定时刻,整个系统都不会存在多个副本,而这些副本都被认为是最新的。任何缓存行的状态都可以通过CPU周期或监听周期来更改。最常用的多处理器写回缓存一致性协议是所谓的写一次协议。有人将这些协议称为写优先协议。在写一次协议中,首次写入缓存中的位置也会同时写入主内存总线。换句话说,在第一次写入周期中,缓存行的处理方式就像写透缓存一样。为什么要这样做呢?这样做可以让其他处理器监听到写入周期,并使其无效化自己对相同行的副本。关键在于,它们应该使副本无效化而不是更新副本,因为这将是写入处理器允许进入总线的唯一写操作。写一次缓存的总线流量要比标准的写回缓存略高,但这是维护一致性所付出的代价之一。
一个处理器必须执行两种类型的写操作。第一种是对私有位置的写操作,正如我们所见,其他处理器不希望看到这些数据,比如循环计数器。第二种写操作是对公共数据或其他处理器希望看到的数据的写操作。在使用写一次协议的处理器中,私有数据的写操作遵循以下顺序:处理器首次将数据写入总线,并使同一内存位置的其他缓存副本失效,然后,在所有后续的写操作中,只在缓存中进行写入,以缓存速度而非主内存速度进行写入。所有这些后续写操作的总线流量也将被消除。只有在行填充期间腾出空间时,Dirty行才会被回写到主内存。从另一个角度来看,缓存控制器假设在第一次写操作周期中的数据是公共的,但如果能够在没有其他设备请求相同信息的情况下连续两次写入相同位置,那么控制器就确定该位置是私有的。
对于公共数据位置的写入周期将以相同的方式进行,直到另一个处理器请求该数据。如果只有第一次写入周期发生,其他处理器将从主内存中更新自己,并告知执行了第一次写操作的处理器,该次写操作不再是第一次写操作。需要执行另一个写一次周期才能使第二个缓存的匹配主内存位置数据的新副本失效。如果在另一个处理器请求副本之前已经发生了多次写入周期,那么主内存将不是最新的,请求的缓存需要获取第一个处理器正在写入的缓存行内容的副本。这种情况被称为干涉,因为具有最新数据的缓存必须干涉,强制读取设备无法看到主内存的当前内容,而是看到更新后的数据。干涉机制将很快进行探讨。

似乎每个缓存行需要另外一个状态位,告诉缓存控制器写入周期是第一次还是后续的写入周期。事实并非如此,因为典型的回写缓存具备支持未使用状态的所有机制。回顾第2.2.4节,典型的回写缓存使用了两个状态位:有效位(Valid)和脏位(Dirty),但通常这些位仅表示三个状态:无效(干净或脏)、有效(干净)和有效(脏)(图4.8)。
如果我们对这两个位进行更详细的解释,允许我们使用未使用状态更好地跟踪缓存行的状态,会怎样呢?这将允许我们存储四个状态:无效、从未被任何CPU写入过的有效、被该CPU写入一次的有效以及被该CPU写入多次的有效(缓存设计者不使用这种术语,但是有更简洁的命名方式,我们将在稍后的第4.3节进行讨论)。通过这样做,我们不能再称这些位为有效和脏位,因为它们的含义现在已经编码,但是我们仍然拥有相同数量的位,并且对于支持这种新协议的缓存控制器所需的改变是简单的。唯一需要缓存控制器进行干涉的状态是缓存中的数据比主内存中的数据更加最新的状态。这就是写入多次状态,有时也被称为私有状态,因为它是缓存中的数据与主内存或其他缓存中的数据不共享或不一致的唯一状态。
一个稍微变化了写一次算法的协议被称为广播或写广播。与写一次方案中由第一次写入引起的使其他缓存副本失效不同,广播缓存协议允许接收新写入数据的缓存保留其缓存中由其他CPU/缓存组合通过总线写入的数据的副本。因此,广播缓存必须通过系统总线周期来区分广播写入和旨在使其他缓存副本失效的写入操作。此外,还需要一整套新的状态来适应这种新方法。稍后的章节将介绍一些广播协议。
干涉有两种类型:间接数据干涉和直接数据干涉。两者都值得深入解释。我们先从更简单的间接数据干涉开始说明。
如先前所述,当缓存检测到对一个被标记为“脏”的位置的读取探查命中时,就会出现需要解决的问题。间接干涉缓存将终止探测读取周期,接管主内存,从被探测地址向适当的主内存位置写入数据(不需要CPU参与),将该行状态从脏状态更新为干净的有效状态,然后释放总线,以便其他CPU可以再次尝试读取所需数据并成功地完成操作。(在探测周期期间更新主内存的过程有时称为XI castout,其中XI代表“交叉询问强制退出”(cross-interrogate castout)。“交叉询问”就是我们已经看到的另一种探测方式,“强制退出”实际上是驱逐,但是术语XI castout的创建者可能并不是真的指驱逐;他们可能执行更新主内存和修改行状态到“有效且从未被任何CPU写入过”的状态,就像你和我一样。)
这种间接干涉之所以称为间接,是因为探测处理器缓存中请求数据的请求不是通过探测的缓存直接将数据交给请求设备,而是通过主内存传递数据,这是一种更为间接的路径。这让人想起一些老电影里的对话,三个人在一个房间里,其中一个告诉另一个告诉第三个某些事情,因为他俩不愿意直接说话。一些间接数据干涉协议允许一个已经被终止请求的设备观察总线流量,如果发生了对匹配地址的写周期,则获取一份副本并继续进行读周期操作,省时并减少了总线流量,相比需要重试读周期的协议。
间接数据干涉确实需要特殊的总线支持。根据总线协议是否支持分裂事务,使用两种中的任何一种方法来终止总线周期,必须提供一种机制,以允许从探测位置的主内存更新优先于其他处理器的任何周期,即使另一个处理器通常可以获得更高的总线权限。
在分裂事务系统中(首次定义于2.1.2节),总线主控发出请求并将自身从总线上移除,等待任务执行者的响应。在分裂事务系统中,被监视的缓存向请求设备发送一个终止/重试的消息,然后在发出另一个请求之前获取对总线的控制权。
在没有分裂事务的系统中(其中总线主控发出请求并一直保持在总线上直到收到响应),要么一个优先级更高的请求强制将请求处理器从总线上移除并将其置于保持状态,直到主内存更新完成,要么一个终止/重试信号导致周期重新启动,但在此期间,一个优先级更高的主控,即被监视的处理器,获得对总线的控制权以更新主内存。显然,如果读周期来自一个在争取总线方面具有更高优先级的处理器,且对探测周期的响应不允许暂时提高优先级,则系统将被锁定,因为更高优先级的处理器在等待较低优先级的处理器无法将数据放入主内存时。
直接数据干预系统允许任何响应的缓存在总线读取探测周期上禁用系统内存,并将探测位置的数据放置在总线上,从而实现了探测的缓存与请求处理器之间的即时通信。这也被称为缓存对缓存传输,因为主内存不参与交易。在这些系统中,可以选择两种方法来解决多个缓存副本存在且主内存没有有效副本的数据问题。最简单的理解方法是使用反射,即主内存具有类似于系统其他CPU缓存上的缓存探测机制的探测机制。其他处理器上的探测机制通常使用缓存标记RAM来监视总线上的匹配地址周期。另一方面,主内存由一块单一的大连续地址块组成,所以它只需要一个地址解码器,实际上是与用于分配主内存的存储空间相同的地址解码器,如果没有被介入的缓存取代,通常会映射到主内存。
如果处理器开始对其它处理器的缓存中的一个地址进行读取周期,并且该地址在另一个处理器的缓存中存储为“多次写入”,则被监视的处理器禁用来自主内存的响应,并将自己的数据放置在总线上,将其状态降级为“有效但未写入”。内存的探测机制检测到了一个未被允许支持的读周期,因此它抓取了在总线上出现的数据副本,并将其暂时放置在主内存系统的写缓冲区中。随着内存延迟的允许,新数据被写入到主内存中。最终,所有缓存和主内存都拥有相同的数据副本,因此该行的所有缓存副本的适当状态为“有效但未写入”。
直接数据干预系统不使用反射的一种缓存状态设计,旨在允许多个缓存副本存在于未在主内存中更新的数据中。这些状态被称为所有权协议。再次假设一个处理器的缓存包含另一个处理器正在请求的主存储器位置的脏副本。当包含更新副本的缓存监视此读取周期时,它将向请求处理器提供数据,就像前面的例子中一样,但主内存不会配备任何在事务发生时更新自身的手段。事实上,主内存是一种传统设计,只有在系统总线写入周期时才会对其进行修改。在这种缓存中主内存更新的唯一时机是当脏行从缓存中逐出时。这需要一组不同的状态,因为现在可以在两个不同的缓存中存在相同位置的两个脏副本。这就是所有权的概念发挥作用的地方。在所有权协议中,修改脏缓存位置的CPU所在的缓存被认为是该位置的所有者。只有所有者负责最终更新主内存,而其他缓存则不负责。因此,当属于拥有该行的缓存的脏行被替换时,该脏行将从缓存中逐出并在主内存中更新,而在不拥有该行的缓存中替换相同脏行将不会导致主内存更新。那么如果没有缓存拥有主内存位置的副本怎么办?这并没有被广泛讨论,但大约一半的论文认为主内存拥有任何潜在未被任何缓存声明的缓存行的所有权,而另外一半则说该行是无主的。主存储器对一行的所有权平衡了东西,并且似乎更容易描述所有权统一性缓存协议中采取的操作,因此我们将在本书的其余部分使用该命名法。当这种协议的缓存需要更新一行时,它将请求所有者提供该行,不管所有者实际上是否是主存储器。
使用所有权协议的缓存解决的一个问题是pingpong的困难,这有点像抖动,因为一行的状态经常改变,导致该行的时序和总线流量增加。除了拼写和发音都很难之外,“pingpong”是一种现象,即正在被缓存审查但不拥有该行的缓存将该行置于私有状态。在间接数据干预系统中,总线通常被占据,因为读取缓存执行了私有位置的无效读取,响应缓存将通过内存写入周期回答,随后让读取设备重新进入总线。然后,正在被修改的副本将再次变为私有状态,以广播写入的代价,只是以便请求缓存进一步审查。这样做不需要太多就能显著地占用主内存总线。作为所有权协议的一个例子,我们将描述一种使用四个状态的典型直接数据干预复制回缓存,这些状态与先前在间接数据干预示例中使用的状态有些类似:无效、有效、CPU仅写入了一次的有效和已经由CPU写入了多次的有效。你会说:“嘿!这几乎与我们之前讨论的协议相同。” 好吧,或者也不相同。请耐心等待,您将看到直接数据干预为协议带来的差异。
最后两种状态是声明该缓存是真正拥有副本的所有者。如果我们观察一个给定行通过其各种状态变化的过程,我们会看到类似于以前的例子中的进展,直到遇到嗅探读命中为止。假设处理器A和B都从无效行开始,并且对于相同的主存储器位置都得到了读取失误。这些行将被更新,并且它们的状态将从无效变为有效。然后处理器A可能会写入这条线路,当其缓存控制器看到状态从有效到仅由该CPU写入一次的有效状态转换时,将广播写入以使总线上的其他副本无效。让我们再让处理器A写入该行,将行的状态从仅被此CPU写入一次的有效状态提升到被此CPU写入多次的有效状态。处理器B缓存中的副本被第一次写入使其无效,由处理器A的缓存广播到总线上,因此在下一个针对此地址的读取失误时,处理器再次尝试更新该行。现在,这个尝试由处理器A的缓存中介完成,行的状态存储在处理器B的缓存中,就像来自主存储器一样。如果处理器B然后替换了这条线路,即使处理器B的缓存中的副本比主存储器中包含的副本更新,也不会发生主存储器写入周期。如果处理器A驱逐该行,其缓存将更新主存储器,并且处理器B的缓存将窥探此更新,但不需要更改该行的状态。该行的所有权已从处理器A的缓存放弃回到主存储器。经过仔细检查,读者会注意到,在这个例子中拥有处理器(处理器A)无法知道其他处理器的缓存中是否存在其被多次写入的有效副本。为了与这些副本保持一致性,拥有处理器必须将每个写入周期放在总线上,从而消除使用复制回退协议的任何优势。需要做一些工作,让缓存知道是否必须广播写周期还是可以保持私有。这要求将被此CPU写入的有效状态进一步划分为两个类别:嗅探和未嗅探的。未嗅探的副本将执行立即的缓存写入周期,而不进行任何总线交互。嗅探副本则知道另一个处理器可能有脏位置的副本,因此需要执行总线写入周期来使其他副本无效;然后拥有缓存就可以将该行的状态重新变为未嗅探。这就是所有权的第五个状态出现的地方。我们将将被此CPU写入的有效状态重命名为被此CPU写入但未经嗅探的有效状态,并将第五种状态设置为被此CPU写入并经过嗅探的有效状态。像这样的缓存通常仅适用于某些总线协议。目前最常见(也可能是我们讨论的最慢版本)的是具有串联层次结构的总线,其中只有在所有其他设备都有机会替代它后,主存储器才会响应。如果有几个缓存连接到总线上,那么在允许主存储器最终满足读取周期之前,每个缓存都有机会响应。分离事务总线是列表中的下一个,就像它们在间接数据干预系统中的使用一样,在主存储器访问延迟之前,它们允许响应缓存给自己留出时间段来响应,向主存储器发出信号说明该周期已由另一个响应方满足。第三种方法涉及使用辅助总线,其提供嗅探数据,而主存储器总线专门用于主存储器传输。然后,读取哪个总线的决定是由请求CPU /缓存的处理器板作出的,而不是系统功能。要支持两总线协议以重新路由读周期,需要不同一组行状态,具体取决于读取处理器希望找到所需的线路的位置。使用在第4.3.4节中讨论的N+1协议的另一种方法是使用智能主存储器。智能主存储器为每个潜在缓存行保留一位,指示该行是否由主存储器拥有或不拥有。当我告诉你这些系统的设计者将不保存此类状态的主存储器称为愚笨的主存储器时,你不会感到惊讶。智能协议是基于缓存和基于存储器的协议之间的混合体,具有更倾向于基于缓存的一侧。
一次写入和所有权协议有时利用写入分配作为一种方法来表明需要其他缓存使任何现有的缓存行失效。这意味着主内存的唯一写入周期将是非可缓存地址(如果存在)或复制回路,这两个周期都不会导致失效,因为没有其他缓存包含这些行的副本。支持此类协议的总线需要机制,在snoop读命中而不是snoop写命中期间允许行失效。随着我们详细研究此类协议,将更清楚地了解到总线将需要支持两种完全不同的读取周期,其中一种允许其他缓存维护所请求行的现有副本,而另一种需要使匹配的行失效。一些示例是Read-for-Ownership或私有读取、Read Shared或公共读取、Write-for-Invalidate和Write-without-Invalidation,它们在第4.3节中有描述。虽然这些状态与维护缓存一致性密切相关,但我们将把它们视为总线协议问题,只描述足以允许彻底理解缓存操作而不是总线操作的内容。但是,总的原则是,Read-for-Ownership周期仅用于写入分配周期,而不用于满足读取未命中。然而,在复杂的所有权协议中,编译器会指示CPU指示读取未命中将在稍后导致写入周期,因此CPU会在编译器决定最好立即使所有其他缓存中的位置无效的位置上对读取未命中进行Read-for-Ownership。尽管大多数设计不能使所有程序都编译以适应缓存的体系结构,但它们依靠幼稚所有权协议的写入未命中来生成其Read-for-Ownership周期。

一个真正不同寻常的所有权一致性协议的好处是,拥有所请求数据副本的缓存可能响应速度比主内存更快,导致某些硬件和软件组合表现出比系统中包含的处理器数量更大的性能增益(图4.9)。这是怎么回事呢?随着越来越多的处理器被添加,存在更多的快速缓存位置可以向其他处理器提供数据,比主内存更快,甚至在一个足够大的系统中,只要有正确的程序,它们就只会将主内存用作放置驱逐线的地方或可能作为DMA的分配区域。当然,这种现象的好处高度依赖于由基准测试软件支持的处理器间通信的数量。有些多处理器系统充分利用这个概念,有时被称为缓存自己的一部分主内存。这是一个缓存和主内存之间分界线变得不那么清晰的地方,一些设计师会称之为缓存,而其他人则认为它根本不是缓存。 Kendall Square Research Corporation就是这样一家公司,他们采用的方法被称为Allcache,意思是将整个主内存实现在几个CPU的缓存中(从16到超过1000个)。在一个非统一内存体系结构(NUMA)中,各种主内存部分被物理上分配在系统中的不同处理器板之间,本地CPU卡所对应的主内存部分对本地CPU的响应速度比其他处理器要快得多,不仅因为接近,还因为本地处理器不需要仲裁系统总线,并且始终优先访问本地内存。从任何处理器的角度来看,主内存将会有一小段快速地址范围,但其余的地址范围都很慢。获得最佳NUMA机器性能的方法是编译代码,尽可能多地引用本地和快速的主内存部分。这意味着代码必须设计以适应硬件,这常常不是可用于系统设计团队的选项。当然,NUMA机器的反面是统一内存体系结构(UMA)机器,其中主内存对所有处理器都是平等可访问的,并且任何地址都会以相同的延迟响应所有处理器。
在本节中,还应该提到支持多个写回缓存的总线另一个命令:后退命令。在某些缓存设计中,后退只是一种总线命令,要求缓存立即停止与系统总线的交互。在复制回缓存一致性协议中,含义是不同的,存在两种类型的后退。完全后退是指当取走命中不得到服务,直到被取走的缓存能够在自己方便的时候服务为止。另一种是受限制的后退,这是指取走命中会导致所有CPU /缓存交互立即停止,以便可以立即服务取走命中。据我所知,没有大量关于哪种优于另一种的思考或研究;它们只被视为备选的窥探技术。
4.3 EXISTING COPY-BACK COHERENCY PROTOCOLS
本节将展示一些现有的多个写回缓存一致性协议的真实例子。你可能已经感觉到这一切变得非常复杂,为了帮助你,我们将通过表格的形式进行说明,对每个协议进行详细说明。这些表格可以很好地定义缓存的状态,所以如果你设计一个缓存,尝试自己制作一个表格会很有帮助。表格中的列表示在任何周期开始时缓存的状态。每一行表示可能对缓存操作或状态改变产生影响的外部事件。本书中的表格并不足以用于设计,你可能会发现自己需要组合几层类似的表格来描述自己硬件的各个部分的动作。
4.3.1 The MESI Protocol
MESI是一种基于写一次缓存的协议,可能是在行业杂志中描述最频繁的一种。为什么呢?因为它的名字很有吸引力。当我告诉妻子(她是神学专业的学生,不是工程专业)一个计算机可以使用MESI协议,使用SCSI通信,并提供图形用户界面时,她觉得很有趣。然后,这与所有这些肮脏的语言似乎不一致,MESI协议排除了缓存使用Dirty位的可能性。
MESI这个首字母缩写代表了一种典型的间接数据干预协议的四个状态,我们在4.2.5节中进行了描述,但为这些状态使用了其他较少描述性但更简单的名称:Modified、Exclusive、Shared和Invalid。下面是这四个状态如何与4.2.5节中所描述的状态对应:Invalid自然而然地对应Invalid状态;Shared是我们最初称之为Valid-and-never-written-to-by-any-CPU状态的状态;Exclusive是指此CPU的Valid-and-written-to-once状态;Modified是MESI协议用来表示Valid-and-written-to-more-than-once-by-this-CPU状态的名称。正如我们将在下一节中看到的,这些状态不仅适用于间接数据干预。
这些命名非常有意义。Shared状态是唯一允许将同一内存位置的另一个副本存储在其他缓存中的状态。如果一个缓存具有Exclusive或Modified行,其他缓存中的所有匹配行将被标记为Invalid(我们将在表4.1中详细介绍这一点)。Exclusive状态告诉缓存控制器,主内存位置与缓存的内容保持一致,并且没有其他相同主内存位置的缓存副本存在;这是主内存位置的一份独家缓存副本(有些人将Shared状态称为非独占状态)。最后,Modified状态表示该地址的唯一当前版本位于此缓存中。
MESI协议最初是为多总线的原始版本开发的,这种总线没有任何支持一致性协议的钩子。因此,它没有写分配或直接数据干预。在简单总线上实现MESI系统时,所有写周期都被视为写入使其无效,并自动使其他缓存中匹配的位置失效。这在表4.1中显示出来。

由于此类系统中的总线不会支持读取使其无效状态,因此在设计MESI缓存时不使用写分配。表4.1显示了在可缓存空间中处理处理器和嗅探器的读取和写入命中和未命中时,四种MESI状态和对应的响应。大多数CPU周期的执行方式与写透或复制回缓存中的类似周期相同,具体取决于事务发生前缓存行所处的状态。两种特殊情况是对已修改行的写未命中和对标记为Exclusive的行的写命中。对已修改行的写未命中可以引起主内存写入周期,而不更新缓存,或者如表4.1所示,可以驱逐当前行的内容,然后进行写透周期,将该行转为Exclusive状态。对Exclusive行的写命中将把该行更新为Modified状态,而不需要总线事务。必须先进入Exclusive状态,然后才能进入Modified状态。不允许缓存行直接从其他两种状态直接进入Modified状态。
总是有特例存在。英特尔的处理器使用写分配实现MESI协议,以弥合处理器的16字节内部缓存行大小和处理器支持的1到4字节写周期之间的差异。这意味着所有写未命中将在处理器总线上显示为读未命中。在这种情况下,如果系统首先将所有读未命中假定为写分配未命中,并且新获取的缓存行的状态自动设置为Exclusive,同时将所有其他缓存副本自动失效,可以提高性能。这要求缓存/CPU组合要获得支持具有读取使其无效机制的主存储器总线,以便读周期可以引起其他缓存的失效,除非设计者愿意忍受偶尔的pingponging问题,即共享一块数据的两个缓存不断相互使对方的副本失效。
在MESI协议中,嗅探周期的处理方式与你对复制回缓存的期望基本相同。忽略嗅探读和写未命中,只有当嗅探的缓存行处于以下状态时才真正注意到嗅探读命中:1)处于Exclusive状态,这种情况下,独占性将降级为Shared状态(主存储器中已存在有效副本),或者2)处于Modified状态,需要中止其他设备的读请求,并允许嗅探缓存将Dirty行的内容更新到主存储器,然后允许嗅探处理器重试中止的总线周期。无论哪种情况,该行的状态都将更改为Shared状态。所有写嗅探命中将使命中的缓存行失效,但对于已修改行的写命中通常意味着软件管理上出现了一些严重问题!
真正的极简主义者会想知道是否可以完全取消Exclusive状态,确实可以,但代价很大。缓存控制器可以轻松确定缓存是从Shared状态向Modified状态转换,并发出总线写入以使任何其他缓存副本失效,就像从Shared状态转换到Exclusive状态一样,但如果该行再也不被写入,缓存控制器就无法得知,并且会驱逐Modified行,无论它是否与主存储器的副本保持一致。这与减少总线流量的目标不符,因此很少使用,但是此方法的一个版本将在第4.3.4节中讨论。
4.3.2 Futurebus+
一个开放的标准总线协议,IEEE Futurebus+(加号让我想起小写字母t,但我怀疑这并不表示该总线会受到有限的接受),利用了总线专家自其他更好的已建立的开放标准总线首次设计以来所积累的丰富理解和发展。Futurebus+使用一种称为总线转换器逻辑(BTL)的具有自己电压I/O级别的分时同步总线,尽管它们与标准逻辑电平如ECL和TTL不兼容,但通过极快的速度弥补了任何差异。Futurebus+基于MESI规定了一种复制回写一致性协议,但假定比原始MESI模型拥有更多的总线支持,使用写分配,并包括了一个被称为"snarf"或"write snarf"的附加机制。当处理器尝试将数据读入缓存,但总线不可用时,缓存能够监听其他正在进行的事务,并且如果请求的地址恰好涉及总线事务,那个处理器的缓存控制器将获取一份副本,并停止尝试争夺总线的控制权。Futurebus+规范允许设计者从广泛的选项菜单中进行选择,并提供了一个选项,允许缓存复制与无效位置地址匹配的总线事务的副本,即使处理器并没有请求它的副本。这看起来很像反射,不是吗?在Futurebus+委员会决定模拟主存储器的反射技术时,几乎是禅意的,因此缓存现在从缓存到主存储器的传输中捕获数据,就像反射式内存在缓存到缓存的传输中捕获数据一样。将反射技术应用于处理器和内存卡上也非常有意义。一些读者可能持有不同意见,那肯定是因为你已经考虑到在处理器尝试获得总线控制权时,处理器所请求的确切地址在总线上的几率几乎为零,并且缓存应该几乎完全填满有效位置。反驳有两个方面。首先,任何可以减少总线流量的协议都将进一步增加可以连接到总线上的处理器数量,并且随着总线流量的减少,此类交互事件发生的可能性将成为整体事件链中更大的一部分。其次,在处理器运行足够长时间以填充其缓存后,被标记为无效的缓存位置很可能曾经是有效的,然后由于另一个处理器将该行置为独占状态而使其失效。如果地址再次出现在总线上,这就自动意味着该行正在从独占状态降级,并且可以在系统中的其他缓存中再次复制。仅仅因为使用Futurebus+,并不要求在每个处理器板上都有复制回写缓存,甚至不要求有多个处理器,但由于总线包含了支持最广泛实现的所有协议,因此可以轻松地实现混搭系统,包括写直通、复制回写和非缓存处理器板。正如我之前所说,有各种方式来实现您自己的板卡,并确保兼容性。


Futurebus+与其基于的MESI协议不同之处在于,它使用直接数据干预(从其他缓存而不是主存储器中读取可缓存位置)和写分配,并且支持了一种围绕缓存实现的分时事务总线协议,而不是反过来。它有三种读周期、两种写入和一种使其他缓存中某个缓存行副本失效的无效命令。相比于Read-for-Invalidate或Write-for-Invalidate命令,这种无效命令更快速地使其他缓存中的缓存行副本失效。此外,通过一种名为tfI'的信号,Snoop命中确认从被侦听的缓存广播到总线上。tfI'是一个总线信号,其定义取决于总线上正在发生的事务类型。在本节中的所有周期中,tfI'表示Snoop命中。通过监视tfI'信号,请求的缓存可以选择立即将一行线路置于排他状态,如果该行线路在其他任何缓存中没有复制。这使得表4.2比MESI的表4.1更复杂,但这样做是值得的,因为它可以减少不必要的写入主存储器总线的次数。内存子系统(如果存在)也需要反射操作,这是另一种节省带宽的措施,因为可以使用高速直接数据干预周期而无需后续周期来更新主存储器。
等等!关于内存子系统的“如果存在”,这是什么意思?Futurebus+的定义允许整个系统在没有主存储器的情况下存在,只要任何活动的主存储器地址在系统中始终得到记录。为此,有一个术语称为“最后机会库”,即该地址最终停留的位置。在具有主存储器的系统中,这是该行的所有者,通常是主存储器。在仅缓存的系统中,最后机会库是行的当前所有者,并且通过总线协议的其他部分(我们在这里不讨论)确保该行始终在系统中的某个位置复制。
要理解使用表4.2的Futurebus+总线命令,应该定义四个状态的名称。它们是排他修改(对应于MESI协议中的修改状态)、排他非修改(MESI的排他状态)、共享非修改(共享状态)和无效。最容易理解的两个总线命令是用于I/O总线主设备(通常是DMA设备)的Read Invalid和Write Invalid命令。尽管Write Invalid命令会自动使所有现有的缓存副本失效,但Read Invalid命令并不会这样做,而是将副本降级为共享非修改状态,并导致排他修改副本的所有者在发生Snoop读命中时进行干预。
Read Shared命令由经历读取缺失周期的处理器发出,任何散播命中都允许数据来自主存(如果散播的副本是共享未修改或排他未修改)或来自散播高速缓存,如果散播命中是指向独占修改位置。在所有这些情况下,散播的高速缓存将断言tP并且会停留在共享未修改状态。 (作为灵活性,Futurebus+规格允许高速缓存无效其行的副本,如果它不想断言tP)。由于主存中使用反射,从排他修改到共享 downgrade 不会导致一致性问题。无论何种情况,在任何Read Shared周期结束时,线路的一个有效副本保留在主存中。现在请求的高速缓存知道是否存在其他缓存副本,因为如果存在任何其他缓存副本,它将收到已断言的tP,如果没有,则收到否定的tP。在这种情况下,如果存在其他缓存副本,新的高速缓存行将被加载为共享未修改状态,如果没有其他缓存有副本,则将立即将其加载到排他未修改状态。如果加载为排他未修改的行随后成为高速缓存写命中的主题,则无需调用写-先操作,从而节省总线带宽,超过标准的MESI实现。最后一个Futurebus+读取周期是读取修改,它由高速缓存使用以信号它遭受写入缺失周期,并且必须为其写入配置申请独占副本。当Read Modified snoop命中在高速缓存上发生时,该高速缓存的行被无效。由于该行可能比正在写入的字长(Futurebus+的行长度为64字节)更长,并且由于散播的排他修改副本可能会与请求处理器将要修改的不同字节进行修改,因此,散播的排他修改行的所有者必须执行直接数据干预以将数据交给新的所有者。两个Futurebus+写周期是Write Invalidate,它已经讨论过了,以及Copyback。Copyback周期用于排除排他修改行。由于这些行是独占的,因此其他任何高速缓存中都不存在副本,因此永远不会有任何散播命中。唯一涉及高速缓存交互的其他Futurebus+总线周期是无效周期。无效是由拥有共享行副本的高速缓存发出的,它想将该行转换为独占修改状态。奇怪的是,Futurebus+委员会决定将其称为写入缺失周期。为了保持一致性,我将遵循更普遍的惯例,并将其称为对共享未修改位置的写命中。在散播无效命令时,任何拥有共享未修改行副本的其他高速缓存都将其副本无效。其他高速缓存中的副本不可能具有任何其他状态,因为在一个高速缓存中处于共享未修改状态的行不能处于任何其他高速缓存的两个排他状态中的任何一个。采用这种相对较落后的方法,我们讨论了表4.2底部的所有散播周期,并跳过了CPU周期。让我们回顾一下。在CPU读取缺失时,如果要被替换的行处于独占修改状态,则必须在读入新数据到高速缓存之前将其从高速缓存写入主存,使用Copyback总线命令来完成。然后,与在任何其他状态的行上发生的CPU读取缺失一样,放置了Read Shared命令在总线上,数据的所有者作出响应,任何散播高速缓存都有机会断言tf*,告诉请求的高速缓存该行确实是共享的。该行复制到高速缓存中,如果断言tf*,则该行标记为共享未修改,但是如果没有断言tf*,则该行立即转换为排他未修改状态。当然,请求的高速缓存不必绝对将该行放入排他未修改状态,因为Futurebus+允许这种选择,但这是一种肯定可以增加整个系统性能速度的选择,因为它消除了执行随后的一次写-一次周期的需要。当读命中时,数据直接从高速缓存传输到CPU,没有任何总线交互或状态更改。
在写命中时,独占修改行将被写入而无需总线交互或状态更改,而独占未修改行将被写入而无需总线交互,但会经历状态升级为独占修改。如果该行最初处于共享未修改状态,则其他副本必须被作废,因此缓存控制器必须在总线上发出作废命令。这是一个快速的、缩略版的写周期,其中数据从未放在总线上。然后可以写入缓存行,其状态将升级为独占修改状态。
写失效周期与读失效一样,在清空任何可能的脏行后都会以相同方式满足。驱逐处理方式与读失效相同:如果该行处于独占修改状态,则首先使用复制回总线命令将其写入主存储器。然后,对于任何行状态,要写入的行都将使用读修改命令从其所有者读取到缓存中,作废所有其他缓存副本,写入到缓存中并立即进入独占修改状态。忽略tf*的状态,因为读修改命令将使任何被监视的副本无效。Futurebus+的另一个独特特点是,它允许在分层总线结构(如图4.10所示)上维护一致性。解决这个令人费解的问题的关键是使用两种代理:内存代理和缓存代理。内存代理在总线的一侧接收读写命令,并响应这些命令,使其在桥的另一侧看起来是从一个简单的主存储器出现的一样。在另一侧,内存代理跟踪缓存和主存储器位置,监视每行的所有权,并逐个基础与每行的所有者进行交互。由于内存代理必须知道何时去主内存还是缓存获取数据的当前值,因此在桥的内部有缓存的内存代理不能使用独占未修改状态。独占未修改状态允许行进入独占修改状态而无需总线交互,因此内存代理无法发现所有权从该总线上的内存转移到缓存时的情况。如果再次查看表4.2,您可以看到仅在不激活tf*线的情况下服务于读失效时,才会进入独占未修改状态,而且只有设计师希望实现这一点。否则,读失效将将新行带入共享未修改状态。如果缓存驻留在由内存代理监视的桥的一侧,则对于内存读失效中的tf*的响应被简单地禁止。

缓存代理只需要关心缓存,因此它较为简单。基于包含性,缓存代理从它所代表的缓存使用的一侧桥上的总线进行嗅探。缓存代理知道其自己总线上缓存的每条线的地址,并仅允许通过与其保护的缓存中可能导致嗅探命中的那些嗅探周期通过到桥的另一侧。这是一种削减桥上流动的总线流量的方法。通过使用缓存代理和内存代理,可以在图4.10的分层结构中沿着层次结构传播嗅探,而不会将所有总线绑定在大量无用的嗅探流量中。
另一个新的多处理器总线规范是Corollary C-bus II。该规范非常类似于Futurebus+,因为它使用了改进版本的MESI协议,并支持写分配、32字节行和直接数据干预。为了支持所有这些,提供了两种特殊的读独占总线周期:读独占和清空独占。两者都用于支持写分配,清空独占命令在写入分配读周期之前使用脏行进行清空。通过使用这些周期从嗅探命中的副本中读入缓存的线将使所有其他缓存中的这些命令作废。
4.3.3 The MOESI Protocol
尽管MOESI(Modified, Owned, Exclusive, Shared, Invalid)的首字母缩写看起来与MESI几乎相同,但实际上它代表了一个非常不同的协议,因为它支持直接数据干预。MOESI的首字母缩写代表的状态与MESI中的使用的名称相同,只是增加了Owned状态,但状态的含义大多不同,并且与第4.2.5节中使用的长名称状态相对应,具体如下:Invalid仍然是Invalid状态;Shared也是最初被称为Valid的状态;Exclusive转变为Valid-and-written-to-once-by-this-CPU;Modified现在成为Valid-and-written-to-more-than-once-by-this-CPU-but-unsnooped;而Owned则表示Valid-and-written-to-more-than-once-by-this-CPU-and-snooped。Modified、Owned和Shared状态显示了MESI和MOESI之间的区别。在MESI中,如果一个处理器有一个Shared line,这意味着主存储器是当前的,并且该行在任何缓存中都没有被写入。在MOESI协议中,Shared表示这只是一个位置的副本,可能是当前的主存储器,但是是所有者数据的精确副本。在MESI中,Modified状态隐含地意味着该位置没有被监视,而在MOESI中,需要明确说明这一点。最后,Owned状态解释了MOESI的直接数据干预和MESI支持的间接数据干预之间的区别。Owned位置可能在其他缓存中有副本,类似于Shared line,但主存储器从未被更新,因此在驱逐时必须由拥有缓存进行写入。

表4.3显示了五个MOESI状态,并列出了在可缓存空间中处理器和嗅探读写命中和失效时的响应。与MESI一样,MOESI模型假设没有特殊的总线支持。状态转换在下面的段落中详细描述。
在CPU读失效时,如果目标行是Owned或Modified状态,它将被驱逐。然后,缓存会从主存储器或拥有缓存中更新该行,并将其装入为Shared状态。如果行来自拥有缓存,这意味着此缓存副本处于Owned或Modified状态,如果状态为Modified,则会降级为Owned。
在对Shared和Exclusive行进行嗅探读命中时,不会进行总线交互,但如果Exclusive副本收到嗅探命中,则会将其自身状态降级为Shared。
与MESI协议类似,所有主内存写入周期都会被其他缓存进行检查以进行失效操作。MESI和MOESI设计通常不使用写分配,这意味着CPU的写失效必须通过总线写入周期在系统中进行广播。然后,写入周期将类似于写透缓存一样通过缓存进行传输。在许多设计中,缓存会忽略写失效。忽略写失效的假设是,当写入时,如果缓存中没有该数据,则很可能在未来也不会引用它。对于加载到各个区域的初始化值等项目确实如此,但对于第一次设置内存中的循环计数器或堆栈达到某个特定深度时并非如此。然而,这些是例外而不是规则,因此MOESI缓存通常会忽略写失效。
在表4.3的示例中,我们选择了更新丢失行的较复杂方法。只有当缓存行与系统的CPU/软件支持的最小写入周期大小相同时才能这样做(并非所有软件在CPU提供时都使用字节写入周期)。如果允许较小的写入,并且要将写失效写入缓存,应考虑写分配协议。该表假设缓存仅支持字写入。由于该设计要求在写失效时替换修改行,因此总线随后必须进行两个周期,即逐出和写入(用于失效)替换行。
如果采用写分配,那么在五状态MOESI之上,以下几个三态和四态协议中的一个可能会被选中。在具有写分配的MOESI系统中,对修改的位置的写失效将消耗三个总线周期:首先是修改行的逐出,然后是读取周期以允许分配,然后是一次性写入周期以使其他缓存中的任何副本失效。这可能是一个相当大的惩罚!
在CPU写命中时,如果位置处于修改状态,则没有总线活动,行将更新并保持在修改状态。类似地,对标记为独占的行的CPU写命中将更新行而不触发任何总线事务,并且结果行将处于修改状态。如果行是共享或拥有的,则会更新行,触发一个写总线周期来使其他缓存中可能存在的任何副本失效,并将状态更新为独占。当写入一个拥有的行时,情况有些奇怪,因为一个被写入多次的较高状态被降级为独占状态,即被写入一次的状态。背后的理论是,拥有的行可能会在另一个CPU的缓存中被复制,因此必须通过总线周期来使可能在其他缓存中过时的任何副本失效。由于主内存总线假定不能区分不同类型的写入周期,因此在写侦听命中时进行失效而不是更新,以确保任何希望通过主内存写入周期将其副本转为独占状态的缓存都能够实现。一旦发生了这个总线周期,主内存就是最新的,没有必要再驱逐具有新独占行的缓存中的那行。由于在替换行时,拥有和修改状态都需要进行逐出操作,所以它们是不合适的,这使得独占成为了对拥有行进行CPU写命中后的唯一合理状态。当然,并不存在对无效位置的写命中。
无论原始状态如何,写取代命中总是会立即使被侦听的行失效。这可能会导致软件问题,就像在MESI缓存中一样,如果一个脏行或已拥有的行被失效。如果一个拥有的副本被逐出,那么同一行在其他缓存中的共享副本将被失效,这可能看起来有点浪费。这些缓存在逐出后必须返回主内存以获取它们之前具有的相同数据。此外,每次对已拥有位置的写命中周期都会发生这个过程。这可能排除了使用MOESI协议,而更倾向于使用较低的协议进行设计。MOESI的一个优点是,它不需要特殊的总线架构,只需一个优先级方案,以允许缓存在发生侦听写命中时关闭主内存。这在设计团队必须将多处理器一致性协议适配到原本没有考虑一致性的现有总线上时,是一个非常重要的考虑因素。
在侦听读命中中,修改的行将直接向请求的缓存提供数据,并且将其状态降级为已拥有。拥有的行将以相同的方式响应侦听读命中,而不会改变响应缓存的行状态。对于独占和共享位置的读侦听命中,缓存不会向总线提供数据,因为该缓存中的数据应该由所有者提供,无论所有者是否是主内存(对于独占行始终如此,但对于共享行可能是或者不是)。所有者可能是另一个缓存。然而,独占副本将降级为共享状态。如果侦听的读命中是对已拥有或修改的行的命中,数据将由缓存提供给总线,并且该行的最终状态将是已拥有。不,无效行不会发生侦听读命中。
相比MESI,MOESI协议的吸引力在于它使用直接数据干预。这加快了处理器间通信的速度,但增加了缓存复杂性,因为缓存现在必须跟踪每行的五个状态,而不是四个。这两种协议都使用标准的主内存写入周期来使其他缓存中的匹配位置失效,并且都不使用写分配,这种方式更复杂,但实际上有助于简化下面将讨论的一些协议。
4.3.4 N+1

Synapse为其N+1容错计算机开发了一种不同的协议(见表4.4)。该协议使用智能主存(在第4.2.5节中描述)并为每个潜在高速缓存行维护一个称为使用模式位的单个位,以指示该行是由主存还是由高速缓存拥有。使用模式位告诉该行是公共的还是私有的。这个额外的位消除了高速缓存需要从主内存中抢占响应系统总线周期的需要,因为如果设置了使用模式位,则主存将禁用自己的响应,从而比串联或其他成本较低的禁止内存方案节省了一些时间。 N + 1的主存还通过在所有内存板上使用15个条目队列来加速其自身的性能。
N+1系统中的每个CPU卡都包含一个16K字节的写回物理缓存,其中每行有两个状态位:有效位和数据修改位。有效位用于表示缓存行的有效性,而数据修改位用于跟踪主存中数据的一致性。Synapse声称能够在系统上支持多达28个处理器,每个处理器都会逐渐增加系统的吞吐量。
该协议只使用了三种状态:无效、有效和脏。这三种状态有效地实现了其他四状态一致性协议所能实现的功能(有些将这些状态称为无效、共享和修改,或者MSI)。对三状态方法的支持采用了两种形式。首先,对于所有写未命中和写命中到有效位置的写入周期,都使用了写分配,这样可缓存地址的总线写操作仅仅是因为驱逐或读取嗅探命中导致的。其次,总线允许两种类型的读取周期:公共读和私有读。公共读周期仅用于读未命中周期的行更新,其中主内存使用模式位保持复位(表示主内存仍然拥有缓存行),并且其他有效副本可以存在于其他缓存中。私有读周期适用于所有写分配周期,以及特别为写未命中周期或写入到有效位置并转为脏状态而产生的行更新读取周期。
该协议有一个有些奇怪的地方是,对于尚未处于脏状态的位置的写命中,缓存会使用私有读命令从主存重新读取该位置。该行先前会通过使用公共读命令进行读未命中读取。重复的读周期会在发生未命中写周期之前使任何其他缓存副本失效,并设置主存储器的使用模式位。新获取的行将作为脏行加载到缓存中。奇怪的是,这个过程没有针对大多数代码执行写周期的方式进行优化。写通常是针对先前读取过的位置进行的。对于N+1协议来说,这意味着大多数写周期会导致两次主存读周期:一次用于读取数据的公共读周期,一次用于写入的私有读周期。这和MESI所需的周期数一样多,因为MESI首先执行主存读取,然后进行第一次写入,但是N+1协议比其他一些涉及更少主存周期的协议产生了更多的总线流量。
在对N+1进行逐步分析时,我们可以看到熟悉的事实,即所有CPU读命中都立即从缓存中服务,无需总线交互或状态变化。在CPU读未命中时,如果目标行是脏行,它将被驱逐,而无论如何,公共读总线命令都会从主存中获取更新的行。新行被存储为有效。
对于CPU写命中,如果该行是脏行,则数据将被覆写,而无需状态更改或总线交互。然而,如果该行是有效行,则会再次从主存中获取,这次使用私有读周期,这将向被嗅探缓存表明它们应使其拥有的该行副本失效,并设置主存储器的使用模式位。本地缓存将立即将该行置为脏态,并将新数据覆盖该行的相应部分。类似的周期用于支持CPU写未命中周期,因为N+1协议是基于写分配的。如果未命中的行是脏行,则会被驱逐。无论现有行的状态如何,私有读都将用于执行写分配,并使该行以脏态结束,新数据覆盖主存提供的内容。所有私有读周期都在主存储器中设置使用模式位。每个潜在的缓存行都有一个使用模式位,该位告诉主存储器不响应对该地址的读取周期,而是让持有脏行副本的缓存来处理。这个额外的位使得主存储器能够比使用更典型的串联优先级机制时更快地响应读取周期。
N+1协议中的嗅探相对简单。如果被嗅探行的起始状态为无效,不会执行任何操作。如果被嗅探行标记为有效,则只会响应由私有读引起的嗅探命中,此时该行将被设置为无效。对于被嗅探的公共读命中或私有读命中的脏行,将使用间接数据干预。这意味着读取周期将被中止,具有脏行副本的缓存将其副本写回主存,并清除使用模式位,然后允许重新尝试读取,并使脏行失效(我不确定为什么对于公共读来说,脏行不会简单地降级为有效)。这就是使用模式位处理的平衡之处。当私有读将一行移入缓存时,该行被设置为脏态,并设置使用模式位。当脏行降级时,更新主存储器,并清除使用模式位。
你会注意到在表4.4中,没有考虑写嗅探命中的可能性,这有些奇怪。为什么?因为N+1协议要求进行写分配,所有对共享或其他缓存位置的写入周期都是由读取周期或使其失效引起的。这意味着在主存储器总线上的写活动仅包括驱逐脏位置(因为没有其他缓存会包含脏行的副本),以及对非可缓存区域的写入。因此,甚至无需包括处理嗅探写命中的逻辑。似乎该架构不使用直接内存访问(DMA)来处理任何磁盘I/O。
4.3.5 Berkeley
伯克利协议在表4.5中有详细的说明。它有四种状态,就像MESI协议一样,分别是无效(Invalid)、未占有(Unowned)、非独占占有(Owned Non-Exclusively)和独占占有(Owned Exclusively)。这些状态与MESI状态相对类似,唯一的区别在于非独占占有状态是共享修改(Shared Modified)状态。MESI的共享状态与伯克利的未占有状态相似(因此我们可以假设伯克利团队没有接受如果没有缓存拥有一行的副本,那么主存储器就是所有者的观念),MESI的修改状态类似于伯克利的独占占有状态,而任何两个协议的无效状态之间几乎没有什么区别。无效就是无效。但是非独占占有状态是一种主存储器与缓存副本不一致的状态,尽管可能存在多个缓存副本。


根据表4.5,对于无效或未占有状态的缓存行,CPU读未命中将仅使用常规读周期更新该行,但是对于非独占占有或独占占有状态的行,该行是脏的,必须被驱逐。这两种状态的区别在于非独占占有行可能在另一个缓存中复制,尽管它与主存储器不一致。在驱逐后,该行的更新方式与其他两种状态相同,并且在所有四个周期中,该行最终处于未占有状态。与其他缓存协议一样,对于CPU读命中,可以直接从缓存中满足请求,而无需更改状态。
读者会注意到,伯克利协议使用术语“常规读取”和“常规写入”,而不是其他协议中使用的更清晰的“公共读取”和“公共写入”。意思是相同的,只是术语不同。常规读取是由CPU读未命中、对非可缓存地址的读取或DMA活动引起的读取周期。常规写入仅来自DMA活动或非可缓存写入周期。CPU写未命中会导致写分配周期,其中必须使用读取-获取所有权总线周期来使其他缓存副本无效。正如我们在Futurebus+和N+1协议中所看到的,使用写分配需要使用特殊的总线周期来支持某种类型的无效读取周期。如果被替换的行最初是非独占占有或独占占有状态,那么在CPU写未命中被服务后,该行的状态变为独占占有。
CPU的写命中与我们所研究的其他协议处理方式不同。如果缓存行在开始时是未拥有或非独占拥有状态,其他副本将需要被作废,因此缓存发送一个用于作废的写请求周期到总线上,就像在MESI或MOESI的写一次周期中发生的那样。作废写请求周期是一种快速写操作,不会更新主存,但会作废其他缓存中匹配的缓存行,就像Futurebus+中的作废信号一样。然后,该行的状态被更改为独占拥有。如果该行最初是独占拥有状态,则只需进行更新而无需任何总线交互。如果该行最初是无效状态,则无法进行CPU的写命中周期。
除了特殊的读写操作外,Berkeley协议还需要支持两个总线机制:写分配和直接数据干预。正如我们在其他协议中看到的那样,写分配需要两种类型的读周期:常规读取和读取以获取所有权。当正在进行写分配周期时(即缓存正在更新从写失效周期中合并进来的数据的缓存行),会发出读取以获取所有权。直接数据干预用于允许拥有缓存向请求缓存提供替换的缓存行,供两种类型的读周期使用。由于存在两种类型的读周期和三种写周期,Berkeley协议中的嗅探涉及很多可能性。这两种读取是常规读取,用于在CPU读取缺失和DMA读取周期时替换缓存行;以及读取以获取所有权,用于支持写分配的读取部分,并且必须导致作废其他缓存副本的请求行。三种写周期是常规写入,由CPU用于写入非缓存地址和DMA设备;用于将缓存行从非独占状态变为独占状态并作废系统中任何其他缓存副本的作废写请求;以及在驱逐过程中用于将需要替换的脏缓存行移回主存的不作废写请求。
在常规读取周期中,仅会对属于被嗅探缓存拥有的缓存行进行响应。这意味着响应将来自于具有该行拥有非独占或拥有独占状态的缓存。在缓存禁用主存响应之后,数据从缓存传输到读取设备,缓存行的最终状态是拥有非独占状态。在这些干预周期中,主存不会更新。这允许在必要时进行快速的缓存之间的传输,但将慢速主存写入的数量减少到绝对最低限度。对于嗅探的作废请求周期,相同的过程发生,只是该行的最终状态是无效的,即使该行最初处于未拥有状态。
嗅探的常规写入和作废写请求周期都会导致任何状态的缓存行变为无效。尽管系统支持比CPU最短的写周期更长的缓存行,但不会丢失数据,因为作废写请求周期仅会对在发出作废写请求命令的缓存中完全匹配的缓存行进行命中。由于它们仅在CPU对原始缓存中的缓存行进行写命中时才会发出,因此作废写请求命中只会遇到处于非独占状态(未拥有和拥有非独占状态)的缓存行。如果该行存在,则在任何其他缓存中不能存在独占拥有状态。因此,常规写入和作废写请求周期之间的区别仅在于主存对它们的响应方式。作废写请求周期不需要主存自身更新,因为该写周期结束时,该行已经由缓存拥有,所以唯一需要持续足够长时间以便主存能够接受副本的是常规写入。
在没有作废的情况下,只有具有未拥有状态的匹配行的缓存可以进行嗅探,因为发出方的缓存行处于拥有非独占或拥有独占状态。在未作废周期的嗅探命中期间,被缓存行的所有权将从拥有该行的缓存移至主存,因此具有未拥有行的缓存对此事务实际上不需要关心。因此,在嗅探的情况下,没有对未作废写请求周期的响应。
其中三种写周期中的两种,作废写请求和无作废写请求,用于缓存数据传输。作废写请求在对未拥有或拥有非独占行的写命中周期中使用,允许作为写入处理器将其自己的缓存行副本转变为拥有独占状态时作废所有嗅探缓存的匹配行。无作废写请求在读取或写入缺失周期中替换拥有独占或拥有非独占行的驱逐过程中使用。无作废写请求周期利用了写入缓存明确知道不存在其他缓存副本的事实,因此它不会打断其他缓存的嗅探周期,因为它预先知道这些周期对除了减慢其他处理器之外不会产生任何影响。如果在多路复用的缓存标记RAM设计中实现了Berkeley协议,并且每次需要嗅探缓存标记RAM时处理器都会停止运行,这一点非常重要。
下面是两个Berkeley协议缓存如何相互作用的示例。假设在一个以Berkeley协议为基础构建的系统中只有两个CPU/缓存子系统。同一行可以在这两个缓存中以Unowned/Unowned或Unowned/Owned Non-Exclusively的方式处于Valid状态。在第一种情况下,主存将包含最新版本的缓存行,但在第二种情况下,两个缓存都包含比主存中更更新的数据。当该行被替换时,拥有Non-Exclusively副本的缓存需要更新主存。因此,总结一下,CPU的写未命中会导致Read-for-Invalidation。如果命中的行不处于Owned Exclusively状态,则CPU的写命中会导致带有Invalidation的Write操作,并且如果该行处于Owned Exclusively状态,则不会引起总线周期。读未命中会导致常规读取周期,并且如果可能,会进行直接干预。驱逐通过无Invalidation的Write操作来处理。对于Owned Exclusively行的常规读取命中会导致干预,并且该行的状态会降级为Owned Non-Exclusively,但如果该行处于Unowned或Owned Non-Exclusively状态,则数据由主存提供且状态不变。Read-for-Invalidation的嗅探命中会导致作废。常规写入和Write-for-Invalidation的嗅探命中会导致作废,并且如果它们引起对Unowned行的嗅探命中,则会忽略无Invalidation的Write操作,这是该周期唯一能够触发的嗅探命中类型。哇!
4.3.6 University of Illinois


伊利诺伊大学协议(表4.6)支持与Futurebus+、Berkeley和MOESI协议类似的直接数据干预。与Berkeley、N+l和Futurebus+类似,这需要比MOESI更多的总线支持,因为伊利诺伊协议具有读取-所有权周期、传统读取周期和使正在读取的缓存得知所读取的行是来自另一个缓存还是来自主存储器的周期,以及一个信号,它类似于Futurebus+中的tf*信号。在这个系统中,主存储器被设计为对同时的读取和写入命令作出响应,就像对简单的写入周期一样,因为在干预过程中,这就是伊利诺伊协议通过同时在总线上放置这两个周期来支持的。此外,与N+1中一样,使用写入分配来消除各个缓存需要监视总线写入流量的需要。伊利诺伊协议中使用的四个状态为无效(Invalid)、共享(Shared)、有效-独占(Valid-Exclusive)和脏(Dirty),它们都与MESI协议中的相应状态非常相似。唯一支持多个缓存中同一行副本的状态是共享状态。从无效状态开始,假设我们遇到一个CPU读取未命中的周期。缓存向所有其他缓存和主存发出标准的读取周期。主存具有最低的响应优先级,第一个接收到嗅探命中的设备将把数据传输给请求的缓存,即使该行只处于共享状态。如果该设备具有有效-独占或脏副本,它会将该行的状态降级为共享。此外,如果嗅探到的副本是脏的,则响应的缓存会通过使用主存写入周期将该行的副本同时复制回主存储器,这与引起嗅探命中的读取周期同时进行。发起请求的缓存将看到数据不是来自主存储器,并将该行的状态设置为共享。如果没有其他缓存响应,主存储器将向请求的缓存提供数据,请求的缓存看到信号表明副本来自主存储器,并将其行的状态设置为有效-独占。与Futurebus+一样,嗅探命中的通知可以消除对某些行执行一次写入的需要,从而减少总线流量。对于共享或有效-独占位置的CPU读取未命中处理方式与刚才描述的CPU读取未命中相同。然而,如果未命中的行是脏的,将引发一次驱逐,然后再进行相同的CPU读取未命中周期。与任何其他缓存一样,读取命中不会导致任何总线活动,并且命中行的状态不会改变。一个稍微不寻常的可能性是,共享行可以被覆盖,而其他处理器却没有注意到缓存行减少了一个副本。这意味着有时处理器将拥有缓存行的唯一副本,并且将该行标记为共享状态,即使它可以合法地将该行的副本标记为有效-独占状态,这将使其在需要对该行进行写入时能够进行更快的写入周期。尽管这可能会对性能产生极小的影响,但它不会影响一致性协议的可靠性。
由于缓存使用写入分配策略,所有CPU的写入未命中周期与其相应的读取未命中周期类似,唯一的区别是使用了一个读取-所有权周期来表示请求的缓存打算立即将该行置为脏状态。如果该行已经被缓存,包含该行副本的优先级最高的缓存将向请求的缓存提供该行,并且所有遇到嗅探命中的缓存都将立即使自己的副本无效。与在本文中讨论的大多数其他一致性协议不同,在一个缓存中将一个缓存行从脏状态转换为另一个缓存中的脏状态不是一个问题的迹象。如果最短的CPU写入周期小于缓存行长度,则通常使用写入分配方案,因此一个缓存/CPU只能写入到一个线路的最低有效字节或字,而下一个缓存/CPU将写入到更高有效字节或字。另一个关于从脏状态转换为脏状态的特殊之处在于,在这种情况下不需要更新主存储器。如果在主存储器中没有构建反射功能,就没有理由期望主存储器保存该线路临时版本的副本。
只有一种类型的CPU写入命中周期会产生总线活动,那就是对共享位置的CPU写入命中。当处理器写入一个已经处于共享状态的行时,缓存控制器会在主存储器总线上发送一个使所有其他共享副本立即无效的无效周期。根据伊利诺伊协议的性质,除了共享状态之外,其他状态都不会存在匹配的行。发送无效信号的缓存现在可以将其行置为脏状态。对有效-独占或脏位置的CPU写入命中将导致立即进行写入周期,并且在脏状态下结束周期。
由于使用了写入分配,嗅探写入命中根本不会发生。任何被写入的内容要么位于不可缓存空间,要么是因为驱逐了一个脏行而被写入,而且一个脏行只能在一个缓存中复制,所以不会发生嗅探命中。
4.3.7 Firefly

DEC Firefly是一种不寻常的实现,因为它从未使用无效状态。如表4.7所示,只有三种状态:共享、有效排他和脏。没有无效状态的缓存是如何工作的?它类似于我们在2.1.3节中查看的缓存,该缓存根本不使用状态位,但通过禁用缓存直到启动例程已给所有缓存行一个替换机会,保证所有位置在冷启动后都是有效的。与MESI和MOESI不同,来自一个CPU /缓存的写周期不会导致其他缓存中匹配地址的失效。相反,Firefly协议要求被嗅探的缓存在嗅探写命中周期期间更新其内容。所有对于本地缓存中不处于独占状态的缓存位置的CPU写周期都会被传播到主内存总线上。因此,对于在共享缓存中未处于独占状态的缓存行的CPU写周期会更新主内存和所有被嗅探的缓存位置。这是广播,并且在4.2.5节中描述过。这个特殊的协议需要一个总线信号来指示另一个处理器的缓存中发生了嗅探命中(与Futurebus+和伊利诺伊大学协议一样),并且与Futurebus+、伊利诺伊大学协议、伯克利和N+l一样,Firefly使用写分配来减少处理写嗅探命中的复杂性。所有总线写周期都可以分为以下三类:1)它们不在可缓存区域内,因此一致性不是问题;2)它们是对于共享缓存行的,因此被具有相同缓存行的共享副本的其他缓存观察到;或3)它们来自脏行的淘汰,因此没有其他缓存可以拥有副本。任何观察到嗅探命中的缓存将不仅告诉其自己的缓存控制器发生了这种情况,还会像Futurebus+中的tP信号那样向整个总线发出信号。这使得起始事务的CPU /缓存可以确定如何处理缓存行。与本章讨论的大多数一致性协议不同,只使用一种读取和写入周期,整个协议通过单个嗅探命中信号处理。 您现在已经注意到,这个缓存与许多其他协议一样,表现出好像它是两种不同类型的缓存,具有两种不同类型的写策略,这取决于正在写入的线路的当前状态。标记为共享的行被视为写通,除非没有其他缓存响应(通过嗅探命中)到这些位置之一的主内存写周期。任何被标记为有效独占或脏的行都被视为如果它在单处理器回写缓存中,直到遇到读嗅探命中。这与4.2.3节中讨论的通过写通策略轻松确保一致性与通过复制回缓存提供的总线带宽的改进的讨论相吻合。只有真正共享的缓存行将使用宝贵的总线带宽。与伊利诺伊协议一样,由于读取嗅探命中而干预的尝试主存读取周期会立即导致驱逐缓存在同一内存周期内启动写命令。这意味着主内存必须响应组合读取和写入命令,就好像它只是一个写周期。总之,缓存的行为就像对于在其他缓存中复制的行是写通的。如果没有其他缓存的缓存副本,则缓存使用复制回写策略。所有嗅探命中都通过嗅探缓存标记标记来响应和主存总线。所有被嗅探的缓存同时提供该行。总线冲突不是问题,因为所有被嗅探的缓存都在同一个总线周期内响应。对共享位置的嗅探命中不会改变位置的状态。对有效独占或脏位置的嗅探命中会将这些位置降级为共享状态。通过任何共享读取或写入活动期间缺乏嗅探命中,行可以升级为有效独占,并在写分配的读周期期间自动加载为脏。对于有效独占线路的CPU写命中周期将升级这些线路的状态为脏,而无需总线交互。
4.3.8 Dragon


施乐帕克(加利福尼亚州的帕洛奥多研究中心,同样也是给我们带来了苹果麦金塔的图形用户界面)设计了一种名为龙(Dragon)的多处理协议(表4.8)。该协议是另一种四态版本(共享干净、共享脏、有效独占和脏),但它与Firefly和Futurebus+类似,使用一个信号来指示总线上的嗅探命中,并且像Firefly一样,它没有无效状态。与Firefly和伊利诺伊大学协议不同,系统总线读取周期不会突然转换为主存写入周期。在系统总线上出现的唯一内存写入周期是非可缓存的写入和驱逐操作。驱逐操作可能导致两种状态:共享脏和脏。它们确实是所有权状态,但在开发这个术语时似乎小心地避免了使用"owned"这个词。
总线确实需要支持一种第三种写入周期,即更新其他高速缓存而不是主存的周期。通过使用这种比主存写入周期要快得多的写入周期,高速缓存行的所有者可以使用广播方式更新所有其他相同高速缓存行的副本,就像Firefly中所发生的那样。与Firefly和Futurebus+一样,在总线上会发出嗅探命中信号,并且每条缓存行都设置为根据总线事务中该信号的状态,在后续CPU写命中周期中使用写透或写回协议。
按步骤执行协议时,高速缓存读取未在其他高速缓存中产生嗅探命中的缺失将作为有效独占行加载到高速缓存中。如果它们产生嗅探命中,则以共享干净(即未拥有)状态加载这些行。响应缓存将其行从有效独占状态转换为共享干净状态,或者从脏状态转换为共享脏状态,但如果嗅探到的行已经是共享干净或共享脏,则其状态不会改变。如果被嗅探到的行处于脏状态,则读取周期将从被嗅探到的高速缓存中满足。如果被嗅探到的行最初是有效独占或共享干净状态,则被嗅探到的高速缓存不会在总线上放置数据,而是将该任务交给主存处理。当然,总线经过设计允许被嗅探的高速缓存禁止主存响应。最后,如果CPU的读取缺失针对本地高速缓存中的脏或共享脏行,则必须将该行驱逐。自然地,读取命中不会产生总线流量,并且可以从本地高速缓存中满足,而不会引起状态改变。
对于CPU的写入缺失,由于采用了写分配方案,其步骤与上一段详细描述的读取缺失相同,只是如果没有其他缓存响应,则加载的行将被标记为脏。如果其他缓存响应,请求的缓存将将其行设置为共享脏,并向其他缓存广播Cache Write周期。这是一个特殊的总线周期,将一行数据写入其他缓存,而不写入主存。作为对此写入周期的响应,任何其他共享脏副本都将更改其状态为共享干净。在此分配的读取部分过程中,任何有效独占的被嗅探副本已经转换为共享干净状态,而脏的被嗅探副本已经将其状态更改为共享脏,因此在Cache Write嗅探命中中不会遇到有效独占和脏状态。
仔细观察,你会发现对于另一个高速缓存中被嗅探为脏的CPU写入缺失,首先在分配的读取过程中将脏副本转换为共享脏状态,然后在循环的Cache Write部分将其从共享脏状态改变为共享干净状态。这是一个相当复杂的两步操作!如果请求缓存中的缺失行也是脏的,那么总线流量的开销也很大。将一条脏或共享脏行的CPU写入缺失替换为从另一个缓存中读取的脏或共享脏行将需要三个总线周期。首先,必须进行驱逐以腾出空间放置新行。其次,请求的CPU/缓存执行读取周期,并将请求的数据从中间缓存读入请求的高速缓存。最后,请求的高速缓存执行Cache Write以更新满足第一个请求的中间缓存中的行!
两种写命中周期分别模拟写透和回写协议。如果该行已经是脏或有效独占状态,那么缓存就会像单处理器系统中的回写缓存一样,只写入缓存,而不会引起任何总线流量。在任一周期结束时,该行的状态将为脏。如果该行是共享干净或共享脏,则"Tite"周期将通过Cache Write周期广播到所有其他高速缓存,但不会广播到主存。在任何一个这些周期结束时,该行的状态将为共享脏,除非没有其他高速缓存响应,在这种情况下,该行可以开始像使用回写策略一样运行,并且状态立即变为脏。主存只有在从拥有缓存中驱逐脏或共享脏行时才更新,即具有脏或共享脏行的缓存。这是使用内存写入周期执行的。由于内存写入周期仅在驱逐或写入不可缓存地址时发生,因此它在被窃听的缓存中所引起的反应是直接的。在另一个缓存中,这种周期可能的唯一状态是共享干净,仅在被驱逐的行是共享脏时才会发生。在这种情况下,驱逐没有任何效果,因为驱逐的行已经与其他缓存中相同行的内容匹配。共享干净行不需要作废,就像在没有同步支持的总线上一样。脏行在另一个缓存中可能没有对应项,因为脏是独占状态。
4.3.9 Others
有几个良好的设计示例可以研究,以了解在回写多处理器系统中如何解决一致性问题的方法。这些包括商用微处理器,它们都有很好的文档,并针对这些确切的问题使用自己的解决方案,缓存控制器,以及一些可以在专业期刊上研究的小型计算机和大型机架构,比如《计算机系统交易》或甚至《电子设计杂志》。
我在这里尝试展示足够的替代方案来引发您的思考,但由于同样非常重要的原因,我避免在整本书中给出任何系统性能的具体数据。首先,您的软件与迄今为止用于运行任何缓存统计的软件都不同。这意味着,如果您使用其他人的统计数据,您将误导自己。其次,您的系统与其他系统不同,因此减少主存读写的收益程度将对您选择的读写策略、行大小甚至一致性协议非常重要。务必在承诺缓存设计之前,努力在实际系统上测量真实统计数据。任何其他方法都将是冒险。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值