本文基于以下软硬件假设:
构架:AARCH64
内核版本:5.14.0-rc51
atf版本:ArmtrustfirmwareV2.5
1psci介绍
psci是arm提供的一套电源管理插口,当前一共包含0.1、0.2和1.0三个版本。它可被用于以下场景:
(1)cpu的idle管理
(2)cpuhotplug以及secondarycpu启动
(3)系统shutdown和reset
但该插口不包含dvfs和设备电源管理(如像GPU之类的外设电源管理)功能。
因为psci与虚拟化以及trustos有一定的关联,为了专注于电源管理相关的实现,故本系列的介绍都不涉及与它们相关的内容。
下边我们将依照电源管理拓扑结构(powerdomain)、电源状态(powerstate)以及armv8安全扩充几个方面介绍psci的一些基础知识
1.1powerdomain
我们上面早已介绍过cpu的拓扑结构,如aarch64构架下每块soc可能会包含多个cluster,而每位cluster又包含多个core,它们共同组成了层次化的拓扑结构。如以下为一块包含2个cluster,每位cluster包含四个core的soc:
因为其中每位core以及每位cluster的电源都可以独立地执行开关操作,因而若core0–core3的电源都关掉了,则cluster0的电源也可以被关掉以增加帧率。若core0–core3中的任一个core须要上电,则似乎cluster0须要先上电。为了更好地进行层次化电源管理,psci在电源管理流程少将以上这种组件都具象为powerdomain。如以下为上例的powerdomain层次结构:
其中systemlevel用于管理整个系统的电源,clusterlevel用于管理某个特定cluster的电源,而corelevel用于管理一个单独core的电源。
1.2powerstate
因为aarch64构架有多种不用的电源状态,不同电源状态的帧率和唤起延后不同。如standby状态会关掉powerdomain的clock,但并不关掉电源。因而它似乎清除了门电路翻转造成的动态帧率linux防火墙设置,但仍然存在漏电压等造成的静态帧率。故其帧率相对较大,但相应地唤起延后就比较低。
而对于powerdown状态,会断掉对应powerdomain的电源,因而其除了去除了动态帧率,还清除了静态帧率,相应地其唤起延后就比较高了。
psci一共为powerdomain定义了四种powerstate:
(1)run:电源和时钟都打开,该domain正常工作
(2)standby:关掉时钟,但电源处于打开状态。其寄存器状态得到保存,打开时钟后就可继续运行。帧率相对较大,但唤起延后较低。arm执行wfi或wfe指令会步入该状态。
(3)retention:它将core的状态,包括调试设置都保存在低帧率结构中,并使其部份关掉。其状态在从低帧率变为运行时能手动恢复。从操作系统角度看,不仅步入方式、延迟等有区别外,其它都与standby相同。它的帧率和唤起延后都介于standby和powerdown之间。
(4)powerdown:关掉时钟和电源。powerdomain掉电后,所有状态都遗失,上电之后软件必须重新恢复其状态。它的帧率最低,但唤起延后也相应地最高。
其实,powerstate的睡眠程度从run到powerdown逐渐加深。而高层级powerdomain的powerstate不应高于低层级powerdomain。如以上事例中core0–core2都为powerdown状态,而core3为standby状态,则cluster0不能为retention或powerdown状态。同样若cluster0为standby状态,而cluster1为run状态,则整个系统必须为run状态。
其实,若core0–core3都为powerdown状态,则cluster1保持其它状态不仅减小帧率之外,并没有其它意义,因而也应当将其设置为powerdown状态。
为了达到上述约束,不同powerdomain之间的powerstate具有以下关系:
psci实现了父leve与子level之间的电源关系协调,如cluster0中最后一个core被设置为powerdown状态后,psci都会将该cluster也设置为powerdonw状态。若其某一个core被设置为run状态,则psci会先将其对应cluster的状态设置为run,之后再设置对应core的电源状态,这也是psci名子的来历(powerstatecoordinateinterface)
1.3armv8的安全扩充
为了提高arm构架的安全性,aarch64一共实现了secure和non-secure两种安全状态。通过一系列硬件扩充,在cpu执行状态、总线、内存、外设、中断、tlb、cache等方面都实现了两种状态之间的隔离。
在这些机制下,secure空间的程序可以访问所有secure和non-secure的资源,而non-secure空间的程序只能访问non-secure资源,却不能访问secure资源。因而可以将一些安全关键的资源放在secure空间,以提高其安全性。
因此aarch64实现了4个异常等级,其中EL3工作在secure空间,而EL0–EL2既可以工作于secure空间,又可以工作于non-secure空间。不同异常等级及不同secure状态的模式下可运行不同类型软件。
如secureEL1和El0用于运行trustos内核及其用户态程序,non-secureEL1和El0用于运行普通操作系统内核(如linux)及其用户态程序,EL2用于运行虚拟机的hypervisor。而EL3运行securemonitor程序(一般为bl31)linux内核启动流程图,其功能为执行secure和nonsecure状态切换、消息转发以及提供类似psci等secure空间服务。以下为其示意图:
psci是工作于nonsecureEL1(linux内核)和EL3(bl31)之间的一组电源管理插口,其目的是让linux实现具体的电源管理策略,而由bl31管理底层硬件相关的操作。因而将cpu电源控制这些影响系统安全的控制权限放在安全等级更高的层级中,因而提高系统的整体安全性。
这么psci怎样从EL1调用EL3的服务呢?虽然它和系统调用是类似的,只是系统调用是用户态程序深陷操作系统内核,而psci是从操作系统内核深陷securemonitor。armv8提供了一条smc异常指令,内核只须要提供合适的参数后,触发该指令即可通过异常的形式步入securemonitor。
2psci软件构架
因为psci是由linux内核调用bl31中的安全服务,实现cpu电源管理功能的。因而其软件构架包含三个部份:
(1)内核与bl31之间的调用插口规范
(2)内核中的构架
(3)bl31中的构架
2.1psci插口规范
psci规定了linux内核调用bl31中电源管理相关服务的插口规范,它包含实现以下功能所需的插口:
(1)cpuidle管理
(2)向系统动态添加或从系统动态移除cpu,一般称为hotplug
(3)secondarycpu启动
(4)系统的shutdown和reset
psci插口规定了命令对应的function_id、接口的输入参数以及返回值。其中输入参数可通过x0–x7寄存器传递,而返回值通过x0–x4寄存器传递。
如secondarycpu启动或cpuhotplug时可调用cpu_on插口,为一个cpu执行上电操作。该插口的格式如下:
(1)function_id:0xcxc4000003
(2)输入参数:使用mpidr值表示的targetcpuid
cpu启动入口的化学地址
contextid,该值用于表示本次调用上下文相关的信息
(3)返回值:可以为success、invalid_parameter、invalid_address、already_on、on_pending或internal_failure
有了以下这种插口的详尽定义,内核和bl31就只需根据该插口的规定,独立开发psci相关功能。因而杜绝了它们之间的耦合,简化了开发复杂度。
2.2内核中的psci构架
内核psci软件构架包含psci驱动和每位cpu的cpu_ops反弹函数实现两部份。其中psci驱动实现了驱动初始化和psci相关插口实现功能,而cpu_ops反弹函数最终也会调用psci驱动的插口。
2.2.1psci驱动
首先我们看一下devicetree中的配置:
psci {
compatible = "arm,psci-0.2"; (1)
method = "smc"; (2)
}
(1)用于指定psci版本
(2)依据该psci由bl31处理还是hypervisor处理,可以指定其对应的深陷形式。若由bl31处理为smclinux内核启动流程图,若由hypervisor处理则为hvc
驱动流程主要是与bl31通讯,以确认其是否支持给定的psci版本,以及相关psci操作函数的实现,其流程如下:
其主要工作即为psci设置相关的反弹函数,该函数定义如下:
static void __init psci_0_2_set_functions(void)
{
…
psci_ops = (struct psci_operations){
.get_version = psci_0_2_get_version,
.cpu_suspend = psci_0_2_cpu_suspend,
.cpu_off = psci_0_2_cpu_off,
.cpu_on = psci_0_2_cpu_on,
.migrate = psci_0_2_migrate,
.affinity_info = psci_affinity_info,
.migrate_info_type = psci_migrate_info_type,
}; (1)
register_restart_handler(&psci_sys_reset_nb); (2)
pm_power_off = psci_sys_poweroff; (3)
}
(1)为psci_ops设置相应的反弹函数
(2)为psci模块设置系统重启时的通知函数
(3)将系统的power_off函数指向相应的psci插口
2.2.2cpu_ops插口
驱动初始化完成后,cpu的cpu_ops就可以调用那些反弹实现psci功能的调用。如下所示,当devicetree中cpu的enable-method设置为psci时,该cpu的cpu_ops将指向cpu_psci_ops。
cpu0: cpu@0 {
...
enable-method = "psci";
…
}
其中cpu_psci_ops的定义如下:
const struct cpu_operations cpu_psci_ops = {
.name = "psci",
.cpu_init = cpu_psci_cpu_init,
.cpu_prepare = cpu_psci_cpu_prepare,
.cpu_boot = cpu_psci_cpu_boot,
#ifdef CONFIG_HOTPLUG_CPU
.cpu_can_disable = cpu_psci_cpu_can_disable,
.cpu_disable = cpu_psci_cpu_disable,
.cpu_die = cpu_psci_cpu_die,
.cpu_kill = cpu_psci_cpu_kill,
#endif
}
如启动cpu的插口为cpu_psci_cpu_boot,它会通过以下流程最终调用psci驱动中的psci_ops函数:
static int cpu_psci_cpu_boot(unsigned int cpu)
{
phys_addr_t pa_secondary_entry = __pa_symbol(function_nocfi(secondary_entry));
int err = psci_ops.cpu_on(cpu_logical_map(cpu), pa_secondary_entry);
if (err)
pr_err("failed to boot CPU%d (%d)n", cpu, err);
return err;
}
2.3bl31中的psci构架
bl31为内核提供了一系列运行时服务,psci作为其标准运行时服务的一部份,通过宏DECLARE_RT_SVC注册到系统中。其相应的定义如下:
DECLARE_RT_SVC(
std_svc,
OEN_STD_START,
OEN_STD_END,
SMC_TYPE_FAST,
std_svc_setup,
std_svc_smc_handler
)
其中std_svc_setup会在bl31启动流程中被调用,以用于初始化该服务相关的配置。而std_svc_smc_handler为其smc异常处理函数,当内核通过psci插口调用相关服务时,最终将由该函数执行实际的处理流程。
上图为psci初始化相关的流程,它主要包含内容:
(1)上面我们早已介绍过powerdomain相关的背景,即psci须要协调不同层级的powerdomain状态,因而其必需要了解系统的powerdomain配置情况。以上流程中蓝色实线框的部份主要就是用于初始化系统的powerdomain拓扑及其状态
(2)因为psci在执行电源相关插口时,最终须要操作实际的硬件。而它们是与构架相关的,因而其操作函数最终须要注册到平台相关的反弹中。plat_setup_psci_ops即用于注册特定平台的psci_ops反弹linux中文乱码,其格式如下:
typedef struct plat_psci_ops {
void (*cpu_standby)(plat_local_state_t cpu_state);
int (*pwr_domain_on)(u_register_t mpidr);
void (*pwr_domain_off)(const psci_power_state_t *target_state);
void (*pwr_domain_suspend_pwrdown_early)(
const psci_power_state_t *target_state);
void (*pwr_domain_suspend)(const psci_power_state_t *target_state);
void (*pwr_domain_on_finish)(const psci_power_state_t *target_state);
void (*pwr_domain_on_finish_late)(
const psci_power_state_t *target_state);
void (*pwr_domain_suspend_finish)(
const psci_power_state_t *target_state);
void __dead2 (*pwr_domain_pwr_down_wfi)(
const psci_power_state_t *target_state);
void __dead2 (*system_off)(void);
void __dead2 (*system_reset)(void);
int (*validate_power_state)(unsigned int power_state,
psci_power_state_t *req_state);
int (*validate_ns_entrypoint)(uintptr_t ns_entrypoint);
void (*get_sys_suspend_power_state)(
psci_power_state_t *req_state);
int (*get_pwr_lvl_state_idx)(plat_local_state_t pwr_domain_state,
int pwrlvl);
int (*translate_power_state_by_mpidr)(u_register_t mpidr,
unsigned int power_state,
psci_power_state_t *output_state);
int (*get_node_hw_state)(u_register_t mpidr, unsigned int power_level);
int (*mem_protect_chk)(uintptr_t base, u_register_t length);
int (*read_mem_protect)(int *val);
int (*write_mem_protect)(int val);
int (*system_reset2)(int is_vendor,
int reset_type, u_register_t cookie);
}
最后我们再看一下psci操作相应的异常处理流程:
即其会按照functionid的值,分别执行相应的电源管理服务,如启动cpu时会调用psci_cpu_on函数,重启系统时会调用psci_system_rest函数等。
3secondarycpu启动
因为psci形式启动secondarycpu的流程,不仅其所执行的cpu_ops不同之外,其它流程与spin-table形式是相同的,因而我们这儿只给出执行流程图,详尽剖析可以参考下篇博文。其中以下流程执行secondarycpu启动相关的一些初始化工作:
在初始化完成且hotplug线程创建完成后,就可通过以下流程唤起cpuhotplug线程:
随后hotplug线程将调用psci反弹函数,并最终触发smc异常步入bl31:
bl31接收到该异常后执行std_svc_smc_handler处理函数,并最终调用平台相关的电源管理插口,完成cpu的上钳工作,以下为其执行流程:
平台相关反弹函数pwr_domain_on将为secondarycpu设置入口函数,之后为其上电使该cpu跳转到内核入口secondary_entry处开始执行。以下为其内核启动流程: