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 的硬件 IDstart_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 可见。这通过两层保证实现:
- sfence.vma:刷新 TLB 变更
- sfence.w.inval / sfence.inval.ir:RISC-V 标准的全量 FENCE
- SBI 的 CPPC (Collaborative Processor Performance Control):通过 HART_SUSPEND 扩展传递性能级别请求,在 deep idle 场景下联合 firmware 做功耗决策
- Linux Sstc (Supervisor Timer) 扩展在 SMP 中的角色:Sstc 将
timeCSR 直接暴露给 S-Mode,绕过 SBI Timer 调用,大幅减少 Hart 启动后的时间基准校准开销 - IOMMU + SVA (Shared Virtual Address) 在热插拔场景下:CPU 拔除前需确保 IOMMU 页表清空,否则 DMA 写入会落到已释放内存上
在 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() |
七、进阶方向
八、总结
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 加速器、嵌入式实时系统中的应用逐渐爆发——正确处理多核启动与热插拔,是这些高可靠场景的必备工程能力。

发表评论 取消回复