赞
踩
对称多处理器(Symmetric Multi-Processor, SMP
)系统包含多个处理器,如 4 核Cortex-A53。在启动过程中,处理器的地位不是平等的; core0
即 0 号处理器为引导处理器,负责执行引导程序和初始化内核;其他 core 处理器称为从处理器,等待引导处理器完成初始化。引导处理器完成初始化内核后,启动从处理器。
引导处理器启动从处理器的方法有 3 种。
spin-table
);Power State Coordination Interface, PSCI
);parking-protocol
), ACPI 是高级配置与电源接口(Advanced Configuration and Power Interface
)。现在很少使用 spin-table
方式启动从核,取而代之的是 PSCI
,PSCI
不仅可以启动从处理器,还可以关闭,挂起等其他核操作,现在基本上 ARM64平台上使用多核启动方式都是 PSCI
。PSCI简单点来说也是需要主处理器给从处理器一个启动地址,然后从处理器从这个地址执行指令。
主处理器通过 SMC
进入EL3
请求开核服务,ATF
中会响应这种请求,通过平台的开核操作来启动从处理器并且设置从处理的一些寄存器e.g. scr_el3、spsr_el3、elr_el3
,然后主处理器,恢复现场,ERET
再次回到 EL1
, 而处理器开核之后会从bl31_warm_entrypoint
开始执行,最后通过 el3_exit
返回到 EL1
的 ELR_EL3
设置的地址。
实际上,所有的从处理器启动后都会从 bl31_warm_entrypoint
开始执行,每个平台都有自己的启动地址寄存器,通过写这个寄存器来获得上电后执行的指令地址。
ARM64 RMVBAR_ELx
Armv8 将异常等级分为 el0 - el3
,其中,el3 为安全监控器,为了实现对它的支持,arm公司设计了一种 firmware 叫做 ATF
(ARM Trusted firmware
)。ATF代码运行在 EL3, 是实现安全相关的软件部分固件,其中会为其他特权级别提供服务,也就是说提供了在 EL3 中服务的手段。
atf/bl31/aarch64/bl31_entrypoint.S
:
bl31_entrypoint
-->el3_entrypoint_common
_exception_vectors=runtime_exceptions //设置el3的异常向量表
-->bl bl31_early_platform_setup //跳转到平台早期设置
-->bl bl31_plat_arch_setup //跳转到平台架构设置
-->bl bl31_main //跳转到bl31_main atf/bl31/aarch64/bl31_main.c:
-->NOTICE("BL31: %s\n", version_string); //打印版本信息
-->NOTICE("BL31: %s\n", build_message); //打印编译信息
-->bl31_platform_setup //执行平台设置
-->/* Initialize the runtime services e.g. psci.初始化运行时服务如psci */
INFO("BL31: Initializing runtime services\n") //打印log信息
-->runtime_svc_init //调用各种运行时服务历程,见下节内容
...
在标准的运行时服务中将服务初始化和处理函数放到 rt_svc_descs
段中,供调用。
链接脚本中:
bl31/bl31.ld.S
:
...
.rodata . : {
__RT_SVC_DESCS_START__ = .; rt_svc_descs段开始
KEEP(*(rt_svc_descs)) //rt_svc_descs段
__RT_SVC_DESCS_END__ = .; rt_svc_descs段结束
}
在 runtime_svc_init
函数中,调用每一个通过 DECLARE_RT_SVC 注册
的服务,其中包括 std_svc
服务:
services/std_svc/std_svc_setup.c
:
DECLARE_RT_SVC (
std_svc,
OEN_STD_START,
OEN_STD_END,
SMC_TYPE_FAST,
std_svc_setup, //初始化
std_svc_smc_handler //处理
);
在遍历每一个注册的运行时服务的时候,会导致 std_svc_setup
调用,其中会做 psci操作集的设置,操作集中我们可以看到对核电源的管理的接口如:核上电,下电,挂起等,我们主要关注上电 .pwr_domain_on = qemu_pwr_domain_on
, 这个接口当我们主处理器 boot 从处理器的时候会用到。
std_svc_setup //services/std_svc/std_svc_setup.c
-->psci_setup //lib/psci/psci_setup.c
设置平台的PSCI操作调用平台的 plat_setup_psci_ops 函数去设置 PSCI 操作eg:qemu
平台:
->plat_setup_psci_ops
->*psci_ops = &plat_qemu_psci_pm_ops;
static const plat_psci_ops_t plat_qemu_psci_pm_ops = {
.cpu_standby = qemu_cpu_standby,
.pwr_domain_on = qemu_pwr_domain_on,
.pwr_domain_off = qemu_pwr_domain_off,
.pwr_domain_suspend = qemu_pwr_domain_suspend,
.pwr_domain_on_finish = qemu_pwr_domain_on_finish,
.pwr_domain_suspend_finish = qemu_pwr_domain_suspend_finish,
runtime_exceptions //el3的异常向量表
->sync_exception_aarch64
->handle_sync_exception
->smc_handler64
...
blr x15 //跳转到处理函数
b el3_exit //从el3 退出 会eret回到el1(后面会讲到)
上面其实主要的是找到服务例程,然后跳转执行,下面是跳转的处理函数:
std_svc_smc_handler //services/std_svc/std_svc_setup.c
->ret = psci_smc_handler(smc_fid, x1, x2, x3, x4, cookie, handle, flags)
/* 64-bit PSCI function */
switch (smc_fid) {
case PSCI_CPU_SUSPEND_AARCH64:
ret = (u_register_t)
psci_cpu_suspend((unsigned int)x1, x2, x3);
break;
case PSCI_CPU_ON_AARCH64:
ret = (u_register_t)psci_cpu_on(x1, x2, x3);
break;
处理函数根据 smc function id
来决定服务, 可以看到 PSCI_CPU_ON_AARCH64
为 0xc4000003
,这正是设备树中填写的 cpu_on
属性的 ID,会委托 psci_cpu_on
来执行核上电任务。
->psci_cpu_on() //lib/psci/psci_main.c
->psci_validate_entry_point() //验证入口地址有效性并保存入口点到一个结构ep中
->psci_cpu_on_start(target_cpu, &ep) //ep入口地址
->psci_plat_pm_ops->pwr_domain_on(target_cpu)
->qemu_pwr_domain_on //实现核上电(平台实现)
->cm_init_context_by_index()
->cm_setup_context() //设置cpu上下文
-> write_ctx_reg(state, CTX_SCR_EL3, scr_el3);
->write_ctx_reg(state, CTX_ELR_EL3, ep->pc);
->write_ctx_reg(state, CTX_SPSR_EL3, ep->spsr)
函数注释:
cm_init_context_by_index()
会通过 cpu 的编号找到 cpu 上下文(cpu_context_t
),存在 cpu 寄存器的值,异常返回的时候写写到对应的寄存器中,然后 eret ,就返回到了 el1。
cm_setup_context() //设置cpu上下文
lib/el3_runtime/aarch64/context_mgmt.c
write_ctx_reg(state, CTX_SCR_EL3, scr_el3);
write_ctx_reg(state, CTX_ELR_EL3, ep->pc)
;
异常返回时执行此地址 于是完成了cpu的启动
psci_cpu_on
主要完成开核的工作,然后会设置一些异常返回后寄存器的值(eg :从el1 -> el3 -> el1
),重点关注 ep->pc
写到 cpu_context
结构的 CTX_ELR_EL3
偏移处(从处理器启动后会从这个地址取指执行)。
实际上,所有的从处理器启动后都会从 bl31_warm_entrypoint
开始执行,在 plat_setup_psci_ops
中会设置(每个平台都有自己的启动地址寄存器,通过写这个寄存器来获得上电后执行的指令地址)。
启动从处理的时候最终调用到 psci 的cpu操作集的 cpu_psci_cpu_boot
函数,会调用上面的 psci_cpu_on
,最终调用 smc,传递第一个参数为 cpu 的 id 标识启动哪个cpu,第二个参数为从处理器启动后进入内核执行的地址 secondary_entry
(这是个物理地址)。
所以综上,最后 smc 调用时传递的参数为 arm_smccc_smc(0xC4000003, cpuid, secondary_entry, arg2, 0, 0, 0, 0, &res)
。
这样陷入 el3 之后,就可以启动对应的从处理器,最终从处理器回到内核(el3->el1
),执行 secondary_entry
处指令,从处理器启动完成。
ATF 中主要是响应内核的smc的请求,然后做开核处理,也就是实际的开核动作,但是从处理器最后还是要回到内核中执行。
init/main.c
start_kernel
->boot_cpu_init // 引导cpu初始化设置引导cpu的位掩码 online active
// present possible都为true
->setup_arch // arch/arm64/kernel/setup.c
-> if (acpi_disabled) // 不支持acpi
psci_dt_init(); // drivers/firmware/psci.c(psci主要文件)
// psci初始化 解析设备树寻找PSCI匹配的节点
else
psci_acpi_init(); // acpi中允许使用psci情况
->rest_init
->kernel_init
->kernel_init_freeable
->smp_prepare_cpus // 准备cpu对于每个可能的cpu,
1. cpu_ops[cpu]->cpu_prepare(cpu)
2. set_cpu_present(cpu, true) cpu处于present状态
->do_pre_smp_initcalls // 多核启动之前的调用initcall回调
->smp_init // smp初始化kernel/smp.c会启动其他从处理器
主要关注两个函数:psci_dt_init
和 smp_init, psci_dt_init
是解析设备树,设置操作函数,smp_init
用于启动从处理器。
->psci_dt_init() //drivers/firmware/psci.c:
->init_fn()
->psci_0_1_init() //设备树中compatible = "arm,psci"为例
//根据设备树method 属性设置 invoke_psci_fn =
// __invoke_psci_fn_smc; (method="smc")
->get_set_conduit_method()
-> invoke_psci_fn = __invoke_psci_fn_smc
-> if (!of_property_read_u32(np, "cpu_on", &id)) {
psci_function_id[PSCI_FN_CPU_ON] = id;
psci_ops.cpu_on = psci_cpu_on;//设置psci操作的开核
}
->psci_cpu_on()
->invoke_psci_fn()
->__invoke_psci_fn_smc()
//x0=function_id x1=arg0, x2=arg1, x3=arg2,...
-> arm_smccc_smc(function_id, arg0, arg1, arg2, 0, 0, 0, 0, &res)
->__arm_smccc_smc()
->SMCCC smc //arch/arm64/kernel/smccc-call.S
-> .macro SMCCC instr
.cfi_startproc
\instr #0 //即是SMC #0 陷入到el3
ldr x4, [sp]
stp x0, x1, [x4, #ARM_SMCCC_RES_X0_OFFS]
stp x2, x3, [x4, #ARM_SMCCC_RES_X2_OFFS]
ldr x4, [sp, #8]
cbz x4, 1f /* no quirk structure */
ldr x9, [x4, #ARM_SMCCC_QUIRK_ID_OFFS]
cmp x9, #ARM_SMCCC_QUIRK_QCOM_A6
b.ne 1f
str x6, [x4, ARM_SMCCC_QUIRK_STATE_OFFS]
1: ret
.cfi_endproc
.endm
启动从处理的时候最终调用到 psci 的 cpu 操作集的 cpu_psci_cpu_boot
函数,会调用上面的 psci_cpu_on
,最终调用 smc:
传递第一个参数为 cpu 的 ID 标识启动哪个cpu,
第二个参数为从处理器启动后进入内核执行的地址 secondary_entry
(这是个物理地址)。
所以综上,最后 smc 调用时传递的参数为 arm_smccc_smc(0xC4000003, cpuid, secondary_entry, arg2, 0, 0, 0, 0, &res)
。
这样陷入 el3 之后,就可以启动对应的从处理器,最终从处理器回到内核(el3->el1
),执行 secondary_entry
处指令,从处理器启动完成。
无论是 spin-table 还是 psci,从处理器启动进入内核之后都会执行secondary_startup
:
__cpu_setup
中设置了 secondary_data
结构中的一些成员:跳转到 secondary_start_kernel
这个C函数继续执行初始化:
当从处理器启动到内核的时候,他们也需要设置异常向量表,设置 mmu 等,然后执行各自的 idle 进程(这些都是一些处理器强相关的初始化代码,一些通用的初始化都已经被主处理器初始化完),当 cpu 负载均衡 的时候会放置一些进程到这些从处理器,然后进程就可以再这些从处理器上欢快的运行。
推荐阅读:
https://blog.csdn.net/rockrockwu/article/details/103698045
https://blog.csdn.net/big2chris/article/details/53048880
https://zhuanlan.zhihu.com/p/373964690
https://zhuanlan.zhihu.com/p/373964690
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。