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架构

DynamIQ,它是基于big.LITTLE进行扩展和设计的,可视作是big.LITTLE技术的演进,但big.LITTLE只是DynamIQ技术支持的诸多功能之一。 它的做法将大小两个集群合并,从而形成一个兼具大小CPU、完全集成化的CPU集群。

1.2. 相关知识介绍

在开始下面一小节启动过程分析前,前面先了解下一些相关的知识:

  • ARMv8的异常等级,ARMv8中异常级别决定了权限级别,分为EL0~3,数字越小,异常越低,权限越低。可以参考下下面图片:

运行等级
  • TF-A (Trusted Firmware-A),是ARM Profile A(ARMv8-a、ARMv7-a)的可信固件的参考实现。简单理解就是处理安全问题,固件的启动都有签名认证,建立了整个启动信任链(Trust Chain)。

  • PSCI (Power State Coordination Interface),由ARM定义的电源管理接口规范,通常由Firmware来实现, Linux系统可以通过smc/hvc指令来进入不同的Exception Level,进而调用对应的实现。ARMv8架构引入了Virtualization,Security等概念, CPU boot、suspend/resumen等操作不再如传统那样单纯,都需要指令调用底层Firmware接口。

1.3. rk3568处理器基本介绍

以lubancat2为例,该板卡是一款多核处理器,内部集成了四个Cortex-A55的CPU,主频最高可达2GHZ。 我们可以使用命令

lscpu

来查看CPU的相关信息。

lscpu输出
  • Architecture:表示处理器所使用的架构,常见的有x86、MIPS、PowerPC、ARM等等,对于rk3568来说,它属于ARMv8架构;

  • Byte Order:表示处理器是大端模式还是小端模式;

  • CPU(s):表示处理器拥有多少个核心,这里有4个,其编号分别对应0~3;

  • On-line CPU(s) list:当前正常运行的CPU编号,可以看到,当前系统中四个核心都处于正常运行的状态;

  • Socket(s):插槽,可以理解为当前板子上有多少个rk3568芯片;

  • Core(s) per socket:芯片厂商会把多个Core集成在一个Socket上,这里表示芯片上面有4个核;

  • Thread(s) per core:进程是程序的运行实例,它依赖于各个线程的分工合作。为此,英特尔研发了超线程技术,通过此技术,英特尔实现在一个实体CPU中,提供两个逻辑线程,让单个处理器就也能使用线程级的并行计算。

  • Vendor ID:芯片厂商ID,比如GenuineIntel、ARM、AMD等;

  • Model name:CPU的型号名称,这里对应的是Cortex-A55;

  • Stepping:用来标识一系列CPU的设计或生产制造版本数据,步进的版本会随着这一系列CPU生产工艺的改进、BUG的解决或特性的增加而改变;

  • CPU min MHz,CPU max MHz:CPU所支持的最小、最大的频率,系统运行过程会根据实际情况,来调整CPU的工作频率,但不会超过最大支持频率;

  • BogoMIPS:MIPS是millions of instructions per second(百万条指令每秒)的缩写,该值是通过函数计算出来的,只能用来粗略计算处理器的性能,并不是十分精确。

  • Flags:用来表示CPU特征的参数。

1.4. linux SMP启动过程

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

虽然SMP模式所有CPU核心共享内存和外设资源,但是在启动阶段它们的地位是不同的,其中core0是主CPU(引导处理器),其他是从CPU(也叫辅处理器), 引导cpu负责执行启动加载程序如uboot,以及初始化内核,系统初始化完成之后主core会启动从处理器。

主处理器启动从处理器的方式有多种,arm处理器一般使用spin-table(自旋表)和psci(Power State Coordination Interface)两种方式,arm64使用psci较多。

Linux内核编译时,CONFIG_SMP配置项用于控制内核是否支持SMP,默认是#define CONFIG_SMP 1。 linux系统中SMP模式的启动流程如图所示,复位之后,CPU0执行ROM code中的代码, 此后ROM code引导CPU0去执行Bootloader(包括TF-A和uboot)的代码和内核代码,

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

内核打印第一行信息

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

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

