Linux 基础体系 · 第 3/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux 启动完整流程:Firmware、Bootloader、Kernel、initramfs 与 systemd

Linux 启动不是一个单独程序完成的动作,而是一条逐级交接控制权、逐步增加硬件和用户态能力的链:

flowchart TD
    A[上电或复位] --> B[Firmware<br/>BIOS 或 UEFI]
    B --> C[Bootloader<br/>GRUB、systemd-boot 或 UEFI Stub]
    C --> D[加载 Kernel、initramfs、内核命令行]
    D --> E[Kernel 解压、初始化 CPU/内存/设备]
    E --> F[initramfs 中的 /init]
    F --> G[发现并挂载真实根文件系统]
    G --> H[switch_root 或 pivot_root]
    H --> I[真实根文件系统中的 /sbin/init]
    I --> J[systemd PID 1]
    J --> K[挂载、设备、网络、服务、登录界面]

这条链并不要求每台机器严格使用相同组件。例如:

  • 固件可以直接加载 Linux 内核,而不经过 GRUB。
  • Bootloader 可以是 GRUB、systemd-boot、U-Boot,也可以是 Linux 内核的 EFI stub。
  • initramfs 可以由 dracut、mkinitcpio、initramfs-tools 等工具生成。
  • PID 1 可以是 systemd,也可以是 OpenRC、BusyBox init 或其他实现。
  • systemd 还可以在 initramfs 阶段运行,但这不等于它已经成为真实根文件系统中的最终 PID 1。

因此,理解启动流程时,应该区分两件事:

  1. 规范或接口保证:例如 UEFI 如何加载 EFI 程序、Linux 如何接收内核命令行。
  2. 主流实现和发行版习惯:例如 Ubuntu 常见 GRUB、Debian 常见 initramfs-tools、Fedora 常见 dracut 和 systemd。

一、先建立四个边界:Firmware、Bootloader、Kernel 和用户态

1. Firmware:硬件上电后的第一段软件

Firmware 是保存在主板、设备或非易失性存储中的固件。PC 上通常指:

  • 传统 BIOS;
  • 现代 UEFI;
  • 某些服务器上的 BMC、Option ROM 等配套固件。

CPU 复位后,不会直接执行磁盘上的 Linux。它首先从预先定义的固件入口开始执行。Firmware 负责完成最早期的环境建立,例如:

  • 设置 CPU 的基本执行环境;
  • 初始化一部分内存控制器和总线;
  • 发现存储设备、键盘、显示设备或网络设备;
  • 按启动顺序寻找可启动介质;
  • 根据启动模式加载下一阶段程序。

Firmware 通常不负责完整加载 Linux 用户态,也不负责启动 systemd。它的职责是把机器从“刚复位、硬件尚未可用”的状态推进到“可以加载一个启动程序”的状态。

2. Bootloader:选择并加载内核

Bootloader 的核心职责是把内核启动所需的数据放到内存中,并把控制权交给内核。典型数据包括:

  • Linux 内核映像;
  • initramfsinitrd
  • Linux 内核命令行;
  • 在某些架构上提供的设备树;
  • 启动参数和固件接口信息。

GRUB 还可以提供启动菜单、多个内核版本选择、恢复模式入口和命令行编辑能力。systemd-boot 则更简单,通常从 EFI System Partition 中读取内核和 initramfs。某些内核启用了 EFI stub 后,UEFI 可以把内核视为 EFI 可执行文件直接启动。

Bootloader 一般不会把所有硬件初始化都重新做一遍。它主要依赖 Firmware 已经提供的启动环境,然后完成内核加载和参数传递。

3. Kernel:从硬件控制权进入内核管理模型

Linux Kernel 接管控制权后,开始建立自己的抽象和资源管理机制,包括:

  • CPU 和调度器;
  • 页表、虚拟内存和内存分配器;
  • 中断控制器;
  • 设备模型和驱动;
  • VFS 与文件系统;
  • 网络协议栈;
  • 内核线程;
  • 用户态进程执行环境。

内核不是“启动完就退出”的加载器。它会持续运行,处理系统调用、异常、中断、调度和设备事件。用户态程序通过系统调用进入内核,内核再通过驱动和硬件交互。这也是后续 systemd 能够执行挂载、创建进程、管理设备和服务的基础。

4. initramfs:内核启动早期的临时用户态

initramfs 是一个通常以压缩 cpio 归档形式保存的临时根文件系统。它不是一个特殊的内核模块,也不是一个完整磁盘分区,而是一组在内核早期阶段可用的用户态文件,例如:

