第二章 多核架构基础:SMP与AMP的区别、多核内存模型、缓存一致性、中断亲和性

好,咱们进入正题。这一章讲的是多核架构的基础知识。说实话,这部分内容有点偏理论,但你要是搞不懂这些,后面移植FreeRTOS多核版本的时候,踩坑都不知道怎么死的。我当年第一次接触多核系统,就是吃了这个亏。

2.1 SMP与AMP:两种多核工作模式

先说说两种最常见的多核架构:SMP和AMP。这两个概念,说白了就是「多个核怎么干活」的问题。

2.1.1 SMP(对称多处理)

SMP,全称Symmetric Multi-Processing。什么意思呢?就是所有核心地位平等,共享同一份内存,跑同一个操作系统。你想想看,就像几个工人共用一张工作台,谁有空谁拿工具干活。

在SMP模式下,FreeRTOS会把任务调度到任意一个空闲核心上。这样做的好处很明显——负载均衡。但坏处呢?缓存一致性、锁竞争,这些麻烦事就来了。

我个人习惯,在SMP系统里做任务划分时,尽量让相关性强的任务跑在同一个核上。为什么?减少缓存失效。这个后面会细说。

2.1.2 AMP(非对称多处理)

AMP,Asymmetric Multi-Processing。每个核心可以跑不同的操作系统,甚至裸机程序。核心之间通过共享内存或消息传递来通信。

举个例子:一个核跑FreeRTOS做实时控制,另一个核跑Linux做人机交互。这种场景在工业控制、汽车电子里很常见。

我在项目中遇到过一个AMP方案:一个Cortex-M4核跑FreeRTOS做电机控制,一个Cortex-M0核做传感器采集。两个核之间用共享内存加信号量通信。嗯,这里要注意,共享内存的访问必须加锁,否则数据就乱套了。

特性 SMP AMP
操作系统 单一OS,所有核共享 每个核可独立运行不同OS
内存 统一内存空间 独立或共享内存
调度 全局调度,负载均衡 每个核独立调度
典型场景 服务器、桌面 嵌入式、工业控制
我的建议:如果你刚开始做多核移植,先从SMP入手。因为FreeRTOS的SMP支持比较成熟,文档也多。AMP虽然灵活,但调试起来很痛苦——两个核各跑各的,出了问题你都不知道该查哪个。

2.2 多核内存模型

内存模型,说白了就是「多个核怎么看到同一块内存」。这里有两个关键概念:一致性内存模型和非一致性内存模型。

2.2.1 一致性内存模型

在一致性内存模型下,所有核心看到的内存视图是一样的。你写我读,数据不会乱。这听起来很美好,但实现起来代价很大——需要硬件维护缓存一致性。

常见的Cortex-A系列处理器,比如Cortex-A53、A72,都支持硬件缓存一致性。但Cortex-M系列呢?大部分不支持。所以你在Cortex-M上做多核,就得自己操心数据同步的问题。

2.2.2 非一致性内存模型

非一致性模型下,不同核心看到的内存可能不一样。比如核0写了一个变量,核1读到的可能是旧值。为什么会这样?因为每个核有自己的缓存,写操作可能只更新了本地缓存,还没刷回主存。

我曾经在一个项目中踩过这个坑:两个核共享一个标志位,核0置位后,核1一直读不到。查了两天才发现,是缓存没刷新。从那以后,我养成了一个习惯——所有共享变量都加上volatile关键字,并且手动插入内存屏障指令。

注意:在Cortex-M多核系统中,如果你使用共享内存,一定要在写操作后执行数据同步屏障(DSB)指令,在读操作前执行数据同步屏障(DMB)指令。否则,你可能会读到脏数据。

2.3 缓存一致性

缓存一致性,这是多核系统里最头疼的问题之一。说白了就是:核0改了数据,核1的缓存里还是旧数据,怎么办?

硬件上,有MESI协议(Modified, Exclusive, Shared, Invalid)来维护一致性。但很多嵌入式处理器不支持这个协议,那就得靠软件来保证。

软件上,常用的方法有:

  • 禁止缓存:对共享内存区域关闭缓存。简单粗暴,但性能会下降。
  • 手动刷新:在关键操作前后,手动刷缓存。比如用CMSIS提供的函数:SCB_CleanInvalidateDCache()
  • 使用原子操作:比如LDREX/STREX指令,保证读-改-写操作的原子性。

我个人习惯,在FreeRTOS多核移植中,对任务控制块(TCB)等关键数据结构,都放在非缓存区域。虽然慢一点,但安全。你想想看,如果调度器读到的TCB是脏数据,那任务切换就全乱套了。

2.4 中断亲和性

中断亲和性,就是「中断来了,交给哪个核处理」。这个在SMP系统里特别重要。

为什么?因为如果所有中断都往一个核上跑,那个核就忙死了,其他核却在看热闹。所以我们要把中断分散到不同的核上。

在ARM GIC(通用中断控制器)中,可以通过设置中断目标寄存器,把某个中断绑定到特定核心。比如:

// 将IRQ 42绑定到核0
GIC_SetTarget(42, 1 << 0);

// 将IRQ 43绑定到核1
GIC_SetTarget(43, 1 << 1);

我在项目中遇到过一个性能问题:一个网络中断把所有核都打断了,导致实时任务频繁被抢占。后来我把网络中断绑定到核2,实时任务绑定到核0和核1,问题就解决了。

避坑指南:我曾经把定时器中断绑定到了所有核上,结果每个核都去处理同一个定时器事件,导致任务被重复执行。记住,周期性中断通常只需要一个核来处理。

嗯,这一章的内容就这些。多核架构的基础知识,说白了就是搞清楚「核之间怎么协作、怎么通信、怎么同步」。下一章我们会进入实战,开始配置FreeRTOS的多核环境。到时候,这些理论就会派上用场了。