一、相关名词解释
SMP:(Symmetric Multi-Processing)对称多处理,一个chip上集成多个核心
SMT:(Simultaneous multithreading)同时多线程,一个核心上实现多个hardware context,以支持多线程。通过复制硬件寄存器状态等手段,同时执行多个线程。
Node:某些Core之间,独享总线和memory,称作Node。Core只访问Node内的memory,因此可以减轻对总线和memory的带宽需求。但是有些场景下,Core会不可避免的访问其它Node的memory,这会造成很大的访问延迟。
NUMA: (Non-uniform Memory Access)不一致内存访问,以内存访问的不一致性为代价,减轻对总线和memory的带宽需求。这种结构对进程调度算法的要求较高,尽量减少跨Node的内存访问次数,以提升系统性能。
HMP:(Heterogeneous Multi-Processing)异构多处理,ARM的一种架构,在乎功耗的存在。HMP架构在一个chip中,封装两类ARM Core,一类为高性能Core(如Cortex-A15,也称作big core),一类为低性能Core(如Cortex-A7,也称作little core),因此HMP也称作big·little架构。
还有big-middle-little架构。
二、CPU拓扑
1. CPU topology除了描述CPU的组成之外,其主要功能是向kernel调度器提供必要的信息,以便让它合理地分配任务,最终达到性能和功耗之间的平衡。
CPU topology:Cluster-->Core-->Threads
2.CPU拓扑框架
------------------------- ---------------------------- | CPU topology driver | | Task Scheduler etc. | ------------------------- ---------------------------- ------------------------------------------------------- | Kernel general CPU topology | ---------------------------------------------------------- ---------------------------------------------------------- | arch-dependent CPU topology | ----------------------------------------------------------
Kernel general CPU topology位于"include/linux/topology.h”中,定义了获取系统CPU topology信息的标准接口。底层的arch-dependent CPU topology会根据平台的特性,实现kernel定义的那些接口。
CPU topology信息有两个重要的使用场景:一是向用户提供当前的CPU信息(eg:lscpu),这是由CPU topology driver实现的;二是向调度器提供CPU core的信息,以便合理的调度任务。
2.1 Kernel general CPU topology
Kernel general CPU topology位于 include/linux/topology.h 中,主要以“#ifndef ... #define”类型的宏定义的形式提供API,其目的是:底层的arch-dependent CPU topology可以重新定义这些宏,只要底层有定义,则优先使用底层的,否则就使用Kernel general CPU topology中的默认API,主要包括:
/* include/linux/topology.h */ #ifndef topology_physical_package_id #define topology_physical_package_id(cpu) ((void)(cpu), -1) #endif #ifndef topology_core_id #define topology_core_id(cpu) ((void)(cpu), 0) #endif #ifndef topology_thread_cpumask #define topology_thread_cpumask(cpu) cpumask_of(cpu) #endif #ifndef topology_core_cpumask #define topology_core_cpumask(cpu) cpumask_of(cpu) #endif #ifdef CONFIG_SCHED_SMT static inline const struct cpumask *cpu_smt_mask(int cpu) { return topology_thread_cpumask(cpu); } #endif static inline const struct cpumask *cpu_cpu_mask(int cpu) { return cpumask_of_node(cpu_to_node(cpu)); }
topology_physical_package_id:用于获取某个CPU的package ID,即socket(X86)或者cluster(ARM),具体意义依赖于具体平台的实现;
topology_core_id:某个CPU的core ID。即第二章所描述的core,具体意义依赖于具体的平台实现;
topology_thread_cpumask:获取和该CPU属于同一个core的所有CPU,通俗的讲,就是姐妹Thread;
topology_core_cpumask:获取和该CPU属于同一个cluster的所有CPU;
cpu_cpu_mask: 获取该CPU属于同一个Node的所有CPU;
cpu_smt_mask: 用于SMT调度(CONFIG_SCHED_SMT)的一个封装,意义同topology_thread_cpumask。
2.2 arch-dependent CPU topology
位于“arch/arm64/include/asm/topology.h”和“arch/arm64/kernel/topology.c”中,主要负责ARM64平台相关的topology转换,包括:
(1) 定义一个数据结构,以及基于该数据结构的变量,用于存储系统的CPU topology
/* arch/arm64/include/asm/topology.h */ struct cpu_topology { int thread_id; int core_id; int cluster_id; cpumask_t thread_sibling; cpumask_t core_sibling; }; extern struct cpu_topology cpu_topology[NR_CPUS];
cluster_id、core_id、thead_id描述了拓扑结构的三个层次,thread_sibling和core_sibling,保存了和该CPU位于相同级别(同一个core和同一个cluster)的所有姐妹CPU。系统中每个CPU(个数由NR_CPUS指定,是从OS的角度看的)都有一个struct cpu_topology变量,用于描述该CPU在整个topology中的地位。以数组的形式维护。
(2)重定义CPU topology有关的宏定义
/* arch/arm64/include/asm/topology.h */ #define topology_physical_package_id(cpu) (cpu_topology[cpu].cluster_id) #define topology_core_id(cpu) (cpu_topology[cpu].core_id) #define topology_core_cpumask(cpu) (&cpu_topology[cpu].core_sibling) #define topology_thread_cpumask(cpu) (&cpu_topology[cpu].thread_sibling)
实现比较简单,从该CPU对应的struct cpu_topology变量中取出指定的字段即可。
(3)提供初始化并构建CPU topology的方法,以便在系统启动时调用
/* arch/arm64/include/asm/topology.h */ void init_cpu_topology(void); void store_cpu_topology(unsigned int cpuid);
init_cpu_topology的调用路径是:kernel_init-->smp_prepare_cpus-->init_cpu_topology,主要完成如下任务:
store_cpu_topology的调用路径是:kernel_init-->smp_prepare_cpus-->store_cpu_topology,在没有从DTS中成功获取CPU topology的情况下,从ARM64的MPIDR寄存器中读取topology信息。
设备树中的cpu-map和clusterX描述了CPU的拓扑结构,具体可参考“Documentation/devicetree/bindings/arm/topology.txt”中的描述。
2.3 CPU topology driver
CPU topology driver位于“driversase opology.c”中,基于“include/linux/topology.h”所提供的API,以sysfs的形式,向用户空间提供获取CPU topology信息的接口,lscpu应用,就是基于该接口实现的。sysfs的格式可参考“Documentation/cputopology.txt”。
/sys/devices/system/cpu/cpuX/topology/下
physical_package_id: //就是此CPU位于的Cluster编号 core_id: //在一个Cluster内此CPU的编号 thread_siblings: //每个CPU核的位掩码,CPU0 CPU1 CPU2分别为0x1 0x2 0x4 thread_siblings_list: //CPU是几这个就是几,CPU0就是0,CPU7就是7 core_siblings: //每个Cluster内的CPU组成的位掩码,若四小核Cluster0就是0x0f,3中核就是0x70 core_siblings_list: //每个Cluster内的CPU组成的数字加中画线显示,若4小核,3中核,1大核,小核的是0-3,中核就是4-6,大核就是7
/sys/devices/system/cpu/下
kernel_max 一共有多少个核,7 kernel_max: 31 offline: 2,4-31,32-63 online: 0-1,3 possible: 0-31 present: 0-31
3. CPU一共有4种状态需要表示:
cpu_possible_bits,系统中包含的所有的可能的CPU core,在系统初始化的时候就已经确定。对于ARM64来说,DTS中所有格式正确的CPU core,都属于possible的core;
cpu_present_bits,系统中所有可用的CPU core(具备online的条件,具体由底层代码决定),并不是所有possible的core都是present的。对于支持CPU hotplug的形态,present core可以动态改变;
cpu_online_bits,系统中所有运行状态的CPU core(后面会详细说明这个状态的意义);
cpu_active_bits,有active的进程正在运行的CPU core。
三、Linux cpu ops
1. 在SMP系统中,Linux kernel会在一个CPU(primary CPU)上完成启动操作。primary CPU启动完成后,再启动其它的CPU(secondary CPUs),这称作secondary CPU boot。一般是CPU0作为boot cpu。
2. CPU(或SOC)中会集成一个ROM,ROM上有CPU厂商在出厂时固化的代码,这些代码会进行一些必要的初始化后,将CPU跳转到其它地址(例如0x20000000),这些地址一般是RAM或者NOR flash,用户代
码可以存放在这些位置。
3. 不同的CPU core可能有着不同的power domain,因而有可能单独上电。
4. CPU hotplug
hotplug功能,是在处理性能需求不高的情况下,从物理上关闭不需要的CPU core,并在需要的时候,将它们切换为online状态的一种手段。和cpuidle类似,cpu hotplug也是根据系统负荷,动态调整
处理器性能,从而达到节省功耗的目的。
hotplug与idle的区别:
处于idle状态的CPU,对调度器来说是可见的,换句话说,调度器并不知道某个CPU是否处于idle状态,因此不需要对它们做特殊处理。而处于un-hotplugged状态CPU,对调度器是不可见,因此调度器必
须做一些额外的处理,包括:主动移除CPU,并将该CPU上的中断等资源迁移到其它CPU上,同时进行必要的负载均衡;反之亦然。
5. 每一个core掉电后,都要检查该core的sibling core是否都已掉电,如果是,则关闭cluster的供电。
6. cpu ops
对ARM64平台来说,kernel使用struct cpu_operations来抽象cpu ops
struct cpu_operations { const char *name; int (*cpu_init)(struct device_node *, unsigned int); int (*cpu_init_idle)(struct device_node *, unsigned int); int (*cpu_prepare)(unsigned int); int (*cpu_boot)(unsigned int); void (*cpu_postboot)(void); #ifdef CONFIG_HOTPLUG_CPU int (*cpu_disable)(unsigned int cpu); void (*cpu_die)(unsigned int cpu); int (*cpu_kill)(unsigned int cpu); #endif #ifdef CONFIG_ARM64_CPU_SUSPEND int (*cpu_suspend)(unsigned long); #endif };
针对ARM64,kernel提供了两种可选的方法,smp spin table和psci,如下:
static const struct cpu_operations *supported_cpu_ops[] __initconst = { #ifdef CONFIG_SMP &smp_spin_table_ops, #endif &cpu_psci_ops, NULL, };
具体使用哪一个operation,是通过DTS中的“enable-method”域指定的,DTS格式如下:
cpus { ... cpu@000 { ... enable-method = "psci"; cpu-release-addr = <0x1 0x0000fff8>; }; ... };
系统初始化的时候,会根据DTS配置获取使用的operations(setup_arch-->cpu_read_bootcpu_ops-->cpu_read_ops),最终保存在一个cpu_ops数组(每个CPU一个)中,供SMP(arch/arm64/kernel/smp.c)使用,如下:
/* arch/arm64/kernel/cpu_ops.c */ const struct cpu_operations *cpu_ops[NR_CPUS];
三、cpu control & hotplug
1. kernel cpu control位于“./kernel/cpu.c”中,是一个承上启下的模块,负责屏蔽arch-dependent的实现细节,向上层软件提供控制CPU core的统一API(主要包括cpu_up/cpu_down等接口的实现)。
2. cpu的四种状态
kernel使用4个bitmap,来保存分别处于4种状态的CPU core:possible、present、active和online。
/* include/linux/cpumask.h */ cpu_possible_mask- has bit 'cpu' set iff cpu is populatable,在启动时就是固定的,作为CPU ID的集合,可理解为存在这个CPU资源。 cpu_present_mask - has bit 'cpu' set iff cpu is populated,cpu_present_mask是动态的,表示当前插入了哪些CPU,可理解为被kernel接管。 cpu_online_mask - has bit 'cpu' set iff cpu available to scheduler,pu_online_mask是cpu_present_mask的动态子集,指示可用于调度的CPU。 cpu_active_mask - has bit 'cpu' set iff cpu available to migration,即是否对调度器可见
如果启用了HOTPLUG,则将强制cpu_possible_mask设置为所有NR_CPUS位。
2.1 possible CPU
possible的CPUs,代表了系统中可被使用的所有的CPU,在boot阶段确定之后,就不会再修改。以ARM64为例,其初始化的过程如下:
(1)系统上电后,boot CPU启动,执行start_kernel(init/main.c),并分别调用 boot_cpu_init 和 setup_arch 两个接口,进行possible CPU相关的初始化。
(2)boot_cpu_init负责将当前的boot CPU放到possible CPU的bitmap中,同理,boot CPU也是present、oneline、active CPU。
/* init/main.c */ static void __init boot_cpu_init(void) { int cpu = smp_processor_id(); //用户获取当前CPU的ID /* Mark the boot cpu "present", "online" etc for SMP and UP case */ set_cpu_online(cpu, true); set_cpu_active(cpu, true); set_cpu_present(cpu, true); set_cpu_possible(cpu, true); }
2.2 present CPU
start_kernel —> rest_init —> kernel_init(pid 1,init task) —> kernel_init_freeable -> smp_prepare_cpus”,轮询所有的possible CPU,如果某个CPU core满足具备相应的cpu_ops指针,cpu ops的.cpu_prepare回调成功,则调用set_cpu_present(),将其设置为present CPU。
2.3 online CPU
已经boot的CPU,会在 secondary_start_kernel 中,调用 set_cpu_online 接口,将其设置为online状态。反之,会在__cpu_disable中将其从online mask中清除。
2.4 active CPU
调度器需要监视 CPU hotplug 有关的每一个风吹草动。由于调度器和CPU控制两个独立的模块,kernel 通过 notifier 机制实现这一功能。每当系统的CPU资源有任何变动,kernel CPU control 模块就会通知调度器,调度器根据相应的event(CPU_DOWN_FAILED、CPU_DOWN_PREPARE等),调用set_cpu_active接口,将某个CPU添加到active mask或者移出active mask。这就是active CPU的意义。
3. 对于支持CPU hotplug功能的平台来说,可以在系统启动后的任意时刻,关闭任意一个secondary CPU(对ARM平台来说,CPU0或者说boot CPU,是不可以被关闭的),并在需要的时候,再次打开它。
4. 在kernel/cpu.c中,cpu_up 接口,只会在使能了 CONFIG_SMP 配置项(意味着是SMP系统)后才会提供。而cpu_down接口,则只会在使能了 CONFIG_HOTPLUG_CPU 配置项后才会提供。
5. per-CPU 的idle线程
boot CPU在执行初始化动作的时候,会通过“smp_init —> idle_threads_init —> idle_init”的调用,为每个CPU创建一个idle线程,如下:
/* kernel/smpboot.c */ static inline void idle_init(unsigned int cpu) { struct task_struct *tsk = per_cpu(idle_threads, cpu); if (!tsk) { tsk = fork_idle(cpu); if (IS_ERR(tsk)) pr_err("SMP: fork_idle() failed for CPU %u ", cpu); else per_cpu(idle_threads, cpu) = tsk; } }
该接口的本质是,为每个CPU fork一个idle thread(由struct task_struct结构表示),并保存在一个per-CPU的全局变量(idle_threads)中。此时,idle thread只是一个task结构,并没有执行。
6. 打开和关闭CPU分析
在当前kernel实现中,只支持通过sysfs的形式,关闭或打开CPU:
echo 0 > /sys/devices/system/cpu/cpuX/online # 关闭CPU echo 1 > /sys/devices/system/cpu/cpuX/online # 打开CPU
CPU online 的软件流程如下:
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线程上。
_cpu_up 接口会在完成一些准备动作之后,调用平台相关的__cpu_up接口,由平台代码完成具体的up操作,如下:
static int _cpu_up(unsigned int cpu, int tasks_frozen) { void *hcpu = (void *)(long)cpu; unsigned long mod = tasks_frozen ? CPU_TASKS_FROZEN : 0; struct task_struct *idle; cpu_hotplug_begin(); idle = idle_thread_get(cpu); ret = smpboot_create_threads(cpu); ret = __cpu_notify(CPU_UP_PREPARE | mod, hcpu, -1, &nr_calls); ret = __cpu_up(cpu, idle); /* Wake the per cpu threads */ smpboot_unpark_threads(cpu); /* Now call notifier in preparation. */ cpu_notify(CPU_ONLINE | mod, hcpu); }
准备动作包括:
(1) 获取idle thread的task指针,该指针最终会以参数的形式传递给arch-specific代码。
(2) 创建一个用于管理CPU hotplug动作的线程(smpboot_create_threads),该线程的具体意义,后面会再说明。
(3) 发送CPU_UP_PREPARE notify。
以ARM64为例,__cpu_up 的内部实现如下:
/* arch/arm64/kernel/smp.c */ int __cpu_up(unsigned int cpu, struct task_struct *idle) { int ret; /* We need to tell the secondary core where to find its stack and the page tables. */ secondary_data.stack = task_stack_page(idle) + THREAD_START_SP; __flush_dcache_area(&secondary_data, sizeof(secondary_data)); /* Now bring the CPU into our world. */ ret = boot_secondary(cpu, idle); if (ret == 0) { /* * CPU was successfully started, wait for it to come online or * time out. */ wait_for_completion_timeout(&cpu_running, msecs_to_jiffies(1000)); cpu_online(cpu); } else { pr_err("CPU%u: failed to boot: %d ", cpu, ret); } secondary_data.stack = NULL; return ret; }
该接口以 idle thread 的 task 指针为参数,完成如下动作:
(1) 将idle线程的堆栈,保存在一个名称为 secondary_data 的全局变量中(这地方很重要,后面再介绍其中的奥妙)。
(2) 执行 boot_secondary 接口,boot CPU,具体的流程。
(3) boot_secondary 返回后,等待对应的CPU切换为online状态。
secondary_startup 接口位于arch/arm64/kernel/head.S中,负责secondary CPU启动后的后期操作,如下:
ENTRY(secondary_startup) /* * Common entry point for secondary CPUs. */ mrs x22, midr_el1 // x22=cpuid mov x0, x22 bl lookup_processor_type mov x23, x0 // x23=current cpu_table cbz x23, __error_p // invalid processor (x23=0)? pgtbl x25, x26, x28 // x25=TTBR0, x26=TTBR1 ldr x12, [x23, #CPU_INFO_SETUP] add x12, x12, x28 // __virt_to_phys blr x12 // initialise processor ldr x21, =secondary_data ldr x27, =__secondary_switched // address to jump to after enabling the MMU b __enable_mmu ENDPROC(secondary_startup) ENTRY(__secondary_switched) ldr x0, [x21] // get secondary_data.stack mov sp, x0 mov x29, #0 b secondary_start_kernel ENDPROC(__secondary_switched)
我们重点关注上面16~17行,以及21~26行的 __secondary_switched,__secondary_switched 会将保存在 secondary_data 全局变量中的堆栈取出,保存在该CPU的SP中,
并跳转到 secondary_start_kernel 继续执行。
CPU启动后,需要先配置好堆栈,才能进行后续的函数调用,这里使用的是该CPU idle thread的堆栈。看一下kernel中“current”指针(获取当前task结构的宏定义)的实现方法:
#define current get_current() #define get_current() (current_thread_info()->task) static inline struct thread_info *current_thread_info(void) { register unsigned long sp asm ("sp"); return (struct thread_info *)(sp & ~(THREAD_SIZE - 1)); }
通过CPU的SP指针,是可以获得CPU的当前task的。也就是说,当CPU SP被赋值为idle thread的堆栈的那一瞬间,当前的上下文已经是idle thread了!
6. 另外,CPU hotplug 还受“maxcpus”命令行参数影响
系统启动的时候,可以通过命令行参数“maxcpus”,告知kernel本次启动所使用的CPU个数,该个数可以小于等于possible CPU的个数。系统初始化时,只会把“maxcpus”所指定个数的CPU置为present状态
Documentationcpu-hotplug.txt”文档是这样描述的:
maxcpus=n Restrict boot time cpus to n. Say if you have 4 cpus, using maxcpus=2 will only boot 2. You can choose to bring the other cpus later online, read FAQ's for more info.
注:
内核中经常有这样的函数,xxx、_xxx 或者 __xxx,区别是一个或者两个下划线,其中的含义是:
xxx接口,通常需要由某个锁保护,一般提供给其它模块调用。它会直接调用_xxx接口;
_xxx接口,则不需要保护,一般由模块内部在确保安全的情况下调用。有时,外部模块确信可行(不需要保护),也可能会直接调用;
__xxx接口,一般提供给arch-dependent的软件层实现,比如这里的arch/arm64/kernel/xxx.c。
理解这些含义后,会加快我们阅读代码的速度,另外,如果直接写代码,也尽量遵守这样的原则,以便使自己的代码更规范、更通用。
参考:
Linux CPU core的电源管理(1)_概述: http://www.wowotech.net/pm_subsystem/cpu_core_pm_overview.html
Linux CPU core的电源管理(2)_cpu topology:http://www.wowotech.net/pm_subsystem/cpu_topology.html
Linux CPU core的电源管理(5)_cpu control及cpu hotplug:http://www.wowotech.net/pm_subsystem/cpu_hotplug.html