4. 核间通信基础

在 VxWorks 多核 AMP 系统中,每个 CPU 核运行独立的操作系统映像或独立的任务集合,核与核之间没有共享的调度器和统一的内核锁。因此,核间通信(Inter-Processor Communication,IPC)必须由开发者显式设计。VxWorks 在 AMP 模式下最常用的两条路径是共享内存与消息队列,前者提供高带宽的数据面,后者提供带同步语义的控制面,二者通常配合使用。

共享内存机制与地址映射

共享内存的本质是一块物理地址连续、被多个核共同映射的 DDR 区域。在 AMP 中,各核的 MMU 配置相互独立,同一段物理内存可以在不同核上映射为不同的虚拟地址。例如物理基址 0x8000_0000、长度 4 MB 的共享区,在核 0 上映射为 0x8000_0000,在核 1 上映射为 0xC000_0000,两者访问的是同一份数据。VxWorks 中通常通过 sysMmuMapAdd() 在启动阶段建立该映射,并设置属性为非缓存(uncached)或写通(write-through),避免各核 L1/L2 缓存副本不一致。

共享区内部需要预先划分:头部存放控制结构(如环形缓冲区的读写指针、序号、锁标志),其后是数据区。控制结构必须使用 volatile 限定并配合内存屏障,防止编译器优化和 CPU 乱序执行破坏可见性顺序。

环形缓冲区设计

环形缓冲区(Ring Buffer)是共享内存上最常用的无锁或轻锁数据结构,适合生产者—消费者模型。其核心是三个变量:head(生产者写入位置)、tail(消费者读取位置)、size(容量,取 2 的幂便于掩码取模)。判断空的条件是 head == tail,判断满的条件是 (head + 1) & (size - 1) == tail,即牺牲一个槽位区分空与满。

单生产者单消费者(SPSC)场景下,若 head 只由生产者修改、tail 只由消费者修改,可做到无锁:生产者先写数据,再执行写屏障,最后更新 head;消费者先读 head,再读数据,最后更新 tail。多生产者或多消费者场景则必须引入自旋锁或原子操作(如 vxAtomicLib 提供的原子加/比较交换)。

typedef struct {
    volatile UINT32 head;      /* 生产者写指针 */
    volatile UINT32 tail;      /* 消费者读指针 */
    UINT32 size;               /* 容量,2 的幂 */
    UINT8  data[SHM_BUF_SIZE]; /* 数据区 */
} RING_BUF;

/* 生产者写入(SPSC 无锁) */
status = ringPut(RING_BUF *rb, UINT8 *src, UINT32 len)
{
    UINT32 h = rb->head, t = rb->tail;
    UINT32 free = (t - h - 1) & (rb->size - 1);
    if (len > free) return ERROR;          /* 空间不足 */
    /* 拷贝数据,处理回绕 */
    ...
    VX_MEM_BARRIER();                      /* 写屏障 */
    rb->head = (h + len) & (rb->size - 1);
    return OK;
}

核间中断(IPI)触发

共享内存只解决“数据放在哪里”,不解决“对方何时来看”。若消费者采用轮询,会浪费 CPU 并增加延迟;因此需要核间中断作为通知机制。在 ARM 多核平台上,IPI 通常通过 GIC 的 SGI(Software Generated Interrupt)实现,VxWorks 提供 vxbIpiSend() 或平台相关的 sysIpiSend() 接口,向目标核发送指定中断号。

典型用法是:生产者写完环形缓冲区并更新 head 后,调用 IPI 通知消费者核;消费者核的 IPI 中断服务程序(ISR)只做最小工作——置位信号量或唤醒任务,真正的数据搬运交给任务上下文完成,避免在 ISR 中长时间占用中断。IPI 中断号需在各核的向量表中预先注册处理函数,且要保证发送与接收的核号映射一致。

消息队列实现

VxWorks 原生 msgQCreate()/msgQSend()/msgQReceive() 是单核内的消息队列,不能跨核直接使用。AMP 下的跨核消息队列需要自行构建,常见做法是“共享内存环形缓冲区 + IPI 通知”的组合:控制消息(长度小、要求可靠)走带确认的队列,数据消息(长度大、要求带宽)走共享内存描述符传递。

一种实用结构是描述符队列:共享内存中维护一个固定长度的描述符数组,每个描述符包含数据地址、长度、类型、序号和状态标志。发送方填充描述符并置状态为 READY,然后发 IPI;接收方在 ISR 中扫描 READY 描述符,处理后置为 DONE。序号用于检测丢包和重复,状态标志的读写必须使用原子操作或内存屏障。

数据一致性保证

跨核数据一致性依赖三个层次:内存属性(共享区设为非缓存或写通,避免缓存副本)、内存屏障(VX_MEM_BARRIER() 保证写顺序对其它核可见)、同步原语(自旋锁、原子变量、序号确认)。对于超过一个总线宽度的数据,不能假设单次读写是原子的,必须用锁或双缓冲(ping-pong)切换指针的方式发布。

共享内存区若被错误地配置为可缓存,两个核可能各自读到旧值,表现为“数据偶尔错、重启后正常”的随机故障。调试时应优先检查 MMU 映射属性,而不是先怀疑业务逻辑。
CPU 核 0(生产者) 任务:采集/打包数据 写环形缓冲区 CPU 核 1(消费者) IPI ISR:置信号量 任务:读取并处理 共享内存区(非缓存) 控制头:head / tail / size / seq 环形数据区(2 的幂容量) 描述符数组:addr / len / type 状态 READY / DONE + 序号 写数据 读数据 IPI 核间中断(SGI):通知“有新数据” 一致性三要素:非缓存映射 + 内存屏障(写后更新 head)+ 原子/锁保护描述符状态

综合来看,一个可用的 AMP 核间通信方案应明确四件事:共享区物理地址与各核虚拟映射、环形缓冲区的容量与空满判定、IPI 中断号与 ISR 职责边界、以及描述符状态与序号的同步规则。把这四点固定下来,再叠加业务消息格式,才能得到稳定可复现的跨核通信链路。