linux之调度管理(6)-SMP cpu 的spin-table启动

时间:2024-11-19 21:48:57

一、smp启动总体介绍

        soc在启动阶段除了一些特殊情况外(如为了加快启动速度,在bl2阶段通过并行加载方式同时加载bl31、bl32和bl33镜像),一般都没有并行化需求。因此只需要一个cpu执行启动流程即可,这个cpu被称为primary cpu,而其它的cpu则统一被称为secondary cpu。为了防止secondary cpu在启动阶段的执行,它们在启动时必须要被设置为一个特定的状态。

当primary cpu完成操作系统初始化,调度系统开始工作后,就可以通过一定的机制启动secondary cpu。显然secondary cpu不再需要执行启动流程代码,而只需直接跳转到内核中执行即可。

故其启动的关键是如何将内核入口地址告知secondary cpu,以使其能跳转到正确的执行位置。为此,aarch64架构实现了两种不同的启动方式,spin-table和psci。其中spin-table方式非常简单,但其只能被用于secondary cpu启动,功能比较单一。

随着aarch64架构电源管理需求的增加(如cpu热插拔、cpu idle等),arm设计了一套标准的电源管理接口协议psci。该协议可以支持所有cpu相关的电源管理接口,而且由于电源相关操作是系统的关键功能,为了防止其被攻击,该协议将底层相关的实现都放到了secure空间,从而可提高系统的安全性。

一般主处理器启动从处理器有以下三种:

(1).ACPI

(2).spin-table

(3).PSCI

第一种ACPI是高级配置与电源接口(Advanced Configuration and Power Interface)一般在x86平台用的比较多,而后两种spin-table(自旋表)和PSCI(电源状态协调协议 Power State Coordination)会在arm平台上使用,本系列主要讲解后两种。主要内容分为上下两篇如下。

二、spin-table启动

spin-table启动流程的示意图如下:

         芯片上电后primary cpu开始执行启动流程,而secondary cpu则将自身设置为WFE睡眠状态,并且为内核准备了一块内存,用于填写secondary cpu的入口地址。uboot负责将这块内存的地址写入devicetree中,当内核初始化完成,需要启动secondary cpu时,就将其内核入口地址写到那块内存中,然后唤醒cpu。

    secondary cpu被唤醒后,检查该内存的内容,确认内核已经向其写入了启动地址,就跳转到该地址执行启动流程。

2.1 secondary cpu初始化状态设置

uboot启动时,secondary cpu会通过以下流程进入wfe状态(arch/arm/cpu/armv8/start.S):

#if defined(CONFIG_ARMV8_SPIN_TABLE) && !defined(CONFIG_SPL_BUILD)
	branch_if_master x0, x1, master_cpu                  (1)
	b	spin_table_secondary_jump                        (2)
	…
master_cpu:                                              (3)
	bl	_main

(1)若当前cpu为primary cpu,则跳转到step 3,继续执行启动流程。其中cpu id是通过mpidr区分的,而启动流程中哪个cpu作为primary cpu可以任意指定。当指定完成后,此处就可以根据其身份确定相应的执行流程

(2)若当前cpu为slave cpu,则执行spin流程。它是由spin_table_secondary_jump(注意:之前执行的代码,设置的寄存器都是各cpu独立的寄存器)函数实现的(arch/arm/cpu/armv8/start.S)。以下为其代码实现:

ENTRY(spin_table_secondary_jump)
.globl spin_table_reserve_begin
spin_table_reserve_begin:
0:	wfe                                           (1)
	ldr	x0, spin_table_cpu_release_addr       (2)
	cbz	x0, 0b                                (3)
	br	x0                                    (4)
.globl spin_table_cpu_release_addr                    (5)
	.align	3
spin_table_cpu_release_addr:
	.quad	0
.globl spin_table_reserve_end
spin_table_reserve_end:
ENDPROC(spin_table_secondary_jump)

(1)secondary cpu当前没有事情要做,因此执行wfe指令进入睡眠模式,以降低功耗

(2)spin_table_cpu_release_addr将由uboot传递给内核,根据step 5的定义可知,其长度为8个字节,在64位系统中正好可以保存一个指针。而它的内容在启动时会被初始化为0,当内核初始化完成,在启动secondary cpu之前会将其入口地址写到该位置

(3)当secondary cpu从wfe状态唤醒后,会校验内核是否在spin_table_cpu_release_addr处填写了它的启动入口。若未填写,即为0,则其会继续进入wfe状态

(4)若内核填入了启动地址,则其直接跳转到该地址开始执行内核初始化流程

2.2 spin_table_cpu_release_addr的传递

        由于在armv8架构下,uboot只能通过devicetree向内核传递参数信息,因此当其开启了CONFIG_ARMV8_SPIN_TABLE配置选项后,就需要在适当的时候将该值写入devicetree中。我们知道uboot一般通过bootm命令启动操作系统(aarch64支持的booti命令,其底层实现与bootm相同),因此在bootm中会执行一系列启动前的准备工作,其中就包括将spin-table地写入devicetree的工作。以下其执行流程图:

 spin_table_update_dt的代码实现如下:

// arch/arm/cpu/armv8/spin_table.c:

   11 int spin_table_update_dt(void *fdt)
   12 {
   13         int cpus_offset, offset;
   14         const char *prop;
   15         int ret;
   16         unsigned long rsv_addr = (unsigned long)&spin_table_reserve_begin;
   17         unsigned long rsv_size = &spin_table_reserve_end -
   18                                                 &spin_table_reserve_begin;
   19   //获取设备树的cpus节点的偏移
   20         cpus_offset = fdt_path_offset(fdt, "/cpus");
   21         if (cpus_offset < 0)
   22                 return -ENODEV;
   23   //寻找每一个device_type属性为cpu的节点
   24         for (offset = fdt_first_subnode(fdt, cpus_offset);
   25         ¦    offset >= 0;
   26         ¦    offset = fdt_next_subnode(fdt, offset)) {
   27                 prop = fdt_getprop(fdt, offset, "device_type", NULL);
   28                 if (!prop || strcmp(prop, "cpu"))
   29                         continue;
   30 
   31                 /*
   32                 ¦* In the first loop, we check if every CPU node specifies
   33                 ¦* spin-table.  Otherwise, just return successfully to not
   34                 ¦* disturb other methods, like psci.
   35                 ¦*///获得enable-method属性,比较属性值是否为 "spin-table"(即是使用自旋表启动方式)
   36                 prop = fdt_getprop(fdt, offset, "enable-method", NULL);
   37                 if (!prop || strcmp(prop, "spin-table"))
   38                         return 0;
   39         }
40  
   41         for (offset = fdt_first_subnode(fdt, cpus_offset);
   42         ¦    offset >= 0;
   43         ¦    offset = fdt_next_subnode(fdt, offset)) {
         //找到cpu节点
   44                 prop = fdt_getprop(fdt, offset, "device_type", NULL);
   45                 if (!prop || strcmp(prop, "cpu"))
   46                         continue;
   47     //重点:设置cpu-release-addr属性值为spin_table_cpu_release_addr的地址!
   48                 ret = fdt_setprop_u64(fdt, offset, "cpu-release-addr",
   49                                 (unsigned long)&spin_table_cpu_release_addr);
   50                 if (ret)
   51                         return -ENOSPC;
   52         }
   53   //设置设备树的保留内存 :添加一个内存区域为16和17行描述的地址范围(这是物理地址)
   54         ret = fdt_add_mem_rsv(fdt, rsv_addr, rsv_size);
   55         if (ret)
   56                 return -ENOSPC;
   57 
   58         printf("   Reserved memory region for spin-table: addr=%lx size=%lx\n",
   59         ¦      rsv_addr, rsv_size);
   60 
   61         return 0;
   62 }

其实,他做的工作主要有两个:

1.将即将供内核使用的设备树的cpu节点的cpu-release-addr属性设置为spin_table_cpu_release_addr的地址(这个地址也就是cpu的释放地址)。

2.将spin_table_reserve_begin到spin_table_reserve_end符号描述的地址范围添加到设备树的保留内存中。

