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里配置多核启动、任务绑定和中断分发。到时候咱们拿实际代码来演示。先消化这些基础,后面才跟得上。