RISC-V SMP 启动与 CPU 热插拔深度实战 格尼

RISC-V SMP 启动与 CPU 热插拔深度实战:从设备树解析到 SBI 调用到调度器就绪

在 x86 和 ARM64 平台上,多核启动(SMP boot)已经被固件和内核封装得非常完善,开发者几乎不需要关心 secondary CPU 是如何从复位状态进入 Linux 调度器就绪队列的。但在 RISC-V 生态中,SMP 启动还保留着更"原始"的风貌:SBI(Supervisor Binary Interface)规范将硬件抽象出一组统一的调用接口,从 M-Mode 固件到 S-Mode 内核之间的协同需要开发者清晰理解 Hart 状态机、设备树协商、以及启动时序的每一个细节。

本文将从零剖析 RISC-V SMP 启动的完整链路,涵盖 Hart 状态机模型、SBI HSM(Hart State Management)扩展的调用流程、设备树中 CPU 节点的含义、secondary 入口代码的实现逻辑,以及 CPU 热插拔在 RISC-V 上的工程化落地。文中所有代码均基于 Linux 6.6+ 内核与 OpenSBI v1.4+。


一、Hart 状态机模型

RISC-V 使用 "Hart"(Hardware Thread)而非 "CPU Core" 来指代执行单元。在 SMP 场景下,每个 Hart 在系统复位后会处于以下四种状态之一:


AVAILABLE → 固件已初始化,等待操作系统启动
STOPPED   → 已请求停止,通常是复位后的初始状态
STARTING  → 启动中,正在执行入口函数
STARTED   → 已启动并运行在操作系统中

HSM 扩展定义了状态转换规则:

转换 调用 描述
STOPPED → STARTING sbi_hart_start() 固件将 Hart 唤醒至指定入口
STARTING → STARTED sbi_hart_stop() self Hart 自行报告进入 STARTED
STARTED → STOPPED sbi_hart_stop() 操作系统请求关闭 Hart
STARTED → SUSPENDED sbi_suspend() 进入低功耗 suspend 状态

关键约束:只有 AVAILABLE 状态的 Hart 才能被 STARTING。这意味着内核在尝试启动一个 Hart 之前,必须先通过 sbi_hart_get_status() 查询其当前状态,避免重复启动或启动处于不可恢复错误状态的 Hart。


二、SBI HSM 接口实现分析

2.1 hart_start 调用流程

hart_start 是 SMP boot 的核心调用,函数原型为:


struct sbi_ret sbi_hart_start(u32 hartid, ulong start_addr, ulong priv)

其中三个参数含义如下:

  • hartid:目标 Hart 的硬件 ID
  • start_addr:Hart 复位后开始执行的物理地址(必须是 supervisor 可访问的内存区域)
  • priv:透明传递给 Hart 的参数(通常通过 a0 寄存器传递到 entry point)

在 OpenSBI 内部,hart_start 的处理逻辑简化如下:


/* opensbi/sbi_hsm.c */
int sbi_hart_start(struct sbi_scmp *dom, u32 hartid,
                    ulong saddr, ulong sdata)
{
    struct sbi_hart_data *hart = &hart_data[hartid];
    
    /* 1. 检查 Hart 状态 */
    if (hart->state != AVAILABLE)
        return SBI_ERR_INVALID_STATE;
    
    /* 2. 设置 Hart 入口地址(写 mscatterwrite 寄存器,如果存在) */
    hart->start_addr = saddr;
    hart->priv_data   = sdata;
    
    /* 3. 发送 IPI 唤醒目标 Hart */
    sbi_platform_ipi_send(dom->platform, hartid);
    
    /* 4. 等待 Hart 确认进入 STARTED(带超时) */
    rc = sbi_hsm_wait_for_completion(hart, HSM_BOOT_TIMEOUT);
    
    return rc ? SBI_ERR_TIMEOUT : SBI_SUCCESS;
}

需要注意:OpenSBI 在 M-Mode 处理完毕后会将目标 Hart 的 mepc 寄存器设置为 start_addr,并将 Hart 的 mstatus.MPP 设置为 Supervisor 模式(1),然后通过 mret 跳转到 S-Mode 的入口地址。这意味着 start_addr 对应的代码必须已经在 S-Mode 下可执行。

2.2 hart_get_status 与状态机守护

在实际工程中,Hart 状态查询是热插拔决策的基础:


struct sbi_ret sbi_hart_get_status(u32 hartid)
{
    if (hartid >= num_harts)
        return SBI_ERR_INVALID_PARAM;
    
