[置顶] 泰晓 RISC-V 实验箱,配套 30+ 讲嵌入式 Linux 系统开发公开课
[置顶] Linux Lab v1.4 升级部分内核到 v6.10,新增泰晓 RISC-V 实验箱支持,新增最小化内核配置支持大幅提升内核编译速度,在单终端内新增多窗口调试功能等Linux Lab 发布 v1.4 正式版,升级部分内核到 v6.10,新增泰晓实验箱支持
[置顶] 泰晓社区近日发布了一款儿童益智版 Linux 系统盘,集成了数十个教育类与益智游戏类开源软件国内首个儿童 Linux 系统来了,既可打字编程学习数理化,还能下棋研究数独提升智力
RISC-V SMP Linux boot process
Corrector: TinyCorrect v0.1 - [spaces] Author: sugarfillet sugarfillet@yeah.net Date: 2023/01/30 Revisor: Falcon falcon@tinylab.org Project: RISC-V Linux 内核剖析 Proposal: RISC-V Linux SMP 技术调研与分析 Sponsor: PLCT Lab, ISCAS
前言
这篇文章 描述了 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 函数执行后续的启动代码
// 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 中移除
关键代码如下:
// 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 启动相关的关键函数,我们一个一个来分析:
// 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 的映射。关键代码如下:
// 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 是可被调度器使用的。关键代码如下:
// 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 线程。关键代码如下:
// 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)相关内容,下文会讲到。
// 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)并不具备可被调度的能力,所以需要对该线程做进一步的初始化。
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
变量中。关键代码如下:
// 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):
- init 线程执行
kernel_init()
函数用于执行进一步的初始化操作并最终加载 init 用户态程序 - kthreadd 线程负责执行
kthreadd()
函数用来初始化其他内核子线程。
由于 init 线程执行过程中需要创建内核线程,所以需要在 kthreadd 线程创建完成之后才能执行,而此代码同步需求通过 kthreadd_done
完成变量来实现。
此刻 idle/init/kthreadd 三个线程都已创建,并且系统开启调度,其中 idle 线程为调度优先级最低的任务,被调度到会通过 cpu_startup_entry(CPUHP_ONLINE)
进入 idle 循环中,
而 kthreadd 线程更多的是用来服务 init 线程中的内核线程创建需求,所以后续的启动流程以 init 线程为主。
// 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()
关键代码如下:
// 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 线程,具体的实现会在下文中展开。
关键代码如下:
// 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 用户态程序。
关键代码如下:
// 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 函数。关键代码如下:
// 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
指令
关键代码如下:
// 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()
参考资料
猜你喜欢:
- 我要投稿:发表原创技术文章,收获福利、挚友与行业影响力
- 知识星球:独家 Linux 实战经验与技巧,订阅「Linux知识星球」
- 视频频道:泰晓学院,B 站,发布各类 Linux 视频课
- 开源小店:欢迎光临泰晓科技自营店,购物支持泰晓原创
- 技术交流:Linux 用户技术交流微信群,联系微信号:tinylab
支付宝打赏 ¥9.68元 | 微信打赏 ¥9.68元 | |
请作者喝杯咖啡吧 |
Read Album:
- 废弃 QEMU xilinx_zynq 板卡的 ignore_memory_transaction_failures
- RISC-V Linux 内核及周边技术动态第 115 期
- RISC-V Linux 内核及周边技术动态第 114 期
- 为 RISC-V OpenSBI 增加 Section GC 功能
- RISC-V Linux 内核及周边技术动态第 113 期