Linux kernel 多核启动
平时看smp系统,比如多个A78核心同时跑起来,很多人会研究多核调度,但是也会比较好奇在什么时候多核被上电的,然后加入linux的smp系统的,今天来研究一下到底是如何从一个boot core到多个core跑起来。
线索1:start kernel 最后一点 rest_init
init(PID 1)
-
起点是
kernel_init()
(init/main.c
),先完成一堆 “freeable” 的内核初始化(kernel_init_freeable()
),挂载根、准备用户态环境…… -
然后按顺序尝试执行用户空间 init:
run_init_process()
(systemd、/sbin/init…)。 -
代码点:
-
init/main.c: kernel_init()
/kernel_init_freeable()
/run_init_process()
-
-
进入用户态后就从“内核态函数”转成真正的
/sbin/init
进程,继续整个用户空间的启动(比如 systemd 主循环)。
kthreadd(PID 2)
-
主体在
kernel/kthread.c:kthreadd()
,循环处理kthread_create()
队列,把请求的内核线程创建好并唤醒运行。 -
之后系统中各种内核子系统(block、rcu、workqueue、net、fs…)创建的 kthread 都由它“接生”。
-
可截图:
kernel/kthread.c
中的kthreadd()
、kthread()
与相关创建/唤醒路径。
idle(swapper/0,PID 0)
-
就是当前引导任务自身,“改行”去跑
cpu_startup_entry()
→do_idle()
。 -
它负责在 CPU 空闲时节能、进入 C-states/停时钟 tickless、处理 IPI 唤醒等。
-
位置:
kernel/sched/idle.c
。 -
注意:SMP 场景下,其它 CPU 上线后,每个 CPU 都会有自己的
swapper/N
idle 线程;而 rest_init() 并不负责拉起其他 CPU,多核上线在后续smp_init()
流程(PSCI/BL31 交互)里完成。
线索2:cpu 相关操作集合
arm64架构先对于cpu的操作集合
可以看到init prepare boot三巨头,不仅usb pcie可以热插拔,cpu也可以热插拔,这帮程序员真的是精力充沛,啥东西都搞的框架一套一套的,功能打磨的越来越完善,热插拔就是加了disable die kill相关ops。
基本上都是通用的psci接口,这一套psci的架构在arm的atf bl31和scp固件都有非常好的支持,走成熟的框架比自己diy要舒服,就算上了虚拟化也一堆人进行适配。
可以看到psci_ops又是一个单独定义的东西,其中cpu_on函数指针是cpu启动的关键
这里一路追踪到了invoke_psci_fn回调函数上面,这个东西究竟是又是谁赋值的,又是一层函数指针?
线索3:cpu init与prepare时机
在start_kernel中有设置架构相关准备
其中有cpu的初始化
其实就是挨个cpu进行一下setup
具体的setup就是调用cpu ops提供的init函数
那么prepare怎么没人执行,我们倒推一下调用链
原来是在rest init后面搞的
其中的freeable
线索4:cpu on 时机
结合上图的smp_init继续探索
先试一下并行启动再试一下串行启动
串行启动
注意这里有cpu_up _cpu_up __cpu_up小心迷路
两个下划线的,这个是arm架构相关的,不带和一个下划线的是通用的
task_struct都掺和进来了
这里终于调用到了cpu_boot回调函数
线索5:从_cpu_up到__cpu_up
start_kernel setup_arch psci_dt_init init_bootcpu_ops smp_init_cpus arch_call_rest_init rest_init kernel_init kernel_init_freeable smp_init bringup_nonboot_cpus _cpu_up
分析了好多代码上电的线索在_cpu_up断开了,我知道__cpu_up就在身边,但是近在眼前远在天边,今天非要揪出来怎么联动的,gpt了一下,解释如下。
cpu_up()
-
位置:
kernel/cpu.c
-
作用:这是 对外的统一接口,用于把一个指定的 CPU 拉起来。比如热插拔、启动时 bring-up 都会走这里。
-
代码特点:
-
会做合法性检查(目标 CPU 是否离线、是否允许上线)。
-
获取全局
cpu_add_remove_lock
,保证并发安全。 -
调用内部
_cpu_up()
。
-
_cpu_up()
-
位置:
kernel/cpu.c
-
作用:作为
cpu_up()
的内部实现,主要封装了一些 上下文管理:-
调用
cpuhp_up_callbacks()
,走 CPU 热插拔的 state machine。 -
每个 CPU hotplug state(如
CPUHP_BRINGUP_CPU
,CPUHP_AP_ONLINE_DYN
)都会有回调,逐步完成从硬件 bring-up 到调度器可用。 -
真正执行 bring-up 的核心调用是
__cpu_up()
, 但它是嵌在 state machine 里被触发的。
-
__cpu_up()
-
位置:架构相关文件
-
ARM64:
arch/arm64/kernel/smp.c
-
x86:
arch/x86/kernel/smpboot.c
-
-
作用:架构相关的核心逻辑,直接发出启动 CPU 的请求。
-
ARM64 下会调用
smp_boot_secondary()
。 -
smp_boot_secondary()
→ 通过 PSCI 调用 ATF/BL31 的CPU_ON
,指定次级 CPU 的入口地址。 -
次级 CPU 被唤醒,从
secondary_startup()
进入 Linux。
-
那总的看起来是状态机控制的通用到架构相关的cpu操作
-
背景
-
早期内核(4.10 以前)CPU 启动/下线代码分散在
smp.c
/cpu.c
等处,逻辑混乱,驱动/子系统很难挂钩。 -
为了解耦,Linus 接受了 Thomas Gleixner 的补丁,把 CPU bring-up/down 抽象成 一条状态机 (cpuhp)。
-
每个子系统(调度器、RCU、timer、irqchip、arch bring-up …)在对应的状态点注册回调函数,保证顺序和依赖。
-
状态机设计
-
核心数据结构:
enum cpuhp_state
(定义在include/linux/cpuhotplug.h
) -
每个状态对应一个阶段,比如:
-
状态机由
cpuhp_up_callbacks()
/cpuhp_down_callbacks()
驱动。
-
典型路径:
cpu_up(cpu)
走的是 从 CPUHP_OFFLINE
→ CPUHP_ONLINE
的正向状态迁移:
cpu_up(cpu) └─> _cpu_up(cpu) └─> cpuhp_up_callbacks(cpu) └─> 依次执行状态机: - CPUHP_BRINGUP_CPU - CPUHP_AP_IDLE_DEAD - CPUHP_AP_SCHED_STARTING - ... - CPUHP_AP_ONLINE_IDLE - CPUHP_ONLINE
-
每个状态点如果有回调,就会执行。
-
如果某一步失败,会回滚状态机(调用对应的 teardown 回调),保证一致性。
我们可以看到cpuhp_invoke_callback_range里有个循环调用cpuhp_invoke_callback的过程,在callback函数中会调用step的single方法,这里的hp是hotplugin的缩写,st的结构体cpuhp_step定义如下
终于在这个地方联系上了之前两个下划线的那个__cpu_up了
-
重要状态说明(上线路径)
-
CPUHP_BRINGUP_CPU
-
由
__cpu_up()
触发,走进smp_boot_secondary()
。 -
此时会通过 PSCI 调 BL31 → CPU_ON,上电次级核。
-
-
CPUHP_AP_IDLE_DEAD
-
次级 CPU 被唤醒后,从
secondary_startup()
进入 Linux。 -
在这里等待被标记为 online,类似一个 “idle but not yet scheduled”。
-
-
CPUHP_AP_SCHED_STARTING
-
调度器初始化次级 CPU 的运行队列(
sched_cpu_starting()
)。 -
这之后,调度器知道这个 CPU 存在,可以调度任务上去。
-
-
CPUHP_AP_ONLINE_IDLE
-
把 CPU 标记为 online,进入 idle task 循环。
-
此时 CPU 已经 fully usable,但还没有用户进程跑上来。
-
-
CPUHP_ONLINE
-
最终状态。
-
代表 CPU 已经完全对系统开放,可以执行普通任务。
-
-
状态机枚举定义
-
include/linux/cpuhotplug.h
-
搜索
enum cpuhp_state
,截一段关键枚举。
-
-
状态机执行逻辑
-
kernel/cpu.c
-
函数
cpuhp_up_callbacks()
,可以截图 while 循环里执行状态回调的部分。
-
-
状态回调注册
-
各子系统在 init 时会调用
cpuhp_setup_state()
注册自己的回调。 -
例如调度器在
kernel/sched/core.c
注册sched_cpu_starting()
。
-
-
次级 CPU 启动入口
-
arch/arm64/kernel/smp.c
→secondary_start_kernel()
-
这是 CPU 上电后真正开始执行 Linux 的地方,从上电到被c代码初始化过,arm自己有一套处理办法
-
线索6:cpu on 如何到 psci
我们找了半天发现终于cpu on了,但是cpu on到底执行到哪里去了还没找到,至少要追踪到操作寄存器吧,内核为了兼容各种各样的架构,各种各样的需求,各种各样的功能驱动,封装的层数太多了,一个操作要找半天,不过要搞这个还是只能继续学习了。
设备树中的定义:
对应驱动代码的定义:
我们看到psci_dt_init做的事情是通过of_find_matching_node_and_match在dts中查找要用哪个psci_of_match data,然后调用它,这个psci_of_match.data其实是个函数
终于找到了大名鼎鼎的smccc陷入bl31 psci电源管理的接口咯
SMCCC 全称是 SMC Calling Convention,中文一般称作 SMC 调用约定。
它是 ARM 定义的一个标准,描述 操作系统/Hypervisor 与安全世界(EL3/EL2/TrustZone 固件)之间的调用接口规范。
-
SMC (Secure Monitor Call):一种特殊指令,用来从普通世界(Non-secure world)切换到安全世界。
-
SMCCC:规定了 SMC 调用时 参数传递、返回值、寄存器使用、调用 ID 编码方式 等,确保不同固件/内核/Hypervisor 之间的兼容性。
-
在 Linux 里,相关头文件在
include/linux/arm-smccc.h
。 -
ARM 官方文档叫: "ARM Secure Monitor Call Calling Convention (SMCCC)"。
线索7:bl31 怎么调到 psci
bl31有bl31_entrypoint和bl31_warm_entrypoint两个entry,二者都会通过el3_entrypoint_common设置runtime_exceptions作为vector地址(tf-a/bl31/aarch64/bl31_entrypoint.S)
在runtime_exceptions的sync_handler64中,会加载rt_svc_descs_indices,进而得到rt_svc_descs的index,进入rt_svc_descs对应entry中
关于rt_svc_descs和psci的关系,大概整理如下
bl31_main //tf-a/bl31/bl31_main.c runtime_svc_init //tf-a/common/runtime_svc.c rt_svc_descs = (rt_svc_desc_t *) RT_SVC_DESCS_START; rt_svc_desc_t *service = &rt_svc_descs[index]; service->init
上述的service->init就是setup函数tf-a/include/common/runtime_svc.h
在setup函数中tf-a/services/std_svc/std_svc_setup.c,psci_setup调用了plat_setup_psci_ops接口,这个也是各个平台可以自己实现的一个函数接口,在定义各类psci的ops