    /* 检查 Hart 是否存在(从设备树或 ACPI 解析而来) */
    if (!hart_available(hartid))
        return (struct sbi_ret){SBI_ERR_DENIED, 0};
    
    return (struct sbi_ret){SBI_SUCCESS, hart_data[hartid].state};
}

OpenSBI 内部维护了一个有限状态机来跟踪每个 Hart 的状态,每次状态变更都会触发审计日志(如果开启 CONFIG_SBI_HSM_DEBUG),这对于调试热插拔时序问题非常有用。


三、Linux 内核侧 Secondary CPU 启动代码

3.1 入口路径与寄存器约定

当 OpenSBI 通过 mret 唤醒 secondary Hart 后,执行流从 arch/riscv/kernel/entry.S 的 secondary_start_kernel 开始:


/* arch/riscv/kernel/entry.S */
SYM_FUNC_START(secondary_start_kernel)
    /* 此时:
     * a0 = hartid (由 OpenSBI 设置)
     * a1 = 设备树物理地址 (由 OpenSBI 设置,通过 fdt 寄存器)
     */
    
    /* 1. 设置 Hart 本地 tp 寄存器(指向 hart 自身数据) */
    mv  tp, a0
    
    /* 2. 跳转到 C 入口,传递 hartid 和 dtb 地址 */
    tail smp_callin
SYM_FUNC_END(secondary_start_kernel)

内核在编译期为每个 Hart 预留了一段 TLS(Thread Local Storage)区域,tp 寄存器指向该区域,后续的 tp 相对寻址用于访问 struct task_struct 等 per-hart 数据。

3.2 smp_callin 核心流程

smp_callin 是 secondary Hart 在 C 语言中的第一个调用点,位于 arch/riscv/kernel/smpboot.c:


/* arch/riscv/kernel/smpboot.c */
asmlinkage __visible void smp_callin(void)
{
    struct task_struct *current_task;
    int hartid = smp_processor_id();
    int cpuid  = hartid;  /* 在简单拓扑中两者等价 */
    
    /* 1. 通知固件:Hart 已进入 STARTED 状态 */
    csr_write(CSR_SIP, 0);   /* 清除挂起的 IPI */
    sbi_hart_started();
    
    /* 2. 初始化 Hart 本地中断控制器描述 */
    riscv_init_ipi();
    
    /* 3. 配置 PMP(物理内存保护)— 如果需要 */
    riscv_pmp_configure(hartid);
    
    /* 4. 通知 CPU 热插拔通知链 */
    notify_cpu_starting(cpuid);
    
    /* 5. 设置 per_cpu 区域指针 */
    set_cpu_online(cpuid, true);
    set_cpu_active(cpuid, true);
    set_cpu_present(cpuid, true);
    set_cpu_possible(cpuid, true);
    
    /* 6. 完成启动,进入空闲循环 */
    cpu_startup_entry(CPUHP_AP_ONLINE_IDLE);
}

关键细节:sbi_hart_started() 不是整个 SMP boot 的必要步骤,但它可以让固件侧的状态机正确转换。缺失这一步将导致固件认为 Hart 处于 STARTING 状态,后续的 sbi_hart_stop() 可能返回不正确的状态或超时。


四、设备树中的 CPU 拓扑描述

4.1 设备树节点规范

RISC-V 使用设备树来描述 CPU 拓扑,典型如下:


cpus {
    #address-cells = <0x01>;
    #size-cells  = <0x00>;
    timebase-frequency = <10000000>;
    
    cpu@0 {
        device_type    = "cpu";
        reg            = <0x00>;
        compatible     = "riscv";
        mmu-type       = "riscv,sv48";
        riscv,cbom-size = <64>;
        riscv,isa      = "rv64imafdcvsu_sstc";
        status         = "okay";
        enable-method  = "cpu-enable-xxx";
        cpu-idle-states = <&CPU_RET_0>;
        
        cache-size     = <1048576>;
        cache-block-size = <64>;
        cache-sets     = <1024>;
        cache-level    = <2>;
        
        interrupt-controller {
            #interrupt-cells = <0x01>;
            interrupt-controller;
            compatible       = "riscv,cpu-intc";
            phandle          = <0x01>;
        };
    };
    
    cpu@1 { /* 类似结构 */ };
    /* ... */
};

