第3章 缓存基础知识:Cache line、写策略与MESI协议

做DMA和缓存一致性设计,不懂Cache的基本原理,那就是在裸奔。说实话,我见过不少工程师一上来就调DMA描述符,结果数据老是乱掉,折腾好几天才发现是Cache没处理好。

这一章,我们把Cache的核心概念讲透。你搞明白了这些东西,后面再讲DMA一致性方案,就顺理成章了。

3.1 什么是Cache line?

Cache本身不是按字节来管理的,而是按“行”来管理的。这个“行”就是Cache line。为什么这么搞?

你可以想想看,如果CPU每次从内存读一个字节,都要去查Cache里的每个字节,那Cache的查找逻辑得多复杂?所以我把它切成一小块一小块,每一块叫做一个Cache line。每次读写,都是以line为单位。

Tip: 典型的Cache line大小是64字节。ARM Cortex-A系列、x86都是这样。我在一个视频编解码项目里见过一次诡异的DMA问题,正是因为加速器写的buffer没对齐到64字节边界,导致Cache line被部分覆盖——那个bug查了我整整两天。

一个Cache line里通常包含这些信息:

  • 有效位(Valid bit):这一行是不是有效数据
  • 标签(Tag):保存内存地址的高位部分,用来判断命中
  • 数据块(Data block):真正存的那64字节内容

举个例子。假设Cache有64个line,每个line64字节。内存地址0x1000到0x103F,正好落在同一个line里。CPU读0x1000时,Cache会把整个0x1000~0x103F都搬进来。这就是所谓的时间局部性和空间局部性。

明白了Cache line,我们再来看一个更关键的问题——写的时候怎么办?

3.2 Cache写策略:写直达 vs. 写回

CPU写完数据,是不是马上写回内存?这个问题,不同芯片有不同的选择。

写策略 操作方式 优缺点 典型应用场景
写直达(Write-Through) 数据同时写入Cache和下一级存储(内存) 实现简单,一致性容易维护;但每次写操作都很慢 早期CPU,对性能要求不高的嵌入式系统
写回(Write-Back) 数据只写入Cache,标记为dirty;当该line被替换时再写回内存 写操作很快,减少内存带宽占用;但一致性复杂 现代高性能处理器(ARM A系列、RISC-V高性能核)

我个人的习惯是,在做DMA设计之前,先搞清楚处理器用的是哪种写策略。你可以在芯片手册里找到L1 Cache的配置寄存器,通常会有CTR_EL0这样的系统寄存器,里面就写着Cache line大小和写策略。

写回策略带来的一个直接后果是:CPU改了数据,可能很久都不写回内存。这时候DMA从内存拿数据,拿到的还是老版本,数据就错了。反过来也一样——DMA把新数据写进内存,但Cache里还躺着旧数据,CPU一读,读到的又是旧数据。

这就是缓存一致性问题的根源。

警告: 使用写回Cache的系统中,DMA操作前后必须做Cache维护操作(Clean/Invalidate)。不做的话,数据损坏几乎是必然的,只是早晚问题。我曾经在一个SoC项目中吃过这个大亏,样片回来后DMA传输的图像总是有条纹——就是因为我们忘了做Cache刷新。

3.3 Cache一致性协议:MESI 简介

上面说的是单核CPU与DMA之间的问题。那多个CPU核呢?每个核有自己的L1 Cache,它们各自写自己的数据,怎么保证最终看到同一个值?

嗯,这就是Cache一致性协议要干的事。MESI是最经典的一种,名字来源于四种状态。

  • M(Modified,已修改):这一行数据被当前CPU核改过了,和其他Cache里的副本不一样。而且它还没写回内存。别的核想看?不行,得先写回或者转移所有权。
  • E(Exclusive,独占):只有当前核有这个数据,和内存内容一致。没有其他核有副本。别人要来读?可以,变成S。
  • S(Shared,共享):多个核都有这个数据的副本,都和内存一致。谁要写?必须先发Invalidate消息,把其他核的副本都干掉。
  • I(Invalid,无效):这一行数据是废的,别人更新过了,我不再信任它。

状态之间怎么切换的?我画了一张图,你看一眼就明白了。

MESI协议状态转换图 MESI 协议核心状态转换图 (仅展示单核视角下的主要状态转移) M Modified E Exclusive S Shared I Invalid Local Read (miss) 从内存取数据 Remote Read 其他核读 Remote Write 其他核写了,无效掉 Local Write 本核写操作 S→E? 如果没别人共享了 Local Write (miss) 写未命中,分配新行 虚线/彩色线表示非强制转移路径,实际协议实现可能会有细微差异

这张图看着复杂,其实核心逻辑就几个字:谁改了数据,别人就不能碰那个line,等它写回去再通知所有人。

关键认知: MESI只是协议族里的一个基础版本。现代芯片用的更多是MOESI(增加了Owned状态)或MESIF(Intel的F状态,用于优化多播)。但万变不离其宗——核心思想没变,只是状态多了,性能更好了。

我建议你记住一点:在做DMA设计时,虽然DMA自己不参与MESI握手,但Cache控制器必须知道DMA的存在。所以很多SoC会在总线层面做“snoop filter”或者“DMA coherency port”——让DMA的访问能够触发Cache的监听逻辑,自动刷掉或者更新对应的行。

后面几章,我们会看到这些机制的具体硬件实现和软件编码。

Tip: 学习MESI最好的办法,不是背状态表,而是自己画一遍状态机。我在给团队做培训时,要求每个工程师用白板画三次——第一次对着笔记,第二次闭眼画,第三次讲给别人听。能讲清楚,才是真懂了。