/bin/sh
/bin/mount
/sbin/cryptsetup
/usr/lib/systemd/systemd
/lib/modules/<kernel-version>/
/etc/
 /init

其中 /init 是关键入口。内核完成早期初始化后,通常会执行 initramfs 中的 /init。这个程序负责解决“真实根文件系统还不能直接访问”的问题,例如:

  • 加载存储控制器驱动;
  • 等待磁盘或网络块设备出现;
  • 组装 RAID;
  • 激活 LVM;
  • 解锁 LUKS 加密卷;
  • 挂载 /usr 或真实根文件系统;
  • 应用内核命令行中的根设备参数;
  • 最后切换到真实根文件系统。

5. systemd:真实用户态中的第一个主要进程

在使用 systemd 的发行版中,真实根文件系统通常包含:

/sbin/init -> /lib/systemd/systemd

内核执行的并不是“systemd 这个名字”,而是内核命令行指定的 init=,或者默认寻找的 /sbin/init。如果 /sbin/init 是 systemd,那么 systemd 就成为 PID 1:

$ ps -p 1 -o pid,comm,args
    PID COMMAND         COMMAND
      1 systemd         /sbin/init

systemd 是用户态程序,不是内核的一部分。它负责:

  • 作为 PID 1 创建和管理服务进程;
  • 回收孤儿进程;
  • 管理挂载点、设备、socket、定时器和服务;
  • 根据依赖关系并行启动任务;
  • 接收来自内核和用户态的状态信息;
  • 提供日志、登录、会话和电源管理等配套能力。

二、Firmware 阶段:BIOS 与 UEFI 的两条路径

1. BIOS 启动路径

传统 BIOS 启动时,常见过程如下:

  1. CPU 从固件入口开始执行。
  2. BIOS 执行 POST 和基本硬件初始化。
  3. BIOS 按启动顺序寻找设备。
  4. BIOS 读取启动设备的第一个扇区,通常是主引导记录 MBR。
  5. BIOS 把其中的启动代码放入内存并跳转执行。
  6. MBR 中的短代码继续加载更完整的 Bootloader,例如 GRUB 的后续部分。
  7. Bootloader 加载 Linux 内核和 initramfs。

早期 MBR 空间很小,不能容纳完整 GRUB。因此 GRUB 在 BIOS 模式下常把不同阶段分散到:

  • MBR 中的第一阶段代码;
  • 磁盘间隙或 BIOS Boot Partition 中的后续代码;
  • 文件系统中的 GRUB 模块和配置。

GPT 磁盘通常还会存在一个保护性 MBR,用于避免旧工具误认为磁盘未分区。BIOS + GPT 场景下,GRUB 常需要单独的 BIOS Boot Partition 来存放其嵌入代码。

2. UEFI 启动路径

UEFI 不以“读取磁盘第一个扇区并执行”作为主要接口,而是能够读取 FAT 文件系统中的 EFI 可执行文件。常见过程是:

  1. UEFI 固件初始化硬件。
  2. 固件读取 NVRAM 中的 Boot Entry。
  3. Boot Entry 指向 EFI System Partition,简称 ESP。
  4. 固件从 ESP 加载一个 .efi 文件。
  5. 该 EFI 程序继续加载 Linux 内核和 initramfs,或者直接加载内核。

ESP 通常是一个较小的 FAT 分区,常见挂载位置为 /boot/efi。它可能包含:

/boot/efi/EFI/
├── BOOT/
│   └── BOOTX64.EFI
├── fedora/
│   └── shimx64.efi
├── ubuntu/
│   ├── shimx64.efi
│   └── grubx64.efi
└── systemd/
    └── systemd-bootx64.efi

实际目录名取决于发行版、架构和安装方式。UEFI 的默认回退路径、NVRAM 启动项和 Secure Boot 策略会共同决定最终加载哪个 EFI 程序。

3. Secure Boot 改变的是信任链,不是启动阶段顺序

Secure Boot 通常要求固件只执行签名可信的 EFI 程序。常见链条是:

UEFI 固件
  -> 已签名的 shim
      -> 已签名或受信任的 GRUB
          -> 已验证的 Linux 内核
              -> initramfs 和用户态

但具体链条因发行版和配置而异。Secure Boot 的核心是代码签名验证和信任根,不是“系统一定使用 GRUB”。

