1. SMP(Symmetrical Multi-Processing)

1.1. 处理器的发展过程

作为核心部件的处理器,它负责对输入的数据进行分析和处理,并进行输出。衡量一个处理器的性能如何,主要有两个方面: 每个时钟周期内可以执行的指令数(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大小核资源的合理调用,使得高性能与低功耗兼得,大大提高了手机电池的续航能力。

big.LITTLE架构

1.2. MP157处理器基本介绍

本书配套的开发板所使用的STM32MP157A系列是一款异构多核处理器,内部集成了两个Cortex A7的CPU,主频最高可达650MHZ。 我们可以使用命令

1
lscpu

来查看CPU的相关信息。

lscpu输出
  • 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表示支持浮点运算。

1.3. linux SMP启动过程

目前支持多核处理器的实时操作系统体系结构有两种,分别是对称多处理SMP(Symmetric Multi-Processing)构架和非对称多处理AMP(Asymmetric Multi-Processing)构架。 AMP模式是在各个CPU核心上均运行一个操作系统(操作系统不一定完全相同),各个操作系统拥有自己专用的内存,相互之间通过访问受限的共享内存进行通信。 而SMP模式由一个操作系统实例控制所有CPU核心,所有CPU核心共享内存和外设资源。相对于AMP模式,SMP模式的操作系统具有可共享内存、较高的性能和功耗比、以及易实现负载均衡等优点, 更能发挥发挥多核处理器的硬件优势。

Linux内核编译时,CONFIG_SMP配置项用于控制内核是否支持SMP。

内核配置CONFIG_SMP

linux系统中SMP模式的启动流程如图所示,复位之后,CPU0和CPU1同时执行ROM code中的代码,此时的CPU0和CPU1运行的是一模一样的指令,此后ROM code引导CPU0去执行Bootloader(包括tfa和uboot)的代码和内核代码,

SMP启动流程

如果你曾经留意过内核启动的输出,你就会发现如图所示的打印信息,提示我们当前内核是在CPU0上运行的,

内核打印第一行信息

而CPU1则进入循环,直到收到CPU0发来的唤醒信号,在这个过程中,CPU0已经为CPU1创建空闲任务, CPU1则被唤醒,开始执行空闲任务。

上面只是简单介绍了整个启动流程,实际上,内核是如何识别到芯片中有几个CPU核的呢?CPU0又是如何唤醒CPU1的呢?首先,为了描述当前系统中各个CPU核心的工作状态,内核在kernel/cpu.c中定义四个cpumask类型的结构体变量,

cpumask类型的结构体变量(位于文件kernel/cpu.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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个。

struct cpumask结构体(位于文件include/linux/cpumask.h)
1
2
3
4
5
/* 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核编号。

1
cat /sys/devices/system/cpu/present

此外,我们可以通过文件/sys/devices/system/cpu/cpu1/online在用户空间控制一个CPU核运行与否。

1
2
3
4
# 关闭CPU1
echo 0 > /sys/devices/system/cpu/cpu1/online
# 打开CPU1
echo 1 > /sys/devices/system/cpu/cpu1/online

接下来,看看内核是如何建立CPU之间的关系的。在设备树根节点下有个/cpus的子节点,其内容如下

/cpus节点(位于文件arch/arm/boot/dts/stm32mp157c.dtsi)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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核心支持的频率。

/cpu0_opp_table节点(位于文件arch/arm/boot/dts/stm32mp157c.dtsi)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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核。

解析/cpus节点(位于文件arch/arm/kernel/devtree.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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中。

初始化present_mask(位于文件arch/arm/kernel/smp.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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);
}
函数smp_init(位于文件kernel/smp.c)
1
2
3
4
5
6
7
8
9
/* 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的过程。

CPU状态值枚举(位于文件include/linux/cpuhotplug.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
* 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,在数组中内置了一些初始化的回调函数,这些回调函数对应初始化过程中的各个状态。

cpuhp_hp_states数组(位于文件kernel/cpu.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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。

_cpu_up函数(位于文件kernel/smp.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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数组中的提供的初始化函数。

_cpu_up函数(位于文件kernel/smp.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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的过程,具体的实现过程:

cpuhp_thread_fun函数(位于文件kernel/smp.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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核启动成功。