基于 RTOS 信号量实现多核(AMP)架构下核间高效通信的陷阱与优化

一、AMP 架构与核间通信基础

AMP(Asymmetric Multi-Processing)架构中,多个核心(如 Cortex-A + Cortex-M)各自运行独立的 RTOS(如 FreeRTOS、RT-Thread),共享外设和内存。核间通信(IPC)通常采用“共享内存 + 同步机制”模式:一个核写入数据,另一个核读取。同步机制最常见的选择是 RTOS 信号量(Semaphore)。

典型流程:

  • 核 A 获取信号量,写入共享内存,释放信号量。
  • 核 B 获取信号量,读取共享内存。

但直接移植单核 RTOS 信号量到 AMP 环境,会引发一系列隐蔽问题。

二、四大陷阱深度解析

陷阱 1:RTOS 信号量是“核内私有”的

每个 RTOS 实例维护自己的信号量对象,其内部结构(如等待队列、计数)存放在该核的 RAM 中。核 B 无法直接操作核 A 的信号量。若简单地在共享内存中放置一个信号量结构体,两个 RTOS 会各自维护一份副本,导致计数不同步,甚至内存损坏。

后果: 信号量失去互斥/同步作用,数据竞争、死锁频发。

陷阱 2:缓存一致性(Cache Coherency)问题

现代处理器(如 Cortex-A7/A9)有 L1/L2 Cache。核 A 写入共享内存后,数据可能仍留在 Cache 中,未写回主存;核 B 读取时可能命中过期的 Cache 行,读到旧数据。信号量操作同样受影响,导致“释放后仍无法获取”或“获取后数据未更新”。

陷阱 3:优先级反转与死锁

若核 A 持有信号量时被高优先级任务抢占,而核 B 在等待该信号量,核 A 的低优先级任务可能长时间得不到调度,核 B 则一直阻塞,形成优先级反转。更严重的是,若两个核互相等待对方释放信号量,则直接死锁。

陷阱 4:中断与任务上下文冲突

核间通信常伴随中断(如 IPI)。若在中断服务程序(ISR)中调用 RTOS 信号量 API(如 xSemaphoreGiveFromISR),但 ISR 上下文与任务上下文对信号量的操作未加保护,可能导致信号量计数错乱。

三、优化方案:从“软信号量”到“硬件同步原语”

方案 1:使用硬件信号量(Hardware Semaphore)

多数 SoC(如 STM32H7、i.MX 8)提供硬件信号量外设,支持原子操作,且对所有核可见。硬件信号量通过专用寄存器实现,不依赖 Cache,天然解决一致性问题。

配置步骤(以 STM32H7 为例):

  1. 使能 HSEM 时钟:__HAL_RCC_HSEM_CLK_ENABLE();
  2. 获取信号量:HAL_HSEM_FastTake(HSEM_ID_0); 若返回 HAL_OK 则成功。
  3. 释放信号量:HAL_HSEM_Release(HSEM_ID_0, 0);

代码示例:

#include "stm32h7xx_hal.h"

#define IPC_SEM_ID 0

void CoreA_WriteData(uint32_t *buf, uint32_t len) {
    // 获取硬件信号量,忙等待
    while (HAL_HSEM_FastTake(IPC_SEM_ID) != HAL_OK);
    // 写共享内存(需确保缓存写回)
    SCB_CleanDCache_by_Addr((uint32_t*)buf, len*4);
    memcpy(shared_mem, buf, len*4);
    // 释放信号量
    HAL_HSEM_Release(IPC_SEM_ID, 0);
}

void CoreB_ReadData(uint32_t *out, uint32_t len) {
    while (HAL_HSEM_FastTake(IPC_SEM_ID) != HAL_OK);
    // 使缓存失效,读取最新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)shared_mem, len*4);
    memcpy(out, shared_mem, len*4);
    HAL_HSEM_Release(IPC_SEM_ID, 0);
}

方案 2:无锁环形缓冲 + 内存屏障

对于高频、单生产者单消费者场景,可用无锁环形缓冲(Ring Buffer),配合内存屏障(__DMB())和缓存维护指令,避免信号量开销。

核心思想:

  • 生产者只写 write_index,消费者只读 read_index
  • 使用 __DMB() 确保数据写入顺序。
  • 使用 SCB_CleanDCacheSCB_InvalidateDCache 保证缓存一致。

代码示例:

#define RING_SIZE 256
uint32_t ring[RING_SIZE];
volatile uint32_t write_idx = 0;
volatile uint32_t read_idx = 0;

// 生产者(核A)
int ring_push(uint32_t data) {
    uint32_t next = (write_idx + 1) % RING_SIZE;
    if (next == read_idx) return -1; // 满
    ring[write_idx] = data;
    __DMB(); // 确保数据写入后再更新索引
    write_idx = next;
    SCB_CleanDCache_by_Addr((uint32_t*)&ring[write_idx], 4);
    return 0;
}

// 消费者(核B)
int ring_pop(uint32_t *data) {
    if (read_idx == write_idx) return -1; // 空
    SCB_InvalidateDCache_by_Addr((uint32_t*)&ring[read_idx], 4);
    *data = ring[read_idx];
    __DMB();
    read_idx = (read_idx + 1) % RING_SIZE;
    return 0;
}

方案 3:混合策略:硬件信号量 + 无锁队列

对于多生产者多消费者,可用硬件信号量保护队列的入队/出队操作,但将数据拷贝放在临界区外,减少持锁时间。

优化点:

  • 入队时先拷贝数据到临时区,再获取信号量,快速入队,释放。
  • 出队类似。

四、注意事项与最佳实践

  • 缓存维护必须成对:写后 Clean,读前 Invalidate。否则数据错乱。
  • 避免在 ISR 中获取信号量:硬件信号量可用轮询,但需设置超时,防止死等。
  • 定义超时机制:所有获取操作加超时,超时后返回错误,避免死锁。
  • 使用内存屏障:在更新索引或标志位前后插入 __DMB(),确保顺序。
  • 性能调优:优先使用无锁环形缓冲,信号量仅用于控制流(如通知事件)。
  • 调试技巧:使用逻辑分析仪观察信号量获取/释放时序,或添加调试计数器。

五、总结

AMP 架构下的核间通信,直接使用 RTOS 信号量是“雷区”。通过硬件信号量、缓存维护、内存屏障及无锁设计,可以构建高效且健壮的 IPC。实际项目中,应根据数据量和实时性要求,选择最合适的同步机制。记住:同步机制越简单,越接近硬件,越可靠。