实际上保留的是spin_table_secondary_jump汇编函数的指令代码段和spin_table_cpu_release_addr地址内存,当然保留是为了在内核中不被内存管理使用,这样这段物理内存的数据不会被覆盖丢失。注意:spin_table_cpu_release_addr地址处被初始化为0(上面汇编19行)。

先来看一下一个使用自旋表作为启动方式的平台设备树cpu节点:

arch/arm64/boot/dts/xxx.dtsi:

cpu@0 {
        device_type = "cpu";
        compatible = "arm,armv8";
        reg = <0x0 0x000>;
        enable-method = "spin-table";
        cpu-release-addr = <0x1 0x0000fff8>;
};

可以发现启动方法为spin-table,释放地址初始化为0x10000fff8。

那么什么时候释放地址spin_table_cpu_release_addr 的内容不是0呢?

那么我们得回到主处理器流程上来:主处理器设置好了设备树,传递给内核设备树地址之后就要启动内核,启动内核之后,执行初始化工作,执行如下路径:

setup_arch //arch/arm64/kernel/setup.c:

        ->smp_init_cpus //arch/arm64/kernel/smp.c

                ->**smp_cpu_setup**

                        ->cpu_ops[cpu]

                                ->cpu_init(cpu)

                                        ->smp_spin_table_ops

                                                ->cpu_init //arch/arm64/kernel/cpu_ops.c

                                                        ->**smp_spin_table_cpu_init**//arch/arm64/kernel/smp_spin_table.c

我们来看下smp_spin_table_cpu_init函数:

//kernel/arch/arm64/kernel/smp_spin_table.c

static phys_addr_t cpu_release_addr[NR_CPUS];

static int smp_spin_table_cpu_init(unsigned int cpu)
{
        struct device_node *dn;
        int ret;

        dn = of_get_cpu_node(cpu, NULL);
        if (!dn)
                return -ENODEV;

        /*
         * Determine the address from which the CPU is polling.
         */
        ret = of_property_read_u64(dn, "cpu-release-addr",
                                   &cpu_release_addr[cpu]);
        if (ret)
                pr_err("CPU %d: missing or invalid cpu-release-addr property\n",
                       cpu);

        of_node_put(dn);

        return ret;
}

可以发现,函数读取设备树的cpu-release-addr属性值到cpu_release_addr[cpu]中,cpu_release_addr变量是个NR_CPUS个元素的数组,每个处理器占用一个元素,其实也就是将之前保存的spin_table_reserve_begin符号的物理地址保存到这个变量中。

现在还没有看到设置释放地址的地方,继续往下看:

主处理器继续执行如下路径:

start_kernel

->arch_call_rest_init

        ->rest_init

                ->kernel_init,

                        ->kernel_init_freeable

                                ->**smp_prepare_cpus** //arch/arm64/kernel/smp.c

                                        ->cpu_ops[cpu]

                                                ->cpu_prepare

                                                        ->smp_spin_table_ops

                                                                ->cpu_init //arch/arm64/kernel/cpu_ops.c

                                                                        ->**smp_spin_table_cpu_prepare**//arch/arm64/kernel/smp_spin_table.c

smp_spin_table_cpu_prepare:

static int smp_spin_table_cpu_prepare(unsigned int cpu)
{
        __le64 __iomem *release_addr;

        if (!cpu_release_addr[cpu])
                return -ENODEV;

        /*
         * The cpu-release-addr may or may not be inside the linear mapping.
         * As ioremap_cache will either give us a new mapping or reuse the
         * existing linear mapping, we can use it to cover both cases. In
         * either case the memory will be MT_NORMAL.
         */
           //将释放地址的物理地址映射为虚拟地址
        release_addr = ioremap_cache(cpu_release_addr[cpu],
                                     sizeof(*release_addr));
        if (!release_addr)
                return -ENOMEM;

        /*
         * We write the release address as LE regardless of the native
         * endianness of the kernel. Therefore, any boot-loaders that
         * read this address need to convert this address to the
         * boot-loader's endianness before jumping. This is mandated by
         * the boot protocol.
         */
         //将secondary_holding_pen地址写到释放地址处
        writeq_relaxed(__pa_symbol(secondary_holding_pen), release_addr);
        __flush_dcache_area((__force void *)release_addr,
                            sizeof(*release_addr));//刷数据cache

        /*
         * Send an event to wake up the secondary CPU.
         */
        sev();//发送事件唤醒从处理器

        iounmap(release_addr);//解除映射

        return 0;
}

