TaterLi 个人博客

[内核学习]ARM64多核启动

上电那一刻,一颗 ARM SoC 上通常只有一个核活着.其它核是谁叫醒的,醒来第一件事干什么,跑起来之后又怎么下线休息,这条线串起来就是这篇.里头还会讲到 PSCI 这层和 firmware 的分权,以及 cpuhp 状态机,全是驱动工程师天天碰但很少往下看的东西.

这篇回答几件事:

先把权力分清.老一代 SoC 内核自己拿寄存器开核,写个 pen address 等副核跳进来,又土又容易和 secure world 打架.现代 ARM 把这活收走了:核的电源,复位,时钟全归 EL3 的 firmware(TF-A 这类)管,内核想要核,发请求.这套接口叫 PSCI,Power State Coordination Interface.

板上设备树里就这么一个节点:

/psci {
	compatible = "arm,psci-1.0";
	method = "smc";
};

method=smc 说的是请求怎么发:执行 SMC 指令,陷入 EL3.冷启动日志能看到内核的探测过程:

Booting Linux on physical CPU 0x0000000000 [0x410fd034]
psci: probing for conduit method from DT.
psci: PSCIv1.1 detected in firmware.
psci: Using standard PSCI v0.2 function IDs
psci: SMC Calling Convention v1.5

第一行说主核是 MPIDR 0x0 的核,MIDR 0x410fd034,解码出来是 Cortex-A53 r0p4.主核不需要谁叫,bootloader 直接把控制权给它.它醒来干活,干到 start_kernel 后期调 smp_init,开始张罗其它核:遍历 DT 的 cpus 节点(板上六个节点,enable-method 全是 "psci"),对每个核调 __cpu_up,arch/arm64/kernel/smp.c:111.

__cpu_up 的活分两半.自己这半:给副核准备一个 idle 线程当它的第一个任务,更新 boot status 让副核知道门开了.对方那半:get_cpu_ops(cpu) 拿到 PSCI 这套操作,调 cpu_boot,它最终就是一条 SMC,CPU_ON,参数是目标核的 MPIDR 和一个入口地址 — head.S 的 secondary_entry.发完之后主核进入等待,轮询一个 completion,等副核自己来点亮.

副核这边被 firmware 从复位拉起来,直接跳到 secondary_entry(head.S:355).第一条正经事是确认自己在哪个 EL,该降级降级(和主核启动同款逻辑).这里有个历史分支值得一提:如果 DT 写的是老的 spin-table 方式,副核会进 secondary_holding_pen(head.S:337),一个小轮询圈:

pen:	ldr	x4, [x3]
	cmp	x4, x2		// 我的 MPIDR 被点名了吗
	b.eq	secondary_startup
	wfe
	b	pen

每个核在圈里 WFE 睡着等点名,主核写 pen_release 把 MPIDR 写进去再逐个踹醒.能用,但核多时代价就出来了,而且把等待逻辑摊给每个核自己轮,不如 PSCI 让 firmware 统一管.主线现在默认 PSCI,板上也是.

越过 pen,secondary_startup(head.S:361)把 MMU 开起来,这段和主核 early boot 共用基础设施,到 __secondary_switched 换好 C 环境,一头扎进 smp.c:195 的 secondary_start_kernel.这个 C 函数才是副核的"报到大厅":初始化自己的 GIC redistributor(每核一个,日志里那段 "GICv3: CPU5: found redistributor 101" 就是它),设置定时器,刷新 TLB 信息,最后 set_cpu_online(cpu, true),complete(&cpu_running) 把主核等的那个 completion 点亮,然后进 idle 循环开始等调度器使唤.热插拔实测把整条链再演一遍,后面贴.

先看启动期怎么登记异构.RK3399 是 4 个 A53 加 2 个 A72,DT 的 cpus 节点一个不落:

cpu@0..cpu@3:  reg=0x0..0x3,  compatible="arm,cortex-a53"
cpu@100/101:   reg=0x100/0x101, compatible="arm,cortex-a72"

第一次见容易懵的是节点名:cpu@100 的 100 是十六进制,对应 MPIDR 0x100 — 第二个簇.核的编号在 ARM 上从来不是连续小整数,MPIDR 里藏着簇号,A53 是 cluster0 的 0-3,A72 是 cluster1 的 0-1.内核解析拓扑全靠这个.还有一对数字值得记:capacity-dmips-mhz,A53 是 0x1e5(485),A72 是 0x400(1024),这俩比值就是"同频率下大核比小核算力强一倍"的量化版,后面调度器讲异构选核(EAS)时就是拿它当输入,数据源头在启动这一步就进了内核.

下面上板玩热插拔.把 cpu5(A72 第二核)下线:

# echo 0 > /sys/devices/system/cpu/cpu5/online
[  367.776574] psci: CPU5 killed (polled 0 ms)

一行日志信息量不小:内核先把这核上的任务迁走,走完下线流程,最后调 PSCI 的 CPU_OFF,核自己进固件关电;"polled 0 ms" 是主核等它咽气的时间.此时 /sys/devices/system/cpu/online 里已经没有 5,cat cpu5/online 得 0.再上线:

# echo 1 > /sys/devices/system/cpu/cpu5/online
[  368.818158] Detected PIPT I-cache on CPU5
[  368.818605] GICv3: CPU5: found redistributor 101 region 0:0x00000000fefa0000
[  368.819311] CPU5: Booted secondary processor 0x0000000101 [0x410fd082]

这就是把前面讲的副核引导链完整重跑一遍:PSCI CPU_ON 叫醒,secondary_entry 进来,redistributor 初始化,报线上线.0x0000000101 就是 MPIDR(注意顶部多个 0x1 是保留位里的 cluster 标记),MIDR 0x410fd082 是 Cortex-A72 r0p2.热插拔的"插"和冷启动的"拉起"在内核眼里是同一条代码路径,这个统一设计很舒服.

内核里管上下线流程的是 cpuhp 状态机,kernel/cpu.c.设计动机很实际:CPU 上线下线时一串子系统都要收尾 — 调度器要迁移任务,perf 要搬上下文,cpufreq 要清理策略,kvm 要放寄存器 — 老内核里这些回调散落各处顺序没人保证.现在全部排成一条有序的状态带,上线从 CPUHP_BRINGUP_CPU 一路往上挪,下线反向走,每站一个回调,挪到 target 为止,中途出错还能回滚.板上看得到它的痕迹:ps 里有六个 cpuhp/N 内核线程(每核一个,负责在目标核现场执行回调),/sys/devices/system/cpu/cpu5/hotplug/ 下有 state 和 target 两个文件,就是当前状态号和目标状态号.热插拔在用户态就是写一个 sysfs 位,内核里翻过去是几十个状态站的搬运,这个反差值得知道.

最后留几个容易忽略的细节:

退出移动版