19、多核处理器的启动流程:主核与从核的同步
多核处理器,说白了就是一块芯片上放了多个CPU核心。
你可能会想:那它们怎么启动?是一起跑,还是一个一个来?
答案是:必须有一个主次顺序。我做过好几款多核芯片的底层启动,每次都要跟这个同步问题打交道。今天咱们就把它彻底讲透。
19.1 主核与从核的基本分工
多核启动的核心思路很简单:
- 主核(Boot Core / Primary Core):负责整个系统的初始化,包括时钟、DDR、中断控制器、外设等。它先跑起来。
- 从核(Secondary Core / Slave Core):等待主核发信号,然后才启动自己的执行流。
为什么不能所有核一起启动?
嗯,这里有个关键问题:如果多个核同时去配置同一个硬件寄存器,结果就是乱套。比如两个核同时写DDR控制器,谁说了算?
所以必须由主核统一指挥,从核乖乖等着。
核心原则:多核启动 = 主核先跑 + 从核等待 + 同步握手。
19.2 典型的多核启动流程
我以ARM Cortex-A系列为例,讲一下最常见的启动步骤。其他架构(RISC-V、MIPS)逻辑类似,只是寄存器名字不同。
- 上电复位:所有核心同时从复位向量地址开始执行。
- 核心身份识别:每个核读取自己的核心ID(如MPIDR寄存器),判断自己是主核还是从核。
- 主核执行初始化:主核继续跑,配置时钟、DDR、中断、MMU等。
- 从核进入低功耗等待:从核执行一个WFI(Wait For Interrupt)或自旋循环,等待主核唤醒。
- 主核发送唤醒信号:主核通过SEV指令或写特定寄存器,唤醒从核。
- 从核跳转到入口函数:从核被唤醒后,读取主核指定的启动地址,跳转执行。
你看,整个过程就像一场接力赛。主核先跑第一棒,然后把接力棒交给从核。
19.3 核心身份识别:谁当主核?
每个核心都有一个唯一的硬件ID。在ARM中,这个ID存在MPIDR寄存器里。
通常,ID为0的核心被指定为主核。其他核心都是从核。
代码实现很简单:
// 伪代码:核心身份识别
uint32_t core_id = read_mpidr();
if (core_id == 0) {
// 我是主核
primary_main();
} else {
// 我是从核
secondary_wait();
}
我在项目中遇到过一个问题:某款芯片的MPIDR寄存器布局不是标准的,导致从核误判成了主核。结果两个核同时去初始化DDR,系统直接挂掉。排查了好久才发现是硬件手册写错了。
注意:不同芯片厂商的MPIDR实现可能有差异。一定要仔细看芯片手册,不要想当然。
19.4 从核的等待机制
从核在等待期间不能闲着乱跑,否则会干扰主核的初始化。常见的等待方式有两种:
| 方式 | 原理 | 功耗 | 适用场景 |
|---|---|---|---|
| 自旋等待(Spin-loop) | 不断轮询一个内存变量 | 高(CPU一直跑) | 简单、实时性要求高 |
| WFI等待 | 进入低功耗状态,等待中断唤醒 | 低 | 省电场景 |
我个人习惯在启动阶段用自旋等待。为什么?因为启动时不在乎那点功耗,但响应速度要快。等系统跑起来后,再切换到WFI模式省电。
自旋等待的代码长这样:
// 从核自旋等待
volatile uint32_t *boot_flag = (uint32_t *)0x80000000;
volatile uint32_t *boot_addr = (uint32_t *)0x80000004;
while (*boot_flag == 0) {
// 空转,等待主核写1
__asm__("wfe"); // 有些架构用WFE代替WFI,更轻量
}
// 主核已经写好了启动地址,跳过去执行
void (*entry)(void) = (void (*)(void))(*boot_addr);
entry();
这里有个坑:boot_flag 和 boot_addr 必须放在所有核心都能访问到的共享内存区域。如果放在某个核的私有Cache里,其他核根本看不到更新。
避坑指南:我曾经把启动标志放在主核的本地SRAM里,结果从核一直读不到更新。后来改成DDR里的共享区域,问题解决。记住:共享变量一定要用volatile,并且考虑Cache一致性。
19.5 主核唤醒从核的机制
主核完成初始化后,需要通知从核开始干活。不同架构的唤醒方式不同:
- ARM:使用SEV(Send Event)指令,或者写GIC(通用中断控制器)的SGI(软件生成中断)。
- RISC-V:通过CLINT(核心本地中断器)的IPI(核间中断)机制。
- DSP:通常通过写特定的控制寄存器。
以ARM为例,主核的唤醒代码:
// 主核:设置从核的启动地址
*((uint32_t *)0x80000004) = (uint32_t)secondary_entry;
__asm__("dsb"); // 确保写操作完成
// 设置启动标志
*((uint32_t *)0x80000000) = 1;
__asm__("dsb");
__asm__("sev"); // 发送事件,唤醒从核
注意这里的 dsb 指令。它保证前面的写操作在内存系统中可见。没有它,从核可能读到旧数据。
我见过一个案例:工程师忘了加 dsb,结果从核被唤醒后读到的启动地址是0,直接跳到了空指针。嗯,这种bug非常难查。
19.6 多核启动的同步点
整个启动过程中,有几个关键的同步点:
- 复位后身份识别:所有核同时读ID,但只有主核继续。
- 从核等待就绪:从核必须等到主核完成初始化。
- 主核唤醒从核:主核发信号,从核响应。
- 从核跳转执行:从核开始跑自己的任务。
每个同步点都需要硬件或软件机制来保证顺序。我习惯用一张图来理解:
这张图把主核和从核的流程并列展示。你可以看到,三个同步点把整个启动过程串联起来。任何一个同步点出问题,系统都起不来。
19.7 实际项目中的注意事项
做了这么多年底层,我总结了几条多核启动的实战经验:
- 共享变量必须加volatile:编译器优化可能把轮询变量优化成死循环。
- 注意Cache一致性:如果从核的Cache里缓存了旧值,它永远看不到主核的更新。必要时可以关闭从核的Cache,或者使用Cache清理指令。
- 启动地址要对齐:有些架构要求启动地址4字节对齐或8字节对齐,不对齐会触发异常。
- 从核的栈要独立:每个从核必须有自己独立的栈空间,不能共用。否则函数调用会互相覆盖。
警告:千万不要在主核初始化完成之前,让从核去访问外设。从核可能读到未初始化的寄存器值,导致不可预知的行为。
19.8 小结
多核启动的核心就是「主核指挥,从核等待」。主核负责全局初始化,从核通过自旋或WFI等待信号。同步点必须用硬件或软件机制保证。
我个人觉得,多核启动的难点不在于代码有多复杂,而在于你能否把时序和同步想清楚。只要把主从关系理清,剩下的就是填代码了。
嗯,这一章就到这里。记住:多核启动,同步第一。
公众号:蓝海资料掘金营,微信deep3321