上面函数主要做两点:

1、CPU 的释放地址处写入secondary_holding_pen的地址,由于获得的内核符号是虚拟地址所以转化为物理地址写到释放地址处。

2.唤醒处于wfe状态的从处理器。

再次回到从处理器睡眠等待的地方:在汇编函数spin_table_secondary_jump中唤醒后执行,wfe的下几行指令,判断spin_table_cpu_release_addr地址处的内容是否为0,这个时候由于主处理器往这个地址写入了释放地址,所有会执行15行指令,跳转到secondary_holding_pen处执行,请注意:这个地址是物理地址,而且从处理器还没有开启mmu,所以从处理器还没有进入虚拟地址的世界。

获得释放地址后的从处理器,犹如脱缰的野马,唤醒后直接进入了内核的世界去执行指令,多么的残暴,来到了如下的汇编函数:

// arch/arm64/kernel/head.S:

691         /*
692         ¦* This provides a "holding pen" for platforms to hold all secondary
693         ¦* cores are held until we're ready for them to initialise.
694         ¦*/
695 ENTRY(secondary_holding_pen)
696         bl      el2_setup                       // Drop to EL1, w0=cpu_boot_mode
697         bl      set_cpu_boot_mode_flag
698         mrs     x0, mpidr_el1
699         mov_q   x1, MPIDR_HWID_BITMASK
700         and     x0, x0, x1
701         adr_l   x3, secondary_holding_pen_release
702 pen:    ldr     x4, [x3]
703         cmp     x4, x0
704         b.eq    secondary_startup    
705         wfe   
706         b       pen
707 ENDPROC(secondary_holding_pen)

小弟CPU,在这个函数中又有一层关卡:689行到701行 判断是否secondary_holding_pen_release被设置为了从处理器的编号,如果设置的不是我的编号,则我再次进入705行执行wfe睡眠等待,行吧,那就等待啥时候主处理器来将secondary_holding_pen_release设置为我的处理器编号吧。那么何时会设置呢?答案是最终要启动从处理器的时候。

再次回到主处理器的处理流程,上面主处理器执行到了smp_prepare_cpus之后,继续往下执行,代码路径如下:

start_kernel

->arch_call_rest_init

        ->rest_init

                ->kernel_init,

        ->kernel_init_freeable

           ->smp_prepare_cpus //arch/arm64/kernel/smp.c

              ->smp_init //kernel/smp.c (这是从处理器启动的函数)

                 ->cpu_up

                     ->do_cpu_up

                        ->_cpu_up

                          ->cpuhp_up_callbacks

                              ->cpuhp_invoke_callback

                                  ->cpuhp_hp_states[CPUHP_BRINGUP_CPU]

                                      ->**bringup_cpu**

                                          ->__cpu_up //arch/arm64/kernel/smp.c

                                                ->boot_secondary

                                                   ->cpu_ops[cpu]

                                                        ->cpu_boot(cpu)

                                                                ->smp_spin_table_ops.cpu_boot //arch/arm64/kernel/cpu_ops.c

                                                                        ->smp_spin_table_cpu_boot //arch/arm64/kernel/smp_spin_table.c

看smp_spin_table_cpu_boot函数:kernel/arch/arm64/kernel/smp_spin_table.c

const struct cpu_operations smp_spin_table_ops = {
        .name           = "spin-table",
        .cpu_init       = smp_spin_table_cpu_init,
        .cpu_prepare    = smp_spin_table_cpu_prepare,
        .cpu_boot       = smp_spin_table_cpu_boot,
};


static int smp_spin_table_cpu_boot(unsigned int cpu)
{
        /*
         * Update the pen release flag.
         */
        write_pen_release(cpu_logical_map(cpu));

        /*
         * Send an event, causing the secondaries to read pen_release.
         */
        sev();

        return 0;
}

