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