从按下电源键到屏幕上出现登录提示,现代系统要在短短几秒内完成一次"接力赛":固件初始化硬件、Bootloader 加载内核镜像、内核解压并自举、挂载根文件系统、启动 PID 1(systemd)并联调所有服务。任何一个环节出问题,都会以"起不来"或"起得慢"的形式暴露出来。
本文沿真实启动路径逐步解剖这段旅程:BIOS/UEFI 固件层、GRUB 引导加载、内核解压与 start_kernel、initramfs 的临时根文件系统、systemd 的并行初始化,最后落地到启动性能优化。它帮助你把"开机慢"这种玄学问题变成可定位、可优化的工程问题。它与 https://plumephp.com/os-overview/(内核态/用户态)和 https://plumephp.com/os-kernel-module/(内核编译)互为补充。
启动全景:一次接力赛
先建立整体地图。一次典型 Linux 启动分七个阶段:
电源开启
│ ① 固件:BIOS/UEFI(POST + 硬件初始化)
▼
Bootloader(GRUB)
│ ② 从磁盘读取并加载内核镜像 + initramfs
▼
内核:实模式 → 保护模式 → 解压
│ ③ 内核自举,初始化硬件子系统
▼
initramfs(临时根文件系统)
│ ④ 加载根文件系统所需的驱动
▼
switch_root:切换到真正的根
│ ⑤ 挂载真实根文件系统
▼
PID 1 = systemd
│ ⑥ 并行启动各服务
▼
getty / 登录界面
每一阶段"只做自己该做的事,然后把接力棒交给下一棒"。
BIOS 与 UEFI:两种固件世界
传统 BIOS 启动(Legacy Boot)
老式 BIOS 的流程固定:POST(Power-On Self Test)检查硬件 → 扫描启动设备 → 读取设备的第一个扇区(MBR,512 字节)→ 把控制权交给其中的引导代码。MBR 里只有 446 字节的引导程序空间,装不下复杂逻辑,于是引导代码再去加载 GRUB 的下一阶段。
BIOS → MBR(引导代码)→ GRUB stage1.5/stage2 → /boot/grub
传统 BIOS 的局限:MBR 分区表只支持 4 个主分区、磁盘最大 2TB;启动链没有任何完整性校验——恶意软件可以在引导早期植入 rootkit。
UEFI 启动
UEFI 取代 BIOS 解决了上述问题:
- GPT 分区表:支持 128+ 分区、TB 级磁盘、冗余分区表。
- UEFI 系统分区(ESP):一个 FAT 格式的专用分区,存放
.efi引导程序(如grubx64.efi、systemd-boot)。 - Boot Manager:固件根据
BootOrder配置直接加载 ESP 中的.efi程序,不再依赖 MBR 引导链。 - Secure Boot:固件只执行有合法签名的
.efi,防止预启动阶段的恶意替换。
UEFI Boot Manager → ESP/EFI/BOOT/BOOTX64.EFI → GRUB(.efi) → 内核
Secure Boot:信任链
Secure Boot 依赖"链式签名验证":固件验证 Bootloader 签名,Bootloader 验证内核签名,内核再验证加载的模块签名。签名来自 UEFI 数据库(db、dbx、KEK、PK)。Linux 发行版签名的 shim.efi + grubx64.efi 就是这条链的典型实现。它和 https://plumephp.com/os-security-hardening-trust/ 中讨论的 TPM 可信启动(信任度量而非信任厂商)形成两种不同哲学。
# 查看当前引导模式
[ -d /sys/firmware/efi ] && echo "UEFI 模式" || echo "传统 BIOS 模式"
# 查看 Secure Boot 状态
mokutil --sb-state
GRUB:Bootloader 的职责
三阶段结构(传统 GRUB 2)
boot.img(MBR,446B) → core.img(含文件系统驱动) → /boot/grub/grub.cfg
boot.img写在 MBR,职责只有一个:加载后续代码。core.img内置了读常见文件系统的驱动,因此能读懂/boot分区。- 最终加载
grub.cfg配置,进入菜单,读取内核vmlinuz与initramfs镜像。
它到底做了什么?
GRUB 加载内核时做的事非常"少":把 vmlinuz 与 initramfs.img 读入内存(并放到内核约定好的物理地址),设置内核启动参数(root=、quiet、splash 等),然后跳转到内核入口。内核启动参数就在这注入:
# /boot/grub/grub.cfg 中常见内核行
linux /vmlinuz-6.1.0 root=/dev/nvme0n1p2 ro quiet splash
initrd /initrd.img-6.1.0
# 查看当前内核命令行
cat /proc/cmdline
启动参数的 root= 尤其关键:它告诉内核"真正的根文件系统在哪"。但这里有个先有鸡还是先有蛋的问题——要挂载 /dev/nvme0n1p2 需要 NVMe 驱动,而驱动在根文件系统里。解法就是 initramfs。
内核启动:从实模式到 start_kernel
实模式与保护模式
内核镜像 vmlinuz 的头部是一个实模式(16 位)的小引导块,它负责最小的初始化:设置栈、检查 CPU、把内核解压目标放到正确位置,然后切换到保护模式(32/64 位)。
; 内核实模式入口(arch/x86/boot/header.S 语义)
; 1. 切换到保护模式
movl %cr0, %eax
orl $0x1, %eax ; 设置 PE 位
movl %eax, %cr0
; 2. 远跳转到 32 位解压代码
ljmp $__BOOT_CS, $startup_32
解压
vmlinuz 里的内核主体是压缩的(CONFIG_KERNEL_XZ 等)。解压代码 decompress_kernel 把真正的内核解压到高地址,然后跳转到 startup_64(64 位入口)。
start_kernel:一切内核初始化的源头
start_kernel() 是内核初始化的总入口(init/main.c)。它依次做:
setup_arch:解析启动参数、建立页表、初始化内存布局。- 初始化调度器(
sched_init)、内存管理(mm_init)。 - 初始化中断(
init_IRQ)、定时器(timekeeping)。 init_task创建 PID 0 的 idle 任务。- 调用
rest_init→ 创建 PID 1(init 进程)与 PID 2(kthreadd)。
关键点:在 start_kernel 阶段,系统只有一个线程的"世界",随后才分化出进程树。这部分机制对应 https://plumephp.com/os-process-thread/ 中 PCB/调度实体的初始化源头。
start_kernel()
├─ setup_arch() 架构相关:内存、CPU、启动参数
├─ trap_init()/init_IRQ() 中断向量表
├─ sched_init() 调度器
├─ mm_init() 内存管理(buddy/slab)
├─ rest_init()
│ ├─ kernel_init() → PID 1(最终 exec systemd)
│ └─ kthreadd() → PID 2(内核线程守护)
initramfs:为什么需要临时根文件系统?
先有鸡还是先有蛋
要挂载真实根,必须先有能读真实根文件系统的驱动(NVMe、LVM、dm-crypt、网络文件系统……)。但驱动模块存在根文件系统里。initramfs 解决这个循环:它是一个极小但完整的临时根文件系统,打包进内存(CONFIG_BLK_DEV_INITRD),包含必要的内核模块、udev/systemd 及挂载工具。
# 查看当前 initramfs 内容
lsinitramfs /boot/initrd.img-$(uname -r) | head -20
# 会看到 lib/modules/...(驱动)、sbin/init、usr/lib/systemd/systemd-udevd 等
initramfs 的工作流程
- 内核把 initramfs 解压为根文件系统(rootfs),PID 1 临时运行其中的
init。 - 加载根文件系统所需驱动(通过 udev 探测硬件)。
- 组装根设备(LVM 激活、dm-crypt 解密、RAID 组装)。
- 挂载真实根到
/root。 - switch_root:切换根、运行真实根里的
/sbin/init(systemd),完成"大权交接"。
# 手动重建 initramfs(升级内核后常用)
update-initramfs -u -k $(uname -r) # Debian/Ubuntu
dracut -f # RHEL/Fedora
如果这里出错,最常见的表现就是卡在 “udevadm settle timed out” 或 “Failed to mount /dev/...",需要进恢复模式排查根设备参数。
systemd 初始化:PID 1 的并行世界
真实根的 PID 1 是 systemd。它接管所有服务的启动,核心设计是按依赖并行启动。
Unit 与依赖图
systemd 把每个"被管理的东西"抽象为 Unit(.service、.mount、.socket、.target 等)。Unit 之间用 After=、Requires=、Wants= 声明依赖,systemd 据此构建一个有向图,无依赖关系的服务同时启动。
# 一个 service unit 示例
[Unit]
Description=My API Server
After=network-online.target
Requires=postgresql.service
[Service]
ExecStart=/usr/bin/my-api-server
Restart=on-failure
User=www-data
[Install]
WantedBy=multi-user.target
multi-user.target 是启动的"汇合点”:它聚合了系统进入多用户模式所需的所有单元。default.target 决定系统最终启动到哪个 target(图形界面 → graphical.target)。
默认 target 与启动入口
systemctl get-default # 查看默认 target
systemctl set-default graphical.target
systemctl list-units --type=service --state=running | wc -l # 正在运行的服务数
与 SysV init 的对比
| 维度 | SysV init | systemd |
|---|---|---|
| 启动方式 | 串行按脚本顺序 | 并行按依赖图 |
| 单位抽象 | 脚本(/etc/init.d/) | Unit 文件(声明式) |
| 日志 | 各写各的 | journald 统一采集 |
| 状态 | 无 | systemctl status 精确 |
SysV 串行启动在 100+ 服务的现代系统上会非常慢,systemd 并行是秒级开机的关键之一。
启动性能优化:把"开机慢"变成可测量
systemd-analyze:三把工具
# 1) 总时间统计
systemd-analyze
# 输出示例:Startup finished in 2.347s (kernel) + 8.213s (initrd) + 5.831s (userspace) = 16.391s
# 2) 关键链:最慢的是哪几个服务
systemd-analyze critical-chain
# 输出类似:
# graphical.target @5.831s
# └─multi-user.target @5.831s
# └─docker.service @5.826s
# └─containerd.service @5.692s
# └─... @...
# 3) 热力图:哪些服务占用了多少时间
systemd-analyze plot > boot.svg # 用浏览器打开,一眼看出串行瓶颈
常见优化手段
# 1) 缩短 initrd 阶段:禁用不需要的驱动/固件
# 检查 initramfs 大小,去掉未使用的硬件模块
# 2) 延迟非关键服务:After=network.target 前的大服务可改为 socket 激活
systemctl enable docker.socket # 按需激活,首次访问才拉起来
# 3) 禁用开机自启的"僵尸"服务
systemctl list-unit-files --state=enabled | grep service
systemctl disable some-heavy.service
# 4) 缩短 udev 等待
# 检查是否有设备 missing 导致 udevadm settle 超时(常见于外接设备未接)
# 5) 固件层面:启用 UEFI Fast Boot、关闭无用的自检项
如何判断瓶颈在哪一段?
systemd-analyze 的三段数字已经给出分界:kernel 段(固件+内核)、initrd 段(驱动加载)、userspace 段(服务)。哪段慢就主攻哪段——kernel 段慢通常是固件/UEFI 配置或内核自检,initrd 段慢通常是驱动/模块过多,userspace 段慢就是服务依赖图问题。
结语
系统启动是一个"信任与接力"的过程:固件把控制权交给 Bootloader,Bootloader 把内核交到内存,内核自举后挂起临时根,再切换回真实根交给 systemd。每一层只暴露最小的接口给下一层,也把下一层需要的最小资源准备好。
对运维与系统工程师来说,理解启动流程的最大价值,是面对"起不来"和"起得慢"时不再靠猜:看一眼 dmesg 知道内核停在哪、用 systemd-analyze critical-chain 找到最慢的服务、检查 /proc/cmdline 确认 root 参数、用 Secure Boot/TPM 保障启动链可信。这些都是可以把抽象问题变成清单问题的工程能力。
延伸阅读
- UEFI 规范(
uefi.org)与 Linux 内核Documentation/x86/boot.rst(启动协议) - GRUB 2 官方手册:
gnu.org/software/grub/manual - systemd 官方文档:
systemd.io的 bootup 与 unit 章节 - initramfs 机制:内核源码
init/initramfs.c与Documentation/admin-guide/initrd.rst - 《Linux Kernel Development》初始化章节(结合 https://plumephp.com/os-kernel-module/ 的编译实验)
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。