可以看到这里将从处理器编号写到了secondary_holding_pen_release中,然后唤醒从处理器,从处理器再次欢快的执行,最后执行到secondary_startup,来做从处理器的初始化工作(如设置mmu,异常向量表等),最终从处理器还是处于wfe状态,但是这个时候从处理器已经具备了执行进程的能力,可以用来调度进程,触发中断等,和主处理器有着相同的地位。

整体的CPU启动流程,如图:

2.3 总结

                spin-table方式的多核启动方式,顾名思义在于自旋,主处理器和从处理器上电都会启动,主处理器执行uboot畅通无阻,从处理器在spin_table_secondary_jump处wfe睡眠,主处理器通过修改设备树的cpu节点的cpu-release-addr属性为spin_table_cpu_release_addr,这是从处理器的释放地址所在的地方,主处理器进入内核后,会通过smp_prepare_cpus函数调用spin-table 对应的cpu操作集的cpu_prepare方法从而在smp_spin_table_cpu_prepare函数中设置从处理器的释放地址为secondary_holding_pen这个内核函数,然后通过sev指令唤醒从处理器,从处理器继续从secondary_holding_pen开始执行(从处理器来到了内核的世界),发现secondary_holding_pen_release不是自己的处理编号,然后通过wfe继续睡眠,当主处理器完成了大多数的内核组件的初始化之后,调用smp_init来来开始真正的启动从处理器,最终调用spin-table 对应的cpu操作集的cpu_boot方法从而在smp_spin_table_cpu_boot将需要启动的处理器的编号写入secondary_holding_pen_release中,然后再次sev指令唤醒从处理器,从处理器得以继续执行(设置自己异常向量表,初始化mmu等),最终在idle线程中执行wfi睡眠。其他从处理器也是同样的方式启动起来,同样最后进入各种idle进程执行wfi睡眠,主处理器继续往下进行内核初始化,直到启动init进程,后面多个处理器都被启动起来,都可以调度进程,多进程还会被均衡到多核。

三、启动secondary cpu

        内核在启动secondary cpu之前当然需要为其准备好执行环境,因为内核中cpu最终都将由调度器管理,故此时调度子系统应该要初始化完成。同时cpu启动完成转交给调度器之前,并没有实际的业务进程,而我们知道内核中cpu在空闲时会执行idle进程。因此,在其启动之前需要为每个cpu初始化一个idle进程。

另外,由于将一个cpu通过热插拔方式移除后,再次启动该cpu的流程,与secondary cpu的启动流程是相同的,因此内核复用了cpu hotplug框架用于启动secondary cpu。而内核为每个cpu都分配了一个独立的hotplug线程,用于执行本cpu相关的热插拔流程。为此,内核通过以下流程执行secondary cpu启动操作:

3.1 idle进程初始化

以下代码为每个非boot cpu分配一个idle进程:

//kernel/kernel/smpboot.c

void __init idle_threads_init(void)
{
        unsigned int cpu, boot_cpu;

        boot_cpu = smp_processor_id();

        for_each_possible_cpu(cpu) {
                if (cpu != boot_cpu)
                        idle_init(cpu);
        }
}

(1)遍历系统中所有的possible cpu

(2)若该cpu为secondary cpu,则为其初始化一个idle进程

3.2 hotplug线程初始化

以下代码为每个cpu初始化一个hotplug线程:

// kernel/kernel/cpu.c

void __init cpuhp_threads_init(void)
{
        BUG_ON(smpboot_register_percpu_thread(&cpuhp_threads));
        kthread_unpark(this_cpu_read(cpuhp_state.thread));
}

其中线程的描述结构体定义如下:

static struct smp_hotplug_thread cpuhp_threads = {
        .store                  = &cpuhp_state.thread,//用于保存cpu上的task struct指针
        .create                 = &cpuhp_create, //线程创建时调用的回调
        .thread_should_run      = cpuhp_should_run, //该回调用于获取线程是否需要退出标志
        .thread_fn              = cpuhp_thread_fun,cpu hotplug主函数,执行实际的hotplug操作
        .thread_comm            = "cpuhp/%u",//该线程的线程名
        .selfparking            = true,//用于设置线程创建完成后,是否将其设置为park状态
};

