第4章 MESI协议详解

说到缓存一致性,MESI协议是绕不开的基础。它定义了四种状态,用来跟踪每个缓存行在多个核之间的“身份”。我刚开始接触时觉得这四状态挺简单,但真正调起DMA来才发现——状态搞错一步,数据就全乱套了。

咱们今天就把MESI拆开揉碎了讲。重点是状态转换,以及在DMA场景下怎么用它来保证数据没错。

4.1 四种状态——一个缓存行的四种活法

每个缓存行可以处于M、E、S、I中的一种。看名字有点抽象,我习惯用“脏”“独享”“分享”“无效”去记。

状态全称缓存行有效?与主存一致?其他核是否有副本?
MModified(已修改)有效不一致(脏)无
EExclusive(独占)有效一致(干净)无
SShared(共享)有效一致(干净)可能有
IInvalid(无效)无效——
记忆小技巧:M是脏的独占,E是干净的独占,S是干净的共享,I就是不在缓存里。想象一下我们几个人一起看一份文件,M就像有人在上面用钢笔改了还没抄回总稿;E就像我一个人独享原件;S就是大家传阅相同内容的复印件;I嘛,那份复印件压根没传到我手上。

4.2 状态转换——核心流程图

状态切换是由本地CPU操作(读/写)和总线嗅探事件(其他核的读/写请求)触发的。举个例子:一个缓存行在E状态,本地CPU写它,它会变成M;如果另一个核发起读请求,总线嗅探到后,E会变成S。

下面这张图把四种状态和主要的转换路径都画出来了。你仔细看箭头上的标签,就能理解每个动作会触发什么变化。

MESI 状态转换图 M 已修改 E 独占 S 共享 I 无效 本地写 远程读 远程写/替换 本地读缺失 本地读缺失(远程有副本) 远程写 本地写(其他核失效) 远程读(写回主存) 说明: 箭头旁标注的是触发状态转换的事件。“本地”指当前CPU,“远程”指其他CPU。DMA设备通常被视为“远程”访问源。
我碰到过一个案例:某次调试中发现两个核共享一个缓存行,但其中一个核通过DMA直接往那片内存写数据。结果另一个核的缓存行还处于E状态,读到的全是旧值。后来发现是DMA控制器没有触发总线写失效,导致S状态没变成I。说白了,DMA的写操作在MESI协议里等同于远程写,必须让其他核的缓存行失效。

4.3 总线事务与嗅探行为

MESI的状态切换不是凭空发生的。每当一个核(或DMA)想读/写内存,它会先发个总线事务到共享总线上。其他核的缓存控制器“监听”到这个事务,就叫缓存嗅探(snooping)。

常见事务类型:

  • BusRd — 总线读事务。其他核看到后,如果自己的缓存行处于M状态,必须先写回主存再共享为S;如果处于E/S状态,直接共享为S。
  • BusWr — 总线写事务。其他核看到后必须将自己的缓存行置为I。
  • BusRdX — 读并独占,通常配合写失效。用于准备写一个当前不在缓存中的行。

我建议你记住这个逻辑:谁写的,谁负责失效别人。如果你用硬件DMA直接写内存,它必须发送总线写失效,不然其他核的缓存就是脏数据。

4.4 MESI在DMA中的实际应用

DMA控制器本质上是一个“远程处理器”。它读写内存时不经过CPU缓存,也就不会自动更新缓存状态。所以进入DMA传输前,你必须手动处理缓存一致性:

  • DMA写内存(CPU读):CPU在读之前必须将对那块内存的缓存行全部失效(Invalidate)。不然CPU会读到老缓存里的内容。
  • CPU写内存(DMA读):CPU在写完后,必须先把缓存行刷回主存(Flush / Clean),再启动DMA。否则DMA读到的可能是过期物理内存。
个人习惯:我在做SoC集成时,会在DMA描述符里加一个“缓存操作掩码”字段。驱动启动DMA前,根据传输方向自动选择是Flush还是Invalidate。这样不容易漏掉。

4.5 避坑指南

我曾经翻过一个大坑:某次用DMA从一个外设接收数据包到内存,然后CPU去处理。因为外设数据量很小,只有几十个字节,刚好落在一个缓存行里。我忘了做CPU缓存Invalidate,结果CPU一直读到老数据。表面看是协议错误,实际上是MESI状态停留在“M”,而DMA写后主存内容变了但缓存不知道。从那以后我给自己定了个规矩:DMA涉及的内存区域,只要不是硬件自动维护一致性的(比如ACE总线),统统用软件做一遍Flush/Invalidate。

另外要注意伪共享(False Sharing)。两个核心各自操作不同变量,但变量落在同一个缓存行里。如果其中一个核做DMA更新,那整个缓存行都会被失效,导致另一个核性能骤降。所以设计缓冲区时,最好按缓存行大小(通常64字节)对齐,变量之间用padding隔开。

MESI协议本身不复杂,但一跟DMA结合,就容易出各种时序和状态错乱。抓住核心——谁修改了数据,谁就要通知别人失效——很多问题就能提前规避。