4. 内核解压与入口:vmlinuz 与 zImage 的区别、自解压过程、内核入口函数 start_kernel

当引导加载程序(如 U-Boot、GRUB)将内核映像加载到内存并跳转到入口点后,真正的内核启动之旅才刚刚开始。对于压缩后的内核映像,首先需要经历一个关键的自解压阶段,然后才能进入 C 语言编写的内核主入口 start_kernel。本章将深入剖析压缩内核映像的格式差异、自解压机制的实现细节,以及 start_kernel 如何初始化整个操作系统。

4.1 vmlinuz 与 zImage:压缩内核的两种面孔

在 Linux 内核的构建产物中,vmlinux 是未经压缩的原始 ELF 内核映像,体积较大。而 vmlinuz 和 zImage 都是经过压缩的内核映像,它们的主要区别在于压缩算法、加载地址限制以及历史演进。

特性 vmlinuz zImage
全称 Virtual Memory LINUX (Compressed) zImage (Compressed Linux Image)
压缩算法 gzip / LZMA / XZ / LZO / LZ4 gzip (传统)
加载地址限制 无限制(可重定位) 低端内存(通常 < 16MB)
解压代码位置 内核映像头部 内核映像头部
适用架构 x86, ARM, RISC-V 等 ARM (传统), x86 (旧版)
现代内核默认 是 否(已被 vmlinuz 取代)
关键区别: 现代 Linux 发行版和嵌入式系统普遍使用 vmlinuz,因为它支持多种压缩算法和重定位,而 zImage 是早期 ARM 和 x86 平台的遗留格式,受限于低端内存地址。

4.2 自解压过程:从压缩数据到可执行内核

无论是 vmlinuz 还是 zImage,它们的头部都包含一段自解压代码(decompressor stub)。这段代码负责将压缩的内核数据解压到指定的内存地址,然后跳转到解压后的内核入口。

4.2.1 自解压流程

  1. 入口点跳转: 引导加载程序跳转到压缩内核的入口地址(通常是 start 或 _start 标签)。
  2. 硬件初始化(最小集): 设置栈指针、清除 BSS 段、保存引导参数(如设备树、ATAG)。
  3. 解压算法选择: 根据编译时选择的算法(gzip/LZMA/XZ 等),调用对应的解压函数。
  4. 解压执行: 将压缩数据从 input_data 区域解压到 output 目标地址(通常为 0x100000 或更高)。
  5. 重定位(可选): 如果内核支持重定位,解压后可能需要进行地址修正。
  6. 跳转到解压后内核: 执行 call start_kernel 或 b start_kernel,进入 C 语言世界。
性能提示: 自解压阶段是启动时间中不可忽视的一部分。在嵌入式系统中,使用 LZ4 或 LZO 等快速解压算法可以显著减少解压时间,代价是压缩率较低(映像体积略大)。

4.2.2 自解压阶段的内联 SVG 流程图

内核自解压流程 内核自解压流程 引导加载程序跳转 最小硬件初始化 (栈/BSS/参数) 选择解压算法 (gzip/LZMA/LZ4) 执行解压 (input_data → output) 重定位 (可选) 跳转到 start_kernel (C 入口) 说明 自解压代码位于压缩内核头部 解压目标地址通常为 0x100000 解压完成后释放头部代码占用的内存

4.3 内核入口函数 start_kernel:操作系统的初始化起点

start_kernel 是 Linux 内核 C 语言部分的入口函数,位于 init/main.c 中。它负责初始化内核的所有核心子系统,是操作系统从“汇编引导”过渡到“C 语言管理”的关键转折点。

4.3.1 start_kernel 的主要初始化步骤

步骤 函数/模块 功能描述
1 setup_arch() 架构相关初始化(页表、内存布局、中断控制器)
2 trap_init() 初始化异常向量表(中断、系统调用入口)
3 mm_init() 内存管理子系统初始化(伙伴系统、SLAB 分配器)
4 sched_init() 进程调度器初始化(就绪队列、调度类)
5 init_IRQ() 中断描述符表初始化
6 softirq_init() 软中断和 tasklet 初始化
7 time_init() 时钟源和时间子系统初始化
8 console_init() 控制台初始化(printk 开始可见)
9 vfs_caches_init() 虚拟文件系统缓存初始化
10 rest_init() 创建 init 进程(PID=1),进入 idle 循环
启动时间关键路径: 在 start_kernel 中,setup_arch()、mm_init() 和 console_init() 通常是耗时最长的三个阶段。优化时可以通过内核配置裁剪不必要的架构初始化代码,或使用早期控制台(early console)来提前输出日志。

4.3.2 start_kernel 初始化流程的内联 SVG 架构图

start_kernel 初始化流程 start_kernel 核心初始化流程 架构初始化 setup_arch() trap_init() init_IRQ() 核心子系统 mm_init() sched_init() time_init() 设备与驱动 console_init() vfs_caches_init() rest_init() start_kernel 执行顺序 架构初始化 → 核心子系统 → 设备与驱动 最终调用 rest_init() 创建 init 进程 (PID=1)

4.4 解压与入口阶段的启动时间测量

在嵌入式系统中,从引导加载程序跳转到压缩内核入口,到 start_kernel 执行完毕,通常占用 50ms ~ 500ms 不等(取决于 CPU 频率、压缩算法和内核大小)。以下是常用的测量方法:

  • 使用硬件定时器: 在自解压代码入口处读取一个硬件计数器(如 ARM 的 CNTPCT),在 start_kernel 开头再次读取,计算差值。
  • 使用 printk 时间戳: 在 start_kernel 中启用 printk.time=1 内核参数,通过日志时间戳估算解压耗时。
  • 使用 bootgraph 工具: 结合 initcall_debug 和 bootgraph.pl 脚本,生成启动时间分布图。
优化建议: 如果自解压时间过长,可以尝试以下方法:
• 使用 LZ4 或 LZO 压缩算法(解压速度比 gzip 快 2-4 倍)。
• 将内核映像存储在更快的存储介质(如 SPI NOR Flash → NAND Flash)。
• 如果硬件支持,使用 XIP(eXecute In Place)技术,跳过解压阶段。

4.5 总结