第2章 缓存一致性概述:为什么需要缓存?问题在哪?DMA进来更乱?
大家好,我是你们的课程讲师。咱们今天聊缓存一致性,这话题听起来有点“八股”,但搞不好就是芯片死机的元凶。我刚开始做SoC的时候,觉得缓存一致性就是个协议问题,搞个MOESI不就完了?直到在项目里亲眼看到DMA搬运完数据,CPU读回来全是错的……你猜怎么着?不是协议不对,是我根本没意识到“一致性”这件事得从系统层面去想。
2.1 为什么需要缓存一致性?
先说点基础的。现代SoC里,CPU不是一个人在战斗。它旁边有缓存——L1、L2甚至L3。缓存的存在是为了弥合CPU和内存之间的速度差。但问题来了:当多个CPU核(或者CPU和DMA)都能访问同一块内存地址时,谁的数据才是“最新”的?
举个例子:核0写在缓存里的数据,核1直接从内存读。如果缓存没写回内存,核1拿到的就是旧数据。这就叫缓存不一致。你想想看,操作系统调度线程的时候,要是看到这种数据,程序逻辑全崩。
核心观点:缓存一致性就是保证:任何时刻,对同一内存地址的读操作,都能返回最近一次写操作的数据。说白了就是所有“眼睛”看到的数据要统一。
我在一个ARM A53多核项目里遇到过:核0跑实时任务,核1跑Linux。核0修改了一个共享变量,忘了cacheline写回,核1读的时候直接触发exception。从那以后,我对cache coherence的敬畏心就上来了。
2.2 缓存一致性问题场景
那哪些场景最容易出问题?我列四个最常见的,你看看是不是都遇到过:
2.2.1 多核CPU间的共享数据
两个核共享一个全局变量。如果一个核写、另一个核读,没有硬件一致性协议(比如MESI),读核可能永远看不到写核的更新。这在小嵌入式MCU里很常见——很多MCU连cache都没有,但一旦上了多核加缓存,就要小心。
2.2.2 CPU与硬件加速器(如GPU、NPU)共享内存
加速器写数据到内存,CPU通过缓存读。如果加速器没有跟CPU缓存保持一致,CPU读到的可能是缓存里的旧副本。这类问题在AI芯片里特别多——我曾经调过一个NPU的驱动,NPU推完数据,CPU死活拿不对,最后发现是加速器没发缓存刷新命令。
2.2.3 CPU与DMA控制器之间的数据交换
这就是本课程的重点了。DMA是直接内存访问,不经过缓存。CPU可能在缓存里保留了一部分数据副本。如果DMA把外部设备的数据搬到内存,而CPU的缓存里恰好有那个内存地址的脏数据,好了,CPU再读就是读的缓存旧数据,DMA搬进去的新数据等于白搬。
2.2.4 使用Write-Back缓存时的DMA问题
Cache写策略分Write-Through(穿透写)和Write-Back(回写)。Write-Through每次写都写内存,但性能差。Write-Back先写缓存,脏cacheline等eviction时才写回内存。DMA搬数据时,如果目标地址在CPU缓存里是脏块,DMA搬完数据后,脏块可能被写回内存,覆盖掉DMA新搬的数据。这就是经典的“脏数据覆盖”问题。
| 场景 | 问题描述 | 典型后果 |
|---|---|---|
| 多核共享变量 | 核A写缓存,核B读内存 | 读到旧值,程序逻辑错误 |
| CPU+加速器 | 加速器写内存,CPU读缓存 | 加速器结果丢失 |
| CPU+DMA | DMA写内存,CPU缓存有脏数据 | 数据被缓存覆盖 |
| Write-Back脏块 | DMA目标地址缓存脏块 | 数据被写回覆盖 |
2.3 DMA引入的一致性问题——深度拆解
好了,咱们重点说说DMA。DMA这家伙很勤快,但不通关缓存。它直接读写物理内存。CPU呢,读写缓存。这两个通路不经过同一个“中间人”,就变成两个世界各自为政。
典型血案:
- CPU往内存地址A写数据(其实是写到了缓存里,内存还是旧值)。
- DMA从外设读数据要写到地址A。
- DMA写完之后,CPU的缓存里那个cacheline还是旧的。
- CPU再读地址A,读的还是缓存里的旧数据——DMA的新数据丢掉了。
反过来也一样:如果CPU想从地址B读内存,但DMA刚往地址B搬了数据,CPU的缓存还存着之前的旧内容,读到的还是陈年旧账。
避坑指南:我曾经在SD卡DMA传输项目里踩过这个坑。CPU准备读SD卡数据,先读了32字节缓存,然后DMA把新数据搬进去,CPU再读剩下的字节——缓存命中率很高,命中的全是旧数据。你猜最后怎么解决的?手动搞了cache flush和invalidate。
那DMA传输到底分几种情况?我画个图给你看。
从图里你能看到:CPU走Cache,DMA走内存。两个路径没有握手。所以当你让DMA搬完数据,CPU再去读——如果Cache里有该地址的旧数据,你就掉坑里了。
2.4 总结一下
缓存一致性不是什么高深理论,它就是个“谁的数据是最新的”的问题。在没有DMA的系统中,靠硬件一致性协议(MESI/MOESI)就能解决。但一旦引入DMA,问题就复杂了——因为DMA不跟Cache打招呼。
个人建议:如果你在做嵌入式SoC设计,尤其是用了带缓存的高性能CPU(比如Cortex-A系列)和DMA控制器,一定要在设计早期就规划好刷新策略。我之前有个教训:等板子调通了才发现DMA和Cache冲突,那会儿改硬件已经来不及,只能靠软件刷Cache,性能掉了一半。
下一节我们会深入具体的缓存一致性协议,看看MOESI、MESI这些“交通规则”是怎么工作的。不过在此之前,你最好想清楚:你的系统里,CPU和DMA到底谁在动同一块内存?如果动了,你怎么保证他们看到的数据是一致的?
嗯,今天就聊这么多。记住,一致性不是协议的事,是系统设计的事。