上电那一刻,一颗 ARM SoC 上通常只有一个核活着.其它核是谁叫醒的,醒来第一件事干什么,跑起来之后又怎么下线休息,这条线串起来就是这篇.里头还会讲到 PSCI 这层和 firmware 的分权,以及 cpuhp 状态机,全是驱动工程师天天碰但很少往下看的东西.
这篇回答几件事:
- 主核怎么知道系统里有哪些核,叫醒它们走什么接口
- 副核从第一条指令到进调度器,中间的完整路径
- CPU 热插拔在板上是什么体验,内核里怎么实现
- 大小核这种异构拓扑在启动期怎么登记
先把权力分清.老一代 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 位,内核里翻过去是几十个状态站的搬运,这个反差值得知道.
最后留几个容易忽略的细节:
- MPIDR 的 affinity 字段按簇分层,A72 在 affinity1=1,所以 0x100.代码里到处 MPIDR_HWID_BITMASK 掩掉保留位,读裸值别直接当小整数比大小.
- possible,present,online 三个 mask 不是一回事:possible 是编译期 NR_CPUS 允许的上限(板上 kernel_max=5 即 NR_CPUS=6),present 是固件报上来的,online 是当下真活着的.热插拔只动 online.
- secondary_holding_pen 只有 spin-table 启动方式才走,DT 里 enable-method 写 psci 的板永远不进那个圈.别拿别板子的 WFE 日志对号.
- 副核上线后的第一个任务不是你写的任何东西,是给它的 idle 线程,它报到完就进 WFI 等第一次调度.
- cpuhp 回调里不能睡是每个写 hotplug state 的驱动的祖训,睡了状态机直接卡死全系统上下线.
- 下线一个核不会让内核"忘记"它,per-cpu 变量还在,只是没人访问;所以反复上下线的开销主要在流程搬运,不在内存.
- PSCI 的 SMC 调用本身要在 EL1 发,这呼应 1.1 的分层:内核不碰核的电源寄存器,它只是 EL3 的客户.想 hack 开核,先想清楚你在哪层.