关键字段解析:

  • reg:Hart ID,Linux 使用此值作为 CPU 的逻辑编号
  • riscv,isa:RISC-V 扩展声明,决定了内核在启动时选择哪些扩展优化路径
  • enable-method:可选,传统上用于指示固件启动方式,现代 RISC-V 通常省略
  • riscv,cbom-size:Cache Block Override and Management 块大小,用于内核 cache flush 操作
  • status:"okay" 表示可用,"disabled" 表示禁用(可用于热拔)

4.2 内核对设备树的解析逻辑

内核启动时通过 arch/riscv/kernel/smpboot.c 的 cpus_of_parse() 遍历设备树:


/* arch/riscv/kernel/smpboot.c -> smp_init_cpus() */
static int __init smp_init_cpus(void)
{
    struct device_node *dn;
    int hartid, rc;
    
    for_each_of_cpu_node(dn) {
        rc = riscv_of_parent_hartid(dn, &hartid);
        if (rc) {
            pr_warn("invalid CPU device tree entry\n");
            continue;
        }
        
        if (hartid >= NR_CPUS) {
            pr_warn("hartid %d too large, increase NR_CPUS\n", hartid);
            continue;
        }
        
        /* 标记该 hart 在 CPU 可能集合中 */
        set_cpu_possible(hartid, true);
        hartid_to_cpuid[hartid] = hartid;
        
        /* 检查 status 字段 */
        if (!of_device_is_available(dn)) {
            set_cpu_present(hartid, false);
            pr_info("hart%d disabled by device tree\n", hartid);
        } else {
            /* 尝试读取 cache 层级信息 */
            riscv_of_core_get_cache(dn, hartid);
        }
    }
    
    return 0;
}
early_initcall(smp_init_cpus);

注意 hartid_to_cpuid 映射表:在简单的一对一拓扑中它与 hartid 相同,但在带线性加速器的 SoC 中可能存在 Hart ID 重排。


五、CPU 热插拔:从 cpu_down() 到 sbi_hart_stop()

5.1 热拔流程详解

Linux 的 CPU 热插拔框架分为多个阶段,在 RISC-V 上的实现与 ARM64 类似,但 firmware 交互层不同:


/* arch/riscv/kernel/cpuhotplug.c */
static int cpu_down(unsigned int cpu)
{
    struct task_struct *idle = idle_thread_get(cpu);
    int ret;
    
    /* 阶段 1:阻止新任务调度 */
    set_cpu_online(cpu, false);
    
    /* 阶段 2:迁移中断与 RCU */
    migrate_irqs_away(cpu);
    rcu_cpu_offline(cpu);
    
    /* 阶段 3:调用固件停 Hart */
    ret = sbi_hart_stop(true);  /* true = do_wfi(执行 WFI 等待唤醒) */
    if (ret) {
        /* 停 Hart 失败,需恢复状态 */
        set_cpu_online(cpu, true);
        return ret;
    }
    
    /* Hart 已停止,重新启动时 firmware 会将 PC 恢复至
     * cpu_suspend 的 return 地址(如果配置了 cpu 休眠)
     * 或 cpu_resume entry(如果需要完全重启) */
}

sbi_hart_stop() 在 OpenSBI 内部的具体行为:


int sbi_hart_stop(bool do_wfi)
{
    /* 1. 通知上层:本 Hart 正在停止 */
    hsm_hart_suspend(current_hartid());
    
    /* 2. 保存 Hart 上下文(PC、寄存器、CSR) */
    hart_context_save(current_hartid());
    
    /* 3. 刷新 cache 一致性 */
    sbi_cache_flush_all();
    
    /* 4. 设置下次 hart_start 后的恢复地址 */
    hart_data[hartid].resume_addr = virt_to_phys(cpu_resume_entry);
    
    /* 5. 清除本地中断 */
    csr_clear(CSR_IE, MIP_MSIP | MIP_MTIP | MIP_MEIP);
    
    /* 6. 执行 WFI 等待下一次启动请求 */
    if (do_wfi)
        __wfi();
    
    return SBI_SUCCESS;
}

5.2 热插时序与内存一致性

CPU 热插拔中最容易出问题的是内存一致性窗口期。考虑以下时序:


时间 →
CPU0: | set_cpu_online(1,false) | ----- 广播 IPI 停止 CPU1 ----→
CPU1:                        | 收到 IPI | 执行 cpu_down() | sbi_hart_stop() | WFI

