【cpu_operations】CPU hotplug smpboot流程概述(3)
CPU hotplug是什么
CPU core的状态
kernel使用4个bitmap,来保存分别处于4种状态的CPU core:possible、present、active和online。
整理软件流程
start_kernel
-> boot_cpu_init // 标记boot core的状态为possible、present、active和online
-> setup_arch
-> psci_dt_init or psci_acpi_init // 初始化psci的ops,选择psci版本,选择smccc模式svc or hvc
-> smp_init_cpus
-> of_parse_and_init_cpu or acpi_parse_and_init_cpus
-> smp_cpu_setup //从dts或者acpi获取cpu信息,并配置逻辑CPU id
-> ops->cpu_init // psci模式为空操作
-> set_cpu_possible(cpu, true); // 标记为possible
-> rest_init
-> kernel_init
-> kernel_init_freeable
-> smp_prepare_cpus // 调用arch-dependent的接口
-> for_each_possible_cpu // 遍历每个possible的CPU
-> ops->cpu_prepare // psci模式检查cpu_on函数是否为空
-> set_cpu_present(cpu, true); // 标记为present
-> smp_init
-> idle_threads_init //为每个一个possible CPU fork一个idle任务
-> bringup_nonboot_cpus //
-> for_each_present_cpu // 判断online CPU的数量,如果超过max停止唤醒更多的CPU
-> cpu_up(cpu, CPUHP_ONLINE); //唤醒从核
-> __cpu_up(cpu, idle); // 调用arch-dependent的接口
-> ops->cpu_boot(cpu); // 调用psci命令,唤醒从核
-> do_basic_setup
-> do_initcalls
-> do_initcall_level // 初始化不同阶段
-> do_one_initcall // 初始化每个module
possible CPU
possible的CPUs,代表了系统中可被使用的所有的CPU,在boot阶段确定之后,就不会再修改。以ARM64为例,其初始化的过程如下。
-
系统上电后,boot CPU启动,执行start_kernel(init/main.c),并分别调用boot_cpu_init和setup_arch两个接口,进行possible CPU相关的初始化。
-
boot_cpu_init负责将当前的boot CPU放到possible CPU的bitmap中,同理,boot CPU也是present、oneline、active CPU(因此,后续的描述,都是针对非boot CPU的)。
-
setup_arch负责根据MPIDR寄存器,以及DTS配置,解析并设置其它的possible CPU。
-
kernel使用一个整形数组(cpu_logical_map,定义如下),保存物理CPU(由ID标示)和逻辑CPU(数组的index)之间的映射,该数组的长度由NR_CPUS决定。通过read_cpuid_mpidr接口,读取当前CPU(boot CPU)的ID(物理ID),并保存在map表的第一个位置。
/* arch/arm64/include/asm/smp_plat.h */
/*
* Logical CPU mapping.
*/
extern u64 __cpu_logical_map[NR_CPUS];
extern u64 cpu_logical_map(unsigned int cpu);
- 如果使能了SMP,则调用
smp_init_cpus
接口从DTS中解析其它CPU的HW ID(通过‘reg’关键字),并保存在cpu_logical_map数组中;对所有cpu_logical_map数组中的CPU,smp_cpu_setup
执行set_cpu_possible操作,将它们设置为possible状态。
present CPU
main.c文件start_kernel函数执行流程,start_kernel—>rest_init—>kernel_init(pid 1,init task)—>kernel_init_freeable
,在kernel_init_freeable中会调用arch-dependent的接口:smp_prepare_cpus。1)构建系统中CPU的拓扑结构;2)拓扑结构构建完成后,根据CPU的拓扑,初始化系统的present CPU mask。
void __init smp_prepare_cpus(unsigned int max_cpus)
{
init_cpu_topology();
this_cpu = smp_processor_id();
store_cpu_topology(this_cpu);
numa_store_cpu_info(this_cpu);
numa_add_cpu(this_cpu);
/*
* If UP is mandated by "nosmp" (which implies "maxcpus=0"), don't set
* secondary CPUs present.
*/
if (max_cpus == 0)
return;
for_each_possible_cpu(cpu) {
if (cpu == smp_processor_id())
continue;
err = ops->cpu_prepare(cpu);
if (err)
continue;
set_cpu_present(cpu, true);
}
}
.cpu_prepare回调主要用于检查某个CPU是否具备执行的条件。如果.cpu_prepare执行成功,则说明该CPU是可以启动的。因此,present CPU的意义是:该CPU已经被kernel识别到,并且具备执行代码的条件,后续可以在需要的时候(如hotpulg的时候),启动该CPU。
online CPU
CPU是否boot,则反映到online mask上,已经boot的CPU,会在secondary_start_kernel中,调用set_cpu_online接口,将其设置为online状态。反之,会在__cpu_disable中将其从online mask中清除。
active CPU
在支持hotplug的SMP系统中,CPU资源可以在任何时候增加或者删除。增加的时候,需要将新增的资源分配给等待的task。删除的时候,需要将那些运行在这些CPU上的task,转移到其它尚存的CPU上(这个过程称作migration)。
每当系统的CPU资源有任何变动,kernel CPU control模块就会通过notifier机制通知调度器,调度器根据相应的event(CPU_DOWN_FAILED、CPU_DOWN_PREPARE等),调用set_cpu_active接口,将某个CPU添加到active mask或者移出active mask。这就是active CPU的意义:从调度器的角度,CPU的状态,即是否对调度器可见,或者说,调度器是否可以把task分配到这个CPU上运行。由此可知,active状态,只是为了方便调度器操作,抽象出的状态,和CPU电源管理之间没有耦合。
CPU hotplug 配置宏
在kernel/cpu.c中,cpu_up接口,只会在使能了CONFIG_SMP
配置项(意味着是SMP系统)后才会提供。
而cpu_down接口,则只会在使能了CONFIG_HOTPLUG_CPU
配置项(意味着支持CPU hotplug)后才会提供。
CPU hotplug主要流程
echo 0 > /sys/devices/system/cpu/cpuX/online
online_store(drivers/base/core.c)
device_online(drivers/base/core.c)
cpu_subsys_online(drivers/base/cpu.c)
cpu_up(kernel/cpu.c)
_cpu_up(kernel/cpu.c)
基本流程
1)up前后,发送PREPARE、ONLINE、STARTING等notify,以便让关心者作出相应的动作,例如调度器、RCU、workqueue等模块,都需要关注CPU的hotplug动作,以便进行任务的重新分配等操作。
2)执行Arch-specific相关的boot操作,将CPU boot起来,最终通过secondary_start_kernel接口,停留在per-CPU的idle线程上。
per-cpu idle线程
首先,boot CPU在执行初始化动作的时候,会通过“smp_init—>idle_threads_init—>idle_init”的调用,为每个CPU创建一个idle线程,为每个CPU fork一个idle thread,并保存在一个per-CPU的全局变量(idle_threads)中。此时,idle thread只是一个task结构,并没有执行。
arch-specific CPU boot
_cpu_up
接口会在完成一些准备动作之后,调用平台相关的__cpu_up
接口,由平台代码完成具体的up操作,准备动作包括:
1)获取idle thread的task指针,该指针最终会以参数的形式传递给arch-specific代码。
2)创建一个用于管理CPU hotplug动作的线程(smpboot_create_threads),该线程的具体意义,后面会再说明。
3)发送CPU_UP_PREPARE notify。
secondary_startup
该接口位于arch/arm64/kernel/head.S中,负责secondary CPU启动后的后期操作。__secondary_switched会将保存在secondary_data全局变量中的堆栈取出,保存在该CPU的SP中,并跳转到secondary_start_kernel继续执行,通过CPU的SP指针,是可以获得CPU的当前task,也就是说,当CPU SP被赋值为idle thread的堆栈的那一瞬间,当前的上下文已经是idle thread了,
至于后面的secondary_start_kernel,就比较简单了,使能GIC、Timers,设置CPU为online状态,使能本地IRQ中断。等等。最后,调用cpu_startup_entry,进入cpuidle的loop中
CPU hotplug调测命令
CPU hotplug文件系统
# ls /sys/devices/system/cpu/cpuX/hotplug/
fail // 状态切换失败后的状态,初始为`CPUHP_INVALID`
stage // 当前状态,可以为`CPUHP_ONLINE`
target // 目标转移状态,可以为`CPUHP_OFFLINE`
动态开关core
echo 0 > /sys/devices/system/cpu/cpuX/online # 关闭CPU
echo 1 > /sys/devices/system/cpu/cpuX/online # 打开CPU
cmdline maxcpus参数
CPU hotplug还受“maxcpus”命令行参数影响:系统启动的时候,可以通过命令行参数“maxcpus”,告知kernel本次启动所使用的CPU个数,该个数可以小于等于possible CPU的个数。系统初始化时,只会把“maxcpus”所指定个数的CPU置为present状态,因此,CPU hotplug只能管理“maxcpus”所指定个数的CPU。