2. 内核启动核心阶段分析:解压、汇编初始化、start_kernel、rest_init

嵌入式 Linux 内核的启动过程是一个高度精密的“接力赛”,从引导加载程序(Bootloader)交出控制权开始,到最终启动用户空间的 init 进程,中间经历了多个关键阶段。理解这些阶段是进行启动优化的基础,尤其是驱动异步 probe 的配置时机,与内核初始化流程紧密相关。

本章将深入剖析内核启动的四个核心阶段:解压、汇编初始化、start_kernel 和 rest_init。我们将重点关注每个阶段完成的关键任务、时间开销特征,以及它们如何为后续的驱动加载和异步 probe 奠定基础。

2.1 阶段一:内核解压(Decompression)

在大多数嵌入式系统中,内核镜像以压缩形式(如 zImage、Image.gz)存储在闪存中。Bootloader 将压缩后的内核加载到 RAM 后,首先执行的是一段自解压代码。

2.1.1 解压流程与关键点

  1. 解压入口:Bootloader 跳转到解压代码的入口(通常位于 arch/arm/boot/compressed/head.S)。
  2. 自解压:解压代码检测自身运行地址,若存在地址冲突,则进行重定位。然后调用解压算法(如 LZ4、LZO、GZIP)将内核解压到指定的内存地址。
  3. 跳转到解压后的内核:解压完成后,跳转到解压后的内核入口(stext 或 _start),进入汇编初始化阶段。
优化要点: 解压阶段是纯 CPU 密集型操作,其耗时与内核镜像大小、压缩算法和 CPU 频率直接相关。选择更快的压缩算法(如 LZ4 替代 GZIP)可以显著减少解压时间,但会略微增加镜像体积。这是启动优化的第一道关卡。
时间占比: 在典型的嵌入式系统中,解压阶段通常占用总启动时间的 5% ~ 15%,具体取决于硬件性能。
内核解压流程示意图 Bootloader 压缩内核 (zImage) 自解压代码 解压后内核 (vmlinux) 关键指标 • 压缩比 • 解压速度 • 镜像大小

2.2 阶段二:汇编初始化(Assembly Init)

解压完成后,CPU 开始执行内核的第一段代码,通常是用汇编语言编写的。这个阶段的主要任务是建立 C 语言运行环境。

2.2.1 核心任务清单

任务 描述 架构相关
设置异常向量表 配置 CPU 异常处理入口(中断、缺页等) 是
初始化页表 建立初始的页表映射(如恒等映射、内核空间映射) 是
使能 MMU 开启内存管理单元,启用虚拟地址 是
设置 C 栈 为 start_kernel 函数准备栈指针 (SP) 否
清零 BSS 段 清除未初始化全局变量的内存区域 否
跳转到 start_kernel 调用 C 语言入口函数 否
避坑指南: 汇编初始化阶段高度依赖具体的 CPU 架构(ARM、RISC-V、x86 等)。在进行启动优化时,需要查阅对应架构的 head.S 文件。例如,ARM64 的 __primary_switch 和 RISC-V 的 _start 流程差异很大。错误的页表配置或栈设置将导致内核无法启动。
汇编初始化阶段流程图 (ARM64 示例) 解压后入口 设置异常向量 初始化页表 使能 MMU 设置 C 栈 / BSS 跳转 start_kernel 此阶段完成后,系统进入 C 语言世界,所有后续初始化都基于虚拟地址运行。

2.3 阶段三:start_kernel — C 语言初始化核心

start_kernel 是内核启动的 C 语言入口,位于 init/main.c。它负责初始化内核的所有核心子系统,是启动过程中最复杂、最耗时的阶段。

2.3.1 初始化流程概览

start_kernel 按照严格的依赖顺序调用一系列初始化函数。以下是关键步骤的列表:

  • trap_init():设置中断向量表(C 语言层面)。
  • mm_init():初始化内存管理器,包括 Buddy 系统和 SLAB 分配器。
  • sched_init():初始化进程调度器,创建 idle 进程。
  • init_IRQ():初始化中断控制器。
  • time_init():初始化系统时钟和定时器。
  • console_init():初始化控制台(printk 开始输出)。
  • vfs_caches_init():初始化虚拟文件系统缓存。
  • arch_call_rest_init():调用 rest_init(),启动后续进程。
关键洞察: 在 start_kernel 中,大部分驱动初始化尚未开始。设备模型(driver model)的初始化发生在 rest_init 阶段。因此,start_kernel 的优化重点在于减少内存、调度、中断等核心子系统的初始化开销,例如通过内核配置裁剪不必要的功能。
start_kernel 核心初始化依赖链 trap_init mm_init sched_init init_IRQ time_init console_init vfs_caches_init arch_call_rest_init 优化关注点 • 通过内核配置 (Kconfig) 裁剪不需要的子系统,减少初始化函数调用。 • 使用 initcall 调试工具 (initcall_debug) 测量每个初始化函数的耗时。

2.4 阶段四:rest_init — 启动用户空间

rest_init 是 start_kernel 的最后一步,标志着内核初始化进入尾声,开始创建关键的内核线程并最终启动用户空间。

2.4.1 rest_init 的核心动作

  1. 创建内核线程 (kernel_thread):
    • kernel_init:这是 1 号进程(init 进程)的前身。它负责执行所有的 initcall(包括驱动初始化),然后尝试挂载根文件系统并执行用户空间的 init 程序。
    • kthreadd:这是 2 号进程,是所有内核线程的父进程,负责管理和创建其他内核线程。
  2. 进入 idle 循环:0 号进程(idle 进程)进入无限循环,当没有其他进程可运行时,CPU 执行 idle 循环(通常执行 WFI/WFE 指令以降低功耗)。
异步 probe 的舞台: 驱动初始化(包括异步 probe)正是在 kernel_init 进程中,通过执行 do_initcalls() 来触发的。理解 rest_init 的流程,是配置驱动异步 probe 的前提。

2.4.2 进程创建关系

进程 PID 角色 与驱动异步 probe 的关系
idle (swapper) 0 空闲进程,CPU 空闲时运行 无直接关系
kernel_init 1 执行 initcalls,启动用户空间 init 直接相关:所有驱动 probe 在此进程上下文中执行
kthreadd 2 内核线程管理器 间接相关:异步 probe 的工作队列线程由此管理
rest_init 进程创建与驱动初始化关系图 rest_init kernel_init (PID 1) kthreadd (PID 2) idle (PID 0) kernel_init 执行 do_initcalls() → 触发所有驱动 probe(包括异步 probe 的启动) 异步 probe 通过工作队列 (workqueue) 在 kthreadd 管理的线程中执行

2.5 阶段总结与优化切入点

内核启动的四个阶段构成了一个完整的初始化链条。为了进行有效的启动优化,我们需要识别每个阶段的瓶颈:

  • 解压阶段:优化压缩算法和镜像大小。
  • 汇编初始化:通常不可优化,但需确保配置正确。
  • start_kernel:裁剪内核配置,减少核心子系统初始化开销。
  • rest_init:这是驱动初始化的主战场。通过配置驱动异步 probe,可以将耗时的驱动初始化任务从同步的 initcall 执行流中剥离,利用多核或空闲 CPU 时间并行执行,从而显著缩短从内核启动到用户空间 init 进程启动的时间。