常见误解是:Secure Boot 会自动阻止所有未签名文件。实际情况更复杂:

  • 固件通常验证它直接加载的 EFI 程序;
  • Bootloader 可能继续验证内核;
  • 内核可能启用模块签名校验;
  • initramfs 中的脚本和普通用户态文件是否被单独验证,取决于启动链设计和系统配置。

因此,Secure Boot 启动失败时,应分别检查 EFI 程序、内核、内核模块和 initramfs,而不是只检查“磁盘是否能读”。

4. 用命令确认当前启动模式和 UEFI 条目

在已经启动的 Linux 中:

$ test -d /sys/firmware/efi && echo UEFI || echo BIOS
UEFI

/sys/firmware/efi 存在通常说明当前系统通过 UEFI 启动。它不是判断主板是否支持 UEFI,而是判断这一次启动是否使用了 UEFI。

查看 UEFI 启动项:

$ sudo efibootmgr -v
BootCurrent: 0003
Timeout: 1 seconds
BootOrder: 0003,0001,0000
Boot0003* ubuntu HD(...)File(\EFI\UBUNTU\SHIMX64.EFI)

该命令会读取并可能访问 EFI 变量。读取通常风险较低,但修改 BootOrder、删除启动项可能导致系统无法启动;生产机器上不应未经验证直接执行修改操作。


三、Bootloader 阶段:内核、initramfs 和命令行如何汇合

1. Bootloader 需要准备什么

以内核能够启动为条件,Bootloader 至少需要让内核获得:

  1. 内核映像;
  2. initramfs 映像,若启动流程需要它;
  3. 内核命令行;
  4. 架构相关的启动信息。

典型 GRUB 配置项类似:

linux   /vmlinuz-6.8.0 root=UUID=... ro quiet
initrd  /initramfs-6.8.0.img

这里:

  • linux 行指定内核;
  • initrd 行指定 initramfs;
  • root=UUID=... 是传给内核的命令行参数;
  • ro 表示初始以只读方式挂载根文件系统;
  • quiet 减少控制台输出,但不会改变核心启动逻辑。

不同发行版的文件名和配置生成方式不同。GRUB 的实际配置可能由 grub-mkconfig、发行版封装脚本或安装器生成,不应直接假定所有系统都使用相同路径。

2. 内核命令行是启动阶段之间的重要数据接口

内核命令行通常可以通过以下方式查看:

$ cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-6.8.0 root=UUID=... ro quiet splash

它不是只给内核本身使用。启动链中的多个组件都可能读取它:

  • 内核读取 root=, ro, console=, panic= 等参数;
  • initramfs 脚本读取存储、加密、网络和调试参数;
  • systemd 读取 systemd.unit=, systemd.log_level= 等参数。

例如:

root=UUID=xxxx
rd.luks.uuid=luks-yyyy
rd.lvm.lv=vg0/root
systemd.unit=rescue.target

其中 rd.* 参数通常是 initramfs 工具约定的参数,不是 Linux 内核统一规范的一部分。dracut 和其他 initramfs 实现支持的参数可能不同,必须以目标发行版和版本的文档、生成内容为准。

root= 也有边界:它只描述根设备或根文件系统位置,不能自动完成 LUKS 解密、LVM 激活、网络启动或驱动加载。若根设备被加密或位于 LVM 中,initramfs 还必须包含对应工具、配置和驱动。

3. 内核映像并不总是“可直接执行文件”

常见 /boot/vmlinuz-* 通常是经过压缩的 Linux 内核映像。Bootloader 将其加载后,内核早期代码会完成自解压,再进入架构相关入口和通用初始化路径。

逻辑上可以表示为:

压缩内核映像
  -> 内核自解压
  -> 架构入口
  -> start_kernel()
  -> rest_init()
  -> 内核线程和第一个用户态进程

函数名称属于 Linux 内核实现细节,可能随版本和架构变化,但它们表达了一个稳定的因果关系:内核先建立自身运行所需的环境,再创建执行用户态 /init 的路径。


四、Kernel 早期初始化:从 CPU 控制权到 /init

1. 内核首先建立自己的运行环境

内核早期阶段需要完成多类初始化:

  • 解析启动参数;
  • 建立页表和内存管理;
  • 初始化中断和时钟;
  • 初始化调度器;
  • 建立 slab 等内核内存分配机制;
  • 注册总线和设备模型;
  • 初始化 VFS、网络等子系统;
  • 加载编译进内核的驱动;
  • 准备 rootfs 和 initramfs。

