第四章 窥探协议(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 事务
严格来说,窥探协议中「写」操作分为两步:
- Read for Ownership (RFO):先获取数据的所有权
- Invalidate:使其他缓存中的副本失效
有些协议把这两步合并成一个事务,叫「Read+Invalidate」或「Read Exclusive」。
4.3.3 Invalidate 事务
这个事务只做一件事:告诉其他缓存「你们的数据副本失效了」。它不传输数据,只传输地址。
注意:Invalidate事务虽然小,但它的延迟直接影响写操作的性能。我在项目中遇到过因为Invalidate响应太慢,导致写操作被阻塞的情况。解决方案是:给Invalidate设置优先级,让它优先于普通读事务处理。
4.4 状态转换图
下面我用SVG画一个MSI协议的状态转换图,把上面讲的内容串起来:
这张图我画得比较详细。你看,从Invalid出发,只有两条路:要么读变成Shared,要么写变成Modified。从Shared出发,写操作会变成Modified,同时发Invalidate通知别人。从Modified出发,如果别人读,你得写回内存然后变成Shared。
经验之谈:我在实际项目中,最常踩的坑是「M状态写回时的时序问题」。你想想看,当你的缓存是M状态时,别人发起总线读,你需要写回内存。但写回需要时间,这期间如果又有新的总线事务进来怎么办?
我的做法是:在写回期间,设置一个「写回进行中」标志,所有新的监听请求都等待。等写回完成后再处理。虽然会增加一点延迟,但保证了数据一致性。
4.5 窥探协议的优缺点
| 优点 | 缺点 |
|---|---|
| 实现简单,状态机清晰 | 总线带宽成为瓶颈 |
| 所有核看到的事务顺序一致 | 扩展性差,核数多了性能下降 |
| 延迟低,适合少量核 | 每个事务都要广播,功耗高 |
| 不需要目录结构 | 总线仲裁复杂 |
我个人觉得,窥探协议最适合4-8个核的场景。超过这个数量,总线带宽就撑不住了。我记得有个项目硬要上16核,结果总线利用率超过90%,性能反而比8核还差。后来我们改用了目录协议才解决问题。
小技巧:如果你在设计窥探协议的总线,建议给不同事务分配优先级。比如Invalidate事务优先级最高,因为它直接影响写操作的延迟。读事务可以稍低一些。我习惯用两级优先级:高优先级给Invalidate和写回,低优先级给普通读。
好了,这一章的内容就到这里。窥探协议虽然看起来简单,但实际实现时有很多细节。下一章我们会讲MESI协议,它比MSI多了一个Exclusive状态,能减少不必要的总线事务。到时候我会结合具体代码,带你一步步实现。
公众号:蓝海资料掘金营,微信deep3321