关键点:CPU1 在 sbi_hart_stop() 返回前必须确保所有 out-of-order 写入对 CPU0 可见。这通过两层保证实现:

  1. sfence.vma:刷新 TLB 变更
  2. sfence.w.inval / sfence.inval.ir:RISC-V 标准的全量 FENCE
  3. 在 RISC-V Linux 源码中,上述调用序列如下:

    
    /* arch/riscv/include/asm/sbi.h */
    static inline void sbi_remote_fence_i(const struct cpumask *mask)
    {
        struct __riscv_flush f = {
            .start = 0,
            .size  = 0,  /* 全刷新 */
            .fid   = SBI_EXT_FI_FENCE_I,
        };
        sbi_ecall(SBI_EXT_FFENCE, SBI_FENCE_I, (__ulong)&f, 0, 0, 0, 0, 0);
    }
    

    5.3 热插拔失败恢复

    在硬件异常(如 sbi_hart_start 超时)场景下,正确的恢复路径是:

    
    static int cpu_up(unsigned int cpu)
    {
        /* 标准路径 */
        int ret = do_cpu_up(cpu, idle_thread_get(cpu));
        if (ret) {
            if (ret == -ETIMEDOUT) {
                /* firmware 唤醒超时,可能是 Hart 已损坏 */
                pr_err("CPU%d up: firmware timeout\n", cpu);
                set_cpu_possible(cpu, false);
                return ret;
            }
            
            /* 其他错误(内存不足等),保留 possible 状态 */
            return ret;
        }
        
        /* 通知调度器 CPU 已就绪 */
        sched_cpu_starting(cpu);
        return 0;
    }
    

    六、调试与性能分析

    6.1 启动时序追踪

    利用内核的 tracepoint 可以精确测量每个阶段的开销:

    
    # 开启 SMP boot 追踪
    echo 1 > /sys/kernel/tracing/events/sched/sched_cpu_hotplug/enable
    echo 1 > /sys/kernel/tracing/events/riscv/riscv_sbi_hsm_call/enable
    
    # 触发 CPU 热插
    echo 0 > /sys/devices/system/cpu/cpu3/online
    echo 1 > /sys/devices/system/cpu/cpu3/online
    
    # 查看追踪结果
    cat /sys/kernel/tracing/trace_pipe
    

    6.2 固件侧调试

    OpenSBI 编译时添加 CONFIG_HSM_DEBUG 后,会在每个 HSM 转换时打印日志:

    
    [opensbi] Hart 4: AVAILABLE → STARTING (entry=0x80a00000)
    [opensbi] Hart 4: STARTING → STARTED (耗时 1.2ms)
    [opensbi] Hart 4: STARTED → STOPPED (sbi_hart_stop 请求)
    [opensbi] Hart 4: STOPPED → STARTING (wake from sbi_hart_start)
    

    6.3 常见陷阱与解法等**

    问题现象 根因 解法
    secondary 卡在 WFI 后无输出 OpenSBI 版本不支持 HSM v0.3 升级 OpenSBI 至 v1.2+
    启动后立即写只读页异常 设备树地址未对齐到 8KB 确保 fdt 对齐并检查 riscv,kernel-start
    热拔后重新上线 Hart ID 错误 hartid_to_cpuid 映射表未重建 清理并重新初始化 per-hart 数据
    CPU suspend 恢复后时钟漂移 未重新校准 time CSR 在 resume 路径中调用 clockevents_resume()

    七、进阶方向

    1. SBI 的 CPPC (Collaborative Processor Performance Control):通过 HART_SUSPEND 扩展传递性能级别请求,在 deep idle 场景下联合 firmware 做功耗决策
      1. Linux Sstc (Supervisor Timer) 扩展在 SMP 中的角色:Sstc 将 time CSR 直接暴露给 S-Mode,绕过 SBI Timer 调用,大幅减少 Hart 启动后的时间基准校准开销
        1. IOMMU + SVA (Shared Virtual Address) 在热插拔场景下:CPU 拔除前需确保 IOMMU 页表清空,否则 DMA 写入会落到已释放内存上

        2. 八、总结

          RISC-V SMP 启动与 CPU 热插拔是理解现代异构计算平台的重要窗口。不同于 x86 的 ACPI/UEFI 或 ARM64 的 PSCI 抽象层,RISC-V 通过 SBI 规范保留了更强的平台可编程性,也使得固件与内核的接口更显式。掌握 Hart 状态机模型、设备树 CPU 节点语义、以及 sbi_hart_start/stop 的时序约束,是在 RISC-V SoC 平台进行系统开发的安全底线。

          在云原生与边缘 AI 爆发的 2026 年,RISC-V 在 DPU 控制器、AI 加速器、嵌入式实时系统中的应用逐渐爆发——正确处理多核启动与热插拔,是这些高可靠场景的必备工程能力。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部