驱动有两种主要形态:

  • 内建驱动:直接编译进内核映像;
  • 模块驱动:以 .ko 文件存在,需要在运行时由 modprobe 等工具加载。

如果根磁盘控制器驱动、文件系统驱动或加密设备依赖的驱动只是模块,而 initramfs 中没有这些模块,内核即使已经启动,也无法访问真实根文件系统。

2. rootfs 与真实根文件系统不是同一个概念

内核启动后会先拥有一个最小的根目录环境,通常称为 rootfs。它由内核提供,用来承载早期的 /init 和 initramfs 内容。

此时的 / 不是磁盘上的真实根分区。例如:

早期:
/
├── init
├── bin/
├── etc/
└── lib/

切换后:
/
├── sbin/init
├── etc/
├── var/
├── usr/
└── home/

initramfs 的内容被展开到早期 rootfs 中。内核随后尝试执行:

/init

/init 不存在、不可执行,或者执行过程中失败,常见错误包括:

Kernel panic - not syncing: Attempted to kill init!

或者:

No working init found. Try passing init= option to kernel.

这类错误与“systemd 服务启动失败”不是同一层问题:此时系统甚至可能还没有进入真实根文件系统中的 systemd。

3. /init 是用户态程序,不是内核函数

内核通过类似 execve 的机制执行 initramfs 中的 /init。这个 /init 可以是:

  • shell 脚本;
  • BusyBox 提供的程序;
  • dracut 的初始化程序;
  • initramfs 中的 systemd;
  • 其他自定义实现。

因此,initramfs 阶段已经是用户态,但它仍然是一个极简、临时、面向启动的用户态环境。它通常没有完整的服务集合,也没有最终系统的 /var、用户账户和应用程序。


五、initramfs 如何找到并切换到真实根文件系统

1. 为什么必须有 initramfs

考虑一个根文件系统位于以下设备上:

/dev/mapper/vg0-root

但这个设备的生成路径是:

SATA/NVMe 控制器
  -> 分区
  -> LUKS 解密
  -> LVM PV
  -> VG 激活
  -> LV root
  -> ext4/XFS 文件系统

内核若没有相应驱动和用户态工具,不能凭空得到 /dev/mapper/vg0-root。initramfs 就是用来执行这条依赖链的临时系统。

一个典型数据流如下:

sequenceDiagram
    participant K as Kernel
    participant I as initramfs /init
    participant D as 设备与驱动
    participant C as cryptsetup
    participant L as LVM
    participant R as 真实根文件系统
    participant P as 最终 PID 1

    K->>I: 执行 /init
    I->>D: 加载模块、等待设备
    D-->>I: 出现磁盘和分区
    I->>C: 解锁 LUKS
    C-->>I: 创建 /dev/mapper/cryptroot
    I->>L: 扫描并激活 VG/LV
    L-->>I: 出现 /dev/mapper/vg0-root
    I->>R: 挂载真实根文件系统
    I->>P: switch_root 后执行 /sbin/init
    P-->>P: systemd 读取 unit 并启动系统

并非所有机器都经过全部步骤。普通未加密单分区系统可能只需要加载磁盘和文件系统驱动,然后直接挂载根分区。

2. initramfs 镜像包含什么

可以用发行版提供的工具查看内容。Fedora、RHEL 系常见:

$ lsinitrd /boot/initramfs-$(uname -r).img | less

Debian、Ubuntu 上常见:

$ lsinitramfs /boot/initrd.img-$(uname -r) | less

输出中通常能看到:

usr/lib/modules/6.x.y/
usr/bin/cryptsetup
usr/sbin/lvm
etc/crypttab
etc/fstab
init

实际路径会因 initramfs 工具版本和是否启用 usr-merge 而变化。查看的目的不是记住固定目录,而是确认以下事实:

  • 当前内核版本对应的驱动是否存在;
  • cryptsetup、LVM、RAID 等工具是否被打包;
  • /init 是否存在;
  • 必要的配置文件和 udev 规则是否被包含。

3. switch_rootpivot_root 的关系

initramfs 挂载真实根文件系统后,需要将它变成新的 /,再执行真实系统的 init。

常见过程是:

  1. 将真实根文件系统挂载到临时目录,例如 /sysroot
  2. 将必要的虚拟文件系统准备好;
  3. 停止或迁移 initramfs 中仍在运行的进程;
  4. 使用 switch_root 或相关机制切换根目录;
  5. 执行新的 /sbin/init

