处理器的发展过程
作为核心部件的处理器,它负责对输入的数据进行分析和处理,并进行输出。衡量一个处理器的性能如何,主要有两个方面:每个时钟周期内可以执行的指令数(IPC: Instruction Per Clock) 和处理器的主频。在单核处理的时代,对于同一代的架构,改良架构来提高IPC 的幅度是非常有限,因此为了提高处理器的性能,只能通过提高CPU 的主频。但随着CPU 的主频不断提高,一些问题也渐渐地凸显出来。当CPU 主频提高到一定程度时,处理器性能并没有出现明显的提升,相反,使得CPU 的能耗大大上升,据测算,主频每增加1G,功耗将上升25 瓦,而在芯片功耗超过150 瓦后,现有的风冷散热系统将无法满足散热的需要。到此,“主频为王”的时代已经走到了尽头,人们不得不另辟蹊径,多核心处理器应运而生。
多核心处理器是指把两个或更多独立处理器封装在一个单一集成电路(IC)中,多核的硬件实现方式可以分为两种,一是将所有具有相同的构架的CPU 集成在一起,称为同构多核,它们之间共享系统资源,生活中使用的台式机或者笔记本电脑的CPU 处理器大多都是采用这种架构的多核心处理器;二是异构多核,往往同时集成了通用处理器、DSP、FPGA 等,主要应用于一些复杂且实时性高的应用场景,如机器人拾取和放置装配线,它需要采集高分辨率的视频图像,并对图像进行处理,正确无误地检测和识别目标,最终通过电机驱动操控机器手臂,完成标记组件的装配。仅靠通用处理器很容易就会负载过重,而导致无法在特定时间完成处理。
早期的手机SoC 用的都是由ARM 公司提出的“Big.Little”架构,它是在一个集成电路中集成了两种不同类型的ARM 核心,一种为高性能Core,称作big core,负责承担高负载的任务,一种为低性能Core,称作little core,用于处理手机的大部分工作负载。随着移动设备的普及,人们对移动设备的性能需求越来越高,相应地便产生了更多能耗,“Big.Little”架构通过对CPU 大小核资源的合理调用,使得高性能与低功耗兼得,大大提高了手机电池的续航能力。
MP157 处理器基本介绍
配套的开发板所使用的STM32MP157A 系列是一款异构多核处理器,内部集成了两个CortexA7 的CPU,主频最高可达650MHZ。我们可以使用命令
lscpu
来查看CPU 的相关信息。
- Architecture:表示处理器所使用的架构,常见的有x86、MIPS、PowerPC、ARM 等等,对于MP157 来说,它属于ARMv7 架构;
- Byte Order:表示处理器是大端模式还是小端模式;
- CPU(s):表示处理器拥有多少个核心,前面说过157A 系列是有两个A7 核心,这里对应了的值为2,其编号分别对应0 和1;
- On-line CPU(s) list:当前正常运行的CPU 编号,可以看到,当前系统中两个A7 核心都处于正常运行的状态;
- Socket(s):插槽,可以理解为当前板子上有多少个MP157 芯片;
- Core(s) per socket:芯片厂商会把多个Core 集成在一个Socket 上,这里表示每块157 芯片上面有2 个核;
- Thread(s) per core:进程是程序的运行实例,它依赖于各个线程的分工合作。为此,英特尔研发了超线程技术,通过此技术,英特尔实现在一个实体CPU 中,提供两个逻辑线程,让单个处理器就也能使用线程级的并行计算。
- Vendor ID:芯片厂商ID,比如GenuineIntel、ARM、AMD 等;
- Model name:CPU 的型号名称,这里对应的是Cortex-A7;
- Stepping:用来标识一系列CPU 的设计或生产制造版本数据,步进的版本会随着这一系列CPU 生产工艺的改进、BUG 的解决或特性的增加而改变;
- CPU min MHz,CPU max MHz:CPU 所支持的最小、最大的频率,系统运行过程会根据实际情况,来调整CPU 的工作频率,但不会超过最大支持频率;
- BogoMIPS:MIPS 是millions of instructions per second(百万条指令每秒) 的缩写,该值是通过函数计算出来的,只能用来粗略计算处理器的性能,并不是十分精确。
- Flags:用来表示CPU 特征的参数。比如参数thumb、thumbee 表示MP157 支持Thumb 和Thumb-2EE 这两种指令模式;vfp 表示支持浮点运算。
linux SMP 启动过程
目前支持多核处理器的实时操作系统体系结构有两种,分别是对称多处理SMP(Symmetric Multi-Processing) 构架和非对称多处理AMP(Asymmetric Multi-Processing) 构架。AMP 模式是在各个CPU核心上均运行一个操作系统(操作系统不一定完全相同),各个操作系统拥有自己专用的内存,相互之间通过访问受限的共享内存进行通信。而SMP 模式由一个操作系统实例控制所有CPU 核心,所有CPU 核心共享内存和外设资源。相对于AMP 模式,SMP 模式的操作系统具有可共享内存、较高的性能和功耗比、以及易实现负载均衡等优点,更能发挥发挥多核处理器的硬件优势。
Linux 内核编译时,CONFIG_SMP 配置项用于控制内核是否支持SMP。
linux 系统中SMP 模式的启动流程如图所示,复位之后,CPU0 和CPU1 同时执行ROM code 中的代码,此时的CPU0 和CPU1 运行的是一模一样的指令,此后ROM code 引导CPU0 去执行Bootloader(包括tfa 和uboot)的代码和内核代码
如果你曾经留意过内核启动的输出,你就会发现如图所示的打印信息,提示我们当前内核是在CPU0 上运行的,
而CPU1 则进入循环,直到收到CPU0 发来的唤醒信号,在这个过程中,CPU0 已经为CPU1 创建空闲任务,CPU1 则被唤醒,开始执行空闲任务。
上面只是简单介绍了整个启动流程,实际上,内核是如何识别到芯片中有几个CPU 核的呢?CPU0又是如何唤醒CPU1 的呢?首先,为了描述当前系统中各个CPU 核心的工作状态,内核在kernel/cpu.c 中定义四个cpumask 类型的结构体变量,
列表1: cpumask 类型的结构体变量(位于文件kernel/cpu.c)
struct cpumask __cpu_possible_mask __read_mostly;
EXPORT_SYMBOL(__cpu_possible_mask);
struct cpumask __cpu_online_mask __read_mostly;
EXPORT_SYMBOL(__cpu_online_mask);
struct cpumask __cpu_present_mask __read_mostly;
EXPORT_SYMBOL(__cpu_present_mask);
struct cpumask __cpu_active_mask __read_mostly;
EXPORT_SYMBOL(__cpu_active_mask);
cpumask 类型的结构体只有一个成员变量——数据类型为unsigned long 的一维数组,一个CPU 核心用数组元素的一个位表示,宏定义BITS_TO_LONGS(bits) 负责计算数组的长度,假设当前有33 个CPU(NR_CPUS=33),经过BITS_TO_LONGS 转换之后,可知需要的数组长度为2 个。
列表2: struct cpumask 结构体(位于文件include/linux/cpumask.h)
/* Don't assign or return these: may not be this big! */
typedef struct cpumask { DECLARE_BITMAP(bits, NR_CPUS); } cpumask_t;
#define DECLARE_BITMAP(name,bits) \
unsigned long name[BITS_TO_LONGS(bits)]
这四个cpumask 类型的变量作用如下:
- __cpu_possible_mask:记录物理存在且可能被激活的CPU 核心对应的编号,由设备树解析CPU 节点获得;
- __cpu_online_mask:记录当前系统正在运行的CPU 核心的编号;
- __cpu_present_mask:动态地记录了当前系统中所有CPU 核心的编号,如果内核配置了CONFIG_HOTPLUG_CPU,那么这些CPU 核心不一定全部处于运行状态,因为有的CPU 核心可能被热插拔了;
- __cpu_active_mask:用于记录当前系统哪些CPU 核心可用于任务调度;
在/sys/devices/system/cpu 目录下,记录了系统中所有的CPU 核以及上述各变量的内容,例如文件present,对应于__cpu_present_mask 变量,执行以下命令,可以查看当前系统中所有的CPU 核编号。
cat /sys/devices/system/cpu/present
此外,我们可以通过文件/sys/devices/system/cpu/cpu1/online 在用户空间控制一个CPU 核运行与否。
# 关闭CPU1
echo 0 > /sys/devices/system/cpu/cpu1/online
# 打开CPU1
echo 1 > /sys/devices/system/cpu/cpu1/online
接下来,看看内核是如何建立CPU 之间的关系的。在设备树根节点下有个/cpus 的子节点,其内容如下
列表3: /cpus 节点(位于文件arch/arm/boot/dts/stm32mp157c.dtsi)
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
clocks = <&rcc CK_MPU>;
clock-names = "cpu";
operating-points-v2 = <&cpu0_opp_table>;
nvmem-cells = <&part_number_otp>;
nvmem-cell-names = "part_number";
};
cpu1: cpu@1 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <1>;
clocks = <&rcc CK_MPU>;
clock-names = "cpu";
operating-points-v2 = <&cpu0_opp_table>;
};
};
该节点描述了当前硬件上存在两个CPU,分别是CPU0 和CPU1,内核代码通过解析该节点,便可以获得当前系统的CPU 核心个数,并且我们可以看到该节点还包含了“operating-points-v2”属性,指向了cpu0_opp_table 节点,该节点是用于配置CPU 核心支持的频率。
列表4: /cpu0_opp_table 节点(位于文件arch/arm/boot/dts/stm32mp157c.dtsi)
cpu0_opp_table: cpu0-opp-table {
compatible = "operating-points-v2";
opp-shared;
opp-650000000 {
opp-hz = /bits/ 64 <650000000>;
opp-microvolt = <1200000>;
opp-supported-hw = <0x1>;
};
opp-800000000 {
opp-hz = /bits/ 64 <800000000>;
opp-microvolt = <1350000>;
opp-supported-hw = <0x2>;
};
};
OPP 驱动会根据芯片内部的版本号,来设置CPU 核心的工作电压和工作频率。这部分的内核代码,最终实现构建CPU 的拓扑关系,并调用函数set_cpu_possible 在possible_mask 相应的CPU 编号位置上置1,表明当前系统存在这个CPU 核。
列表5: 解析/cpus 节点(位于文件arch/arm/kernel/devtree.c)
void __init arm_dt_init_cpu_maps(void)
{
/* 省略部分代码*/
cpus = of_find_node_by_path("/cpus");
if (!cpus)
return;
/* 省略部分代码*/
for (i = 0; i < cpuidx; i++) {
set_cpu_possible(i, true);
cpu_logical_map(i) = tmp_map[i];
pr_debug("cpu logical map 0x%x\n", cpu_logical_map(i));
}
}
内核已经掌握了当前系统的CPU 相关信息,接下来就应该让其他CPU 纳入内核的管理,开始卖力干活了。在SMP 初始化之前,内核需要初始化present_mask,之后便根据present_mask 中的内容来打开对应的CPU 了。具体实现方式是将possible_mask 的值复制到present_mask 中。
列表6: 初始化present_mask(位于文件arch/arm/kernel/smp.c)
void __init smp_prepare_cpus(unsigned int max_cpus)
{
unsigned int ncores = num_possible_cpus();
if (ncores > 1 && max_cpus) {
init_cpu_present(cpu_possible_mask);
}
}
void init_cpu_present(const struct cpumask *src)
{
cpumask_copy(&__cpu_present_mask, src);
}
列表7: 函数smp_init(位于文件kernel/smp.c)
/* Called by boot processor to activate the rest. */
void __init smp_init(void)
{
/* 省略部分代码*/
for_each_present_cpu(cpu) {
if (!cpu_online(cpu))
cpu_up(cpu);
}
}
smp_init() 函数会遍历present_mask 中的cpu,如果cpu 没有online,那么调用cpu_up() 函数。该函数是SMP 启动过程最关键的一环。SMP 系统在启动的过程中,即刚上电时,只能用一个CPU 来执行内核初始化,这个CPU 称为“引导处理器”,即BP,其余的处理器处于暂停状态,称为“应用处理器”,即AP。代码的注释中列出了BP 和AP 之间初始化过程的大致状态,左侧是CPU 上电过程,需要经历OFFLINE->BRINGUP_CPU->AP_OFFLINE-> AP_ONLNE->AP_ACTIVE 的过程。
列表8: CPU 状态值枚举(位于文件include/linux/cpuhotplug.h)
/*
* CPU-up CPU-down
*
* BP AP BP AP
*
* OFFLINE OFFLINE
* | ^
* v |
* BRINGUP_CPU->AP_OFFLINE BRINGUP_CPU <- AP_IDLE_DEAD (idle thread/play_dead)
* | AP_OFFLINE
* v (IRQ-off) ,---------------^
* AP_ONLNE | (stop_machine)
* | TEARDOWN_CPU <- AP_ONLINE_IDLE
* | ^
* v |
* AP_ACTIVE AP_ACTIVE
*/
enum cpuhp_state {
CPUHP_INVALID = -1,
CPUHP_OFFLINE = 0,
/* 省略部分代码*/
CPUHP_AP_ONLINE_DYN_END = CPUHP_AP_ONLINE_DYN + 30,
CPUHP_AP_X86_HPET_ONLINE,
CPUHP_AP_X86_KVM_CLK_ONLINE,
CPUHP_AP_ACTIVE,
CPUHP_ONLINE,
};
内核在kernel/cpu.c 里提供了一个cpuhp_step 类型的数组:cpuhp_hp_states,在数组中内置了一些初始化的回调函数,这些回调函数对应初始化过程中的各个状态。
列表9: cpuhp_hp_states 数组(位于文件kernel/cpu.c)
static struct cpuhp_step cpuhp_hp_states[] = {
[CPUHP_OFFLINE] = {
.name = "offline",
.startup.single = NULL,
.teardown.single = NULL,
},
#ifdef CONFIG_SMP
[CPUHP_BRINGUP_CPU] = {
.name = "cpu:bringup",
.startup.single = bringup_cpu,
.teardown.single = NULL,
.cant_stop = true,
},
[CPUHP_ONLINE] = {
.name = "online",
.startup.single = NULL,
.teardown.single = NULL,
},
};
下面我们看一下OFFLINE->BRINGUP_CPU 的这个过程,cpu_up 函数最终会调用_cpu_up函数, 传入的实参target 为CPUHP_ONLINE(cpuhp_state 中的最大值) , 第四行代码比较CPUHP_ONLINE 和CPUHP_BRINGUP_CPU 的大小, 最终返回较小值, 即CPUHP_BRINGUP_CPU。
列表10: _cpu_up 函数(位于文件kernel/smp.c)
static int _cpu_up(unsigned int cpu, int tasks_frozen, enum cpuhp_state target)
{
/* 省略部分代码*/
target = min((int)target, CPUHP_BRINGUP_CPU);
ret = cpuhp_up_callbacks(cpu, st, target);
out:
cpus_write_unlock();
arch_smt_update();
return ret;
}
cpuhp_up_callbacks 函数的作用,就如函数名称一样,是用来调用cpuhp_hp_states 数组中的提供的初始化函数。
列表11: _cpu_up 函数(位于文件kernel/smp.c)
static int cpuhp_up_callbacks(unsigned int cpu, struct cpuhp_cpu_state *st,
enum cpuhp_state target)
{
enum cpuhp_state prev_state = st->state;
int ret = 0;
while (st->state < target) {
st->state++;
ret = cpuhp_invoke_callback(cpu, st->state, true, NULL, NULL);
if (ret) {
if (can_rollback_cpu(st)) {
st->target = prev_state;
undo_cpu_up(cpu, st);
}
break;
}
}
return ret;
}
st->state 记录了当前AP 核的状态,默认上电后,AP 是处于CPUHP_OFFLINE 的状态,因此,cpuhp_up_callbacks 函数便会执行cpuhp_hp_states 数组中提供的(CPUHP_OFFLINE+1)至CPUHP_BRINGUP_CPU 所有阶段的回调函数,来启动当前的AP 核,经过BRINGUP_CPU的状态之后,当前的AP 核就会运行空闲任务,与此同时,BP 核唤醒cpuhp/0 进程,完成CPUHP_AP_ONLINE_IDLE->CPUHP_ONLINE 的过程,具体的实现过程:
列表12: cpuhp_thread_fun 函数(位于文件kernel/smp.c)
static void cpuhp_thread_fun(unsigned int cpu)
{
if (cpuhp_is_atomic_state(state)) {
local_irq_disable();
st->result = cpuhp_invoke_callback(cpu, state, bringup, st->node, &st->last);
local_irq_enable();
WARN_ON_ONCE(st->result);
} else {
st->result = cpuhp_invoke_callback(cpu, state, bringup, st->node, &st->last);
}
}
我们可以看到在这个进程又调用了前面的提到的函数cpuhp_invoke_callback,最终AP 核达到CPUHP_ONLINE 的状态,由内核进行调度,和BP 核一起承担工作负载。
上述的过程只是分析了单个AP 核的启动过程,假设现在系统中有多个AP 核,那么内核会为每个AP 核执行相同的操作,直到所有的AP 核启动成功。
参考资料:嵌入式Linux 驱动开发实战指南-基于STM32MP1 系列