§1.1 arch/arm64/kernel/psci.c:和固件的 CPU 电源接口
平台:QEMU virt + ARM64,-kernel Image。没有真 ATF,也没有 U-Boot。
0. 这条要明白什么
ARM64 上 CPU 的上电、下电、改 PC,不是内核去写 SoC 复位寄存器。内核只发 PSCI 请求;真正动电源的是更高异常级的固件。
本文件是客户这一侧的适配层:把 struct cpu_operations(SMP/热插拔用的虚表)翻成 PSCI 的 CPU_ON / CPU_OFF / AFFINITY_INFO。
清单写成「ATF/U-Boot」,是 BSP 口头习惯。virt 上对端是 QEMU。真板最常见是 ATF(BL31)。U-Boot 首先是加载 Image 的 bootloader,只有少数没 ATF 的板子才会顺带实现 PSCI。
1. 三个「psci」不要并成一份
| 文件 | 角色 | 管什么 |
|---|---|---|
本文件 arch/arm64/kernel/psci.c |
ARM64 的 cpu_ops |
某一颗 CPU 的上电/下电 |
drivers/firmware/psci/psci.c |
通用 PSCI 驱动 | 发 hvc/smc;还挂整机 SYSTEM_OFF / SYSTEM_RESET |
arch/arm64/kernel/smp.c |
启动/热插拔流程 | 何时调用上面这张表 |
调用方向永远是:
smp.c (时机)
→ 本文件 cpu_psci_ops.cpu_boot / cpu_die / cpu_kill
→ psci_ops.* (drivers/firmware/psci/psci.c 填好的函数指针)
→ invoke_psci_fn → hvc/smc
→ 固件(virt = QEMU;真板 = ATF)
本文件 没有 hvc 指令,也 没有 poweroff。它只决定「这次要调 PSCI 的哪一个功能」。
2. 对端到底是谁
真板典型:
U-Boot / 厂商 bootloader 加载 Image,x0 = DTB ← 启动协议
ATF (BL31, EL3) 实现 PSCI,接 smc ← 电源接口 ★ 本条
Linux 本文件 + firmware/psci.c ← 客户
你现在的 virt:
QEMU -kernel 兼当 bootloader(填 x0)
QEMU 自己模拟 PSCI 兼当 ATF(接 hvc)
Linux 客户侧完全一样
两件不同的事:
- 谁把内核加载进来 ------ bootloader(现在是 QEMU,以后可以是 U-Boot)。
- 谁给 CPU 上电/改入口 ------ PSCI 实现者(现在是 QEMU,真板是 ATF)。
加 U-Boot 只换第 1 件;加 ATF 才换第 2 件。本条关心第 2 件。
DT 里和本文件对上的是 cpu 节点的 enable-method = "psci"(cpu_ops.c 用这个名字找到 cpu_psci_ops)。psci { method = "hvc"; } 则是 firmware 那份驱动选 hvc 还是 smc,不是本文件读的。
3. 文件本体:一张表
全文就是给 cpu_psci_ops 填回调。.name = "psci" 必须和 DT enable-method 字符串一致。
const struct cpu_operations cpu_psci_ops = {
.name = "psci",
.cpu_init = cpu_psci_cpu_init, // 空实现,返回 0
.cpu_prepare = cpu_psci_cpu_prepare, // 启动前确认固件有 cpu_on
.cpu_boot = cpu_psci_cpu_boot, // ★ 上电:CPU_ON
.cpu_can_disable = ..., // 这颗核能不能热拔
.cpu_disable = ..., // 拔之前再检查一次
.cpu_die = cpu_psci_cpu_die, // ★ 本核请求下电:CPU_OFF
.cpu_kill = cpu_psci_cpu_kill, // ★ 另一核确认已经关掉
};
没有 .cpu_postboot:从核起来后不必在本文件做额外同步。
谁在什么时候调这些指针(smp.c):
| 回调 | 谁调 | 何时 |
|---|---|---|
cpu_init |
smp_cpu_setup() |
setup_arch → smp_init_cpus,登记阶段 |
cpu_prepare |
smp_prepare_cpus() |
kernel_init_freeable,拉核之前 |
cpu_boot |
boot_secondary() ← __cpu_up |
smp_init,真正点亮从核 |
cpu_disable |
热插拔下线路径 | 目标核还在跑,允许失败并中止 |
cpu_die |
目标核自己 cpu_die() |
点了不归路,必须成功 |
cpu_kill |
另一颗核 arch_cpuhp_cleanup_dead_cpu |
轮询固件:那颗核是不是真关了 |
4. 上电:cpu_psci_cpu_boot
这是第 1 个月必精读的一个函数。
cpu_psci_cpu_boot(cpu)
pa = __pa_symbol(secondary_entry) 物理地址:从核 MMU 关着
psci_ops.cpu_on(cpu_logical_map(cpu), pa)
两个参数就是 PSCI CPU_ON 契约的全部:
- MPIDR (
cpu_logical_map(cpu),来自 DTcpu节点reg),不是 Linux 逻辑 CPU 号。 - 入口物理地址 (
secondary_entry)。固件把目标核的 PC 设到这里。
psci_ops.cpu_on 在 setup_arch → psci_dt_init() 里已经填好(virt 上是 psci_0_2_cpu_on)。本文件不关心 hvc 还是 smc。
返回 0 只表示固件接单。从核是否 online,仍由 smp.c 的 __cpu_up 等 cpu_running。细节见拉核笔记第 3~5 节。
cpu_prepare 只做一件事:没有 psci_ops.cpu_on 就拒绝拉这颗核(-ENODEV)。cpu_init 为空:PSCI 不需要按 CPU 再读一份私有数据(对比 spin-table 还要读 cpu-release-addr)。
5. 下电:看函数名,不深挖热插拔
第 1 个月知道「对称的下电也走固件」即可,不必顺着 cpuhp 状态机走。
三步,故意拆开:
1. cpu_can_disable / cpu_disable 还在内核里,可以拒绝
- 固件没实现 CPU_OFF → -EOPNOTSUPP
- Trusted OS 驻留在这颗核上 → -EPERM(psci_tos_resident_on)
2. cpu_die 目标核自己调,发 CPU_OFF,不再返回
3. cpu_kill 另一颗核轮询 AFFINITY_INFO,确认 OFF
cpu_die 里的 power_state 字段,注释写明现有实现基本不用,填一个 POWER_DOWN 即可。
cpu_kill 最多等约 100ms,是因为 cpu_die 和 cpu_kill 并发:杀得太早会误判「还活着」。查不到 affinity_info 就当成功(没有手段确认,只能假设)。
virt 学习阶段很少做 echo 0 > /sys/devices/system/cpu/cpu1/online。知道下电同样是 PSCI、同样不写复位寄存器,这条就够。
6. 整机电源不在本文件
poweroff / reboot 挂在 drivers/firmware/psci/psci.c 的 psci_0_2_set_functions():
pm_power_off = psci_sys_poweroff→SYSTEM_OFF- restart handler →
SYSTEM_RESET
和本文件的 CPU_ON/CPU_OFF 是同一份契约、同一次 hvc 通道,但是:
| 本文件 | firmware/psci.c | |
|---|---|---|
| 粒度 | 一颗 CPU | 整台机器 |
| 调用者 | smp.c 的 cpu_ops |
kernel_power_off / reboot |
| 第 1 个月 | 要知道「不在这里」 | 第 3.1 再顺着走 |
7. 重点(合上文件能用的)
- 客户 vs 管家 :Linux 发请求;ATF(virt 上是 QEMU)管 CPU 电源。从核起不来,先查
enable-method、PSCI 节点、固件,不是某个外设probe。 - 本文件只做翻译 :
cpu_boot→CPU_ON,cpu_die→CPU_OFF,cpu_kill→AFFINITY_INFO。发指令的是另一份psci.c。 - ATF ≠ U-Boot:U-Boot 管加载;ATF 管 PSCI。virt 上两件都由 QEMU 做。
- 核级 ≠ 整机 :开关某一颗核走本文件;关机/重启走 firmware 的
SYSTEM_*。 - MPIDR + 物理入口 :
CPU_ON只认硬件编号和secondary_entry的物理地址。