pivot_root 是内核提供的根文件系统切换系统调用;switch_root 通常是用户态工具,用于更适合 initramfs 场景的切换流程。两者不应简单视为同义命令。现代 initramfs 实现一般封装了具体细节。

切换完成后,早期 initramfs 的 /init 不再是系统的最终 init。真实根文件系统中的 systemd 才成为 PID 1。


六、systemd 阶段:PID 1 如何把静态文件变成运行系统

1. systemd 读取的不是一个“启动脚本”,而是一组 unit

systemd 使用 unit 描述系统资源和动作。常见 unit 类型包括:

类型 含义
.service 一个长期运行或一次性完成的进程
.target 一组 unit 的逻辑集合
.mount 一个挂载点
.device 一个内核设备出现的状态
.socket socket 激活对象
.timer 定时触发器
.automount 自动挂载点
.path 路径变化触发器

例如一个服务:

[Unit]
Description=Example Worker
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/sbin/example-worker
Restart=on-failure

[Install]
WantedBy=multi-user.target

这里有三个不同概念:

  • After=:只规定顺序,不会自动拉起对方;
  • Wants=:建立较弱的拉起关系;
  • Requires=:建立更强的依赖关系,但不等同于顺序;
  • WantedBy=:主要用于 systemctl enable 时生成依赖链接。

一个常见错误是把 After=network-online.target 理解成“网络一定可用”。它只表达 systemd job 的排序关系;网络管理器是否真的等待地址、路由和上游连通性,取决于具体网络服务和配置。

2. systemd 的启动目标

systemd 通常通过默认 target 进入系统状态:

$ systemctl get-default
graphical.target

常见 target 包括:

  • emergency.target:尽可能少的环境,用于极端修复;
  • rescue.target:单用户救援环境;
  • multi-user.target:多用户文本模式;
  • graphical.target:在多用户基础上增加图形登录等组件。

可以查看默认目标的实际指向:

$ systemctl list-dependencies graphical.target
graphical.target
● ├─multi-user.target
● └─display-manager.service

这不是固定输出。不同发行版、桌面环境和安装软件会产生不同依赖。

3. systemd 如何处理并发启动

systemd 会把启动请求转换为一组 job,并依据依赖与顺序约束安排执行。若两个服务之间没有顺序依赖,它们可能并行启动。

例如:

A Requires=B
A After=B
C 与 A、B 无依赖

可以推出:

B 必须先于 A 完成启动顺序;
C 可以与 A 或 B 并行;
但 C 若实际需要网络或文件系统,必须显式声明相应依赖。

因此,服务启动顺序不是 unit 文件在磁盘上的排列顺序,也不是服务名称的字典序。启动图由依赖关系决定。

查看实际启动耗时:

$ systemd-analyze
Startup finished in 4.231s (firmware) + 1.102s (loader) + 2.874s (kernel) + 5.410s (userspace) = 13.617s
graphical.target reached after 5.392s in userspace

这些时间由 systemd 根据当前启动记录计算。它们不是性能保证,也不能简单相加来推断每个服务的串行耗时,因为很多任务是并行执行的。

进一步查看关键路径:

$ systemd-analyze critical-chain
graphical.target @5.392s
└─multi-user.target @5.391s
  └─example.service @4.800s +500ms
    └─network-online.target @4.700s

critical-chain 展示的是影响某个目标到达时间的一条关键依赖链,不代表所有启动任务,也不表示其他服务没有运行。

4. PID 1 的特殊职责

PID 1 不只是“第一个普通进程”。它还承担:

  • 孤儿进程重新归属;
  • 回收退出子进程,避免僵尸进程积累;
  • 处理系统关机、重启和进入特定 target;
  • 维护服务状态;
  • 根据 unit 规则重新启动失败服务。

可以查看 systemd 的基本状态:

$ systemctl is-system-running
running

可能的状态包括:

  • initializing:仍在初始化;
  • starting:正在启动;
  • running:系统达到运行状态;
  • degraded:有失败 unit,但 systemd 本身运行;
  • maintenance:进入救援或紧急维护状态;
  • offline:未能正常确认运行状态。

degraded 不等于“系统完全不可用”。它表示至少有 unit 失败,需要进一步查看:

$ systemctl --failed
$ systemctl status <unit>
$ journalctl -b -u <unit>

七、完整启动时序中的关键状态

可以把典型现代 Linux 启动抽象成以下状态转换:

S0: CPU 复位
  条件:Firmware 入口可执行

S1: Firmware 完成基本初始化
  条件:找到可启动 EFI 程序或 BIOS 启动代码

