4. 缓存一致性问题:多核系统的“数据打架”
各位同学,今天我们来聊聊多核开发里最让人头疼的问题之一——缓存一致性。说白了,就是两个核各自拿着同一份数据的“复印件”,结果一个改了,另一个不知道,程序就跑飞了。
我刚开始接触双核STM32H7时,就栽过这个跟头。当时一个核采集数据,另一个核做运算,结果算出来的值忽大忽小,查了三天才发现是缓存没同步。嗯,从那以后,我每次写多核代码,第一件事就是画缓存数据流图。
4.1 什么是缓存一致性?
先看一个最简单的场景。CPU Core0从地址0x20001000读了一个变量x=5。这个值被缓存到了Core0的L1 Cache里。接着Core1也从同一个地址读x,它发现自己的Cache里没有,就从内存加载,也拿到了5。
问题来了:如果Core0把x改成10,会发生什么?
- Core0的Cache:x=10(已修改,标记为脏)
- Core1的Cache:x=5(还是旧值)
- 主存:x=5(还没被写回)
这就是典型的缓存不一致。三个地方存着同一个变量,值却不一样。你让Core1拿x做判断,它拿到的永远是过期的5,而不是最新的10。
4.2 多核系统中数据不一致的场景分析
实际项目中,我总结出三种最常见的“翻车”场景。每种我都吃过亏,你们记下来,能省不少调试时间。
场景一:写后读不一致(Write-Read Inconsistency)
Core0写了一个新值到变量flag,Core1等着这个flag变成1才继续执行。结果Core1一直读到0,死循环了。
// Core0
flag = 1; // 写操作,数据留在Core0的Cache里
// Core1
while(flag == 0); // 读操作,从自己的Cache读,永远是0
为什么会这样?因为Core0的写操作只更新了自己的Cache,没有通知Core1。Core1的Cache里还是旧的flag=0。
场景二:写后写不一致(Write-Write Inconsistency)
两个核同时往同一个地址写数据。Core0写5,Core1写10。最后主存里存的是哪个?取决于谁最后写回。但两个核的Cache里各存各的,谁也不知道对方写过。
| 时间 | Core0 Cache | Core1 Cache | 主存 |
|---|---|---|---|
| T0 | x=0 | x=0 | x=0 |
| T1 | x=5(脏) | x=0 | x=0 |
| T2 | x=5(脏) | x=10(脏) | x=0 |
| T3 | x=5(脏)→写回 | x=10(脏) | x=5 |
| T4 | x=5 | x=10(脏)→写回 | x=10 |
你看,最终主存是10,但Core0的Cache里还是5。如果Core0再读一次x,它拿到的是5,不是10。这就是典型的脏数据问题——一个核写了新值,另一个核还拿着旧值当宝贝。
场景三:迁移共享数据(Shared Data Migration)
这个更隐蔽。一个数据结构原本在Core0的Cache里,Core1要访问它,硬件自动把数据从Core0的Cache搬到了Core1的Cache。但Core0那边还留着旧副本,不知道数据已经被“借走”了。
我举个例子。你在Core0上初始化了一个环形缓冲区,然后Core1开始往里面写数据。Core0如果还按原来的Cache内容去读,读到的全是垃圾。
4.3 经典的“脏数据”与“过期数据”问题
这两个词你们肯定听过,但很多人分不清。我用自己的话给你们讲透。
脏数据(Dirty Data)
一个核修改了Cache里的数据,但还没写回主存。这时候Cache里的数据就是“脏”的。脏数据本身没问题——它是最新的。问题在于其他核看不到这个脏数据。
你可以把脏数据想象成你手里改好的草稿,还没复印给别人看。别人手里拿的还是旧版本。
过期数据(Stale Data)
一个核的Cache里存着某个地址的数据,但主存或其他核的Cache里已经有更新的版本了。这个数据就是“过期”的。
过期数据更危险。因为它看起来是合法的——读操作不会报错,数据格式也对,但值就是不对。我见过有人查了整整一周的bug,最后发现是一个全局状态变量在Core1的Cache里过期了,导致状态机跳错了分支。
4.4 缓存一致性问题的根源
说了这么多,根源其实就一条:每个核有自己的私有缓存,而且这些缓存之间没有自动同步的硬件协议。
在STM32H7这样的异构多核芯片里,Cortex-M7和Cortex-M4各自有独立的L1 Cache。它们共享主存(AXI SRAM或DTCM),但Cache之间没有像Cortex-A系列那样的硬件一致性协议(如MESI)。
这意味着什么?意味着软件必须自己管好缓存同步。你写一个共享变量,得手动做三件事:
- 写完后刷Cache:把脏数据写回主存
- 读之前无效化Cache:让其他核重新从主存加载
- 或者干脆不用Cache:把共享内存区配置为强序非缓存
下面这张图把整个问题串起来了,你们可以保存下来当参考。
4.5 什么时候会触发不一致?
不是所有共享数据都会出问题。我总结了几条经验法则:
- 只读共享:安全。所有核只读不写,Cache里存多少份都一样。
- 一写多读:危险。写核必须刷Cache,读核必须无效化Cache。
- 多写多读:极度危险。建议用硬件信号量或MPU隔离。
- DMA与CPU共享:同样危险。DMA直接访问主存,CPU的Cache可能还是旧数据。