第四章 窥探协议(Snooping)原理

大家好,我是老张。今天咱们聊聊缓存一致性协议里最经典的一种实现——窥探协议。英文叫Snooping,说白了就是「偷听」。为什么叫这个名字?你想想看,每个缓存控制器都在总线上竖着耳朵听,看别人在干什么,这就是窥探的本质。

我在做第一个多核处理器项目时,就被这个协议折腾得不轻。当时我们团队选的是MESI协议,结果总线带宽估算出了问题,差点导致流片延期。嗯,这些坑我后面会一一讲到。

4.1 总线监听机制

窥探协议的核心机制,就是所有缓存控制器共享一条总线。每个核的缓存控制器不仅要处理本地请求,还要监听总线上的所有事务。

我习惯把总线监听比作一个「广播系统」:

  • 发起者:某个CPU核发起读写请求
  • 监听者:其他所有缓存控制器
  • 总线:广播通道,所有事务可见

这里有个关键点:总线事务是原子的。什么意思?就是当一个核在总线上发起事务时,其他核必须等到这个事务完成才能发起新事务。这保证了全局顺序性。

核心要点:总线监听机制保证了所有缓存控制器看到的总线事务顺序是一致的。这是缓存一致性的基础。

4.2 状态机转换:Invalid/Shared/Modified

咱们先看最简单的三态协议——MSI。它只有三个状态:

状态 含义 本地数据是否有效 与其他缓存是否一致
Invalid (I) 无效状态 -
Shared (S) 共享状态 是(与其他核一致)
Modified (M) 修改状态 否(独占且已修改)

状态转换的规则,我总结成一句话:读操作不改变所有权,写操作需要独占

具体来说:

  • 本地读命中:状态不变(I→I, S→S, M→M)
  • 本地读缺失:发起总线读事务,从I变为S
  • 本地写命中:如果当前是M状态,直接写;如果是S状态,需要先发总线Invalidate,从S变为M
  • 本地写缺失:发起总线读+Invalidate,从I变为M
  • 监听到总线读:如果自己是M状态,需要写回内存并变为S
  • 监听到总线Invalidate:如果自己是S状态,变为I

避坑指南:我曾经在实现M状态写回时,忘记处理「写回过程中又有新的总线读请求」的情况。结果导致数据丢失。正确的做法是:写回操作必须原子完成,期间不能响应新的监听请求。

4.3 总线事务类型

窥探协议中,总线事务主要有三种:

4.3.1 Read 事务

当缓存缺失时,发起总线读事务。这个事务会:

  • 从内存读取数据
  • 通知其他缓存:如果你们有Modified副本,请写回

4.3.2 Write 事务

严格来说,窥探协议中「写」操作分为两步:

  1. Read for Ownership (RFO):先获取数据的所有权
  2. Invalidate:使其他缓存中的副本失效

有些协议把这两步合并成一个事务,叫「Read+Invalidate」或「Read Exclusive」。

4.3.3 Invalidate 事务

这个事务只做一件事:告诉其他缓存「你们的数据副本失效了」。它不传输数据,只传输地址。

注意:Invalidate事务虽然小,但它的延迟直接影响写操作的性能。我在项目中遇到过因为Invalidate响应太慢,导致写操作被阻塞的情况。解决方案是:给Invalidate设置优先级,让它优先于普通读事务处理。

4.4 状态转换图

下面我用SVG画一个MSI协议的状态转换图,把上面讲的内容串起来:

Invalid (I) Shared (S) Modified (M) 本地读缺失 总线读 本地写缺失 总线读+Invalidate 本地写命中 总线Invalidate 监听到总线读 写回内存 监听到Invalidate 监听到读+Invalidate 写回后失效 本地写命中 本地读命中

这张图我画得比较详细。你看,从Invalid出发,只有两条路:要么读变成Shared,要么写变成Modified。从Shared出发,写操作会变成Modified,同时发Invalidate通知别人。从Modified出发,如果别人读,你得写回内存然后变成Shared。

经验之谈:我在实际项目中,最常踩的坑是「M状态写回时的时序问题」。你想想看,当你的缓存是M状态时,别人发起总线读,你需要写回内存。但写回需要时间,这期间如果又有新的总线事务进来怎么办?

我的做法是:在写回期间,设置一个「写回进行中」标志,所有新的监听请求都等待。等写回完成后再处理。虽然会增加一点延迟,但保证了数据一致性。

4.5 窥探协议的优缺点

优点 缺点
实现简单,状态机清晰 总线带宽成为瓶颈
所有核看到的事务顺序一致 扩展性差,核数多了性能下降
延迟低,适合少量核 每个事务都要广播,功耗高
不需要目录结构 总线仲裁复杂

我个人觉得,窥探协议最适合4-8个核的场景。超过这个数量,总线带宽就撑不住了。我记得有个项目硬要上16核,结果总线利用率超过90%,性能反而比8核还差。后来我们改用了目录协议才解决问题。

小技巧:如果你在设计窥探协议的总线,建议给不同事务分配优先级。比如Invalidate事务优先级最高,因为它直接影响写操作的延迟。读事务可以稍低一些。我习惯用两级优先级:高优先级给Invalidate和写回,低优先级给普通读。

好了,这一章的内容就到这里。窥探协议虽然看起来简单,但实际实现时有很多细节。下一章我们会讲MESI协议,它比MSI多了一个Exclusive状态,能减少不必要的总线事务。到时候我会结合具体代码,带你一步步实现。


公众号:蓝海资料掘金营,微信deep3321