S2: Bootloader 运行
  条件:内核映像、命令行和 initramfs 可读取

S3: Kernel 早期初始化
  条件:内存、调度、中断和基本设备模型可用

S4: initramfs 用户态
  条件:/init 存在且可执行

S5: 真实根文件系统可访问
  条件:根设备出现、必要驱动和文件系统可用

S6: 最终 PID 1 运行
  条件:真实根中的 /sbin/init 可执行

S7: systemd 启动目标
  条件:目标依赖中的必要 unit 成功或被允许失败

S8: 系统可用
  条件:达到预期 target,登录、网络或应用服务按需求就绪

每个状态的失败都对应不同证据来源:

阶段 常见失败 主要证据
Firmware 找不到启动盘、Secure Boot 拒绝 固件界面、UEFI 日志、启动项
Bootloader GRUB 命令行、找不到内核 GRUB 屏幕、ESP、efibootmgr
Kernel panic、驱动缺失、内存初始化失败 控制台、dmesg、串口日志
initramfs 找不到根设备、LUKS/LVM 失败 initramfs 控制台、内核命令行
switch_root 真实根挂载失败、/sbin/init 缺失 initramfs 日志、挂载检查
systemd 服务失败、依赖失败、目标未达到 journalctl -bsystemctl

八、一个端到端的检查示例

下面的命令用于检查当前系统“实际使用了什么”,而不是猜测发行版默认配置。

1. 确认内核、命令行和 PID 1

$ uname -r
6.8.0-xx-generic

$ cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-6.8.0 root=UUID=... ro quiet splash

$ readlink -f /sbin/init
/usr/lib/systemd/systemd

$ ps -p 1 -o pid,comm,args
    PID COMMAND         COMMAND
      1 systemd         /sbin/init

这些结果分别说明:

  • 当前运行内核版本;
  • Bootloader 传递的命令行;
  • /sbin/init 实际指向的程序;
  • PID 1 是否由 systemd 承担。

如果 readlink -f /sbin/init 不是 systemd,不能继续假设后面的行为符合 systemd 模型。

2. 确认根文件系统和挂载关系

$ findmnt /
TARGET SOURCE                     FSTYPE OPTIONS
/      /dev/mapper/vg0-root       xfs    rw,relatime

这个结果说明真实根文件系统来自 LVM 映射设备,文件系统类型为 XFS。它暗示启动时至少可能需要:

  • LVM 工具;
  • 对应块设备驱动;
  • XFS 驱动;
  • 若上层有加密,还需要 cryptsetup 和加密驱动。

再查看块设备层级:

$ lsblk -f
NAME          FSTYPE      UUID                                 MOUNTPOINTS
nvme0n1
├─nvme0n1p1   vfat        ...                                  /boot/efi
└─nvme0n1p2   crypto_LUKS ...
  └─cryptroot LVM2_member ...
    └─vg0-root xfs         ...                                  /

lsblk -f 显示的是当前运行状态,不能单独证明 initramfs 中已经包含所有必要组件;还需要查看 initramfs 内容或在失败环境中检查日志。

3. 查看本次启动日志

$ journalctl -b

-b 表示当前启动。查看上一次启动:

$ journalctl -b -1

但这依赖 journald 持久化日志是否启用。若日志只保存在内存中,重启后可能没有 -b -1 可查。

按内核消息过滤:

$ journalctl -k -b

按服务过滤:

$ journalctl -b -u ssh.service

显示失败服务:

$ systemctl --failed

若要观察服务启动失败的直接原因,通常应先看:

$ systemctl status example.service
$ journalctl -b -u example.service --no-pager

systemctl status 适合快速摘要,journalctl 更适合获取完整错误、退出码和前后文。


九、启动失败的分层诊断

1. 屏幕完全没有固件画面

可能位于 Firmware 之前或 Firmware 阶段:

  • 电源、主板、内存或 CPU 硬件问题;
  • 显示输出切换到其他接口;
  • 固件配置损坏;
  • 存储设备未被识别。

此时修改 GRUB、重建 initramfs 都没有意义,因为控制权尚未交给 Bootloader。

2. 出现 “No bootable device” 或 UEFI 找不到条目

重点检查:

sudo efibootmgr -v
lsblk

还需要确认:

  • 启动模式是否从 UEFI 改成了 BIOS,或反过来;
  • ESP 是否存在且可读;
  • Boot Entry 是否指向正确的 EFI 文件;
  • 磁盘是否被固件识别;
  • Secure Boot 是否拒绝该 EFI 程序。