struct cpumask __cpu_isolated_mask __read_mostly;
EXPORT_SYMBOL(__cpu_isolated_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;

  • __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节点和/psci节点(位于arch/arm64/boot/dts/rockchip/rk3568.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
cpus {
        #address-cells = <2>;
        #size-cells = <0>;

        cpu0: cpu@0 {
                device_type = "cpu";
                compatible = "arm,cortex-a55";
                reg = <0x0 0x0>;
                enable-method = "psci";
                clocks = <&scmi_clk 0>;
                operating-points-v2 = <&cpu0_opp_table>;
                cpu-idle-states = <&CPU_SLEEP>;
                #cooling-cells = <2>;
                dynamic-power-coefficient = <187>;
        };

        cpu1: cpu@100 {
                device_type = "cpu";
                compatible = "arm,cortex-a55";
                reg = <0x0 0x100>;
                enable-method = "psci";
                clocks = <&scmi_clk 0>;
                operating-points-v2 = <&cpu0_opp_table>;
                cpu-idle-states = <&CPU_SLEEP>;
        };

        cpu2: cpu@200 {
                device_type = "cpu";
                compatible = "arm,cortex-a55";
                reg = <0x0 0x200>;
                enable-method = "psci";
                clocks = <&scmi_clk 0>;
                operating-points-v2 = <&cpu0_opp_table>;
                cpu-idle-states = <&CPU_SLEEP>;
        };

        cpu3: cpu@300 {
                device_type = "cpu";
                compatible = "arm,cortex-a55";
                reg = <0x0 0x300>;
                enable-method = "psci";
                clocks = <&scmi_clk 0>;
                operating-points-v2 = <&cpu0_opp_table>;
                cpu-idle-states = <&CPU_SLEEP>;
        };

        /*............*/
};

psci {
        compatible = "arm,psci-1.0"; //匹配PSCI,使用psci_0_2_init
        method = "smc";              //通过smc指令
};

该cpus节点描述了当前硬件上存在四个CPU,分别是CPU0、CPU1、CPU2和CPU3,内核代码通过解析该节点,便可以获得当前系统的CPU核心个数, 其中enable-method属性是psci,说明从处理器启动是通过psci方式。

psci节点说明了使用的版本和陷入el3异常等级的方法,版本是使用PSCI v0.2,这我们从启动信息也可以看出:

[    0.000000] psci: probing for conduit method from DT.
[    0.000000] psci: PSCIv1.1 detected in firmware.
[    0.000000] psci: Using standard PSCI v0.2 function IDs
[    0.000000] psci: Trusted OS migration not required
[    0.000000] psci: SMC Calling Convention v1.2

另外,我们可以看到cpus节点中还包含了“operating-points-v2”属性,指向了cpu0_opp_table节点,该节点是用于配置CPU核心支持的频率等。

/cpu0_opp_table节点(位于arch/arm64/boot/dts/rockchip/rk3568.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
cpu0_opp_table: cpu0-opp-table {
                compatible = "operating-points-v2";
                opp-shared;

                mbist-vmin = <825000 900000 950000>;
                nvmem-cells = <&cpu_leakage>, <&core_pvtm>, <&mbist_vmin>;
                nvmem-cell-names = "leakage", "pvtm", "mbist-vmin";
                rockchip,pvtm-voltage-sel = <
                        0        84000   0
                        84001    91000   1
                        91001    100000  2
                >;
                rockchip,pvtm-freq = <408000>;
                rockchip,pvtm-volt = <900000>;
                rockchip,pvtm-ch = <0 5>;
                rockchip,pvtm-sample-time = <1000>;
                rockchip,pvtm-number = <10>;
                rockchip,pvtm-error = <1000>;
                rockchip,pvtm-ref-temp = <40>;
                rockchip,pvtm-temp-prop = <26 26>;
                rockchip,thermal-zone = "soc-thermal";
                rockchip,temp-hysteresis = <5000>;
                rockchip,low-temp = <0>;
                rockchip,low-temp-adjust-volt = <
                        /* MHz    MHz    uV */
                        0      1608   75000
                >;

                opp-408000000 {
                        opp-hz = /bits/ 64 <408000000>;
                        opp-microvolt = <850000 850000 1150000>;
                        opp-microvolt-L0 = <850000 850000 1150000>;
                        opp-microvolt-L1 = <825000 825000 1150000>;
                        opp-microvolt-L2 = <825000 825000 1150000>;
                        clock-latency-ns = <40000>;
                };
                opp-600000000 {
                        opp-hz = /bits/ 64 <600000000>;
                        opp-microvolt = <850000 825000 1150000>;
                        opp-microvolt-L0 = <850000 850000 1150000>;
                        opp-microvolt-L1 = <825000 825000 1150000>;
                        opp-microvolt-L2 = <825000 825000 1150000>;
                        clock-latency-ns = <40000>;
                };
                opp-816000000 {
                        opp-hz = /bits/ 64 <816000000>;
                        opp-microvolt = <850000 850000 1150000>;
                        opp-microvolt-L0 = <850000 850000 1150000>;
                        opp-microvolt-L1 = <825000 825000 1150000>;
                        opp-microvolt-L2 = <825000 825000 1150000>;
                        clock-latency-ns = <40000>;
                        opp-suspend;
                };
                /*.........*/
};

OPP驱动会根据芯片内部的版本号,来设置CPU核心的工作电压和工作频率。

内核了解当前系统的CPU0相关信息,接下来就应该让其他CPU纳入内核的管理,开始卖力干活了。 在SMP初始化之前,内核需要初始化present_mask,之后便根据present_mask中的内容来打开对应的CPU了。 具体实现方式是将possible_mask的值复制到present_mask中。

初始化present_mask(位于arch/arm64/kernel/smp.c)
 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void __init smp_prepare_cpus(unsigned int max_cpus)
{
        int err;
        unsigned int cpu;
        unsigned int this_cpu;

        init_cpu_topology();   //解析当前/cpus节点,获得CPU拓扑结构

        this_cpu = smp_processor_id();
        store_cpu_topology(this_cpu);  //没有从DTS中成功获取CPU topology的情况下,从ARM64的寄存器获取相关信息
        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;

        /*
        * Initialise the present map (which describes the set of CPUs
        * actually populated at the present time) and release the
        * secondaries from the bootloader.
        */
        for_each_possible_cpu(cpu) {

                per_cpu(cpu_number, cpu) = cpu;

                if (cpu == smp_processor_id())
                        continue;

                if (!cpu_ops[cpu])
                        continue;

                err = cpu_ops[cpu]->cpu_prepare(cpu);
                if (err)
                        continue;

                set_cpu_present(cpu, true); //设置__cpu_present_mask
                numa_store_cpu_info(cpu);
        }
}
函数smp_init和cpu_up(位于内核文件kernel/smp.c,kernel/cpu.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* Called by boot processor to activate the rest. */
void __init smp_init(void)
{
        /* 省略部分代码 */
        cpuhp_threads_init(); //为每个core都创建一个"cpuhp/%u"内核线程等,当前cpu的"cpuhp/%u"线程:"cpuhp/0"

        pr_info("Bringing up secondary CPUs ...\n");
        /* FIXME: This should be done in userspace --RR */
        for_each_present_cpu(cpu) {
                if (num_online_cpus() >= setup_max_cpus)
                        break;
                if (!cpu_online(cpu))
                        cpu_up(cpu);
        }
        /*...........*/
}

int cpu_up(unsigned int cpu)
{
        return do_cpu_up(cpu, CPUHP_ONLINE); //CPUHP_ONLINE表示的一个参数cpu,唤醒要达到的状态
}

smp_init()函数会遍历present_mask中的cpu,如果cpu没有online,那么调用cpu_up()函数,该函数是SMP启动过程最关键的一环, 后面调用PSCI的操作函数cpu_psci_ops,最终调用smc,陷入el3之后,请求开核服务,可以启动对应的从处理器,最终从处理器回到内核(el3->el1), 执行secondary_entry处指令(在arch/arm64/kernel/head.S),从处理器启动完成。

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_CREATE_THREADS]= {
                .name                   = "threads:prepare",
                .startup.single         = smpboot_create_threads,   //回调函数
                .teardown.single        = NULL,
                .cant_stop              = true,
        },
        [CPUHP_PERF_PREPARE] = {
                .name                   = "perf:prepare",
                .startup.single         = perf_event_init_cpu,
                .teardown.single        = perf_event_exit_cpu,
        },
        /*...............*/
};

下面我们看一下OFFLINE->BRINGUP_CPU的这个过程,前面的do_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
11
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();
        cpu_up_down_serialize_trainwrecks(tasks_frozen);
        return ret;
}

cpuhp_up_callbacks函数的作用,就如函数名称一样,是用来调用cpuhp_hp_states数组中的提供的初始化函数。

cpuhp_up_callbacks函数(位于文件kernel/cpu.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核启动成功。