连贯性高级主题 在第 7 章和第 8 章中,我们在最简单的系统模型的背景下介绍了窥探和目录一致性协议,这些模型足以解释这些协议的基本问题。 在本章中,我们将在几个方向上扩展对一致性的表述。 在第 9.1 节中,我们讨论了为更复杂的系统模型设计一致性协议所涉及的问题。 在第 9.2 节中,我们描述了适用于窥探和目录协议的优化。 在第 9.3 节中,我们解释了如何确保一致性协议保持有效(即避免死锁、活锁和饥饿)。 在第 9.4 节中,我们提出了令牌一致性协议 [12],这是一类包含窥探和目录协议的协议。 我们在第 9.5 节中简要讨论了一致性的未来。 9.1 系统模型 到目前为止,我们假设了一个简单的系统模型,其中每个处理器核心都有一个物理寻址的单级回写数据缓存。
在第 7 章和第 8 章中,我们在最简单的系统模型的背景下介绍了窥探和目录一致性协议,这些模型足以解释这些协议的基本问题。 在本章中,我们将在几个方向上扩展对一致性的表述。 在第 9.1 节中,我们讨论了为更复杂的系统模型设计一致性协议所涉及的问题。 在第 9.2 节中,我们描述了适用于窥探和目录协议的优化。 在第 9.3 节中,我们解释了如何确保一致性协议保持有效(即避免死锁、活锁和饥饿)。 在第 9.4 节中,我们提出了令牌一致性协议 [12],这是一类包含窥探和目录协议的协议。 我们在第 9.5 节中简要讨论了一致性的未来。
到目前为止,我们假设了一个简单的系统模型,其中每个处理器核心都有一个物理寻址的单级回写数据缓存。 该系统模型省略了商业系统中通常存在的许多功能,例如指令缓存(第 9.1.1 节)、转换后备缓冲区(第 9.1.2 节)、虚拟寻址缓存(第 9.1.3 节)、直写式 高速缓存(第 9.1.4 节)、相干 DMA(第 9.1.5 节)和多级高速缓存(第 9.1.6 节)。
所有现代内核都至少具有一级指令缓存,这就提出了是否以及如何支持指令缓存一致性的问题。 尽管真正的自修改代码很少见,但当操作系统加载程序或库、即时 (JIT) 编译器生成代码或动态运行时系统重新优化程序时,包含指令的缓存块可能会被修改。
将指令缓存添加到一致性协议表面上很简单; 指令高速缓存中的块是只读的,因此处于稳定状态 I 或 S。此外,内核从不直接写入指令高速缓存; 核心通过对其数据缓存执行存储来修改代码。 因此,指令高速缓存的一致性控制器仅在观察到从另一个高速缓存(可能是其自己的 L1 数据高速缓存)到处于状态 S 的块的 GetM 时才采取行动,并简单地使该块无效。
由于多种原因,指令高速缓存一致性不同于数据高速缓存一致性。 最重要的是,一旦获取,一条指令可能会在内核的管道中保留缓冲多个周期(例如,考虑一个内核,它用一长串加载填充其 128 条指令窗口,每个加载都会一直错过到 DRAM)。 修改代码的软件需要某种方式来了解写入何时影响了所获取的指令流。 某些架构(例如 AMD Opteron)使用跟踪管道中的指令的单独结构来解决此问题。 如果此结构检测到正在运行的指令发生更改,它将刷新管道。 然而,由于指令的修改频率远低于数据,其他架构要求软件显式管理一致性。 例如,Power架构提供了icbi(指令缓存块无效)指令来使指令缓存条目无效。
转换后备缓冲区 (TLB) 是保存特殊类型数据的缓存:从虚拟地址到物理地址的转换。 与其他缓存一样,它们必须保持一致。 与指令缓存一样,它们历史上并未参与处理数据缓存的相同全硬件一致性协议。 TLB 一致性的传统方法是 TLB shotdown [18],这是一种软件管理的一致性方案,可能有也可能没有某些硬件支持。 在经典实现中,内核使转换条目无效(例如,通过清除页表条目的 PageValid 位)并向所有内核发送处理器间中断。 每个内核接收其中断,捕获到软件处理程序,或者使其 TLB 中的特定转换条目无效,或者刷新其 TLB 中的所有条目(取决于平台)。 每个核心还必须确保没有正在运行的指令使用现已过时的翻译,通常是通过刷新管道来确保。 然后,每个内核使用处理器间中断将确认发送回发起内核。 发起核心等待所有确认,确保在修改翻译(或重用物理页)之前所有过时的翻译条目均已失效。 某些架构提供特殊支持来加速 TLB 击落。 例如,Power架构通过使用特殊的tlbie(TLB无效条目)指令消除了昂贵的处理器间中断; 发起核心执行 tlbie 指令,该指令将无效的虚拟页号广播到所有核心,并且仅在所有核心完成无效后才完成。
最近的研究建议消除 TLB 击落,并将 TLB 合并到数据和指令缓存的现有全硬件一致性协议中 [16]。 这种全硬件解决方案比 TLB 击落更具可扩展性,但它需要对 TLB 进行修改,使其能够以与数据和指令缓存相同的方式进行寻址。 也就是说,TLB 必须探听内存中保存转换的块的物理地址。
当前系统中的大多数缓存(以及本入门中迄今为止讨论的所有缓存)都是使用物理地址访问的,但也可以使用虚拟地址访问缓存。 我们在图 9.1 中说明了这两个选项。 虚拟寻址缓存(“虚拟缓存”)相对于物理寻址缓存(“物理缓存”)具有一个关键优势:地址转换的延迟不在关键路径之内。1 这一延迟优势对于一级缓存很有吸引力 ,其中延迟至关重要,但对于延迟不太重要的较低级别缓存来说通常不太有吸引力。 然而,虚拟缓存给一致性协议的架构师带来了一些挑战:
由于实现虚拟缓存的复杂性,它们在当前系统中很少使用。 然而,它们已经在许多早期的系统中使用,并且它们有可能在未来再次变得更加相关。
我们的基线系统模型假设回写 L1 数据缓存和共享回写 LLC。 另一种选择是直写式缓存,它有几个优点和缺点。 明显的缺点包括显着更大的带宽和将数据写入到内存层次结构的下一个较低级别的功率。 在现代系统中,这些缺点有效地限制了对 L1 缓存的直写/回写决策。
直写式 L1 的优点包括以下几点。
直写式缓存对多线程核心和共享 L1 缓存提出了一些挑战。 回想一下,TSO 需要写入原子性,因此所有线程(执行存储的线程除外)必须同时查看存储。 因此,如果两个线程 T0 和 T1 共享相同的 L1 数据缓存,则 T0 到块 A 的存储必须阻止 T1 访问新值,直到其他缓存中的所有副本都已失效(或更新)。 尽管存在这些复杂性和缺点,一些设计仍然使用直写式 L1 高速缓存,包括 Sun Niagara 处理器和 AMD Bulldozer。
在第 2 章中,当我们第一次介绍一致性时,我们观察到,只有当有多个参与者可以读取和写入缓存和内存时,才会出现不一致性。 如今,最明显的参与者集合是单个芯片上的多个核心,但缓存一致性问题首先出现在具有单核心和直接内存访问 (DMA) 的系统中。 DMA 控制器是在显式系统软件控制下(通常以页粒度)读取和写入内存的参与者。 读取内存的 DMA 操作应该找到每个块的最新版本,即使该块驻留在处于状态 M 或 O 的缓存中。同样,写入内存的 DMA 操作需要使该块的所有过时副本无效。
通过向 DMA 控制器添加相干缓存,从而让 DMA 参与一致性协议,可以直接提供相干 DMA。 在这样的模型中,DMA 控制器与专用内核没有区别,保证 DMA 读取始终找到块的最新版本,而 DMA 写入将使所有过时的副本无效。
然而,由于多种原因,向 DMA 控制器添加一致高速缓存是不可取的。 首先,DMA 控制器具有与传统内核截然不同的局部性模式,并且它们通过内存进行流式传输,几乎没有时间重用(如果有的话)。 因此,DMA 控制器对于大于单个块的高速缓存几乎没有用处。 其次,当 DMA 控制器写入块时,它通常会写入整个块。 因此,使用 GetM 获取块是浪费的,因为整个数据将被覆盖。 许多一致性协议使用特殊的一致性操作来优化这种情况。 我们可以想象向本入门中的协议添加一个新的 GetM-NoData 请求,该请求寻求 M 许可,但只需要确认消息而不是数据消息。 其他协议使用特殊的 PutNewData 消息,该消息会更新内存并使所有其他副本(包括 M 和 O 中的副本)无效。
通过要求操作系统有选择地刷新缓存,DMA 也可以在没有硬件缓存一致性的情况下工作。 例如,在启动到页 P 或从页 P 发起 DMA 之前,操作系统可以使用类似于 TLB Shootdown 的协议(或使用某些其他页刷新硬件支持)强制所有高速缓存刷新页 P。 这种方法效率低下,因此通常只在某些嵌入式系统中出现,因为操作系统必须保守地刷新页面,即使它的任何块都不在任何缓存中。
TODO