不要因为“Linux 分区还在”就认为一定能启动。UEFI 启动项、ESP 中的 EFI 文件和磁盘上的 Linux 文件是三个不同层次。

3. GRUB 出现,但找不到内核或 initramfs

可能原因包括:

  • /boot 未挂载或内容不完整;
  • GRUB 配置引用了已删除的内核;
  • ESP 或 /boot 文件系统损坏;
  • 内核升级过程中断;
  • 手工修改了启动配置。

在已经启动的系统中,可以检查:

ls -lh /boot/vmlinuz-* /boot/init*
findmnt /boot
findmnt /boot/efi

如果 /boot 是独立分区,未挂载时看到的 /boot 目录可能只是根文件系统上的空目录,从而导致错误判断。

4. 内核 panic 或 “No working init found”

这说明问题可能发生在内核进入最终用户态之前。常见原因:

  • 内核文件损坏;
  • initramfs 缺失;
  • /init 不存在或不可执行;
  • 内核命令行的 root= 指向错误设备;
  • 关键驱动没有编译进内核或未放入 initramfs;
  • initramfs 中的动态链接器或库文件不完整;
  • 真实根文件系统损坏。

可以在 Bootloader 菜单临时编辑命令行,删除 quiet,增加内核日志可见性。临时增加参数不会修改磁盘上的配置,但会改变本次启动行为;不要把实验参数直接写入生产永久配置。

5. initramfs 提示找不到根设备

典型表现是:

ALERT! UUID=... does not exist.
Gave up waiting for root file system device.

应按设备链逐层验证:

  1. UUID 是否与实际分区一致:

    blkid
    lsblk -f
    
  2. 控制器驱动是否存在;

  3. 分区是否能被识别;

  4. 若使用 LUKS,容器是否能打开;

  5. 若使用 LVM,VG 和 LV 是否能激活;

  6. 文件系统是否能挂载;

  7. root=rd.luks.*rd.lvm.* 是否与实际布局匹配;

  8. initramfs 是否是在硬件布局改变后重新生成。

重建 initramfs 的命令随发行版不同,不能混用:

# Debian/Ubuntu 常见
sudo update-initramfs -c -k "$(uname -r)"
# 或更新所有已安装内核
sudo update-initramfs -u -k all

# Fedora/RHEL 等常见
sudo dracut --force

这些命令会修改 /boot 中的启动文件。若 /boot 空间不足、目标内核版本不对或命令执行中断,可能产生新的启动故障。生产环境应先确认:

df -h /boot
uname -r
ls -lh /boot

6. 进入真实根后 systemd 启动失败

如果已经看到 systemd 日志或进入救援环境,问题通常已经越过 Firmware、Bootloader 和大部分 initramfs 阶段。重点转向:

  • /etc/fstab 中的错误或不存在设备;
  • 文件系统挂载失败;
  • 服务配置错误;
  • 服务依赖未满足;
  • SELinux/AppArmor 或权限问题;
  • /usr/var 等独立文件系统未挂载;
  • 磁盘空间或 inode 耗尽。

检查挂载失败:

systemctl --failed
journalctl -b -p err
findmnt --verify

findmnt --verify 会检查 fstab 与当前挂载描述之间的常见问题,但不能替代对所有启动依赖的验证。

进入救援目标可以在 Bootloader 临时加入:

systemd.unit=rescue.target

进入紧急目标:

systemd.unit=emergency.target

两者差异在于启动的用户态设施数量不同。emergency.target 更接近最小维护环境,适合根文件系统或关键挂载问题;rescue.target 通常会提供更多基础服务。救援环境下通常需要手工将根文件系统重新挂载为可写:

mount -o remount,rw /

该操作会改变磁盘内容。对于怀疑文件系统损坏的机器,不应在未完成备份或一致性评估时随意写入。


十、几个容易混淆的边界

1. initramfs 与 initrd 不是完全相同的历史概念

传统 initrd 通常可以表现为一个内存块设备上的文件系统;现代 Linux 更常用 initramfs,本质上是由内核展开到 rootfs 的 cpio 归档。

很多命令、文件名和文档仍然使用 initrd 这个历史名称,例如:

/boot/initrd.img-...

文件名叫 initrd 不代表其内部一定采用旧的 initrd 机制。判断实际内容应查看对应发行版的生成工具和镜像结构。

2. /init/sbin/init 是两个不同阶段的入口

典型系统中:

initramfs:/init              # 临时用户态入口
真实根:/sbin/init             # 最终用户态入口,可能是 systemd

如果 /init 失败,systemd 可能还没有启动;如果 /sbin/init 失败,则通常已经完成了根文件系统切换。

3. systemd 不是所有 Linux 的必需组件

Linux 内核只要求存在可执行的 init 入口,并不要求它必须是 systemd。可以通过内核参数指定:

init=/bin/sh

这会让内核尝试执行指定程序,常用于故障分析。但它会绕过正常 systemd 初始化,可能没有挂载、网络、设备管理和日志服务;在生产系统上仅应作为临时救援手段。

4. root= 不等于“内核自己挂载所有根设备”

root=/dev/...root=UUID=... 只是描述目标。真正让设备出现的可能是:

  • 内核内建驱动;
  • initramfs 中的模块;
  • udev 规则;
  • mdadm;
  • cryptsetup;
  • LVM;
  • 网络根文件系统工具。

所以看到正确的 root=UUID,并不能证明启动一定成功。

5. systemd 的“服务启动完成”不一定代表应用真正可用

一个服务可能在进程成功执行后就被认为已启动,但应用内部仍可能:

  • 尚未完成端口监听;
  • 尚未完成数据库连接;
  • 尚未加载配置;
  • 尚未准备好接受请求。

对于需要严格就绪语义的程序,应使用合适的 Type=、通知机制、健康检查和应用层探针,而不是仅靠 After= 猜测可用性。


十一、如何把启动证据串成一条因果链

排查时可以按“最后出现的证据”定位阶段:

没有固件画面
  -> Firmware 或硬件层

有固件,但没有 Bootloader
  -> 启动项、ESP、BIOS 启动代码或 Secure Boot

有 Bootloader,但找不到内核
  -> /boot、ESP、Bootloader 配置

内核开始输出,但找不到根设备
  -> 驱动、initramfs、root=、LUKS、LVM、RAID、文件系统

已经挂载根,但没有 systemd
  -> /sbin/init、动态链接器、根文件系统内容

systemd 已运行,但 target 未达到
  -> unit、挂载、依赖、服务、权限、资源

系统已登录,但应用不可用
  -> systemd 之后的服务就绪和应用自身状态

这条链的价值在于避免跨层修改。例如:

  • 根设备找不到时,优先检查 initramfs 和设备链,而不是修改 systemd unit;
  • UEFI 找不到 EFI 文件时,重建 initramfs 通常没有帮助;
  • systemd 服务失败时,重新安装 Bootloader 通常也不能解决应用配置错误。

十二、启动流程与关机流程的对称性

启动是从固件逐步进入用户态;关机则大体反向进行:

  1. systemd 停止或隔离目标;
  2. 服务收到停止信号并退出;
  3. 挂载点被卸载或重新挂载为只读;
  4. systemd 请求内核执行重启、关机或挂起;
  5. Firmware 或硬件电源控制完成最终动作。

若服务忽略停止信号,systemd 可能在超时后发送更强的终止信号。若文件系统仍有未写回数据,内核需要完成同步和卸载。强制断电会跳过这些步骤,增加文件系统和应用数据损坏风险。

可以查看上一次关机前后的日志,但日志是否持久化取决于 journald 配置:

journalctl -b -1 -e

如果系统因突然断电而没有正常关机,日志可能只保留到断电前最后一部分,不能把“没有日志”理解为“没有故障”。


十三、最终模型:每一层只负责解决自己的前置条件

一个可靠的抽象是:

Firmware 解决:
    CPU 能否找到并执行下一阶段程序?

Bootloader 解决:
    内核能否得到并启动所需的映像和参数?

Kernel 解决:
    内存、调度、中断、设备和基本内核抽象是否建立?

initramfs 解决:
    真实根文件系统所依赖的设备链能否被构造?

systemd 解决:
    真实根上的用户态资源能否按依赖关系进入目标状态?

启动成功不是某个单点程序“运行了”这么简单,而是满足一系列条件:

Firmware 可执行
∧ Bootloader 可加载
∧ Kernel 可初始化
∧ /init 可执行
∧ 根设备可发现并挂载
∧ /sbin/init 可执行
∧ systemd 依赖图可处理
∧ 目标服务达到所需状态

其中任一条件失败,故障表现都会不同。理解 Firmware、Bootloader、Kernel、initramfs 和 systemd 之间的控制权、数据流和职责边界,才能把启动问题从“黑屏或卡住”还原为可验证的具体状态转换。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。