3、内核启动流程:主核初始化,从核唤醒,启动向量表配置
多核启动,说白了就是让多个CPU核心有序地跑起来。你想想看,一上电所有核心都同时开始执行代码,那不乱套了?所以必须有个主从关系,有个先后顺序。
我个人习惯把多核启动分成三个阶段:主核初始化、从核唤醒、启动向量表配置。这三个阶段环环相扣,任何一个出问题,系统都起不来。
3.1 主核初始化:谁先跑,谁说了算
系统上电后,所有核心都会从复位向量地址开始执行。但我们需要指定一个核心作为主核(通常是Core 0),让它先完成全局初始化。
主核要干的事情,我列一下:
- 初始化中断控制器(GIC或APIC)
- 配置系统时钟和定时器
- 初始化内存管理单元(MMU)
- 建立内核数据结构(就绪队列、任务控制块池等)
- 初始化外设驱动
这里有个关键点:其他核心必须等待。我在项目中遇到过一个问题,从核在主核还没初始化完MMU时就尝试访问内存,结果直接触发异常。嗯,这个坑踩过一次就记住了。
核心要点:主核初始化期间,从核必须处于自旋等待状态,不能执行任何可能访问共享资源的代码。
3.2 从核唤醒:怎么让其他核心动起来
主核初始化完成后,就该唤醒从核了。不同架构的唤醒方式不一样,我以ARM和RISC-V为例说明。
3.2.1 ARM架构:PSCI协议
ARM多核处理器通常使用PSCI(Power State Coordination Interface)协议。主核通过SMC指令调用PSCI服务,发送CPU_ON请求来唤醒从核。
// 伪代码:主核唤醒从核
void wake_up_core(int core_id) {
// 设置从核的启动地址
psci_cpu_on(core_id, secondary_startup_addr);
// 等待从核确认
while (!core_online[core_id]) {
// 自旋等待
__asm__("wfe");
}
}
从核被唤醒后,会跳转到secondary_startup_addr执行。这个地址指向一个专门的从核启动函数。
3.2.2 RISC-V架构:CLINT中断
RISC-V的做法更直接。主核通过CLINT(Core Local Interruptor)向从核发送软件中断,从核在中断服务函数中完成初始化。
// RISC-V从核唤醒示例
void riscv_wake_secondary(int hart_id) {
// 设置从核的启动PC
*CLINT_MSIP(hart_id) = 1; // 触发软件中断
// 从核在中断处理中跳转到启动地址
}
我的经验:从核唤醒后,第一件事是初始化自己的私有资源,比如本地定时器、私有中断等。我曾经因为忘记初始化从核的本地定时器,导致任务调度时时间片计算全乱套了。
3.3 启动向量表配置:入口地址怎么设
启动向量表,说白了就是告诉CPU:发生异常或中断时,该跳到哪里去执行。多核环境下,每个核心都需要有自己的向量表,或者共享一个但要有区分机制。
3.3.1 向量表的结构
典型的向量表包含以下条目:
| 偏移地址 | 异常类型 | 说明 |
|---|---|---|
| 0x00 | 复位向量 | 系统启动时执行的入口 |
| 0x04 | 未定义指令 | 执行非法指令时触发 |
| 0x08 | 软件中断 | 用于从核唤醒等场景 |
| 0x0C | 预取指令中止 | 指令访问异常 |
| 0x10 | 数据中止 | 数据访问异常 |
| 0x14 | IRQ | 普通中断请求 |
| 0x18 | FIQ | 快速中断请求 |
3.3.2 多核向量表的配置策略
我见过两种做法:
- 共享向量表:所有核心使用同一个向量表,通过核心ID区分处理逻辑。优点是节省内存,缺点是访问时需要加锁。
- 私有向量表:每个核心有独立的向量表,放在各自的内存区域。优点是访问快,缺点是占用更多内存。
我个人更倾向于私有向量表。为什么?因为中断响应速度在RTOS里太重要了。我在一个工业控制项目里用过共享向量表,结果两个核心同时触发中断时,锁竞争导致响应延迟增加了30%。后来改成私有向量表,问题就解决了。
注意:配置私有向量表时,一定要确保每个核心的向量表地址正确写入对应的VBAR(Vector Base Address Register)寄存器。我曾经在调试时发现一个核心的向量表地址写错了,结果它一触发中断就跳到了未知地址,系统直接崩溃。
3.4 完整的启动流程示例
我把整个流程串起来,给一个简化的代码示例:
// 主核启动流程
void primary_core_main(void) {
// 1. 初始化系统全局资源
system_global_init();
// 2. 配置主核向量表
set_vbar((uint32_t)&primary_vector_table);
// 3. 唤醒从核
for (int i = 1; i < NUM_CORES; i++) {
wake_up_core(i);
}
// 4. 等待所有从核就绪
wait_for_all_cores_ready();
// 5. 启动RTOS调度器
rtos_start_scheduler();
}
// 从核启动流程
void secondary_core_main(void) {
// 1. 配置私有向量表
set_vbar((uint32_t)&secondary_vector_table);
// 2. 初始化本地资源
local_timer_init();
private_interrupt_init();
// 3. 通知主核已就绪
core_ready[get_core_id()] = 1;
// 4. 等待调度器启动
wait_for_scheduler();
// 5. 进入空闲任务循环
idle_task_loop();
}
避坑指南:我曾经在从核初始化时,把本地资源的初始化顺序搞反了。先初始化了中断控制器,后初始化定时器,结果定时器中断来了,中断控制器还没准备好处理。嗯,从那以后我养成了习惯:先初始化硬件,再使能中断。
3.5 启动过程中的常见问题
我总结几个实际项目中遇到过的坑:
- 从核启动地址对齐问题:有些架构要求启动地址必须4KB对齐,否则唤醒失败。我吃过这个亏,查了两天才发现是地址没对齐。
- 缓存一致性问题:主核修改了启动代码,但从核的缓存里还是旧数据。记得在唤醒从核前执行缓存刷新操作。
- 中断优先级配置:从核唤醒过程中,如果收到高优先级中断,可能导致初始化中断。建议在初始化完成前屏蔽所有外部中断。
好了,多核启动流程就讲到这里。说白了就是:主核先跑,从核等着,主核准备好了再叫从核起来干活。这个思路贯穿整个多核RTOS的设计,后面章节还会反复提到。