2、多核启动流程:主核与从核的启动过程、CPU亲和性设置、启动向量表配置
好,咱们今天聊聊多核启动。说实话,这块儿是很多做嵌入式开发的朋友第一次接触SMP时最头疼的地方。我当年第一次调双核Cortex-A7的时候,从核死活不起来,折腾了两天才发现是启动向量表地址写错了。嗯,这种坑踩过一次,你就再也不会忘了。
2.1 主核与从核的启动过程
多核处理器上电后,并不是所有核都同时跑起来的。硬件设计上,通常只有一个核会先执行复位向量,这个核我们叫它主核(Primary Core),其他核则被硬件暂停在某个状态,等待主核去唤醒它们,这些叫从核(Secondary Core)。
为什么会这样?你想想看,如果所有核同时从复位向量开始跑,谁来初始化内存控制器?谁来配置MMU?谁来建立页表?这些全局资源必须由单个核先搞定,否则大家一起写同一个寄存器,结果就是乱套。
在RT-Thread上,主核的启动流程大致是这样的:
- 主核从复位向量开始执行,完成底层硬件初始化(时钟、DDR、中断控制器等)
- 初始化RT-Thread内核对象(调度器、信号量、消息队列等)
- 创建初始线程,启动调度器
- 通过某种机制唤醒从核
从核那边呢?它被唤醒后,会执行一段独立的启动代码。这段代码通常放在一个固定的物理地址上,我习惯把它叫做从核入口函数。从核在这里完成自己的初始化,比如配置自己的栈指针、使能本地中断、然后进入空闲线程等待调度。
关键点:主核和从核共享同一份内核代码,但每个核有自己的栈空间和线程控制块。调度器会根据核的ID来分配线程。
我记得在某个项目中,从核启动后一直卡在自旋锁上。查了半天,发现是从核的缓存没有使能,导致主核写的共享变量从核读不到。嗯,这里要注意,多核启动时缓存一致性是个大坑。
2.2 CPU亲和性设置
CPU亲和性,说白了就是让某个线程固定在某个核上跑。为什么要这么做?我给你举几个场景:
- 中断处理:网卡中断绑定到核0,避免中断在不同核间迁移导致缓存抖动
- 实时任务:高优先级任务绑定到专用核,不被其他任务干扰
- 性能优化:线程频繁访问的数据如果固定在某个核的L1缓存里,性能会好很多
RT-Thread里设置CPU亲和性,主要通过rt_thread_control接口配合RT_THREAD_CTRL_BIND_CPU命令来实现。我贴一段代码,你一看就明白:
/* 创建一个线程,并绑定到核0 */
rt_thread_t tid = rt_thread_create("my_task",
my_entry, RT_NULL,
2048, 25, 10);
if (tid != RT_NULL) {
/* 设置CPU亲和性掩码,只允许在核0上运行 */
rt_uint32_t cpu_mask = 1 << 0;
rt_thread_control(tid, RT_THREAD_CTRL_BIND_CPU, &cpu_mask);
rt_thread_startup(tid);
}
这里cpu_mask是一个位掩码,每一位代表一个核。比如0x03表示允许在核0和核1上运行,0x01表示只允许在核0上运行。
个人经验:我建议在系统初始化阶段就把关键中断和实时任务绑定好。曾经有个项目,因为没做亲和性设置,导致高优先级任务在核间来回迁移,每次迁移都要刷TLB,延迟从10微秒飙到了50微秒。绑定后问题立刻解决。
另外,RT-Thread的调度器在判断线程是否可运行时,会检查当前核是否在线程的亲和性掩码中。如果不在,调度器会跳过这个线程,继续找下一个。所以,如果你把所有线程都绑到核0上,那核1就只会跑空闲线程,白白浪费了算力。
2.3 启动向量表配置
启动向量表,也叫异常向量表,是处理器发生异常或中断时跳转的入口地址表。在多核系统中,每个核都有自己的向量表,或者所有核共享同一个向量表,这取决于硬件设计。
ARM Cortex-A系列处理器,每个核都有独立的VBAR(Vector Base Address Register)寄存器。这意味着你可以给每个核设置不同的向量表地址。我个人习惯的做法是:
- 主核的向量表放在DDR的起始地址附近
- 从核的向量表放在一个独立的物理页上
- 所有核的向量表内容相同,但地址不同
在RT-Thread的启动代码中,向量表的配置通常放在汇编阶段。以ARMv7架构为例:
/* 设置向量表基地址 */
ldr r0, =__vector_start
mcr p15, 0, r0, c12, c0, 0 /* 写VBAR寄存器 */
对于从核,我们需要在从核入口函数里也做同样的操作。不过要注意,从核启动时可能还在物理地址空间运行,还没开启MMU。这时候向量表地址必须是物理地址。
我曾经踩过的坑:有一次从核启动后,一触发中断就死机。查了三天,发现是从核的向量表地址写的是虚拟地址,但当时MMU还没使能。处理器拿着虚拟地址去取中断向量,结果取回来一堆垃圾数据。嗯,从那以后我每次写从核启动代码,都会在注释里加一句「确保向量表地址是物理地址」。
另外,有些处理器支持GIC(通用中断控制器)的路由功能。你可以配置某个中断只发给特定核。比如网卡中断只发给核0,定时器中断只发给核1。这样配合CPU亲和性,能做出非常精细的中断负载均衡。
总结一下多核启动的几个要点:
| 阶段 | 主核 | 从核 |
|---|---|---|
| 上电复位 | 执行复位向量 | 暂停等待唤醒 |
| 硬件初始化 | 初始化全局资源 | 等待主核信号 |
| 内核启动 | 初始化调度器、创建线程 | 执行入口函数、配置本地资源 |
| 正常运行 | 参与调度 | 参与调度 |
好了,多核启动这块儿就聊到这儿。下一节我们会深入讲讲核间通信和同步机制,那才是真正考验系统设计能力的地方。