3、多核编程基础:SMP与AMP模式对比、核间通信机制、缓存一致性

好,咱们进入第三章。说实话,多核编程这块,是智驾域控里最绕不开、也最容易踩坑的地方。很多做单核RTOS出身的兄弟,一上来就被SMP、AMP这些概念搞懵了。别急,我慢慢拆开讲。

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

先问个问题:你手头有四个CPU核,你打算怎么用?

最简单的思路——让一个操作系统管所有核,任务随便往哪个核上扔。这就是SMP(对称多处理)。另一种思路——每个核跑独立的操作系统,各管各的,互不干扰。这就是AMP(非对称多处理)

我在项目里见过不少团队,一开始都选SMP,觉得省事。结果呢?缓存冲突、锁竞争、优先级反转,搞得焦头烂额。我个人习惯是:先想清楚每个核要干什么,再决定用哪种模式

对比项 SMP AMP
操作系统数量 1个OS管理所有核 每个核独立OS(可不同)
任务调度 全局统一调度 各核独立调度
内存管理 共享同一地址空间 通常独立地址空间
典型场景 通用计算、负载均衡 功能隔离、安全关键系统
调试难度 中等(需处理竞态) 较高(跨OS通信复杂)

核心观点:智驾域控里,我强烈建议用AMP为主、局部SMP的混合模式。比如,安全相关的感知模块跑在独立核上(AMP),而一些非实时的后处理任务可以共享核(SMP)。

3.2 核间通信机制:数据怎么跨核传递?

AMP模式下,核与核之间是“孤岛”。你得搭桥。常用的桥有三种:共享内存、Mailbox、RPMSG。我一个个说。

3.2.1 共享内存(Shared Memory)

最直接的方式——划出一块物理内存,两个核都能读写。但问题来了:你写我读,怎么保证数据完整?

我在项目中遇到过:两个核同时写一个变量,结果数据错乱,定位了三天才发现是共享内存没加保护。嗯,这里要注意:共享内存必须配合同步机制

// 伪代码:共享内存 + 自旋锁
volatile uint32_t *shared_data = (uint32_t *)0x80000000;
volatile uint32_t *lock = (uint32_t *)0x80000004;

void core0_write(uint32_t val) {
    while (__atomic_test_and_set(lock, 1)); // 自旋等待
    *shared_data = val;
    __atomic_clear(lock, 1);
}

uint32_t core1_read(void) {
    uint32_t val;
    while (__atomic_test_and_set(lock, 1));
    val = *shared_data;
    __atomic_clear(lock, 1);
    return val;
}

避坑指南:我曾经在Cortex-R5F上用过自旋锁,结果发现锁变量本身被缓存了,导致另一个核永远看不到锁释放。解决方案:把锁变量放在非缓存(non-cacheable)内存区域。

3.2.2 Mailbox(硬件邮箱)

共享内存适合传大量数据,但如果你只想发个“通知”或“事件”,用Mailbox更轻量。Mailbox本质上是硬件提供的一组寄存器和中断机制。

工作流程很简单:

  • 核A往Mailbox寄存器写一个值
  • 硬件自动触发核B的中断
  • 核B在中断服务里读取Mailbox,知道“哦,核A找我有事”

我习惯把Mailbox当作控制通道,共享内存当作数据通道。两者配合使用,效率最高。

3.2.3 RPMSG(远程处理器消息传递)

如果你用Linux+RTOS的异构系统,RPMSG是OpenAMP框架里推荐的方式。它封装了共享内存和Mailbox的细节,给你一个简单的socket-like接口。

// RPMSG 示例(简化)
// 发送端
struct rpmsg_endpoint *ept;
rpmsg_send(ept, "Hello from Core0", 16);

// 接收端
static int rpmsg_cb(struct rpmsg_endpoint *ept, void *data, size_t len) {
    printf("Received: %s\n", (char *)data);
    return 0;
}

说白了,RPMSG就是帮你把共享内存和Mailbox打包好了,你只管发消息就行。但代价是——性能不如手写的共享内存方案。我建议:高频小数据用RPMSG,大数据块用共享内存

3.3 缓存一致性(Cache Coherency)

这是多核编程里最头疼的问题,没有之一。你想想看:核A把变量X改成5,但X还在核A的L1 Cache里,没写回内存。核B去内存读X,读到的还是旧的3。这就出事了。

硬件上,有些芯片(比如Cortex-A系列)有硬件缓存一致性协议(MESI/MOESI),自动帮你同步。但智驾域控里常用的Cortex-R系列,很多没有硬件一致性。怎么办?

我的经验:在Cortex-R上做核间通信,所有共享数据必须放在非缓存(Non-cacheable)写透(Write-through)内存区域。别偷懒用Cacheable内存,否则你会被“幽灵般的数据不一致”折磨到崩溃。

具体做法:

  • 方案一:在MPU(内存保护单元)里把共享内存区域配置为Non-cacheable。简单粗暴,但性能会下降。
  • 方案二:手动维护一致性。每次写共享数据后,调用clean cache指令;每次读之前,调用invalidate cache指令。
// 手动维护缓存一致性(ARMv7-R 示例)
void write_shared(uint32_t *addr, uint32_t val) {
    *addr = val;
    // 将addr所在的cache line写回内存
    __clean_cache_line((uint32_t)addr);
}

uint32_t read_shared(uint32_t *addr) {
    // 使addr所在的cache line失效,强制从内存读取
    __invalidate_cache_line((uint32_t)addr);
    return *addr;
}

我曾经踩过的坑:有一次我忘了在读取前做invalidate,结果一个核改了数据,另一个核死活读不到新值。查了两天,最后发现是Cache在“作祟”。从那以后,我养成了习惯:所有跨核共享变量,读写前后都加上Cache维护操作

3.4 小结

这一章内容不少,我帮你捋一下重点:

  • SMP vs AMP:智驾域控推荐AMP为主,局部SMP。安全关键功能独立核运行。
  • 核间通信:共享内存传数据,Mailbox传事件,RPMSG简化开发。根据场景选合适的。
  • 缓存一致性:没有硬件一致性时,用Non-cacheable内存或手动维护Cache。别偷懒。

下一章,我会讲具体怎么在RTOS里配置多核启动、任务绑定和中断分发。到时候咱们拿实际代码来演示。先消化这些基础,后面才跟得上。