From c13fd33933547e3b2a725a75e8388332763d7532 Mon Sep 17 00:00:00 2001 From: Zhangjin Wu Date: Tue, 16 Jul 2024 12:11:08 +0800 Subject: [PATCH] add smp-linux-boot Signed-off-by: Zhangjin Wu --- _posts/2024-07-16-12-10-27-smp-linux-boot.md | 507 +++++++++++++++++++ 1 file changed, 507 insertions(+) create mode 100644 _posts/2024-07-16-12-10-27-smp-linux-boot.md diff --git a/_posts/2024-07-16-12-10-27-smp-linux-boot.md b/_posts/2024-07-16-12-10-27-smp-linux-boot.md new file mode 100644 index 000000000..edaefd9cd --- /dev/null +++ b/_posts/2024-07-16-12-10-27-smp-linux-boot.md @@ -0,0 +1,507 @@ +--- +layout: post +author: 'sugarfillet' +title: 'RISC-V SMP Linux boot process' +draft: false +album: 'RISC-V Linux' +license: 'cc-by-nc-nd-4.0' +permalink: /smp-linux-boot/ +description: 'RISC-V SMP Linux boot process' +category: + - 开源项目 + - Risc-V +tags: + - Linux + - RISC-V + - U-Boot + - SPL + - OpenSBI + - WFI + - SMP +--- + +> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.1 - [spaces] +> Author: sugarfillet +> Date: 2023/01/30 +> Revisor: Falcon falcon@tinylab.org +> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux) +> Proposal: [RISC-V Linux SMP 技术调研与分析](https://gitee.com/tinylab/riscv-linux/issues/I5MU96) +> Sponsor: PLCT Lab, ISCAS + + +## 前言 + +[这篇文章][1] 描述了 RISC-V 平台的固件启动过程,其中指出在 M 模式下的 U-Boot SPL 和 opensbi 确认了唯一的 boot_hart,S 模式下,其他 hart 挂起在 opensbi 中执行 wfi 指令,U-Boot proper 运行在 boot_hart 中,加载并移交系统控制权给 Linux。本文在此基础上,继续分析在 Linux 中的 SMP 启动过程。 + +**说明**: + +* 本文的 Linux 版本采用 `Linux v6.0` +* 本文着重分析 SMP 的启动过程,涉及到的其他内容:内存管理、调度原理,点到为止 + +## 内核入口 _start_kernel + +运行在 boot hart 上的 U-Boot proper 在加载 Linux 时为其传递两个参数,a0 存储启动 hart(boot_hart),a1 存储 dtb 的物理地址。Linux 启动时调用 `_start_kernel` 进行当前 hart 的基本初始化并调用 `start_kernel()` C 函数,其关键流程如下: + +- 清零 IE IP 寄存器以关闭中断 +- 设置 gp 寄存器 +- 通过 a0 传递的 boot_hart 保存在 boot_cpu_hartid 变量中 +- setup_vm 以 a1 传递的 dtb 地址做初期的虚拟内存配置 +- relocate_enable_mmu 执行开启 mmu 情况下的内核重定位 +- setup_trap_vector 用于设置当前 hart 的中断向量表 +- 设置线程指针(init_task)和栈指针,并调用 start_kernel 函数执行后续的启动代码 + +```c +// arch/riscv/kernel/head.S : 196 + +_start_kernel: + csrw CSR_IE, zero + csrw CSR_IP, zero + ... + /* Save hart ID and DTB physical address */ + mv s0, a0 + mv s1, a1 + + la a2, boot_cpu_hartid + REG_S a0, (a2) + ... + mv a0, s1 + call setup_vm + ... + la a0, early_pg_dir + call relocate_enable_mmu + ... + call setup_trap_vector + CSR_TVEC handle_exception + + la tp, init_task + la sp, init_thread_union + THREAD_SIZE + ... + tail start_kernel +``` + +在分析 `start_kernel` 函数中的 SMP 启动流程之前,我们先了解下 cpu 在启动过程中的几个状态。 + +## cpu 状态 + +Linux 中对 cpu 状态通过几组 bitmap -- `cpu_*_mask` 来表示,每个 bitmap 中的一位则表示一个 cpu 是否在当前状态: + +- cpu_possible_mask 代表可以被系统使用的 cpu,一般来说,就是在 dts 文件中定义的 cpu 节点 +- cpu_present_mask 代表已经被系统接管的 cpu +- cpu_online_mask 代表可以被调度器使用的 cpu +- cpu_active_mask 表示在开启 cpu hotplug 的系统中,调度器在收到 cpu 资源变更的事件时对 cpu 状态的定义 + - 比如,某个 cpu 下线,调度器需要把当前 cpu 的执行上下文迁移到其他 cpu 上去做,则会把当前 cpu 从 cpu_active_mask 中移除 + +关键代码如下: + +```c +// include/linux/cpumask.h : 97 + +#define cpu_possible_mask ((const struct cpumask *)&__cpu_possible_mask) +#define cpu_online_mask ((const struct cpumask *)&__cpu_online_mask) +#define cpu_present_mask ((const struct cpumask *)&__cpu_present_mask) +#define cpu_active_mask ((const struct cpumask *)&__cpu_active_mask) + +``` + +以上对于 cpu 状态的描述可能过于简单,下文以 cpu 状态的变化为线索来观察 SMP 的启动过程,同时加深对上述状态的理解。 + +## start_kernel + +这里先简单罗列下 `start_kernel()` 函数中与 SMP 启动相关的关键函数,我们一个一个来分析: + +```c +// init/main.c : 936 + +start_kernel() + smp_setup_processor_id(); + boot_cpu_init(); + setup_arch(); + setup_smp(); + ... + smp_prepare_boot_cpu(); + boot_cpu_hotplug_init(); + ... + sched_init(); + ... + arch_call_rest_init(); + rest_init(); +``` + +`smp_setup_processor_id()` 将 `_start_kernel` 中赋值的 `boot_cpu_hartid` 存储到 `__cpuid_to_hartid_map[0]` 中,`__cpuid_to_hartid_map` 数组用来存储 cpuid 与 hartid 的映射。关键代码如下: + +```c +// init/main.c : 772 + +smp_setup_processor_id(); + __cpuid_to_hartid_map[0] = boot_cpu_hartid; +``` + +`boot_cpu_init()` 函数通过 `struct task_info * (init_task)->cpu` 获取当前 boot cpu id,默认为 0,并将四个 `cpu_*_mask` 中第 0 个 bit 置位,表示 boot cpu 是可被调度器使用的。关键代码如下: + +```c +// kernel/cpu.c : 2667 + +boot_cpu_init() + int cpu = smp_processor_id(); + raw_smp_processor_id() + (current_thread_info()->cpu) + (struct task_info *)riscv_current_is_tp->cpu // 0 + // Mark the boot cpu "present", "online" etc + set_cpu_online(cpu, true); + set_cpu_active(cpu, true); + set_cpu_present(cpu, true); + set_cpu_possible(cpu, true); + + __boot_cpu_id = cpu; +``` + +`setup_arch()` 中调用 `setup_smp()` 函数,此函数遍历 dts 中的每个 cpu 节点并获取 hartid,并与 `__cpuid_to_hartid_map[]` 进行映射,这里需要注意的是 `__cpuid_to_hartid_map[0]` 总是为 `boot_cpu_hartid`;映射完成之后,对每个 cpuid 设置 cpu_ops 操作集,在开启 `SBI_EXT_HSM` 拓展的情况下,设置 cpu_ops[cpuid] 为 `cpu_ops_sbi`,最后把对应的 cpu 在 `cpu_possbile_mask` 中对应的 bit 置位,意味着这些 cpu 对与系统来说是可见的。 + +`cpu_ops_sbi` 中声明 SMP 启动以及热插拔过程的函数列表,比如,在把非 boot cpu 设置为 present 之前会调用 `sbi_cpu_prepare` 函数;`sbi_cpu_start` 函数用于在指定 cpu 上执行 idle 线程。关键代码如下: + +```c +// arch/riscv/kernel/smpboot.c : 73 + +setup_smp() + for_each_of_cpu_node + riscv_of_processor_hartid(dn, &hart) + cpuid_to_hartid_map(cpuid) = hart + early_map_cpu_to_node(cpuid, of_node_to_nid(dn)); + cpuid++ + for (cpuid = 1; cpuid < nr_cpu_ids; cpuid++) + cpu_set_ops(cpuid); + cpu_ops[cpuid] = &cpu_ops_sbi; + set_cpu_possible(cpuid, true); + +// arch/riscv/kernel/cpu_ops_sbi.c : 120 + +const struct cpu_operations cpu_ops_sbi = { + .name = "sbi", + .cpu_prepare = sbi_cpu_prepare, + .cpu_start = sbi_cpu_start, +#ifdef CONFIG_HOTPLUG_CPU + .cpu_disable = sbi_cpu_disable, + .cpu_stop = sbi_cpu_stop, + .cpu_is_stopped = sbi_cpu_is_stopped, +#endif +}; +``` + +`smp_prepare_boot_cpu()` 函数调用 `init_cpu_topology()` 函数解析 dts 中描述的 package/cluster/core/thread 的 cpu 相关信息,并记录到 `struct cpu_topology cpu_topology[NR_CPUS]` 中。 + +`boot_cpu_hotplug_init()` 函数设置 boot_cpu 到 `cpus_booted_once_mask` 中标志 boot_cpu 已启动,并设置 percpu 变量 cpuhp_state.state 为 `CPUHP_ONLINE`,此操作涉及 cpu 热插拔(cpuhp)相关内容,下文会讲到。 + +```c +// kernel/cpu.c : 2690 + +void __init boot_cpu_hotplug_init(void) +{ +#ifdef CONFIG_SMP + cpumask_set_cpu(smp_processor_id(), &cpus_booted_once_mask); +#endif + this_cpu_write(cpuhp_state.state, CPUHP_ONLINE); +} + +``` + +## 0/1/2 号线程 + +在 `_start_kernel` 汇编函数中,通过如下两个指令设置线程指针和栈指针构造了 C 语言的执行环境,但此时的上下文(init_task)并不具备可被调度的能力,所以需要对该线程做进一步的初始化。 + +```c + la tp, init_task + la sp, init_thread_union + THREAD_SIZE +``` + +`sched_init()` 负责调度相关的初始化,其中会调用 `idle_init()` 将当前的执行上下文设置为 0 号线程 (idle),其中最有代表性的一个设置就是 0 号线程有了名字 - swapper。之后调用 `idle_thread_set_boot_cpu()` 函数将当前线程保存在 boot cpu 的 `idle_threads` 变量中。关键代码如下: + +```c +// kernel/sched/core.c : 9613 + +sched_init() + + init_idle(current, smp_processor_id()) // set up an idle thread for a given CPU + __sched_fork(0, idle); + idle->__state = TASK_RUNNING + idle->flags |= PF_IDLE | PF_KTHREAD | PF_NO_SETAFFINITY; + kthread_set_per_cpu(idle, cpu) + kthread->cpu = cpu + idle->sched_class = &idle_sched_class; + sprintf(idle->comm, "%s/%d", INIT_TASK_COMM, cpu); // set idle->comm is "swapper" + + idle_thread_set_boot_cpu(); +``` + +在 `start_kernel` 经历一系列的初始化后,会在结尾处会调用 `rest_init()`,此函数先后创建 1 号线程(init)和 2 号线程 (kthreadd): + +1. init 线程执行 `kernel_init()` 函数用于执行进一步的初始化操作并最终加载 init 用户态程序 +2. kthreadd 线程负责执行 `kthreadd()` 函数用来初始化其他内核子线程。 + +由于 init 线程执行过程中需要创建内核线程,所以需要在 kthreadd 线程创建完成之后才能执行,而此代码同步需求通过 `kthreadd_done` 完成变量来实现。 + +此刻 idle/init/kthreadd 三个线程都已创建,并且系统开启调度,其中 idle 线程为调度优先级最低的任务,被调度到会通过 `cpu_startup_entry(CPUHP_ONLINE)` 进入 idle 循环中, +而 kthreadd 线程更多的是用来服务 init 线程中的内核线程创建需求,所以后续的启动流程以 init 线程为主。 + +```c +// init/main.c : 681 + +rest_init() + + pid = user_mode_thread(kernel_init, NULL, CLONE_FS); + wait_for_completion(&kthreadd_done); + kernel_init_freeable(); + + pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); + + system_state = SYSTEM_SCHEDULING; + + complete(&kthreadd_done); + + schedule_preempt_disabled() // scheule once + schedule() + cpu_startup_entry(CPUHP_ONLINE); // idle loop + while (1) + do_idle(); +``` + +## cpu 热插拔状态 + +除了上文提到的的 cpu 四种状态,linux 中也引入 cpu 热插拔状态用来细化管理 cpu 状态从 present 状态到 online 状态的切换。 + +cpu 热插拔机制通过 `cpuhp_state` 枚举来定义 cpu 热插拔状态,并在 `cpuhp_hp_states` 数组中为每个状态绑定在 cpu 启动或者关闭时需要执行的回调函数,在 cpu 启动过程中,会依次执行该状态之前的所有状态绑定的启动回调函数,在 cpu 关闭时执行对应的关闭回调函数。cpu 热插拔状态又进一步划分三个阶段来实现在不同的启动或者关闭阶段对热插拔 cpu 的回调函数的调用: + +- `PREPARE` 阶段从 `CPUHP_OFFLINE` 开始到 `BRINGUP_CPU` 结束,此状态段由 boot cpu 来调用回调函数,可参考下文的 `cpu_up()` 函数 +- `STARTING` 阶段从 `BRINGUP_CPU + 1` 开始到 `CPUHP_AP_ONLINE_IDLE - 1` 结束,此状态段有非 boot cpu 的早期启动代码来调用回调函数,可参考下文的 `smp_callin()` 函数 +- `ONLINE` 阶段从 `CPUHP_AP_ONLINE_IDLE` 开始到 `CPUHP_ONLINE` 结束,此状态段有非 boot cpu 的热插拔线程来调用回调函数,可参考热插拔线程的线程函数 `cpuhp_thread_fun()` + +关键代码如下: + +```c +// include/linux/cpuhotplug.h : 57 + +enum cpuhp_state { + CPUHP_OFFLINE = 0, // PREPARE section invoked on a control CPU + CPUHP_OFFLINE + 1, + ... + BRINGUP_CPU, + CPUHP_AP_IDLE_DEAD, // STARTING section invoked on the hotplugged CPU in low level + ... + CPUHP_AP_ONLINE, + CPUHP_TEARDOWN_CPU, + CPUHP_AP_ONLINE_IDLE, // Online section invoked on the hotplugged CPU from the hotplug thread + ... + CPUHP_ONLINE +}; + +// kernel/cpu.c : 1565 + +static struct cpuhp_step cpuhp_hp_states[] = + ... + [CPUHP_CREATE_THREADS]= { + .name = "threads:prepare", + .startup.single = smpboot_create_threads, + .teardown.single = NULL, + .cant_stop = true, + }, + + ... + [CPUHP_BRINGUP_CPU] = { + .name = "cpu:bringup", + .startup.single = bringup_cpu, + .teardown.single = finish_cpu, + .cant_stop = true, + }, + ... +} + +``` + +## kernel_init + +在 0/1/2 号线程创建之后,boot cpu 上主要执行 init 线程的线程函数 -- `kernel_init()`,其中有关 SMP 启动相关流程的代码主要集中 `kernel_init_freeable()` 函数中,此函数中与 SMP 启动相关的关键过程如下: + +`smp_prepare_cpus()` 函数对非 boot cpu 执行 `cpu_ops.cpu_prepare()` 操作,并将 cpu 的状态设置为 present。RISC-V 架构下的 `cpu_prepare()` 相对简单,只是简单判断 `cpu_ops.cpu_start` 函数是否定义。 + +`smp_init()` 函数首先调用 `idle_threads_init()` 为所有的非 boot cpu 创建 idle 线程,并将线程绑定到对应 cpu 的 `idle_threads` 变量,之后调用 `cpuhp_threads_init()` 为 online 的 cpu 创建 cpuhp 线程(目前这里只有 boot cpu 是 online 的),cpuhp 线程的线程函数为 `cpuhp_thread_fun`,负责执行热插拔 `ONLINE` 阶段的启动回调函数,最后调用 `bringup_nonboot_cpus()` 对处于 present 状态的非 boot cpu 执行 `cpu_up(cpu, CPUHP_ONLINE)` 设置 cpu 热插拔状态为 `CPUHP_ONLINE`,经过热插拔的三个阶段后,最终执行 idle 线程,具体的实现会在下文中展开。 + +关键代码如下: + +```c +// init/main.c : 1600 + +kernel_init_freeable() + smp_prepare_cpus(setup_max_cpus); + cpu_ops[cpuid]->cpu_prepare(cpuid); // sbi_cpu_prepare + set_cpu_present(cpuid, true); + + smp_init() + idle_threads_init(); + for_each_possible_cpu(cpu) + idle_init(cpu) // 创建 idle 线程 + + cpuhp_threads_init(); + cpuhp_init_state() // 初始化热插拔线程唤醒的完成变量 + smpboot_register_percpu_thread(&cpuhp_threads) // 为所有 online cpu 创建 cpuhp 线程 + + bringup_nonboot_cpus(setup_max_cpus) // Bringing up secondary CPUs + for_each_present_cpu(cpu) { + cpu_up(cpu, CPUHP_ONLINE) +``` + +`cpu_up()` 函数调用 `cpuhp_invoke_callback_range()` 来依次执行热插拔 `PREPARE` 阶段中的状态的启动回调函数,值得关注的两个状态及其启动回调函数为:`CPUHP_CREATE_THREADS` - `smpboot_create_threads()`,此函数用来为目标 cpu 创建 cpuhp 内核线程。`CPUHP_BRINGUP_CPU` - `bringup_cpu()`,此函数为 `PREPARE` 阶段调用的最后一个启动回调函数,其具体的实现过程如下: + +- 获取当前 cpu 的 idle 线程后,调用 `__cpu_up()` 函数执行 `cpu_ops[cpu]->cpu_start()` -- `sbi_cpu_start()`,后者调用 sbi 的 HSM 拓展在目标 cpu 上开始执行 `secondary_start_sbi` 汇编函数,同时传递 idle 线程指针和栈顶指针,之后等待目标 cpu 初始化过程中释放 `cpu_running` 完成变量 +- 调用 `bringup_wait_for_ap()` 函数等待目标 cpu 释放 `&st->done_up` 完成变量,继而激活目标 cpu 的热插拔线程来执行 `ONLINE` 段中的启动回调函数 + +在以上两个完成变量得到释放后,在 boot cpu 上的 `cpu_up()` 函数以此路径 -- `bringup_nonboot_cpus` => `smp_init` => `kernel_init_freeable` => `kernel_init` 返回使得 boot cpu 的 init 线程可以继续执行,最终加载并运行 init 用户态程序。 + +关键代码如下: + +```c +// kernel/cpu.c : 1395 + +cpu_up() + struct cpuhp_cpu_state *st = per_cpu_ptr(&cpuhp_state, cpu); + cpuhp_set_state(cpu, st, target); + st->target = target; + target = min((int)target, CPUHP_BRINGUP_CPU); // do PREPARE section + cpuhp_up_callbacks(cpu, st, target); + cpuhp_invoke_callback_range(true, cpu, st, target); + while (cpuhp_next_state(bringup, &state, st, target)) + cpuhp_invoke_callback(cpu, state, bringup, NULL, NULL); + +// kernel/cpu.c : 589 + +bringup_cpu() + tidle = idle_thread_get(cpu) + __cpu_up(cpu, idle); + start_secondary_cpu(cpu, tidle); + cpu_ops[cpu]->cpu_start(cpu, tidle); // sbi_cpu_start + wait_for_completion_timeout(&cpu_running) // wait for cpu_running + return bringup_wait_for_ap(cpu); + wait_for_ap_thread(st, true); // wait for &st->done_up + kthread_unpark(st->thread); + cpuhp_kick_ap(cpu, st, st->target); + +// arch/riscv/kernel/cpu_ops_sbi.c : 65 + +static int sbi_cpu_start(unsigned int cpuid, struct task_struct *tidle) +{ + unsigned long boot_addr = __pa_symbol(secondary_start_sbi); // boot_addr + unsigned long hartid = cpuid_to_hartid_map(cpuid); + unsigned long hsm_data; + struct sbi_hart_boot_data *bdata = &per_cpu(boot_data, cpuid); // per_cpu boot_data + + /* Make sure tidle is updated */ + smp_mb(); + bdata->task_ptr = tidle; + bdata->stack_ptr = task_stack_page(tidle) + THREAD_SIZE; // setup stack_ptr by tidle + /* Make sure boot data is updated */ + smp_mb(); + hsm_data = __pa(bdata); + return sbi_hsm_hart_start(hartid, boot_addr, hsm_data); // call HSM SBI_EXT_HSM_HART_START +} +``` + +## secondary_start_sbi + +boot cpu 调用 `sbi_cpu_start()` 函数让非 boot cpu 执行 `secondary_start_sbi` 汇编函数,其中设置 tp 和 sp 为 `sbi_hsm_hart_start()` 传递的线程指针和栈顶指针,相当于为当前 cpu 的代码执行创建了 C 语言环境,则可正常调用 `smp_callin()` C 函数。关键代码如下: + +```c +// arch/riscv/kernel/head.S : 131 + +secondary_start_sbi: + ... + /* a0 contains the hartid & a1 contains boot data */ + li a2, SBI_HART_BOOT_TASK_PTR_OFFSET + add a2, a2, a1 + REG_L tp, (a2) // 设置 tp + li a3, SBI_HART_BOOT_STACK_PTR_OFFSET + add a3, a3, a1 + REG_L sp, (a3) // 设置 sp + + ... + + tail smp_callin + +``` + +`smp_callin()` 作为非 boot cpu 的所执行的第一个 C 函数,执行如下内容: + +- `notify_cpu_starting()` 函数依次执行热插拔状态的 `STARTING` 段内状态的启动回调函数 +- `set_cpu_online()` 函数设置 cpu 状态为 online +- 释放完成变量 `cpu_running` 用于唤醒 `__cpu_up()` 函数,使得 boot cpu 上的 init 线程得以继续执行 +- `complete_ap_thread()` 释放 `&st->done_up` 完成变量唤醒 `bringup_wait_for_ap()` 函数,从而激活目标 cpu 的热插拔线程来执行 `ONLINE` 段中的启动回调函数 +- 最后执行 `do_idle()` 进入低功耗的 idle 循环,在不考虑 cpuidle framework 的情况下,会执行架构级的 idle 函数,此函数在 RISC-V 架构中为 `wfi` 指令 + +关键代码如下: + +```c +// arch/riscv/kernel/smpboot.c : 155 + +smp_callin() + notify_cpu_starting(curr_cpuid); + target = min((int)st->target, CPUHP_AP_ONLINE); // do STARTING section + cpuhp_invoke_callback_range_nofail(true, cpu, st, target); + + set_cpu_online(curr_cpuid, 1); // set online bitmap + + complete(&cpu_running); // wakeup __cpu_up() + + cpu_startup_entry(CPUHP_AP_ONLINE_IDLE); + cpuhp_online_idle() + st->state = CPUHP_AP_ONLINE_IDLE; + complete_ap_thread(st, true); // wakeup bringup_wait_for_ap() + while (1) + do_idle() + cpuidle_idle_call + if (cpuidle_not_available(drv, dev)) // if no cpuidle framework + default_idle_call() + arch_cpu_idle() // wfi + +``` + +## 小结 + +最后我们从 cpu 状态和热插拔状态变化的角度对 SMP 的启动流程总结如下: + +在 `start_kernel()` 中,`setup_smp()` 函数负责解析 hart 资源,建立 hartid 与 cpuid 的映射,为每个 cpu 设置 cpu_ops 操作集,将 cpu 设置为 possible 状态。 + +在 init 线程的 `smp_prepare_cpus()` 过程中调用 `cpu_ops[]->cpu_prepare`,将非 boot cpu 设置为 present 状态;后续的 `smp_init()` 过程中为非 boot cpu 分配 idle 线程结构,并通过 `cpu_up()` 执行热插拔 `PREPARE` 阶段的启动回调函数,此阶段结尾的启动回调函数 `bringup_cpu()` 调用 sbi 的 HSM 拓展接口让非 boot cpu 开始执行代码。 + +非 boot cpu 执行热插拔 `STARTING` 阶段的启动回调函数,将 cpu 设置为 online 状态,唤醒 boot cpu 上的 init 线程,唤醒当前 cpu 上热插拔线程执行热插拔的 `ONLINE` 阶段的启动回调函数,最终进入 idle 循环。 + +``` +BP (boot cpu) AP (noboot cpus) +------------------------------------------------------ +setup_smp() [possible] + +| + +.cpu_prepare() [present] + +| + +cpu_up() + PREPARE + .cpu_start() sbi_hart_start > smp_callin() + STARTING + [online] + wait < cpu_running complele() + wait < st->done_up complete_ap_thread() + +| | + +kernel_init()... cpuhp/1 + ONLINE + | + + do_idle() + +``` + +## 参考资料 + +- [riscv smp in boot flow][1] + +[1]: https://gitee.com/tinylab/riscv-linux/blob/master/articles/20221212-riscv-smp-in-boot-flow.md