2. Linux启动流程全景:从BIOS/UEFI到init进程的完整链路
理解Linux系统的启动流程是内核调试与根文件系统挂载分析的基础。本章将沿着从硬件上电到用户空间首个进程启动的完整链路,逐一剖析每个阶段的职责、数据流与关键交接点。掌握这一全景图,后续分析initramfs机制与根文件系统挂载时才能精准定位问题所在。
2.1 启动流程总览:六个核心阶段
Linux启动可划分为六个逻辑阶段,每个阶段由不同的组件主导,并完成特定的初始化任务。下表总结了各阶段的起止点与核心产出:
| 阶段 | 起始事件 | 结束事件 | 核心组件 | 关键产出 |
|---|---|---|---|---|
| 1. 固件阶段 | CPU复位 | Bootloader加载 | BIOS/UEFI | 硬件自检、启动设备选择 |
| 2. Bootloader阶段 | MBR/GPT引导 | 内核解压入口 | GRUB2 / U-Boot | 内核映像与initramfs加载到内存 |
| 3. 内核早期初始化 | start_kernel() | init进程启动 | 内核自身 | 架构初始化、内存管理、调度器 |
| 4. initramfs阶段 | rootfs挂载 | 根文件系统切换 | initramfs中的init | 驱动加载、块设备就绪 |
| 5. 根文件系统挂载 | pivot_root() | 根文件系统就绪 | 内核VFS层 | 真正的根文件系统挂载 |
| 6. init进程启动 | execve(/sbin/init) | 用户空间初始化 | systemd / SysVinit | 系统服务启动 |
关键认知: 启动流程的本质是“控制权移交”——从固件到Bootloader,从Bootloader到内核,从内核到initramfs,最终到根文件系统上的init进程。每一次移交都伴随着执行环境的质变。
2.2 固件阶段:BIOS与UEFI的引导决策
CPU上电复位后,首先执行的是固件代码。传统BIOS与现代UEFI在引导方式上存在显著差异,直接影响内核加载路径。
2.2.1 BIOS引导流程
- POST(上电自检): 检测CPU、内存、基本I/O设备。
- MBR读取: 读取启动磁盘的第一个扇区(512字节),检查最后两个字节是否为
0x55AA。 - Bootloader加载: 将MBR中的第一阶段引导代码(446字节)加载到内存
0x7C00处并执行。
2.2.2 UEFI引导流程
- 安全启动(Secure Boot): 验证Bootloader签名的合法性。
- EFI系统分区(ESP): 读取FAT32格式的ESP分区中的
.efi文件。 - Boot Manager: 根据NVRAM中的启动项顺序加载Bootloader(如
grubx64.efi)。
嵌入式场景提示: 在ARM/嵌入式平台中,固件阶段通常由U-Boot SPL(Secondary Program Loader)完成,其作用等价于BIOS的MBR读取阶段,但支持从NAND Flash、SD卡、网络等多种介质启动。
2.3 Bootloader阶段:内核与initramfs的加载
Bootloader的核心职责是将内核映像(vmlinuz)与initramfs从存储介质加载到内存,并设置好启动参数后跳转到内核入口。以GRUB2为例,其工作流程如下:
- Stage1.5/Stage2加载: 从文件系统(ext4、Btrfs等)读取GRUB模块。
- 配置文件解析: 读取
/boot/grub/grub.cfg,显示启动菜单。 - 内核加载: 将vmlinuz解压到内存地址
0x100000(1MB处)。 - initramfs加载: 将initramfs映像加载到内存,并将物理地址通过
boot_params传递给内核。 - 设置启动参数: 填充
setup_header中的cmd_line_ptr,指向内核命令行。 - 跳转到保护模式: 执行
ljmp指令进入32位保护模式,跳转到startup_32。
常见陷阱: 如果initramfs映像过大(超过内存可用连续区域),Bootloader可能加载失败。GRUB2在加载大型initramfs时可能触发“Out of memory”错误,此时需要调整
GRUB_CMDLINE_LINUX中的crashkernel参数或减小initramfs体积。
2.4 内核早期初始化:从start_kernel到rootfs
内核入口startup_32(或startup_64)完成架构相关的初始化后,调用start_kernel()——这是内核启动的C语言入口点。以下是最关键的初始化序列:
| 调用顺序 | 函数 | 初始化内容 | 对后续的影响 |
|---|---|---|---|
| 1 | setup_arch() |
解析启动参数、初始化页表、设置内存布局 | 确定物理内存映射与DMA区域 |
| 2 | trap_init() |
设置中断描述符表(IDT) | 使能异常与中断处理 |
| 3 | mm_init() |
初始化内存管理器(伙伴系统、slab分配器) | 动态内存分配可用 |
| 4 | sched_init() |
初始化调度器、创建init_task | 多任务调度基础就绪 |
| 5 | vfs_caches_init() |
初始化VFS(虚拟文件系统)缓存 | 文件系统操作可用 |
| 6 | populate_rootfs() |
解压并挂载initramfs到rootfs | 临时根文件系统就绪 |
关键转折点:
populate_rootfs() 是启动流程中第一次与文件系统相关的操作。内核将嵌入的initramfs(或Bootloader传递的initramfs)解压到内存中的tmpfs,并挂载为rootfs。此时,内核拥有了一个可读写的临时根文件系统。
2.5 initramfs阶段:临时根文件系统的作用
在populate_rootfs()完成后,内核调用kernel_init(),该函数会尝试执行initramfs中的/init程序。initramfs是一个基于tmpfs的内存文件系统,其核心使命是:
- 加载必要的内核模块: 特别是存储控制器驱动(如NVMe、AHCI、virtio-blk)。
- 发现根设备: 扫描块设备,识别根文件系统所在的分区。
- 执行根文件系统切换: 通过
pivot_root()或switch_root()将根文件系统从initramfs切换到真正的存储设备。
调试技巧: 在initramfs的
/init脚本中添加echo或ls命令,可以输出当前可用的块设备列表。例如:ls -l /dev/sd* /dev/nvme* /dev/vd*,用于排查根设备未被发现的问题。
2.6 根文件系统挂载:pivot_root与switch_root
initramfs中的init进程完成驱动加载和设备发现后,需要将根文件系统切换到真正的存储设备上。这一过程涉及两个关键系统调用:
| 系统调用 | 函数原型 | 行为 | 使用场景 |
|---|---|---|---|
pivot_root() |
int pivot_root(const char *new_root, const char *put_old) |
将当前根文件系统移动到put_old目录,并将new_root设置为新根 |
内核2.6时代的标准做法 |
switch_root() |
int switch_root(const char *newroot, const char *init, ...) |
删除initramfs中的所有文件,执行chroot到新根,并执行新的init |
现代busybox initramfs的标准做法 |
关键区别:
pivot_root() 保留initramfs在put_old目录中,需要手动卸载;switch_root() 直接销毁initramfs,更干净且节省内存。现代initramfs(如dracut生成的)默认使用switch_root。
2.7 init进程启动:用户空间的起点
根文件系统挂载成功后,内核执行run_init_process(),按以下顺序查找并执行init程序:
/sbin/init/etc/init/bin/init/bin/sh(作为fallback)
一旦init进程启动(PID=1),内核的启动职责结束,控制权完全移交给用户空间。init进程负责:
- 读取
/etc/inittab(SysVinit)或/etc/systemd/system/default.target(systemd)。 - 启动系统服务、挂载
/etc/fstab中的文件系统、配置网络。 - 启动getty提供登录提示符。
全景回顾: 从BIOS/UEFI到init进程,整个链路经历了“硬件初始化 → Bootloader加载 → 内核早期初始化 → initramfs过渡 → 根文件系统挂载 → 用户空间init”六个阶段。每个阶段都是下一个阶段的前提,任何一个环节的失败都会导致系统无法正常启动。后续章节将深入initramfs的内部构造与根文件系统挂载的详细机制。