系统启动全流程:从 BIOS/UEFI、GRUB 到内核与 systemd

从按下电源键到登录界面,系统要经历固件初始化、Bootloader、内核解压、驱动加载、initramfs、systemd 初始化多个阶段。本文沿真实启动路径逐步解剖:BIOS 与 UEFI 的差异、Secure Boot 信任链、GRUB 如何加载内核、内核从实模式到 start_kernel 的旅程、initramfs 为什么存在,以及 systemd 的并行启动与依赖图,最后给出用 systemd-analyze 做启动性能优化的完整方法。

从按下电源键到屏幕上出现登录提示,现代系统要在短短几秒内完成一次"接力赛":固件初始化硬件、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)。它依次做:

  1. setup_arch:解析启动参数、建立页表、初始化内存布局。
  2. 初始化调度器(sched_init)、内存管理(mm_init)。
  3. 初始化中断(init_IRQ)、定时器(timekeeping)。
  4. init_task 创建 PID 0 的 idle 任务。
  5. 调用 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 的工作流程

  1. 内核把 initramfs 解压为根文件系统(rootfs),PID 1 临时运行其中的 init。
  2. 加载根文件系统所需驱动(通过 udev 探测硬件)。
  3. 组装根设备(LVM 激活、dm-crypt 解密、RAID 组装)。
  4. 挂载真实根到 /root。
  5. 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 initsystemd
启动方式串行按脚本顺序并行按依赖图
单位抽象脚本(/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 保障启动链可信。这些都是可以把抽象问题变成清单问题的工程能力。


延伸阅读

  1. UEFI 规范(uefi.org)与 Linux 内核 Documentation/x86/boot.rst(启动协议)
  2. GRUB 2 官方手册:gnu.org/software/grub/manual
  3. systemd 官方文档:systemd.io 的 bootup 与 unit 章节
  4. initramfs 机制:内核源码 init/initramfs.c 与 Documentation/admin-guide/initrd.rst
  5. 《Linux Kernel Development》初始化章节(结合 https://plumephp.com/os-kernel-module/ 的编译实验)

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「os」更多文章

  1. 虚拟化与容器隔离:从 KVM/QEMU 到 namespace 与 cgroup
  2. 操作系统安全加固与可信计算:LSM、内核加固、TPM 与容器逃逸防护
  3. 实时操作系统:RTOS 内核设计、优先级反转与 Linux PREEMPT_RT