3.3 hotplug回调线程唤醒

内核使用以下流程唤醒特定cpu的hotplug线程,用于执行实际的cpu启动流程:

        由于cpu启动时需要与一系列模块交互以执行相应的准备工作,为此内核为其定义了一组hotplug状态,用于表示cpu在启动或关闭时分别需要执行的流程。以下为个阶段状态定义示例(由于该数组较长,故只截了一小段):

//kernel/kernel/cpu.c

static struct cpuhp_step cpuhp_hp_states[] = {
	[CPUHP_OFFLINE] = {
		.name			= "offline",
		.startup.single		= NULL,
		.teardown.single	= NULL,
	},
	…
	[CPUHP_BRINGUP_CPU] = {
		.name			= "cpu:bringup",
		.startup.single		= bringup_cpu,
		.teardown.single	= finish_cpu,
		.cant_stop		= true,
		}
…
	[CPUHP_ONLINE] = {
		.name			= "online",
		.startup.single		= NULL,
		.teardown.single	= NULL,
	},
}
  • 以上每个阶段都可包含startup.single和teardown.single两个回调函数,分别表示cpu启动和关闭时需要执行的流程。其中在cpu启动时,将会从CPUHP_OFFLINE状态开始,依次执行各个阶段的startup.single回调函数。其中CPUHP_BRINGUP_CPU及之前的阶段都在secondary cpu启动之前执行。
  • 而CPUHP_BRINGUP_CPU阶段的回调函数bringup_cpu,会实际触发secondary cpu的启动流程。它将通过cpu_ops接口调用spin-table函数,启动secondary cpu,并等待其启动完成。
  • 当secondary cpu启动完成后,将唤醒hotplug线程,其将继续执行CPUHP_BRINGUP_CPU之后阶段相关的回调函数。

3.3 cpu操作函数

        cpu_ops函数由bringup_cpu调用,以触发secondary cpu启动。它是根据设备树中解析出的enable-method属性确定的。

int __init init_cpu_ops(int cpu)
{
	const char *enable_method = cpu_read_enable_method(cpu);   (1)
			…
	cpu_ops[cpu] = cpu_get_ops(enable_method);                 (2)
		…
}

(1)获取该cpu enable-method属性的值

(2)根据其enable-method获取其对应的cpu_ops回调

其中spin-table启动方式的回调如下:

const struct cpu_operations smp_spin_table_ops = {
	.name		= "spin-table",
	.cpu_init	= smp_spin_table_cpu_init,
	.cpu_prepare	= smp_spin_table_cpu_prepare,
	.cpu_boot	= smp_spin_table_cpu_boot,
}

3.4 触发secondary cpu启动

 以上流程都准备完成后,触发secondary cpu启动就非常简单了。只需调用其cpu_ops回调函数,向其对应的spin_table_cpu_release_addr位置写入secondary cpu入口地址即可。以下为其调用流程:

其中smp_spin_table_cpu_boot的实现如下:

static int smp_spin_table_cpu_boot(unsigned int cpu)
{
	write_pen_release(cpu_logical_map(cpu));    (1)
	sev();                                      (2)

	return 0;
}

(1)向给定地址写入内核entry

(2)通过sev指令唤醒secondary cpu启动

此后,该线程将等待cpu启动完成,并在完成后将其设置为online状态

3.5 secondary cpu执行流程

        aarch64架构secondary cpu的内核入口函数为secondary_entry(arch/arm64/kernel/head.S),以下为其执行主流程:

由于其底层相关初始化流程与primary cpu类似,因此此处不再介绍。我们这里主要看一下它是如何通过secondary_start_kernel启动idle线程的:

//kernel/arch/arm64/kernel/smp.c

/*
 * This is the secondary CPU boot entry.  We're using this CPUs
 * idle thread stack, but a set of temporary page tables.
 */
