一、为什么需要管理cpu
cpu作为计算机系统的核心,主要执行运算和控制功能。在程序员眼中,只需要为其提供合适的电源和时钟,它就可以从指定位置开始读取和执行指令。因此,在传统的单核系统中,操作系统并不需要对cpu做特殊的管理。
由于cpu的执行速度与其运行频率相关,因此在摩尔定律的驱动下,起先英特尔等芯片产商主要通过提高单核主频的方式,来提高cpu性能。但是随着芯片频率的提高,其运行功耗也会成指数级上升,因此在频率提高到一定程度之后,继续提升的难度就变得越来越大。
为此,芯片产商改变了设计思路,转而通过在同一个die上集成多个相同的cpu核心来实现性能提升,这就是我们平常所说的smp。由于系统启动时主要执行一些镜像加载,以及软硬件初始化相关的任务。而这些流程并没有并行执行的需求,因此smp系统通常都由一个主cpu执行启动流程。而其它cpu则会等到操作系统初始化完成之后才会启动。
更进一步,虽然smp系统的性能很强,但实际上很多时候并不需要这么高的计算能力,如我们当前只是使用浏览网页或听音乐等功能,此时完全没必要使用多个工作在最高频率的cpu核。若不对其做相应的管理,这种cpu性能的浪费会造成系统功耗增加。特别是对于移动设备,可能会严重影响其续航能力。
为此,需要对cpu的功耗做更精细的管理,如在负载较低时调低cpu的频率和电压,当cpu进入idle状态时关闭其时钟,甚至将某些cpu完全从系统中移除等。以上这些功能的实现,首先自然离不开硬件的支持,其次也需要软件为其提供相应的驱动程序和管理策略,这也是为什么内核需要提供cpu管理相关模块的原因。在这个系列中,我们将以aarch64架构为例,按以下顺序分别介绍这些模块的原理和实现机制:
(1)cpu参数的初始化流程
(2)smp cpu的启动流程
(3)cpu hotplug原理和实现
(4)cpu idle原理和实现
(5)cpu dvfs原理和实现
二、如何标识cpu id
arm64在smp系统中会为每个处理器分配一个表示其affinity关系的寄存器mpidr,该寄存器标识了不同处理器之间的亲和度。如位于相同cluster的处理器亲和度更高,在它们之间迁移进程的代价比跨cluster之间的迁移更小,因而可以给调度器的负载均衡策略提供参考。以下为其寄存器定义:
由于在系统中每个处理器都具有唯一的mpidr值,因此它又可以被用于标识cpu的硬件id。与其它设备一样,arm64架构通过dts来配置每个cpu的硬件id,其示例格式如下:
cpu0: cpu@0 {
compatible = "arm,cortex-a53";
device_type = "cpu";
reg = <0x0 0x1>;
enable-method = "psci";
next-level-cache = <&CLUSTER0_L2>;
clocks = <&stub_clock 0>;
operating-points-v2 = <&cpu_opp_table>;
cpu-idle-states = <&CPU_SLEEP &CLUSTER_SLEEP>;
#cooling-cells = <2>;
dynamic-power-coefficient = <311>;
};
即在内核中cpu也被抽象成了一种设备,并且可以为其配置一系列的参数。其中reg即用于配置其硬件id,而其它参数将会在后面相关模块中逐步介绍。
由于硬件id的格式与不同实现相关,为了方便统一管理,内核引入了cpu的逻辑id,将逻辑id与硬件id绑定后,即可以使用逻辑id来引用相关cpu。
cpu逻辑id的定义如下:源码路径:kernel/arch/arm64/kernel/setup.c
u64 __cpu_logical_map[NR_CPUS] = { [0 ... NR_CPUS-1] = INVALID_HWID };
其中数组下标为cpu的逻辑id,数组成员用于保存其硬件id。linux内核规定primary cpu的逻辑id为0,而secondary cpu可通过dts获取其硬件id。
由于启动时的运行cpu即为primary cpu,因此其硬件id可在启动流程中从mpidr寄存器中直接读出。以下为其初始化流程图:
其中smp_setup_processor_id的实现如下:
//kernel/arch/arm64/kernel/setup.c
void __init smp_setup_processor_id(void)
{
u64 mpidr = read_cpuid_mpidr() & MPIDR_HWID_BITMASK;
set_cpu_logical_map(0, mpidr);
/*
* clear __my_cpu_offset on boot CPU to avoid hang caused by
* using percpu variable early, for example, lockdep will
* access percpu variable inside lock_release
*/
set_my_cpu_offset(0);
pr_info("Booting Linux on physical CPU 0x%010lx [0x%08x]\n",
(unsigned long)mpidr, read_cpuid_id());
}
(1)从mpidr寄存器读取primary cpu的硬件id
(2)将该硬件id保存到__cpu_logical_map数组的第一个成员中,从而将其逻辑cpu号设置为0
由于secondary cpu在启动初期就已通过wfe睡眠,而并不会参与启动流程,因此必须要由primary cpu帮助其完全cpu id的设置工作。而其参数需要从dts对应的dtb文件中读取,以下为其流程图:
其中of_parse_and_init_cpus会遍历dtb中所有的cpu节点,获取其reg值,然后将其转换为64位后设置到对应的__cpu_logical_map数组中。以下为其代码主要实现:
//kernel/arch/arm64/kernel/smp.c
void __init smp_init_cpus(void)
{
int i;
if (acpi_di