4. 内核启动阶段

当引导程序(GRUB、U-Boot 或 UEFI)把控制权交给内核镜像后,Linux 启动进入最“黑盒”的一段:从自解压到 start_kernel(),再到设备探测与模块加载,最终交出 init 进程。这一段通常只占整体启动时间的 20%~40%,但在嵌入式、云主机、虚拟机场景中,往往是优化收益最直接的部分。理解它的阶段划分与耗时点,是后续用 initcall_debug、bootgraph、ftrace 做定量优化的前提。

一、核心原理:内核启动的四个连续阶段

内核启动并非“一步到位”,而是严格串行的四个阶段,前一阶段未完成,后一阶段无法开始:

  • 解压阶段(Decompression):引导程序加载的是压缩内核(vmlinuz / zImage),内核头部自带解压器,将自身解压到内存并跳转到解压后的入口。
  • 早期初始化(Early Init):建立临时页表、初始化内存管理、解析设备树或 boot_params、设置控制台,随后进入 start_kernel()。
  • 设备探测与子系统初始化(Probe & Initcall):按 initcall 等级依次执行驱动、总线、文件系统、网络等初始化函数。
  • 模块加载与根文件系统挂载(Module & Rootfs):加载 initramfs/initrd,按需加载内核模块,挂载真实根文件系统,最后启动 init。

这四个阶段在时间轴上首尾相接,任何一段的阻塞都会直接推迟用户空间可见的启动完成时间。

内核启动四阶段时间轴与典型耗时点 时间 ① 解压阶段 vmlinuz → 解压器 → 解压后内核入口 耗时点:压缩算法 镜像体积 / 内存带宽 ② 早期初始化 页表 / 内存管理 设备树解析 / 控制台 耗时点:DTB 解析 early console 初始化 ③ 设备探测/initcall 总线 / 驱动 / 子系统 按 initcall 等级串行 耗时点:驱动 probe 超时等待 / 固件加载 ④ 模块/根文件系统 initramfs / initrd 模块加载 / 挂载 rootfs 耗时点:模块依赖 根设备探测等待 关键观测点与工具 • 解压耗时:内核命令行 printk.time=1 观察第一条时间戳 • initcall 耗时:initcall_debug 打印每个 initcall 的起止时间 • 驱动 probe:/sys/kernel/debug/devices_deferred 查看延迟探测设备 • 整体可视化:scripts/bootgraph.pl 生成启动时序图 • 函数级追踪:ftrace function_graph 定位热点函数 • 模块加载:dmesg | grep -i module 查看加载顺序

二、方法流程:如何定位内核阶段耗时

内核启动优化的第一步不是“改代码”,而是“量时间”。推荐按以下顺序采集数据:

  1. 在引导参数中加入 initcall_debug 与 printk.time=1,让每个 initcall 都带时间戳。
  2. 抓取 dmesg,用 bootgraph.pl 生成可视化时序图,快速识别长耗时 initcall。
  3. 对可疑驱动,用 ftrace 的 function_graph 追踪其内部函数调用链。
  4. 检查 /sys/kernel/debug/devices_deferred,确认是否存在因依赖未就绪而反复延迟探测的设备。
# 采集 initcall 耗时
dmesg | grep -E "initcall|calling" | head -50

# 生成启动时序图
dmesg | ./scripts/bootgraph.pl > boot.svg

# 追踪某个驱动的 probe 函数
echo 1 > /sys/kernel/debug/tracing/events/ftrace/function/enable
echo 'pci_probe*' > /sys/kernel/debug/tracing/set_ftrace_filter
cat /sys/kernel/debug/tracing/trace_pipe

三、案例场景:一次真实的 1.8 秒内核耗时优化

某 ARM64 嵌入式设备冷启动总耗时 6.2 秒,其中内核阶段占 2.4 秒。通过 initcall_debug 发现:

initcall 阶段 耗时 主要原因 优化动作
解压阶段 0.35 s gzip 压缩率低、镜像 28 MB 改用 zstd 压缩,镜像降至 19 MB
early_initcall 0.12 s DTB 节点过多,解析慢 裁剪未使用设备树节点
device_initcall 1.10 s USB 控制器 probe 等待 PHY 就绪 改为模块按需加载,延迟 0.9 s
late_initcall 0.23 s 网络协议栈初始化 保留,属必要初始化

优化后内核阶段从 2.4 秒降至 1.1 秒,整体启动时间缩短约 21%。关键结论是:设备探测阶段的驱动 probe 往往是最大变量,而解压阶段则受镜像体积与压缩算法直接影响。

四、误区与复盘

误区一:只看总时间,不看阶段分布。很多团队直接对比“优化前后总启动时间”,却不知道时间花在解压还是 probe。没有阶段数据,优化就是盲猜。

误区二:把所有驱动都编译进内核。编译进内核意味着必须在启动时同步 probe,无法并行、无法延迟。对非关键外设,改为模块加载通常能显著缩短内核阶段。

误区三:忽略 initramfs 的重复解压。initramfs 本身是压缩的 cpio,内核解压一次,用户空间可能再解压一次,造成重复开销。

复盘建议:建立“内核启动基线”,每次内核配置或驱动变更后重新采集 initcall_debug 数据,对比各阶段耗时变化。把 bootgraph.pl 生成的时序图纳入 CI 产物,让性能回归可视化、可追踪。

内核启动阶段的优化核心在于“先测量、再定位、后裁剪”。解压阶段关注压缩算法与镜像体积,早期初始化关注设备树规模,设备探测关注驱动 probe 的同步阻塞,模块加载关注依赖顺序与 initramfs 设计。只有把四个阶段拆开量化,才能把内核启动时间从“不可控”变成“可优化”。