3. Linux内核启动流程

本章深入剖析ARM架构下车载Linux内核从引导加载程序(Bootloader)到第一个用户态进程(init)的完整启动链路。理解这一流程是进行内核驱动启动适配的基础,尤其在资源受限且安全要求极高的车载环境中,每一阶段的初始化顺序与内存布局都直接影响系统的稳定性和实时性。

3.1 启动流程总览:四阶段模型

ARM Linux内核的启动可划分为四个核心阶段,每个阶段承担不同的硬件抽象与软件初始化职责:

阶段 执行主体 核心任务 输出状态
Stage 1 Bootloader (U-Boot) 硬件最小初始化、加载内核映像到RAM、传递设备树(DTB)与启动参数 内核映像位于RAM起始地址,DTB指针在r2寄存器
Stage 2 内核解压代码 (head.S / decompress) 自解压、页表一级初始化、开启MMU、跳转到解压后的内核入口 MMU开启,内核运行在虚拟地址空间
Stage 3 start_kernel() 架构无关初始化、中断/定时器/调度器/内存管理子系统初始化 内核核心子系统就绪,准备创建第一个进程
Stage 4 init 进程 挂载根文件系统、执行用户态初始化脚本、启动系统服务 系统进入用户态运行
车载适配要点: 在车载系统中,Bootloader 阶段通常需要额外完成安全启动校验(Secure Boot)、硬件看门狗初始化以及早期故障诊断(EDR)日志区域的映射,这些操作必须在传递给内核之前完成。
ARM Linux 内核启动四阶段流程 ARM Linux 内核启动四阶段流程 Stage 1 Bootloader 硬件初始化 / 加载内核 Stage 2 内核解压 页表初始化 / 开启MMU Stage 3 start_kernel() 子系统初始化 Stage 4 init 进程 用户态初始化 关键传递参数 r0 = 0 | r1 = 机器类型 (DT模式时为 ~0) | r2 = DTB 物理地址指针 ~100ms ~50ms ~200ms 用户态

3.2 引导加载程序到内核入口

Bootloader(以U-Boot为例)完成硬件初始化后,通过以下步骤将控制权移交给内核:

  1. 加载内核映像:从存储介质(eMMC、NAND Flash或网络)将压缩后的内核映像(zImage/Image.gz)加载到指定的RAM地址(通常为 0x80008000)。
  2. 加载设备树:将编译好的DTB文件加载到RAM中,确保其不与内核映像重叠。
  3. 设置启动参数:在ARMv7-A及更早架构中,通过tagged list传递参数;在ARMv8-A(AArch64)中,统一使用DTB传递。
  4. 跳转到内核入口:设置寄存器 r0=0,r1=机器类型号(DT模式时可为~0),r2=DTB物理地址,执行跳转指令。
车载环境差异: 车载Bootloader通常集成 OPTEE(可信执行环境)初始化,在跳转内核前会建立安全内存隔离区,并将安全监控模式(Monitor Mode)的入口地址传递给内核。

3.3 内核解压与页表初始化

内核入口代码位于 arch/arm/boot/compressed/head.S(压缩内核)或 arch/arm/kernel/head.S(非压缩内核)。核心操作包括:

3.3.1 自解压流程

  • 重定位:如果内核加载地址与解压地址冲突,先将自身拷贝到安全位置。
  • 解压算法:调用 decompress_kernel(),支持gzip/lzo/lz4等算法,将内核解压到目标地址(通常为 0x80008000 或 0x80080000)。
  • 缓存刷新:解压完成后,刷新I-Cache和D-Cache,确保指令一致性。
  • 跳转到解压后内核:设置新栈指针,跳转到解压后的内核入口 stext。

3.3.2 页表初始化(早期MMU设置)

在 head.S 的 __create_page_tables 阶段,内核建立恒等映射(Identity Mapping)和内核虚拟地址映射:

映射类型 虚拟地址范围 物理地址范围 页表属性
恒等映射 0x80000000 ~ 0x80008000 + 内核大小 与虚拟地址相同 可读/可执行,禁止写(用于MMU开启瞬间)
内核映射 0xC0000000 ~ 0xC0000000 + 内核大小 0x80000000 ~ 0x80000000 + 内核大小 可读/可写/可执行
DTB映射 临时映射在高端地址 DTB物理地址 可读
关键约束: 在开启MMU的瞬间(执行 __turn_mmu_on),PC指针必须位于恒等映射区域内,否则CPU将立即触发页错误。这是ARM启动代码中最容易出错的环节之一。
早期页表初始化与MMU开启流程 早期页表初始化与MMU开启流程 (head.S) __create_page_tables 建立恒等 + 内核映射 __enable_mmu 设置SCTLR.M位 __mmu_off → __turn_mmu_on PC在恒等映射区 ARMv7-A 短描述符页表格式 (TTBR0) Level 1 页表 (2MB 节映射): [31:20] 节基地址 | [19:0] 节内偏移 恒等映射: 虚拟 0x80000000 → 物理 0x80000000 (可执行) 内核映射: 虚拟 0xC0000000 → 物理 0x80000000 (可读写执行)

3.4 start_kernel 函数分析

start_kernel() 位于 init/main.c,是内核启动流程中架构无关部分的起点。它按严格顺序调用各个子系统的初始化函数,任何顺序错误都可能导致系统崩溃。以下是核心调用序列:

// start_kernel() 核心调用序列 (简化)
asmlinkage __visible void __init start_kernel(void)
{
    char *command_line;
    char *after_dashes;

    // 1. 早期架构初始化
    setup_arch(&command_line);          // 解析DTB,建立最终页表,初始化CPU类型

    // 2. 内存管理初始化
    mm_init();                          // 建立伙伴系统、slab分配器

    // 3. 调度器初始化
    sched_init();                       // 初始化进程调度器

    // 4. 中断与定时器
    init_IRQ();                         // 初始化中断控制器 (GIC)
    init_timers();                      // 初始化定时器子系统

    // 5. 控制台与打印
    console_init();                     // 初始化串口控制台

    // 6. 进程与线程
    fork_init(totalram_pages);          // 初始化进程描述符缓存
    proc_caches_init();                 // 创建proc文件系统缓存

    // 7. 最终初始化
    rest_init();                        // 创建kernel_init和kthreadd进程
}

3.4.1 setup_arch() 关键职责

  • DTB解析:调用 early_init_dt_scan() 扫描设备树,获取内存大小、CPU核数、chosen节点(bootargs)。
  • 页表最终化:在早期恒等映射基础上,建立完整的低端内存映射(PAGE_OFFSET 到 high_memory)。
  • CPU类型检测:读取MIDR寄存器,匹配CPU capability列表。
  • 机器初始化:调用 init_machine() 回调,初始化板级特定设备(如GPIO、时钟控制器)。

3.4.2 rest_init() 与第一个进程

rest_init() 是 start_kernel() 的最后一步,它创建了两个关键内核线程:

进程 PID 职责 后续行为
kernel_init 1 执行用户态init程序 尝试执行 /sbin/init → /etc/init → /bin/init → /bin/sh
kthreadd 2 内核线程守护进程 负责创建所有其他内核线程(如kswapd、flush)
车载场景差异: 在车载系统中,kernel_init 通常不会直接执行标准init程序,而是执行一个轻量级的 initramfs 中的早期用户态程序,该程序负责挂载安全文件系统、启动核心服务(如CAN网关、诊断代理)后,再切换到真正的根文件系统。

3.5 从内核态到用户态:init进程的诞生

kernel_init 线程在完成以下操作后,通过 run_init_process() 切换到用户态:

  1. 解锁文件系统:调用 do_mounts() 挂载根文件系统(从 root= 参数或DTB的 chosen 节点获取)。
  2. 打开控制台:将文件描述符0/1/2重定向到 /dev/console。
  3. 执行init程序:通过 sys_execve() 加载init二进制文件,此时CPU特权级从SVC模式切换到用户模式(ARMv7-A的USR模式或ARMv8-A的EL0)。
常见启动失败点: 在车载系统中,根文件系统挂载失败是最常见的启动故障原因。通常由于存储控制器驱动未正确初始化、MMC时钟配置错误或DTB中 reg 属性与硬件实际地址不匹配导致。建议在 console_init() 之后立即添加早期打印(early_printk)以捕获挂载阶段的错误信息。