4. 内核启动阶段

Bootloader 把内核镜像与设备树交给 CPU 之后,控制权正式移交给 Linux 内核。从这一刻起,系统进入内核启动阶段:先自解压并完成早期体系结构初始化,再解析设备树获得硬件描述,随后按依赖顺序初始化各子系统与驱动,最后通过 initramfs 过渡并挂载真正的根文件系统,启动第一个用户态进程。理解这一阶段,是排查“卡在开机 Logo”“找不到根文件系统”“驱动没加载”等问题的关键。

一、核心原理:内核启动的四段式推进

内核启动并不是一条直线,而是由若干阶段串联而成,每个阶段都有明确的职责边界:

  • 解压与自引导:内核镜像通常是压缩格式(zImage、Image.gz、Image.lz4 等)。解压头(decompressor)先把内核解压到内存,再跳转到解压后的内核入口。此阶段还没有完整的 C 运行环境,依赖汇编与极简 C 代码。
  • 早期初始化:建立临时页表、初始化内存管理、识别 CPU 与 SMP、解析 cmdline 与设备树,为后续驱动初始化准备运行环境。
  • 驱动与子系统初始化:按 initcall 等级依次执行,从核心子系统(时钟、中断、GPIO、电源)到总线(platform、I2C、SPI)再到具体外设驱动。
  • 根文件系统挂载与 init 启动:先挂载 initramfs 作为临时根,加载必要模块、等待存储设备就绪,再切换(switch_root)到真实根文件系统,执行 /sbin/init 或 /init。
内核启动的本质是“先建立最小可用环境,再逐步扩展能力”。任何一步缺失或顺序错误,都会导致启动中断,且往往只留下极少的日志线索。

二、设备树加载:硬件描述的来源

在 ARM/ARM64 等平台上,内核不再硬编码板级信息,而是通过设备树(Device Tree)获得硬件描述。Bootloader 把 DTB 的物理地址通过寄存器或启动协议传给内核,内核在早期解析 DTB,得到 CPU 数量、内存布局、中断控制器、时钟、串口、存储控制器等关键信息。

设备树中的 chosen 节点尤其重要,它承载 bootargs 与 stdout-path:

/ {
    chosen {
        bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait rw";
        stdout-path = &uart0;
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x0 0x80000000 0x0 0x40000000>;
    };
};

如果 DTB 与内核版本不匹配、节点缺失或 bootargs 写错,常见现象是串口无输出、内存识别异常或根文件系统找不到。因此设备树是内核启动阶段最需要先确认的输入之一。

三、驱动初始化:initcall 的分级执行

内核使用 initcall 机制组织驱动初始化顺序,不同等级对应不同的宏,执行顺序由等级决定:

等级 宏 典型用途
early early_initcall 极早期平台代码、调试设施
core core_initcall 核心子系统、总线框架
postcore postcore_initcall 依赖核心但早于普通设备
arch arch_initcall 体系结构相关初始化
subsys subsys_initcall 子系统级初始化
device device_initcall 绝大多数外设驱动
late late_initcall 依赖其他驱动的收尾工作

驱动与设备通过总线匹配:设备树节点被转换为 platform_device,驱动注册后由总线进行 match,匹配成功则调用 probe。若驱动未编入内核、模块未放入 initramfs,或 compatible 字符串不匹配,设备就不会被初始化。

四、initramfs 与根文件系统挂载

initramfs 是一个被内核直接解压到内存的 cpio 归档,作为临时根文件系统。它的作用是“过渡”:在真实存储驱动可能尚未就绪时,先提供一个可运行的用户态环境,完成模块加载、设备等待、文件系统检查,再切换到真实根。

  1. 内核解压 initramfs 到 rootfs(ramfs/tmpfs),并执行其中的 /init。
  2. /init 加载存储、文件系统、加密等模块,等待设备节点出现。
  3. 挂载真实根文件系统到 /root 或类似目录。
  4. 调用 switch_root 切换根目录,释放 initramfs 占用,执行真实根的 /sbin/init。
# 典型内核日志片段
[    0.000000] Booting Linux on physical CPU 0x0
[    0.000000] Linux version 5.10.110
[    0.000000] Machine model: MyBoard
[    0.000000] Memory: 1024MB available
[    0.512000] Unpacking initramfs...
[    0.980000] Freeing initrd memory
[    1.230000] mmc0: new HS200 MMC card
[    1.560000] EXT4-fs (mmcblk0p2): mounted filesystem
[    1.570000] Run /sbin/init as init process
若日志停在 “Unpacking initramfs...” 之后没有后续,优先检查 initramfs 是否损坏、/init 是否可执行、以及根文件系统分区是否存在。

五、案例与排障场景

场景一:设备卡在开机 Logo,串口无输出。此时问题多在内核解压或早期初始化,需确认内核镜像格式、加载地址、DTB 地址与串口配置。

场景二:日志显示 “VFS: Cannot open root device”。说明内核已运行到挂载根文件系统阶段,但 root= 参数指向的设备不存在或驱动未就绪。可检查分区名、rootwait、存储驱动是否编入内核或 initramfs。

场景三:系统能启动但某外设不可用。通常是驱动 probe 失败,可通过 dmesg | grep -i probe 与 /sys/bus/platform/devices 查看匹配情况,重点核对 compatible 与时钟、电源、GPIO 依赖。

常见误区:把 initramfs 当成“可选装饰”。在存储驱动为模块、根分区加密或需要等待设备枚举的场景中,缺少 initramfs 会直接导致根文件系统挂载失败。

六、阶段关系总览

下图把内核启动阶段的关键对象、顺序与依赖关系集中呈现,便于对照日志定位问题:

内核启动阶段:从镜像到根文件系统 内核镜像 zImage / Image.gz 解压头 + 内核 设备树 DTB chosen / memory 中断 / 时钟 / 串口 bootargs console / root rootwait / rw 早期初始化 页表 / 内存 / SMP cmdline 解析 initcall 分级执行:驱动与子系统初始化 early / core postcore / arch subsys device late probe 匹配 initramfs(临时根) cpio 归档 → 解压到 rootfs 执行 /init:加载模块、等待设备 挂载真实根到 /root switch_root 切换根目录 真实根文件系统 ext4 / f2fs / ubifs /sbin/init 或 /init 用户态服务启动 进入 Android init 阶段 switch_root 初始化完成后进入根挂载

把这张图与串口日志对照,可以快速判断当前处于哪个阶段:有解压日志说明镜像与加载地址正确;有设备树解析与内存信息说明 DTB 正常;有 initcall 与 probe 日志说明驱动在推进;出现 initramfs 与 switch_root 日志则说明已接近用户态。掌握这条链路,后续分析 Android init 与属性服务时就有了清晰的起点。