asmlinkage notrace void secondary_start_kernel(void)
{
        u64 mpidr = read_cpuid_mpidr() & MPIDR_HWID_BITMASK;
        struct mm_struct *mm = &init_mm;
        const struct cpu_operations *ops;
        unsigned int cpu;

        cpu = task_cpu(current);
        set_my_cpu_offset(per_cpu_offset(cpu));

        /*
         * All kernel threads share the same mm context; grab a
         * reference and switch to it.
         */
        mmgrab(mm);
        current->active_mm = mm;----------------------(1)

        /*
         * TTBR0 is only used for the identity mapping at this stage. Make it
         * point to zero page to avoid speculatively fetching new entries.
         */
        cpu_uninstall_idmap();--------------------------(2)

        if (system_uses_irq_prio_masking())
                init_gic_priority_masking();

        rcu_cpu_starting(cpu);
        trace_hardirqs_off();

        /*
         * If the system has established the capabilities, make sure
         * this CPU ticks all of those. If it doesn't, the CPU will
         * fail to come online.
         */
        check_local_cpu_capabilities();

        ops = get_cpu_ops(cpu);
        if (ops->cpu_postboot)
                ops->cpu_postboot();--------------------(3)

        /*
         * Log the CPU info before it is marked online and might get read.
         */
        cpuinfo_store_cpu();

        /*
         * Enable GIC and timers.
         */
        notify_cpu_starting(cpu);

        ipi_setup(cpu);

        store_cpu_topology(cpu);
        numa_add_cpu(cpu);

        /*
         * OK, now it's safe to let the boot CPU continue.  Wait for
         * the CPU migration code to notice that the CPU is online
         * before we continue.
         */
        pr_info("CPU%u: Booted secondary processor 0x%010lx [0x%08x]\n",
                                         cpu, (unsigned long)mpidr,
                                         read_cpuid_id());
        update_cpu_boot_status(CPU_BOOT_SUCCESS);
        set_cpu_online(cpu, true);-------------------------(4)
        complete(&cpu_running);-----------------------------(5)

        local_daif_restore(DAIF_PROCCTX);

        /*
         * OK, it's off to the idle thread for us
         */
        cpu_startup_entry(CPUHP_AP_ONLINE_IDLE);---------(6)
}

(1)由于内核线程并没有用于地址空间,因此其active_mm通常指向上一个用户进程的地址空间。而cpu初始化时,由于之前并没有运行过用户进程,因此将其初始化为init_mm

(2)idmap地址映射仅仅是用于mmu使能时地址空间的平滑切换,在mmu使能完成后已经没有作用。更进一步,由于idmap页表所使用的ttbr0_elx页表基地址寄存器,正常情况下是用于用户空间页表的,在调度器接管该cpu之前也必须要将其归还给用户空间

(3)执行cpu_postboot回调

(4)由secondary cpu已经启动成功,故将其设置为online状态

(5)唤醒cpu hotplug线程

(6)让cpu执行idle线程,其代码实现如下:

void cpu_startup_entry(enum cpuhp_state state)
{
	arch_cpu_idle_prepare();
	cpuhp_online_idle(state);
	while (1)
		do_idle();
}

至此,cpu已经启动完成,并开始执行idle线程了。最后当然是要通知调度器,将该cpu的管理权限移交给调度器了。它是通过cpu hotplug的以下回调实现的:

static struct cpuhp_step cpuhp_hp_states[] = {
…
[CPUHP_AP_SCHED_STARTING] = {
		.name			= "sched:starting",
		.startup.single		= sched_cpu_starting,
		.teardown.single	= sched_cpu_dying,
}
…
}

以下为该函数的实现:

int sched_cpu_starting(unsigned int cpu)
{
…
sched_rq_cpu_starting(cpu);        (1)
sched_tick_start(cpu);             (2)
…
}

(1)用于初始化负载均衡相关参数,此后该cpu就可以在其后的负载均衡流程中拉取进程

(2)tick时钟是内核调度器的脉搏,启动了该时钟之后,cpu就会在时钟中断中执行调度操作,从而让cpu参与到系统的调度流程中.