4、内核入口点分析:Linux内核在ARM64上的入口函数(stext/__primary_switch)、启动CPU与辅助CPU的分离路径

ARM64架构下,Linux内核的入口点设计体现了多核处理器启动的典型模式:主启动CPU(Primary CPU)负责全局初始化,而辅助CPU(Secondary CPU)则等待被唤醒并完成各自的初始化。本章将深入剖析内核入口函数 stext 与 __primary_switch 的执行逻辑,并详细阐述启动CPU与辅助CPU的分离路径。

4.1 内核入口点:stext

stext 是ARM64 Linux内核的第一个C语言入口点,位于 arch/arm64/kernel/head.S 中。它负责从汇编启动阶段过渡到C语言环境,并完成早期初始化。其核心任务包括:

  • 设置页表:为内核空间建立恒等映射(identity mapping)和线性映射。
  • 启用MMU:使能内存管理单元,进入虚拟地址空间。
  • 跳转到C代码:调用 start_kernel 函数。

以下是 stext 的简化执行流程:

关键点: stext 执行时,CPU仍处于物理地址空间,所有地址操作必须使用物理地址。页表建立后,MMU使能,CPU才切换到虚拟地址空间。
/*
 *  arch/arm64/kernel/head.S - stext 入口
 */
SYM_CODE_START(stext)
    /* 1. 保存启动信息(设备树地址等) */
    mov x20, x0          // DTB地址保存在x20

    /* 2. 设置页表(恒等映射 + 线性映射) */
    bl __create_page_tables

    /* 3. 启用MMU,跳转到虚拟地址空间 */
    bl __enable_mmu

    /* 4. 跳转到C语言入口 */
    b start_kernel
SYM_CODE_END(stext)

4.2 主启动CPU路径:__primary_switch

__primary_switch 是主CPU在启用MMU后执行的关键切换函数。它负责完成从汇编到C语言的最终过渡,并设置主CPU的启动上下文。

其核心步骤包括:

  1. 设置栈指针:为主CPU分配并初始化内核栈。
  2. 跳转到C代码:调用 start_kernel,进入内核初始化主流程。
  3. 标记CPU在线:在 start_kernel 中,主CPU会将自己标记为“在线”,并开始初始化子系统。
提示: __primary_switch 仅在主CPU上执行一次。辅助CPU不会执行此函数,而是通过 secondary_startup 路径启动。

4.3 辅助CPU启动路径:secondary_startup

辅助CPU的启动路径与主CPU不同。它们通常由主CPU通过CPU热插拔框架或PSCI(Power State Coordination Interface)唤醒。辅助CPU的入口点是 secondary_startup。

辅助CPU的启动流程如下:

步骤 描述 关键函数/操作
1 等待唤醒 辅助CPU在低功耗状态等待主CPU通过PSCI唤醒。
2 进入内核入口 辅助CPU从 secondary_entry 开始执行。
3 设置页表 复用主CPU已建立的页表,无需重新创建。
4 启用MMU 使能MMU,进入虚拟地址空间。
5 初始化本地资源 调用 secondary_start_kernel,初始化中断、调度等。
6 标记在线 将辅助CPU标记为“在线”,加入调度域。

4.4 启动CPU与辅助CPU的分离路径

ARM64内核通过CPU ID和启动标志来区分主CPU和辅助CPU。主CPU的ID通常为0,但也可以通过设备树或ACPI指定。辅助CPU的启动路径完全独立,直到它们被唤醒并加入系统。

分离路径的核心差异如下:

  • 入口点不同:主CPU从 stext 开始,辅助CPU从 secondary_startup 开始。
  • 页表创建:主CPU负责创建页表,辅助CPU直接复用。
  • 初始化范围:主CPU初始化全局资源(内存、中断控制器等),辅助CPU仅初始化本地资源(栈、per-CPU变量等)。
  • 同步机制:主CPU通过 cpu_up 和 smp_cpus_done 等待所有辅助CPU完成初始化。
注意: 如果辅助CPU在启动过程中失败(例如页表损坏或MMU配置错误),主CPU可能无法检测到,导致系统启动不完全。调试时需检查 CPU hotplug 状态。

4.5 启动流程SVG图:主CPU与辅助CPU分离路径

下图展示了ARM64 Linux内核启动过程中,主CPU与辅助CPU的分离路径:

ARM64 Linux内核启动:主CPU与辅助CPU分离路径 ARM64 Linux内核启动:主CPU与辅助CPU分离路径 主CPU (Primary CPU) stext 入口 创建页表 (__create_page_tables) 启用MMU (__enable_mmu) __primary_switch → start_kernel 初始化全局资源 辅助CPU (Secondary CPU) 等待唤醒 (PSCI) secondary_startup 入口 复用页表,启用MMU secondary_start_kernel 初始化本地资源 CPU ID 0 唤醒信号 主CPU创建页表并初始化全局资源,辅助CPU复用页表并初始化本地资源

4.6 关键代码片段:主CPU与辅助CPU入口

以下代码展示了主CPU与辅助CPU入口点的差异:

/*
 * 主CPU入口:stext
 */
SYM_CODE_START(stext)
    mov x20, x0          // 保存DTB地址
    bl __create_page_tables
    bl __enable_mmu
    b start_kernel
SYM_CODE_END(stext)

/*
 * 辅助CPU入口:secondary_startup
 */
SYM_CODE_START(secondary_startup)
    mov x20, x0          // 保存CPU ID
    bl __enable_mmu      // 复用页表,仅启用MMU
    b secondary_start_kernel
SYM_CODE_END(secondary_startup)
关键差异: 主CPU调用 __create_page_tables 创建页表,而辅助CPU直接调用 __enable_mmu 复用已有页表。这是两者启动路径的核心区别。

4.7 总结

ARM64 Linux内核的入口点设计通过 stext 和 secondary_startup 实现了主CPU与辅助CPU的分离路径。主CPU负责全局初始化,辅助CPU则专注于本地资源初始化。这种设计确保了多核系统的高效启动和可扩展性。