第七章 监听一致性协议 A Primer on Memory Consistency and Cache Coherence - 2nd Edition
第七章 监听一致性协议
本章将介绍监听一致性协议。监听协议是最早广泛部署的协议类型,目前仍在各种系统中使用。与目录协议(第八章)相比,监听协议具有许多吸引人的特性,包括低延迟的一致性事务和概念上更简单的设计。
我们首先在 7.1 节对监听协议进行高层介绍,然后在 7.2 节展示一个采用完整但简单的三状态(MSI)监听协议的系统。该系统和协议作为基线,后续将在此基础上添加系统特性和协议优化,包括增加独占状态(7.3 节)、拥有状态(7.4 节)以及高性能互连网络(7.5 和 7.6 节)。接着讨论采用监听协议的商用系统(7.7 节),最后在 7.8 节探讨监听协议的未来。若读者不想深入细节,可略过 7.3-7.6 节。
7.1 监听协议概述
监听协议基于一个核心思想:所有一致性控制器以相同顺序 “监听”(snoop)一致性请求,并协同 “执行正确操作” 以维护一致性。通过要求发往同一缓存块的请求按顺序到达,监听系统使分布式一致性控制器能够正确更新代表缓存块状态的有限状态机。
传统监听协议将请求广播到所有一致性控制器(包括发起请求的控制器),请求通常通过有序广播网络(如总线)传输。有序广播确保所有控制器以相同顺序观察到一系列请求,即请求具有全局顺序。由于全局顺序涵盖了每个块的顺序,这保证了所有控制器能正确更新缓存块状态。
为说明按块顺序处理请求的重要性,考虑表 7.1 和 7.2 的示例:核心 C1 和 C2 均希望获取处于 M 状态的块 A。在表 7.1 中,三个控制器观察到相同的块请求顺序,协同维护了单写多读(SWMR)不变量,块所有权按 LLC / 内存→C1→C2 转移。每个控制器根据观察到的请求独立得出正确的块状态。相反,表 7.2 显示,若 C2 与 C1 和 LLC / 内存观察到的块请求顺序不同,将导致不一致:首先,C1 和 C2 同时持有 M 状态块,违反 SWMR 不变量;随后,无控制器认为自己是所有者,导致请求无法响应(可能引发死锁)。
传统监听协议为所有块的请求创建全局顺序(尽管一致性仅需按块顺序)。全局顺序便于实现要求内存引用全局有序的一致性模型(如 SC 和 TSO)。以表 7.3 为例,涉及块 A 和 B,每个块仅被请求一次(满足按块顺序),但 C1 和 C2 对 GetM 和 GetS 请求的观察顺序乱序,导致违反 SC 和 TSO 模型。
侧边栏:监听如何依赖请求全局顺序
初看表 7.3 的问题,读者可能认为是由于 C1 持有 M 副本而 C2 仍有 S 副本,导致块 A 在周期 1 违反 SWMR 不变量。但表 7.4 展示了相同场景但强制请求全局顺序的情况:直到周期 4 前与表 7.3 一致,存在表面上的 SWMR 违反,但由于核心以相同顺序看到请求,C2 在读取块 B 新值前已使块 A 无效。因此,当 C2 读取块 A 时必须获取新值,从而保证 SC 和 TSO 执行正确。
传统监听协议通过请求全局顺序,基于监听顺序的逻辑时间确定请求的观察时刻。在表 7.4 中,由于全局顺序存在,C1 可推断 C2 会先看到块 A 的 GetM 请求,因此 C2 无需在接收消息时发送特定确认。这种请求接收的隐式确认,是监听协议与下章讨论的目录协议的关键区别。
我们在侧边栏进一步讨论全局顺序的深层需求。要求广播请求按全局顺序被观察,对实现传统监听协议的互连网络有重要影响:由于多个控制器可能同时发起请求,网络必须将请求序列化以形成全局顺序,该机制称为协议的序列化点(ordering point)。一般流程为:控制器发起请求→网络在序列化点排序并广播→发起控制器通过监听请求流确定自身请求的顺序。以总线系统为例,控制器通过仲裁逻辑确保总线上同一时刻仅传输单个请求,仲裁逻辑即序列化点,决定请求在总线上的顺序。一个微妙但关键的点是:仲裁逻辑对请求排序的瞬间即确定其顺序,但控制器可能需通过监听总线(观察前后请求)来感知这一顺序,因此控制器可能在序列化点之后若干周期才观察到全局顺序。
至此我们仅讨论了请求而非响应。这是因为监听协议的核心围绕请求设计,对响应消息的约束极少 —— 响应可通过独立网络传输,无需支持广播或顺序要求。由于响应携带数据(通常比请求长很多),使用更简单、低成本的网络传输响应有显著优势。值得注意的是,响应不影响一致性事务的序列化:逻辑上,包含广播请求和单播响应的事务在请求排序时即视为发生,与响应到达时间无关。请求出现在总线到响应到达请求者的时间间隔会影响协议实现(如期间是否允许其他控制器请求该块?如何处理?),但不影响事务的序列化顺序。
7.2 基线监听协议
本节将介绍一种简单、未优化的监听协议,并描述其在两种不同系统模型上的实现。第一个简单系统模型展示了实现监听一致性协议的基本方法,第二个稍复杂的基线系统模型则说明,即使是相对简单的性能改进也可能影响一致性协议的复杂度。这些示例揭示了监听协议的关键特性,同时暴露了效率问题,正是这些问题促使后续章节引入优化特性。7.5 节和 7.6 节将讨论如何将此基线协议适配到更先进的系统模型中。
7.2.1 协议高层描述
基线协议仅有三种稳定状态:M、S 和 I,通常称为 MSI 协议。与 6.3 节的协议类似,该协议假设采用回写缓存。除非块在某缓存中处于 M 态,否则其所有者为 LLC / 内存。在给出详细规范前,我们先通过高层抽象说明协议的基本行为。图 7.1 和 7.2 分别展示了缓存控制器和内存控制器的稳定状态转移。
需注意三个符号约定:
图 7.1 中,状态转移弧标注的是总线上观察到的一致性请求,有意省略了加载、存储和一致性响应等其他事件;
缓存控制器的一致性事件标注 “Own”(请求发起者)或 “Other”(非发起者);
图 7.2 中,内存块状态采用缓存中心法命名(如内存状态为 M 表示某缓存中块处于 M 态)。
7.2.2 简单监听系统模型:原子请求与原子事务
图 7.3 所示的简单系统模型与图 2.1 的基线模型几乎相同,唯一区别是将通用互连网络具体化为总线。每个核心可向缓存控制器发起加载 / 存储请求,缓存控制器在需要时选择驱逐块。总线确保一致性请求的全局顺序,所有控制器通过监听总线获取请求。与前章示例类似,该系统模型的原子性特性简化了一致性协议,具体包括以下两点:
原子请求(Atomic Requests):一致性请求在发起周期内完成排序,避免了请求发起至排序期间因其他请求导致的块状态变更;
原子事务(Atomic Transactions):一致性事务具有原子性,同一块的后续请求需在前序事务完成(响应出现在总线上)后才能发起(不同块的请求不受此限制)。
尽管比当前多数系统简单,该模型与 20 世纪 80 年代成功的 SGI Challenge 机器类似 [5]。
协议详细规范
表 7.5 和 7.6 给出了简单系统模型的详细一致性协议。与 7.2.1 节的高层描述相比,最大区别是缓存控制器增加了两个临时状态,内存控制器增加了一个临时状态。由于简单系统模型的原子性限制大幅减少了消息交错的可能,该协议的临时状态极少。
回顾思考题 6:在 MSI 监听协议中,缓存块只能处于三种一致性状态。对吗?
答案:错误!即使最简单的系统模型也存在超过三种状态,因为有临时状态。
表中阴影单元格表示不可能(或至少错误)的转移。例如,缓存控制器不应接收未请求块的 Data 消息(如缓存中处于 I 态的块);“(A)” 标注的单元格因原子事务约束而不会出现(当前事务完成前禁止其他请求)。空白单元格表示无需操作的合法转移。表格省略了许多实现细节,本章后续协议也省略了与其他核心事务 Data 消息对应的事件(核心无需对总线上其他核心事务的 Data 消息采取行动)。
与所有 MSI 协议一样,加载操作可在 S 态和 M 态命中,存储操作仅在 M 态命中。加载未命中时,缓存控制器发送 GetS 请求;存储未命中时发送 GetM 请求<sup>2</sup>。临时状态 ISD、IMD、SMD 表示请求已发送但未收到数据响应(Data)。由于请求已排序,事务逻辑上已确定,块分别对应 S、M、M 态,但加载 / 存储需等待数据到达<sup>3</sup>。数据响应出现在总线上后,缓存控制器将数据块写入缓存,转入相应的稳定态 S 或 M,并执行挂起的加载 / 存储操作。
系统模型的原子性从两方面简化了缓存未命中处理:
原子请求确保缓存控制器升级块权限(如 I→S、I→M、S→M)时,无需担心其他核心请求插队,可直接转入 ISD、IMD、SMD 临时态等待响应;
原子事务确保当前事务完成前,同一块的后续请求无法发起,避免了在临时态处理其他请求的复杂性。
数据响应可来自内存控制器或持有 M 态块的其他缓存:
持有 S 态块的缓存可忽略 GetS 请求(由内存控制器响应),但需在收到 GetM 请求时使块无效以维护一致性;
持有 M 态块的缓存必须响应 GetS 和 GetM 请求,分别发送数据响应并转入 S 态或 I 态。
LLC / 内存有两种稳定态(M、IorS)和一种临时态(IorSD):
在 IorS 态,内存控制器作为所有者响应 GetS 和 GetM 请求(表示无缓存持有 M 态块);
在 M 态,内存控制器不直接响应数据请求(M 态缓存为所有者且持有最新数据),但收到 GetS 请求时,缓存会转入 S 态,内存控制器需获取数据、更新内存并准备响应后续请求,因此转入临时态 IorSD 等待所有者缓存发送数据。
缓存控制器因替换策略驱逐块时,可能触发两种一致性降级:S→I 和 M→I。其中:
S→I 降级可 “静默” 执行(直接从缓存驱逐块,无需与其他控制器通信),因为其他控制器状态不受影响;
M→I 降级必须通信(M 态块是系统唯一有效副本,不可直接丢弃),缓存控制器需在总线上发送 PutM 请求并将数据写回内存控制器。LLC 收到 PutM 请求后转入 IorSD 态,收到 Data 消息后转入 IorS 态<sup>4</sup>。
原子请求特性通过防止 PutM 请求排序前出现降级请求(如其他核心的 GetM 请求),简化了缓存控制器;原子事务特性则确保 PutM 事务完成前禁止其他请求,简化了内存控制器。
运行示例
表 7.7 展示了一个典型场景的协议执行过程,初始时所有缓存块处于 I 态,LLC / 内存处于 IorS 态。
核心 C1(加载未命中)和 C2(存储未命中)均请求同一块,C1 的 GetS 请求因总线仲裁先排序,原子事务特性确保 C2 的 GetM 请求在 C1 事务完成后才发送;
内存控制器在周期 3 向 C1 返回数据,C1 块转入 S 态;
C2 的 GetM 请求发起后,总线上的 C1 监听该请求并使自身块无效(S→I),内存控制器向 C2 返回数据,C2 块转入 M 态;
C1 再次发起 GetS 请求,此时 C2 作为 M 态所有者响应数据并转入 S 态,同时向内存控制器写入数据(LLC / 内存成为所有者,需更新为最新副本)。
最终,C1 和 C2 均处于 S 态,LLC / 内存处于 IorS 态。
7.2.3 基线监听系统模型:非原子请求与原子事务
本章大部分内容使用的基线监听系统模型与 7.2.2 节的简单模型不同,其允许非原子请求。非原子请求源于多种实现优化,最常见的是在缓存控制器与总线之间插入消息队列(甚至单个缓冲区)。由于请求的发起时间与排序时间分离,协议必须处理简单模型中不存在的 “脆弱窗口”(即请求发起后但未排序前的时间段)。基线模型仍保留原子事务特性(7.5 节才会放宽这一限制)。
表 7.8 和 7.9 给出了包含所有临时状态的详细协议规范。与简单模型的协议相比,最大区别在于临时状态数量显著增加。放宽原子请求特性后,缓存控制器可能在发起请求与观察到自身请求出现在总线之间的时间段内,监听到其他控制器的请求,从而引发多种复杂场景。
以 I 态到 S 态的转移为例:
缓存控制器发起 GetS 请求,块状态从 I 转为 ISAD(等待自身请求排序)。此时块逻辑上仍为 I 态,无法执行加载 / 存储操作,且需忽略其他节点的请求;
当控制器监听到总线上自身的 GetS 请求时,请求完成排序,块逻辑上转为 S 态,但因数据未到达,仍无法执行加载操作。控制器将状态转为 ISD,等待前所有者的数据响应;
由于原子事务特性,数据消息是同一块的下一条一致性消息。响应到达后,事务完成,块转入稳定 S 态,执行加载操作。
I 态到 M 态的转移过程类似。
S 态到 M 态的转移体现了脆弱窗口引发状态变更的可能性:
核心尝试向 S 态块发起存储时,控制器发送 GetM 请求并转入 SMAD 态。此时块仍为 S 态,允许加载命中,并忽略其他核心的 GetS 请求;
若其他核心的 GetM 请求先排序,控制器必须将状态转为 IMAD,防止后续加载命中。这种脆弱窗口使得添加 “升级事务”(Upgrade Transaction)变得复杂,如侧边栏所述。
侧边栏:无原子请求系统中的升级事务
在原子请求协议中,升级事务(Upgrade)是缓存从 S 态转为 M 态的高效方式:Upgrade 请求使所有共享副本无效,且只需等待请求排序(总线仲裁延迟),无需像 GetM 那样等待数据从 LLC / 内存返回。
但在非原子请求场景下,由于请求发起与排序之间存在脆弱窗口,升级事务可能因其他核心的 GetM 或 Upgrade 请求排序在前,导致请求者失去共享副本。最简单的解决方案是引入新临时态,使缓存等待自身 Upgrade 排序。当 Upgrade 排序后(会使其他 S 副本无效但不返回数据),核心需再发起 GetM 请求才能转入 M 态。
高效处理升级事务的难点在于 LLC / 内存需判断何时发送数据。例如,核心 C0 和 C2 均持有共享块 A 并发起 Upgrade,同时 C1 发起 GetS 请求。若总线排序为 C0→C1→C2:
C0 的 Upgrade 成功,LLC / 内存(IorS 态)应转为 M 态但不发送数据,C2 需使自身 S 副本无效;
C1 的 GetS 请求命中 C0 的 M 态块,C0 返回数据并将 LLC / 内存更新为 IorS 态;
C2 的 Upgrade 请求最终排序时,因已失去共享副本,需 LLC / 内存响应。但此时 LLC / 内存处于 IorS 态,无法判断该 Upgrade 是否需要数据。尽管存在解决方案,但超出本指南范围。
脆弱窗口对 M 态到 I 态的降级影响更显著:
驱逐 M 态块时,控制器发送 PutM 请求并转入 MIA 态(与简单模型不同,不立即发送数据)。在 PutM 请求排序前,块仍为 M 态,控制器需响应其他核心的请求;
若 PutM 排序前无其他请求,控制器监听到自身 PutM 后,向内存控制器发送数据并转入 I 态;
若期间有其他 GetS/GetM 请求,控制器需先以 M 态响应,再转入 IIA 态等待 PutM 排序。此时直接转入 I 态会导致内存控制器陷入临时态(因收到 PutM 请求),而直接发送数据可能覆盖有效数据。解决方案是控制器在 IIA 态监听到 PutM 时,向内存控制器发送特殊 NoData 消息,告知自身非所有者,帮助内存控制器退出临时态。为此,内存控制器需新增临时态 MD(XD 表示收到 NoData 后转回 X 态,收到数据则转入 IorS 态)。
7.2.4 运行示例
表 7.10 的示例与表 7.7 类似,但移除了原子请求特性:
C1 和 C2 可同时发起请求并变更状态,假设 C1 的 GetS 先排序,原子事务特性确保 C2 的 GetM 在前序事务完成后才发送;
LLC / 内存响应 C1 使其转入 S 态,C2 的 GetM 请求发起后,C1 无效化自身副本,LLC / 内存响应 C2 使其转入 M 态;
C1 再次发起 GetS,C2(所有者)响应数据并转入 S 态,同时更新 LLC / 内存为最新副本。最终状态与简单模型一致。
7.2.5 协议简化设计
该协议通过牺牲性能实现相对简单性,主要简化包括:
原子事务:消除了许多可能的转移(表中 “(A)” 标注)。例如,控制器处于 IMD 态时,不会监听到其他核心对同一块的请求。若事务非原子,需重新设计协议处理此类事件(见 7.5 节);
存储请求处理:对 S 态块的存储请求直接发起 GetM 并转入 SMAD 态。更高性能但复杂的方案是使用升级事务(如侧边栏所述)。
7.3 添加独占状态
接下来几节将讨论许多重要的协议优化(初次阅读时可略过或速读)。其中一种常用优化是添加独占(E)状态,本节将介绍如何在 7.2.3 节的基线协议基础上引入 E 状态,构建 MESI 监听协议。回顾第六章,若缓存块处于 E 态,则表示该块有效、只读、干净、独占(其他缓存无副本)且拥有所有权。缓存控制器可无需发起一致性请求,直接将块从 E 态静默升级为 M 态。
7.3.1 设计动机
几乎所有商用一致性协议都采用 E 状态,因其优化了一种常见场景。与 MSI 协议相比,MESI 协议在核心先读取后写入块的场景中优势显著(这是单线程应用等场景的典型流程):
MSI 协议:加载未命中时通过 GetS 获取读权限,后续存储时需通过 GetM 获取写权限,需两次事务;
MESI 协议:若 GetS 请求时无其他缓存访问块,则获取 E 态而非 S 态。后续存储时无需发起 GetM,控制器可直接将 E 态升级为 M 态,允许核心写入。此场景下,E 态可减少一半的一致性事务。
7.3.2 进入独占状态的方法
实现 E 态的关键是让 GetS 请求者判断是否存在其他共享者,主要有两种方案:
总线上添加线或 “共享者” 信号:
GetS 请求排序时,持有块的缓存控制器置位 “共享者” 信号;
请求者若检测到信号置位,则块转入 S 态;若无信号,转入 E 态。
缺点:需额外实现线或信号,在非共享总线架构中(如 7.6 节)复杂度高。
LLC 维护额外状态:
LLC 区分 I 态(无共享者)和 S 态(有共享者),MSI 协议中无需此区分;
I 态:内存控制器返回标注为 “独占” 的数据,请求者转入 E 态;
S 态:返回普通数据,请求者转入 S 态。
挑战:LLC 需跟踪最后一个共享者释放块的时机,要求缓存控制器驱逐 S 态块时发送 PutS 消息,内存控制器维护共享者计数。此方案复杂度和带宽开销较高。
简化方案(保守 S 态):LLC 的 S 态表示 “零个或多个共享者”,允许 S 态块静默驱逐(即使最后一个共享者驱逐块,LLC 仍保持 S 态)。仅当 M 态块通过 PutM 写回时,LLC 转入 I 态。此方案虽可能错失部分 E 态机会,但避免了 PutS 事务,仍能覆盖多数场景。
本节的 MESI 协议采用保守 S 态方案,既避免高速总线中线或信号的工程难题,也无需显式 PutS 事务。
7.3.3 协议高层描述
图 7.4 和 7.5 展示了 MESI 协议的稳定状态转移,与 MSI 协议的区别如下:
缓存侧:GetS 请求根据 LLC/memory 状态转入 S 或 E 态,E 态可静默升级为 M 态;
LLC / 内存侧:新增 I 态(无共享者),与保守 S 态(零或多个共享者)区分(MSI 协议中合并为 IorS 态)。
侧边栏:E 态非所有权状态的 MESI 协议
若 E 态不视为所有权状态(即 E 态块由 LLC/memory 所有),则当缓存将 E 态升级为 M 态时,内存控制器无法知晓块当前状态(E 或 M)。此时总线上的 GetS/GetM 请求需由缓存判断是否响应,而内存控制器需等待一段时间:
若缓存(M 态)响应数据,内存控制器不动作;
若超时未响应,内存控制器才认为自己是所有者并响应。
此方案可能增加内存响应延迟,需缓存响应延迟可预测且短暂,部分实现通过预取数据隐藏延迟,但会增加带宽和功耗开销。
7.3.4 协议详细规范
表 7.11 和 7.12 给出 MESI 协议的详细规范(临时状态已标注),与 MSI 协议的差异用粗体标出:
缓存侧:新增稳定态 E 和临时态 EIA;
LLC / 内存侧:新增临时状态,以支持 I 态与保守 S 态的区分。
协议保留基线 MSI 协议的简化设计(如原子事务)。
7.3.5 运行示例
表 7.13 的示例与 MSI 协议的关键区别在于:
C1 发起 GetS 请求时,LLC/memory 处于 I 态(无共享者),返回独占数据,C1 块转入 E 态(而非 MSI 的 S 态);
后续流程与 MSI 类似,但临时状态存在差异。
通过 E 态优化,C1 的存储操作无需发起 GetM 事务,直接从 E 态升级为 M 态,减少了一次一致性事务。
7.4 添加拥有状态
第二项重要优化是引入 “拥有状态”(Owned State,O 态)。本节将介绍如何在 7.2.3 节的基线协议基础上添加 O 态,构建 MOSI 监听协议。回顾第六章,若缓存块处于 O 态,则表示该块有效、只读、脏状态,且缓存为所有者(即必须响应该块的一致性请求)。我们沿用基线 MSI 协议的系统模型:事务是原子的,但请求是非原子的。
7.4.1 设计动机
与 MSI 或 MESI 协议相比,添加 O 态在一种特定且重要的场景中具有优势:当缓存持有 M 态或 E 态块并收到其他核心的 GetS 请求时。在 7.2.3 节的 MSI 协议和 7.3 节的 MESI 协议中,缓存必须将块从 M 态或 E 态降级为 S 态,并向请求者和内存控制器同时发送数据。之所以必须向内存控制器发送数据,是因为响应缓存放弃了所有权(降级为 S 态),LLC / 内存成为所有者,因此必须持有数据的最新副本以响应后续请求。
添加 O 态带来两项核心优势:
减少 LLC / 内存的更新开销:当缓存处于 M 态(或 E 态)并收到 GetS 请求时,无需向 LLC / 内存发送额外数据消息;
避免不必要的 LLC 写入:若块在写回 LLC 之前被再次写入,O 态可避免临时写入 LLC 的无效操作。
历史上,对于多芯片多处理器,O 态还有第三项优势:允许后续请求由缓存(而非延迟更高的内存)响应。但在本指南的包含性 LLC 多核系统模型中,LLC 的访问延迟远低于片外 DRAM,因此缓存响应的优势更多体现在减少 LLC 交互而非内存交互。
7.4.2 协议高层描述
图 7.6 和 7.7 展示了稳定状态转移的高层逻辑。关键区别在于:当持有 M 态块的缓存收到其他核心的 GetS 请求时,MOSI 协议中缓存将块状态转为 O 态(而非 S 态),并保留所有权(而非将所有权转移给 LLC / 内存)。因此,O 态使缓存无需更新 LLC / 内存。
7.4.3 协议详细规范
表 7.14 和 7.15 给出了 MOSI 协议的详细规范(临时状态已标注),与 MSI 协议的差异用粗体标出:
缓存侧:新增稳定态 O 和两个临时态(OIA 用于处理 O 态块的替换,OMA 用于存储后升级回 M 态);
内存控制器:无需新增临时态,但将原 M 态重命名为 MorO 态(因内存控制器无需区分 M 态和 O 态)。
为保持规范简洁,协议将 PutM 和 PutO 事务合并为单一 PutM 事务 —— 即缓存通过 PutM 驱逐 O 态块。这一设计不影响协议功能,但有助于表格规范的可读性。
MOSI 协议保留了基线 MSI 协议的所有简化设计(如原子事务)。
7.4.4 运行示例
表 7.16 的示例与 MSI 协议的流程基本一致,直到 C1 的第二次 GetS 请求出现在总线上:
MOSI 协议:C2(持有 M 态块的所有者)响应 C1 的 GetS 请求,将状态转为 O 态(而非 S 态),保留块的所有权,且无需将数据回写至 LLC / 内存(除非后续驱逐该块,本例未展示)。
通过 O 态优化,缓存避免了向 LLC / 内存发送冗余数据,减少了总线事务和存储操作,提升了协议效率。
7.5 非原子总线
基准 MSI 协议以及 MESI 和 MOSI 变体均依赖于原子事务假设。这种原子性极大简化了协议设计,但也牺牲了性能。
7.5.1 动机
实现原子事务的最简单方法是使用带有原子总线协议的共享线路总线,即所有总线事务都由不可分割的请求 - 响应对组成。拥有原子总线类似于拥有非流水线处理器核心,无法使可并行进行的活动重叠。图 7.8 展示了原子总线的操作。由于一致性事务会占用总线直至响应完成,原子总线可直接实现原子事务。然而,总线吞吐量受限于请求和响应的延迟总和(包括请求与响应之间的任何等待周期,图中未显示)。考虑到响应可能由片外内存提供,这种延迟会成为总线性能的瓶颈。
图 7.9 展示了流水线化的非原子总线的操作。其关键优势在于后续请求可在总线上序列化,无需等待前一个请求的响应,因此使用同一组共享线路时,总线可实现更高的带宽。然而,实现原子事务变得困难得多(但并非不可能)。原子事务特性限制了对同一数据块的并发事务,但不限制对不同数据块的并发事务。SGI Challenge 通过快速查表来检查同一数据块是否已有未完成的事务,从而在流水线总线上强制实现原子事务。
7.5.2 按序响应与乱序响应
非原子总线的一个主要设计问题是采用流水线方式还是拆分事务方式。如图 7.9 所示,流水线总线按请求顺序提供响应;如图 7.10 所示,拆分事务总线可按与请求顺序不同的顺序提供响应。
相对于流水线总线,拆分事务总线的优势在于低延迟响应不必等待对先前请求的高延迟响应。例如,若请求 1 针对内存拥有且不在 LLC 中的数据块,请求 2 针对片上缓存拥有的数据块,那么按照流水线总线的要求,强制响应 2 等待响应 1 会导致性能损失。
拆分事务总线引发的一个问题是响应与请求的匹配。在原子总线上,响应显然对应最近的请求;在流水线总线上,请求者必须跟踪未完成请求的数量,以确定哪条消息是对其请求的响应;在拆分事务总线上,响应必须携带请求或请求者的标识。
7.5.3 非原子系统模型
我们假设一个如图 7.11 所示的系统,请求总线和响应总线分离且独立运行。除内存控制器不连接请求总线外,每个一致性控制器都与两条总线相连。我们画出用于缓冲传入和传出消息的 FIFO 队列,因为在一致性协议中考虑这些队列很重要。值得注意的是,如果一致性控制器在处理来自请求总线的传入请求时停滞,那么该请求之后(序列化在停滞请求之后)的所有请求在控制器处理完当前停滞请求之前都不会被处理。无论消息类型或地址如何,这些队列都按严格的 FIFO 方式处理。
7.5.4 基于拆分事务总线的 MSI 协议
在本节中,我们对基准 MSI 协议进行修改,使其适用于具有拆分事务总线的系统。拆分事务总线不会改变稳定状态之间的转换,但会对具体实现产生重大影响,特别是可能出现更多的转换。
在表 7.17 和 7.18 中,我们详细说明了该协议,现在有许多原子总线不可能出现的转换。例如,缓存现在可以接收针对其处于 ISD 状态的数据块的 Other-GetS 请求。所有这些新出现的转换都针对缓存正等待数据响应的瞬态数据块,在等待数据时,缓存首先观察到针对该数据块的另一个一致性请求。回顾 7.1 节,事务是根据其请求在总线上的排序时间来排序的,而不是数据到达请求者的时间。因此,在这些新出现的转换中,缓存实际上已经完成了事务,只是尚未收到数据。回到我们的 ISD 示例,缓存数据块实际上处于 S 状态,因此在这种状态下收到 Other-GetS 请求无需采取任何操作,因为拥有处于 S 状态数据块的缓存不必响应 Other-GetS 请求。
然而,除上述示例外,其他新出现的转换更为复杂。假设缓存中一个处于 IMD 状态的数据块在总线上观察到 Other-GetS 请求,该缓存数据块实际上处于 M 状态,因此该缓存是数据块的所有者,但尚未拥有数据块的数据。由于缓存是所有者,必须响应 Other-GetS 请求,但在收到数据之前无法响应。解决这种情况的最简单方法是让缓存暂停处理 Other-GetS 请求,直到其 Own-GetM 请求的数据响应到达。此时,缓存数据块将变为 M 状态,缓存将拥有有效数据可发送给 Other-GetS 请求者。
对于其他新出现的转换,在缓存控制器和内存控制器处,我们也选择暂停处理,直到数据到达以满足未完成的请求。这是最简单的方法,但引发了三个问题:
性能损失:如我们在下一节讨论的,这会牺牲一定的性能。
死锁风险:如果控制器在等待另一个事件(消息到达)时因某条消息而停滞,架构师必须确保等待的事件最终会发生。循环停滞链可能导致死锁,必须避免。在本节的协议中,停滞的控制器肯定会收到使其恢复的消息。这很容易理解,因为控制器已经看到了自己的请求,停滞只影响请求网络,而控制器正在响应网络上等待数据消息。
请求处理顺序问题:令人惊讶的是,停滞一致性请求可能使请求者在处理自己的请求之前观察到对其请求的响应。考虑表 7.19 中的示例:核心 C1 发出对数据块 X 的 GetM 请求,并将 X 的状态变为 IMAD;C1 在总线上观察到自己的 GetM 请求,状态变为 IMD;LLC / 内存是 X 的所有者,花费很长时间从内存中检索数据并将其放到总线上;与此同时,核心 C2 发出对 X 的 GetM 请求,该请求在总线上序列化,但无法被 C1 处理(即 C1 停滞);C1 发出对数据块 Y 的 GetM 请求,该请求随后在总线上序列化,此对 Y 的 GetM 请求在 C1 处排在先前停滞的一致性请求(来自 C2 的 GetM 请求)之后,因此 C1 无法处理自己对 Y 的 GetM 请求;然而,所有者 C2 可以处理对 Y 的 GetM 请求,并快速响应 C1,因此 C1 可以在处理自己的请求之前观察到对其 GetM 请求的响应。这种可能性需要添加瞬态状态。在这个例子中,核心 C1 将数据块 Y 的状态从 IMAD 变为 IMA。同样,协议还需要添加瞬态状态 ISA 和 SMA。在这些响应先于请求被观察到的瞬态状态中,数据块实际上处于先前的状态。例如,处于 IMA 状态的数据块在逻辑上处于 I 状态,因为 GetM 请求尚未被处理;如果数据块处于 IMA 状态,缓存控制器不会响应观察到的 GetS 或 GetM 请求。我们将 IMA 与 IMD 进行对比:在 IMD 状态下,数据块在逻辑上处于 M 状态,缓存控制器在数据到达后必须响应观察到的 GetS 或 GetM 请求。
该协议与本章前面的协议还有一个区别,涉及 PutM 事务。处理方式不同的情况是,当核心(如核心 C1)发出 PutM 请求,而另一个核心对同一数据块的 GetS 或 GetM 请求排在 C1 的 PutM 请求之前时,C1 在观察到自己的 PutM 请求之前从 MIA 状态转换为 IIA 状态。在本章前面的原子协议中,C1 观察到自己的 PutM 请求,并向 LLC / 内存发送 NoData 消息,该消息通知 LLC / 内存 PutM 事务已完成(即无需等待数据)。在这种情况下,C1 不能向 LLC / 内存发送数据消息,因为 C1 的数据是过时的,协议不能向 LLC / 内存发送会覆盖数据最新值的过时数据。在本章的非原子协议中,我们在 LLC 中每个数据块的状态中增加一个字段,用于保存数据块当前所有者的标识。LLC 在每次更改数据块所有权的事务中更新该所有者字段。通过使用所有者字段,LLC 可以识别总线上出现非所有者发出的 PutM 请求的情况,这正是 C1 在观察到自己的 PutM 请求时处于 IIA 状态的情况。因此,LLC 知道发生了什么,C1 不必向 LLC 发送 NoData 消息。与原子协议相比,为简单起见,我们选择修改非原子协议中 PutM 事务的处理方式。允许 LLC 直接识别这种情况比要求使用 NoData 消息更简单;在非原子协议中,系统中可能有大量 NoData 消息,且 NoData 消息可能在其关联的 PutM 请求之前到达。
7.5.5 基于拆分事务总线的优化型无停滞 MSI 协议
如前所述,在拆分事务总线系统中,我们通过在新出现的转换场景中引入停滞机制牺牲了部分性能。例如,当缓存中一个处于 ISD 状态的块接收到针对该块的 Other-GetM 请求时会进入停滞,而不是直接处理请求。然而,可能存在一个或多个在 Other-GetM 之后针对其他块的请求,缓存本可以在不停滞的情况下处理这些请求。通过停滞单个请求,协议会阻塞该请求之后的所有请求,延迟这些事务的完成。理想情况下,我们希望一致性控制器能处理停滞请求之后的其他请求,但需要注意:为了支持内存请求的全局顺序,监听机制要求一致性控制器按接收顺序处理请求,不允许重新排序。
解决该问题的方法是按顺序处理所有消息,而非停滞。我们的方案是引入瞬态状态,用于记录一致性控制器已接收但需等待后续事件完成的消息。回到 ISD 状态的缓存块示例,若控制器在总线上观察到 Other-GetM 请求,则将块状态改为 ISDI(表示 “处于 I 状态,即将进入 S 状态,等待数据,数据到达后将转为 I 状态”)。类似地,处于 IMD 状态的块接收到 Other-GetS 请求时,状态变为 IMDS,并必须记住 Other-GetS 的请求者。当缓存的 GetM 请求的数据响应到达时,控制器会将数据发送给 Other-GetS 请求者,并将块状态转为 S。
除了瞬态状态的增加,无停滞协议还引入了潜在的活锁问题。假设一个处于 IMDS 状态的缓存块接收到其 GetM 请求的数据响应,如果控制器立即将块状态转为 S 并将数据发送给 Other-GetS 请求者,那么它将无法执行最初触发 GetM 请求的存储操作。如果核心重新发出 GetM 请求,相同的情况可能反复出现,导致存储操作始终无法完成。为确保避免这种活锁,我们要求处于 ISDI、IMDI、IMDS 或 IMDSI 状态(或其他具有额外稳定一致性状态的协议中的类似状态)的缓存,在接收到请求数据时对该块执行一次加载或存储操作。执行完一次操作后,缓存方可改变状态并将块转发给其他缓存。我们将在 9.3.2 节对活锁问题进行更深入的讨论。
表 7.20 和 7.21 详细说明了无停滞 MSI 协议的规范。最明显的区别是瞬态状态的数量。这些状态本身并不复杂,但确实增加了协议的整体复杂度。
我们未在内存控制器中移除停滞机制,因为这并不可行。以处于 IorSD 状态的块为例,内存控制器接收到核心 C1 的 GetM 请求后会进入停滞。理论上,控制器可以在等待数据时将块状态改为 IorSDM,但在此状态下,控制器可能接收到核心 C2 的 GetS 请求。如果控制器不对该 GetS 请求停滞,就必须将块状态改为 IorSDMIorSD。在此状态下,控制器还可能接收到核心 C3 的 GetM 请求。由于无法将 LLC / 内存所需的瞬态状态数量限制在较小范围(即少于核心数量),为简化设计,我们仍让内存控制器采用停滞机制。
7.6 总线互连网络的优化
本章至今假设的系统模型中,一致性请求和响应共享单一总线,或使用独立的请求 / 响应总线。本节将探讨两种可提升性能的其他系统模型。
7.6.1 数据响应的独立非总线网络
我们已强调监听系统需要为广播一致性请求提供全局顺序。表 7.2 的示例表明,缺乏一致性请求的全局顺序会导致不一致。但一致性响应无需全局排序,也无需广播。因此,响应可通过不支持广播或排序的独立网络传输,例如交叉开关、网格、环面、蝶形网络等。
使用独立的非总线网络传输一致性响应有以下优势:
可实现性:高速共享总线难以实现,尤其对于总线上有多个控制器的系统,其他拓扑可采用点对点链路。
吞吐量:总线一次只能传输一个响应,其他拓扑可同时传输多个响应。
延迟:使用总线传输响应需每次仲裁总线使用权,其他拓扑可允许响应无需仲裁直接发送。
7.6.2 一致性请求的逻辑总线
监听系统要求广播一致性请求具有全局顺序。共享总线是实现广播全局顺序的最直接方式,但并非唯一方式。有两种方法可在无物理总线的情况下实现与总线相同的全局有序广播特性(即逻辑总线):
具有物理全局顺序的其他拓扑
共享总线是实现广播全局顺序的典型拓扑,但其他拓扑也可实现。一个典型例子是树形拓扑,一致性控制器位于树叶节点。若所有一致性请求单播至树根,再由树根广播至全树,则每个控制器观察到的一致性广播顺序一致。该拓扑的序列化点为树根。Sun Microsystems 的 Starfire 多处理器 [3] 采用了树形拓扑,我们将在 7.7 节详细讨论。
逻辑全局顺序
即使网络拓扑本身不提供自然顺序,也可通过逻辑时间对请求排序实现全局顺序。Martin 等人 [6] 设计了一种名为 “时间戳监听” 的协议,可在任意网络拓扑上运行。缓存控制器发出一致性请求时,向所有控制器广播请求并标记该广播应遵循的逻辑时间。协议需确保:
(a) 每个广播具有唯一逻辑时间;
(b) 控制器按逻辑时间顺序处理请求(即使物理接收顺序不同);
(c) 逻辑时间 T 的请求不会在控制器已处理完 T 之后的请求后才到达。
Agarwal 等人提出了类似的 “网络内监听排序”(INSO)方案 [1]。
回顾思考题 7:监听缓存一致性协议要求核心通过总线通信。判断正误?
答案:错误!监听机制需要全局有序的广播网络,但该功能无需物理总线即可实现。
7.7 案例研究
本节介绍两个实际监听系统的案例:Sun Starfire E10000 和 IBM Power5。
7.7.1 Sun Starfire E10000
Sun Microsystems 的 Starfire E10000 [3] 是商业系统中采用监听协议的典型案例。其一致性协议本身并无特别之处,采用的是带写回缓存的典型 MOESI 监听协议。E10000 的独特之处在于它如何扩展到 64 个处理器。架构师基于三个重要观察进行了创新,我们依次讨论。
观察一:共享线路监听总线难以扩展到大量核心,主要受电气工程限制。
为此,E10000 仅使用点对点链路而非总线。它通过逻辑总线而非物理总线广播一致性请求。监听协议的关键洞察在于,它们需要一致性请求的全局顺序,但这一顺序并不需要物理总线。如图 7.12 所示,E10000 将逻辑总线实现为树形结构,处理器作为叶节点。树中的所有链路都是点对点的,从而消除了对总线的需求。处理器将请求单播到树的顶部,在那里进行序列化,然后广播到整个树。由于在根节点进行序列化,树提供了全局有序的广播。一个给定的请求可能在不同时间到达两个处理器,这是允许的;重要的约束是处理器观察到相同的请求全局顺序。
观察二:通过使用多条(逻辑)总线,可以在保持一致性请求全局顺序的同时获得更高的带宽。
E10000 有四条逻辑总线,一致性请求按地址交错分布在它们之间。通过要求处理器按固定的预定义顺序监听逻辑总线来强制全局顺序。
观察三:数据响应消息比请求消息大得多,不需要一致性请求所需的全局有序广播网络。
许多先前的监听系统实现了数据总线,不必要地同时提供广播和全局排序,同时限制了带宽。为提高带宽,E10000 将数据网络实现为交叉开关。同样,使用点对点链路而非总线,交叉开关的带宽远远超过总线(物理或逻辑)的可能带宽。
E10000 的架构针对可扩展性进行了优化,这种优化设计要求架构师考虑非原子请求和非原子事务。
7.7.2 IBM POWER5
IBM Power5 [8] 是一款双核芯片,两个核心共享 L2 缓存。每个 Power5 芯片都有一个结构总线控制器 (FBC),使多个 Power5 芯片可以连接在一起创建更大的系统。大型系统最多包含八个节点,每个节点是一个包含四个 Power5 芯片的多芯片模块 (MCM)。
从抽象角度看,IBM Power5 似乎使用了基于拆分事务总线实现的相当典型的 MESI 监听协议。然而,这种简单描述忽略了几个值得讨论的独特特性。我们特别关注两个方面:互连网络的环形拓扑和 MESI 一致性状态的新颖变体。
环形网络上的监听一致性
Power5 使用的互连网络与我们迄今讨论的非常不同,这些差异对一致性协议有重要影响。最重要的是,Power5 用三个单向环连接节点,分别用于传输三种类型的消息:请求、监听响应 / 决策消息和数据。单向环不提供全局顺序,除非所有消息都必须从环上的同一个节点开始,而 Power5 并非如此。相反,请求者将请求消息发送到环上,然后在看到请求绕环一周返回时吸收该请求。每个节点在环上观察请求,节点中的每个处理器确定其监听响应。第一个观察到请求的节点提供一个单一的监听响应,该响应是该节点上所有处理器的聚合监听响应。监听响应不是实际的数据响应,而是芯片或节点将采取的行动的描述。由于没有全局有序的网络,芯片 / 节点不能立即行动,因为它们可能无法就如何响应做出一致的决定。监听响应通过监听响应环传输到下一个节点。该节点类似地产生一个单一的监听响应,该响应聚合了第一个节点的监听响应以及第二个节点上所有处理器的监听响应。当所有节点的聚合监听响应到达请求者芯片时,请求者芯片确定每个处理器应如何响应该请求。请求者芯片将此决定沿环广播到每个节点。这个决策消息由环中的每个节点 / 芯片处理,被确定为提供数据响应的节点 / 芯片将该数据响应通过数据环发送给请求者。
由于缺乏全局有序的互连网络,这个协议比典型的监听协议复杂得多。该协议仍然具有一致性请求的全局逻辑顺序,但没有全局有序的网络,节点不能立即响应请求,因为请求在全局顺序中的位置尚未由其在网络上的出现时间确定。尽管复杂,Power5 设计的优点是只有点对点链路和环形拓扑的简单性(例如,环形中的路由非常简单,因此交换速度可能比其他拓扑更快)。还有其他协议利用了环形拓扑并探索了环形的排序问题 [2,4,7]。
一致性状态的额外变体
Power5 协议从根本上说是一个 MESI 协议,但它对其中一些状态有几种 “变体”。我们在表 7.22 中列出了所有状态。有两个新状态值得强调。首先是共享状态的 SL 变体。如果 L2 缓存以 SL 状态持有一个块,它可以向同一节点上的处理器的 GetS 请求响应数据,从而减少该事务的延迟并减少片外带宽需求;这种提供数据的能力将 SL 与 S 区分开来。
另一个有趣的新状态是 T (标记) 状态。当一个块处于 Modified 状态并收到 GetS 请求时,它进入 T 状态。与在 MESI 协议中会降级到 S 状态或在 MOSI 协议中会降级到 O 状态不同,缓存将状态更改为 T。处于 T 状态的块类似于 O 状态,因为它具有比内存中值更新的值,并且其他缓存中可能有也可能没有处于 S 状态的该块副本。与 O 状态一样,该块可以在 T 状态下被读取。令人惊讶的是,T 状态有时被描述为读写状态,这违反了 SWMR 不变量。实际上,对 T 状态的存储可以立即执行,因此确实在实际(物理)时间上违反了 SWMR 不变量。然而,该协议仍然基于环顺序在逻辑时间上强制执行 SWMR 不变量。尽管这种排序的细节超出了本入门的范围,但我们认为将 T 状态视为 E 状态的变体有助于理解。回想一下,E 状态允许静默转换到 M;因此,只要状态(原子地)转换到 M 状态,就可以立即对处于 E 状态的块执行存储。T 状态类似;在 T 状态下的存储会立即转换到 M 状态。然而,由于可能也有处于 S 状态的副本,在 T 状态下的存储还会导致立即在环上发出无效消息。其他核心可能试图从 I 或 S 升级到 M,但 T 状态充当一致性排序点,因此具有优先级,无需等待确认。尚不清楚这个协议是否足以支持强内存一致性模型,如 SC 和 TSO;然而,正如我们在第 5 章中讨论的,Power 内存模型是最弱的内存一致性模型之一。这个标记状态优化了生产者 - 消费者共享的常见场景,其中一个线程写入一个块,然后一个或多个其他线程读取该块。生产者可以重新获得读写访问权,而不必每次都等待太久。
7.8 监听协议的讨论与未来展望
监听系统在早期多处理器中颇为流行,因其设计简洁且在当时主流的小规模系统中,扩展性不足的问题并不显著。对于这类非扩展性系统,监听协议还具备性能优势:每个监听事务仅需两条消息即可完成,相较之下,基于目录的协议通常需要三条消息。
尽管监听协议有其优势,但如今已不再广泛使用。即使在小规模系统中(这类场景对扩展性要求不高),监听协议也逐渐式微。主要原因在于,实现全局有序广播网络的成本过高,而目录协议采用的低成本互连网络已能满足需求。此外,对于大规模扩展系统,监听协议显然并不适配。在核心数量极多的系统中,广播请求所需的互连网络带宽以及监听每个请求所需的一致性控制器带宽都极易成为瓶颈。因此,这类系统需要更具扩展性的一致性协议,而这正是我们在下一章介绍目录协议的初衷。
不过,即便在可扩展系统中,监听协议仍可发挥作用。正如我们将在 9.1.6 节中讨论的,应对扩展性挑战的一个有效策略是分而治之。例如,由多个多核芯片组成的系统可采用混合架构:芯片内部使用监听一致性协议维持一致性,芯片间则采用可扩展的目录协议。