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 启动过程中的常见问题

我总结几个实际项目中遇到过的坑:

  1. 从核启动地址对齐问题:有些架构要求启动地址必须4KB对齐,否则唤醒失败。我吃过这个亏,查了两天才发现是地址没对齐。
  2. 缓存一致性问题:主核修改了启动代码,但从核的缓存里还是旧数据。记得在唤醒从核前执行缓存刷新操作。
  3. 中断优先级配置:从核唤醒过程中,如果收到高优先级中断,可能导致初始化中断。建议在初始化完成前屏蔽所有外部中断。

好了,多核启动流程就讲到这里。说白了就是:主核先跑,从核等着,主核准备好了再叫从核起来干活。这个思路贯穿整个多核RTOS的设计,后面章节还会反复提到。