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。

核心定义:缓存一致性(Cache Coherence)就是保证多个核的私有缓存中,同一地址的数据在任何时刻都“看起来”是一致的。注意是“看起来”,不是物理上完全同步。

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内容去读,读到的全是垃圾。

我个人习惯:所有跨核共享的数据结构,在定义时就加上__attribute__((section(".shared_ram"))),强制放到DTCM或SRAM中,并配置为非缓存区。这样从根源上避免迁移问题。

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)。

这意味着什么?意味着软件必须自己管好缓存同步。你写一个共享变量,得手动做三件事:

  1. 写完后刷Cache:把脏数据写回主存
  2. 读之前无效化Cache:让其他核重新从主存加载
  3. 或者干脆不用Cache:把共享内存区配置为强序非缓存

下面这张图把整个问题串起来了,你们可以保存下来当参考。

多核缓存不一致问题全景图 Core0 (M7) L1 Cache x=10 (脏) Core1 (M4) L1 Cache x=5 (过期) 主存 (AXI SRAM) x=5 (未更新) Core0写x=10,未写回主存 → 脏数据问题 Core1读x,从自己Cache拿 → 过期数据问题 三个地方存x,三个值不一样 根源:无硬件一致性协议

4.5 什么时候会触发不一致?

不是所有共享数据都会出问题。我总结了几条经验法则:

  • 只读共享:安全。所有核只读不写,Cache里存多少份都一样。
  • 一写多读:危险。写核必须刷Cache,读核必须无效化Cache。
  • 多写多读:极度危险。建议用硬件信号量或MPU隔离。
  • DMA与CPU共享:同样危险。DMA直接访问主存,CPU的Cache可能还是旧数据。
我建议:在项目初期就定义好“共享数据清单”。哪些变量跨核,哪些被DMA访问,统一放到非缓存区。别等到调试阶段再一个个找,那时候头都大了。