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 解压流程与关键点
- 解压入口:Bootloader 跳转到解压代码的入口(通常位于
arch/arm/boot/compressed/head.S)。 - 自解压:解压代码检测自身运行地址,若存在地址冲突,则进行重定位。然后调用解压算法(如 LZ4、LZO、GZIP)将内核解压到指定的内存地址。
- 跳转到解压后的内核:解压完成后,跳转到解压后的内核入口(
stext或_start),进入汇编初始化阶段。
优化要点: 解压阶段是纯 CPU 密集型操作,其耗时与内核镜像大小、压缩算法和 CPU 频率直接相关。选择更快的压缩算法(如 LZ4 替代 GZIP)可以显著减少解压时间,但会略微增加镜像体积。这是启动优化的第一道关卡。
时间占比: 在典型的嵌入式系统中,解压阶段通常占用总启动时间的 5% ~ 15%,具体取决于硬件性能。
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 流程差异很大。错误的页表配置或栈设置将导致内核无法启动。
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 的优化重点在于减少内存、调度、中断等核心子系统的初始化开销,例如通过内核配置裁剪不必要的功能。
2.4 阶段四:rest_init — 启动用户空间
rest_init 是 start_kernel 的最后一步,标志着内核初始化进入尾声,开始创建关键的内核线程并最终启动用户空间。
2.4.1 rest_init 的核心动作
- 创建内核线程 (kernel_thread):
- kernel_init:这是 1 号进程(init 进程)的前身。它负责执行所有的
initcall(包括驱动初始化),然后尝试挂载根文件系统并执行用户空间的 init 程序。 - kthreadd:这是 2 号进程,是所有内核线程的父进程,负责管理和创建其他内核线程。
- kernel_init:这是 1 号进程(init 进程)的前身。它负责执行所有的
- 进入 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 的工作队列线程由此管理 |
2.5 阶段总结与优化切入点
内核启动的四个阶段构成了一个完整的初始化链条。为了进行有效的启动优化,我们需要识别每个阶段的瓶颈:
- 解压阶段:优化压缩算法和镜像大小。
- 汇编初始化:通常不可优化,但需确保配置正确。
- start_kernel:裁剪内核配置,减少核心子系统初始化开销。
- rest_init:这是驱动初始化的主战场。通过配置驱动异步 probe,可以将耗时的驱动初始化任务从同步的 initcall 执行流中剥离,利用多核或空闲 CPU 时间并行执行,从而显著缩短从内核启动到用户空间 init 进程启动的时间。