4、核间同步与通信:共享资源保护、无锁编程与核间中断

各位好,我是老张。今天聊的这个话题,说实话是制动控制多核开发里最让人头疼的部分。核间同步与通信,搞不好就是各种死锁、数据错乱、跑飞。我这些年踩过的坑,有一半都跟这个有关。

咱们先理清一个概念:多核处理器里,每个核都有自己的缓存和寄存器,但共享内存和外设。这就好比几个工人在同一张桌子上干活,你拿我的扳手,我动你的图纸,不乱才怪。所以,核间同步与通信,本质上就是解决「谁先用、怎么传」的问题。

4.1 共享资源保护:MPU/MMU配置

先说共享资源保护。制动控制系统里,关键数据比如轮速、制动压力、控制指令,都是多个核需要访问的。如果不加保护,一个核在写,另一个核在读,读到一半的数据就是错的。

MPU(内存保护单元)和MMU(内存管理单元),就是干这个的。它们给内存区域设置访问权限。比如,我把某个关键数据区设为「只读」,其他核就只能读,不能写。谁想写,硬件直接报错。

我个人习惯,在项目初期就把所有共享内存区域规划好。哪些是只读的,哪些是读写的,哪些是核私有的。然后通过MPU/MMU把这些规则固化下来。

关键配置原则:

  • 最小权限原则:每个核只给它能访问的最小内存范围。能只读就别给读写。
  • 隔离关键数据:制动控制中的安全关键变量(如制动指令),必须放在独立区域,并设置写保护。
  • 非缓存区域:共享数据区建议配置为非缓存(Non-cacheable),避免缓存一致性问题。

举个例子,我在一个项目中配置MPU,把核0的私有堆栈和核1的私有堆栈完全隔离开。结果调试时发现,一个野指针写穿了核0的堆栈,但核1完全不受影响。嗯,这就是MPU的价值。

// 伪代码:配置MPU保护共享数据区
MPU_Region_Config(region_id=0, 
                  base_addr=0x20000000, 
                  size=0x1000, 
                  access=READ_WRITE, 
                  execute=NEVER);
// 这个区域只有当前核能读写,其他核访问会触发异常

注意:MPU/MMU配置不当,会导致系统启动失败或运行时异常。我曾经因为MPU区域重叠,导致一个核无法访问自己的堆栈,系统直接挂掉。调试了两天才找到原因。所以,配置完成后一定要做边界测试。

4.2 无锁编程与原子操作

锁,比如互斥锁,是保护共享资源的常用手段。但在制动控制这种实时性要求极高的场景下,锁的开销太大了。你想想看,一个核在等锁,其他核也在等,系统响应时间就上去了。

所以,无锁编程就派上用场了。它的核心思想是:利用硬件提供的原子操作,保证对共享变量的读写是「不可分割」的。说白了,就是一次完成,不会被中断。

原子操作包括:原子加、原子减、原子比较并交换(CAS)、原子加载/存储等。这些操作由CPU硬件保证,不会被其他核打断。

原子操作 说明 典型应用
原子加/减 对变量进行加1或减1,不可中断 计数器、引用计数
CAS 比较并交换:如果当前值等于期望值,则更新为新值 无锁队列、状态标志
原子加载/存储 保证读取或写入操作的原子性 共享标志位、简单状态

我建议,能用原子操作解决的问题,就别用锁。比如,一个核要更新制动压力值,另一个核要读取。如果这个值是一个32位整数,直接用原子加载和存储就够了。

// 原子操作示例:无锁状态更新
volatile uint32_t brake_status;

// 核0:更新状态
atomic_store(&brake_status, BRAKE_ACTIVE);

// 核1:读取状态
uint32_t status = atomic_load(&brake_status);
if (status == BRAKE_ACTIVE) {
    // 执行相应动作
}

避坑指南:我曾经在无锁队列的实现中,因为内存序(Memory Order)没搞对,导致一个核写入的数据,另一个核读到的还是旧值。后来加了内存屏障(Memory Barrier)才解决。记住:原子操作只是保证原子性,不保证可见性。多核之间,需要配合内存屏障或Acquire/Release语义。

无锁编程的难点在于,你要确保所有可能的并发路径都是安全的。说白了,就是不能有「ABA问题」、不能有「写后读」的乱序。我个人习惯,在实现无锁数据结构后,用形式化验证工具跑一遍,或者做大量的压力测试。

4.3 核间中断(IPI)与消息传递

有时候,一个核需要主动通知另一个核做某件事。比如,核0检测到紧急制动,需要核1立刻响应。这时候,轮询就不行了,太慢。得用核间中断(IPI)

IPI,说白了就是一个核给另一个核发一个硬件中断。接收核的中断服务程序(ISR)会立即执行。这种方式延迟极低,通常在微秒级别。

我常用的模式是:IPI + 消息队列。发送核通过IPI通知接收核,接收核在ISR中从消息队列里取出数据,然后处理。

// 核0:发送IPI并写入消息
void send_message_to_core1(uint32_t msg) {
    // 写入共享消息队列
    message_queue_push(core1_queue, msg);
    // 触发核1的IPI
    trigger_ipi(CORE1, IPI_ID_MESSAGE);
}

// 核1:IPI中断服务程序
void ipi_handler(void) {
    uint32_t msg = message_queue_pop(core1_queue);
    process_message(msg);
}

这里有个细节:消息队列本身也是共享资源,需要保护。但好消息是,在ISR中,我们可以用原子操作来操作队列,避免锁。因为ISR执行时间越短越好。

消息传递的几种模式:

  • 点对点:一个核发给另一个核,简单直接。
  • 广播:一个核发给所有其他核,用于全局状态同步。
  • 邮箱:每个核有一个固定的邮箱,其他核往里写数据。

我记得有一次,在调试制动系统的故障响应时,核0检测到传感器故障,需要核1切换控制策略。我用IPI通知核1,核1在ISR中读取故障码,然后切换。整个过程不到10微秒。如果换成轮询,至少得几十微秒,而且还不确定。

注意:IPI中断服务程序一定要短小精悍。不要在ISR里做复杂计算或等待锁。否则,会影响其他核的实时性。我一般只在ISR里做「取数据、设标志、发信号」这三件事,具体处理放到任务级。

最后总结一下:核间同步与通信,核心就是「保护共享资源」和「高效传递信息」。MPU/MMU是硬件防火墙,原子操作是轻量级工具,IPI是快速通道。这三者配合使用,才能构建一个稳定、高效的制动控制多核系统。

嗯,今天就聊到这儿。下一章咱们讲讲任务调度与负载均衡,那又是另一个有意思的话题。