2、硬件架构基础:SMP与AMP的区别,Cache一致性协议(MESI),内存屏障指令

好,咱们正式开始第二讲。

这一章,我打算聊聊多核处理器最底层的那些事儿。说白了,就是硬件长什么样,以及它怎么“骗”过软件,让我们觉得多个核在协同工作。

我个人习惯,在讲RTOS适配之前,先把硬件架构摸透。不然你后面调试起来,遇到一些诡异的内存乱序问题,会一头雾水。

2.1 SMP 与 AMP:两种多核的“活法”

多核处理器,不是简单地把几个CPU核焊在一起就完事了。它们之间怎么分工,怎么看待内存,这决定了你RTOS的设计思路。

主要有两种模式:SMPAMP

2.1.1 SMP(对称多处理)

SMP,全称 Symmetric Multi-Processing。翻译过来就是“对称多处理”。

什么叫对称?

就是所有CPU核都是平等的。它们共享同一块物理内存,共享同一个操作系统实例。你想想看,一个RTOS跑在4个核上,每个核都能看到全局变量,都能调度任务。

特点:

  • 共享内存:所有核访问同一个物理地址空间。
  • 单一OS镜像:一个RTOS管理所有核。
  • 负载均衡:任务可以在任意核上调度执行。

核心要点: 在SMP系统中,你写的“全局变量”,所有核都能看到。但这既是好事,也是麻烦的开始。

我在项目中遇到过一个问题:一个全局计数器,两个核同时++,结果最后值比预期少了很多。这就是典型的SMP下的竞态问题。嗯,这里要注意,SMP虽然编程模型简单,但同步开销不小。

2.1.2 AMP(非对称多处理)

AMP,全称 Asymmetric Multi-Processing。非对称多处理。

这个就有点意思了。每个核可能跑不同的操作系统,甚至不同的程序。它们之间可能不共享内存,或者只共享一小块约定的区域。

特点:

  • 私有内存或分区共享:每个核有自己的“地盘”。
  • 多个OS镜像:核A跑RTOS,核B跑裸机程序,核C跑Linux。
  • 核间通信:通过共享内存、Mailbox等方式交换数据。

我建议,如果你做AMP,一定要把核间通信协议设计好。我曾经见过一个项目,两个核通过共享内存传数据,结果因为Cache没刷干净,读到了脏数据。那调试过程,简直了。

特性 SMP AMP
内存模型 完全共享 私有或部分共享
OS实例 单一 多个
任务调度 全局负载均衡 核间静态分配
同步复杂度 高(需要锁、屏障) 低(核间通信为主)
典型场景 高性能计算、通用RTOS 异构计算、隔离安全域

我的经验: 做RTOS内核适配,SMP是主流。因为AMP的核间通信开销太大,不适合实时性要求极高的场景。但如果你做功能安全隔离,AMP反而是更好的选择。

2.2 Cache一致性协议(MESI)

好,接下来是重头戏。为什么SMP下,两个核同时写一个变量会出问题?

根本原因在于:Cache

每个CPU核都有自己的L1 Cache。核A改了变量x,但只写到了自己的Cache里。核B去读内存,读到的还是旧值。这就叫“Cache不一致”。

为了解决这个问题,硬件工程师发明了Cache一致性协议。最经典的就是MESI协议。

2.2.1 MESI 的四种状态

MESI是四个状态的缩写:

  • M(Modified):修改。这一行数据只在当前核的Cache里,并且和内存不一致。其他核不能有这份数据。
  • E(Exclusive):独占。数据只在当前核的Cache里,但和内存一致。其他核没有。
  • S(Shared):共享。数据在多个核的Cache里,且都和内存一致。
  • I(Invalid):无效。这一行数据已经过时了,不能使用。

说白了,就是每个Cache行都带一个“标签”,告诉硬件:我这数据是干净的、脏的、还是别人也有的。

关键点: 当核A要写一个处于S状态的数据时,它必须先发一个“Invalidate”消息给其他核,让它们把对应的Cache行标记为I。然后自己变成M状态。这个过程叫“写失效”。

我记得有一次调试一个网络协议栈,数据包总是校验失败。查了三天,最后发现是DMA和CPU之间的Cache一致性没处理好。DMA直接把数据写到了内存,但CPU的Cache里还是旧数据。嗯,从那以后,我对MESI协议就格外上心。

2.2.2 MESI 的局限性

MESI不是万能的。它有一个典型问题:伪共享(False Sharing)

什么意思?

假设核A频繁访问变量a,核B频繁访问变量b。但a和b恰好在同一个Cache行里。那么,核A写a时,会把核B的Cache行置为I。核B要写b时,又得重新加载。一来一回,性能损失巨大。

避坑指南: 我曾经在项目中,因为两个线程的锁变量和计数器放在相邻地址,导致性能直接腰斩。解决办法很简单:用 __attribute__((aligned(64))) 让它们不在同一个Cache行里。

2.3 内存屏障指令

好,硬件有MESI协议保证Cache一致性。但这就够了吗?

不够。

因为现代处理器为了性能,会乱序执行指令。你写的代码是A=1; B=2;,但CPU可能先执行B=2,再执行A=1。在单核下没问题,但在多核下,这就乱套了。

所以,我们需要内存屏障指令。它告诉CPU:在这条指令之前的所有内存操作,必须在这条指令之后的操作之前完成。

2.3.1 常见的屏障指令

不同的架构,指令不同。但原理类似:

  • ARMDMB(数据内存屏障)、DSB(数据同步屏障)、ISB(指令同步屏障)。
  • x86mfencelfencesfence
  • RISC-Vfence

我建议,在RTOS的临界区进出、以及核间通信时,一定要加上内存屏障。否则,你可能会看到一些“不可能发生”的现象。

举个例子:

// 核A
flag = 1;
// 这里需要加一个写屏障,确保flag先被其他核看到
// 然后再发送中断
send_ipi();
// 核B
while(flag == 0) {
    // 等待
}
// 这里需要加一个读屏障,确保读到的是最新的flag
// 然后才能使用数据
data = shared_buffer;

你看,如果没有屏障,核B可能先看到中断,再读到flag=0。这就出问题了。

2.3.2 屏障的类型

内存屏障不是只有一种。根据作用范围,可以分为:

  • Load-Load屏障:保证所有读操作在屏障前的,先于屏障后的读操作完成。
  • Store-Store屏障:保证所有写操作在屏障前的,先于屏障后的写操作完成。
  • Load-Store屏障:保证屏障前的读操作,先于屏障后的写操作。
  • 全屏障:保证所有内存操作都按顺序完成。

在实际的RTOS内核中,我们通常用全屏障。因为简单,不容易出错。但代价是性能损失。如果你追求极致性能,可以按需使用部分屏障。

我的习惯: 在编写自旋锁、信号量等同步原语时,我会在锁释放时加一个Store-Store屏障,在锁获取时加一个Load-Load屏障。这样既保证了正确性,又比全屏障快一些。

2.4 总结一下

这一章,我们聊了三个核心概念:

  1. SMP vs AMP:决定了你的RTOS是全局调度还是分区调度。
  2. MESI协议:硬件如何保证Cache一致性,以及伪共享这个坑。
  3. 内存屏障:软件如何应对CPU乱序执行,保证多核间的可见性。

嗯,这些都是基础中的基础。下一章,我们会把这些知识用到实处,开始讲RTOS如何在多核上启动和初始化。到时候,你会看到这些概念是如何落地到代码里的。

记住一句话:没有正确的硬件理解,就没有可靠的多核RTOS。