2. 引言:启动只是一场接力赛
我第一次真正搞懂Linux启动过程,不是靠读源码,也不是靠背启动图,而是靠一个特别朴素的比喻:整个开机过程就是一场四人接力赛。第一棒是固件,把系统唤醒;第二棒是引导加载器,把内核请上场;第三棒是内核自己,完成硬件到软件的初始化;最后一棒是init系统,也就是systemd,负责把所有用户态服务跑起来。每一棒接力的那一刻,都是最容易掉棒的地方,也是我们排查启动故障时的核心发力点。
这个思路对谁最有用?两类人。一类是刚接触Linux运维、天天在虚拟机里折腾、却搞不明白“开机卡住到底卡在哪一步”的新手;另一类是已经会用一些命令、但对启动流程的因果链没有整体认知、遇到内核 panic 或者挂载失败就到处乱试的进阶用户。这篇文章不会像教科书那样按时间线把所有细节铺开,而是沿着接力棒递进的顺序,把每一棒是什么、它们之间怎么衔接、每一棒最容易掉链子的点在哪里讲透,再附上我踩过的坑和排查套路。
2. 四棒接力:先看整张流程图再逐个拆解
2.1 启动全程的五个关键节点
为了后续讨论方便,先把整条链路用“接力棒”的方式标记出来:
- 接力点0:按下电源键,CPU 从固定地址取指令,固件(BIOS 或 UEFI)开始运行。
- 接力点1:固件完成硬件自检,把控制权交给 Bootloader(绝大多数场景下是 GRUB2)。
- 接力点2:GRUB 加载内核镜像 vmlinuz 和 initramfs,并把控制权交给内核。
- 接力点3:内核解压、初始化核心子系统,挂载 initramfs 中的临时根文件系统,然后切换到真正的根文件系统。
- 接力点4:内核执行 PID=1 的 init 程序(即 systemd),systemd 按依赖关系启动所有用户态服务。
每一棒都有自己的“跑道”和“交接区”。排查启动问题,本质上就是在确认“当前这一棒到底有没有跑完,交接失败到底是哪一方的问题”。
2.2 为什么用接力赛来类比最贴切
接力赛的妙处在于:每一棒运动员各司其职,但最终成绩取决于交接是否顺畅。Linux 启动也一样——固件把控制权交给 GRUB 的时候,GRUB 如果找不到配置文件,或者磁盘路径变了,就直接掉棒;GRUB 把控制权交给内核的时候,如果 initramfs 里缺了磁盘驱动,内核连根分区都挂不上;内核把控制权交给 systemd 时,如果默认 target 损坏,系统会掉进 rescue 模式而不是正常进入多用户环境。
这个类比还有一个实用价值:它帮我们建立“分段定位”的排查思路。启动卡住时,不用慌,先判断卡在哪一个接力区——是在 GRUB 菜单之前,还是内核日志停止输出之后,还是 systemd 的某个服务超时。分段定位比从头到尾瞎查快十倍。
2.3 我建议每个想搞懂启动过程的人都跑一遍的验证方法
光看文章不如动手做一次实验。准备一台虚拟机(我用的是某开源虚拟化平台的默认配置),按以下步骤走一遍,整个过程大概半小时:
- 开机进入 GRUB 菜单(如果是双系统或者有 GRUB 的机器,开机时按 Shift 或 Esc 调出菜单)。
- 在 GRUB 菜单界面按
e编辑启动项,在 linux 那一行末尾加上systemd.log_level=debug,然后按 Ctrl+X 启动。 - 开机后查看
journalctl -b输出,重点看 systemd 启动阶段的服务顺序和耗时。 - 再正常重启一次,对比两次启动日志的差异。
这一套操作下来,你对“启动过程是分阶段的”这个感知会非常强烈,比看十篇文章都管用。
3. 第一棒:固件阶段——从按下电源键到 GRUB 出场
3.1 BIOS 和 UEFI 的分工
固件这一棒的核心任务就两个:自检和找引导程序。传统 BIOS 的工作方式非常直接——先执行 POST(加电自检),然后按照 CMOS 里配置的启动顺序,逐个扫描磁盘的 MBR 区域,如果发现 MBR 的最后两个字节是0x55AA,就把这一段 512 字节的代码加载到内存并跳转执行。
UEFI 则是另一种玩法了。它不自找磁盘上的引导代码,而是直接读取 ESP(EFI 系统分区)里的.efi文件,比如/EFI/BOOT/BOOTX64.EFI。UEFI 固件实际上是一个小型操作系统,它提供了更完善的运行时服务,也让启动过程更快、更安全(安全启动 Secure Boot 可以校验引导程序的签名)。
从运维排障的角度,固件阶段最容易遇到的问题有三类:
- 开机直接黑屏,连品牌 Logo 都不出现,这是硬件级问题,和系统无关。
- 能看到固件界面但 GRUB 菜单不出现,大概率是引导顺序不对,或者 GRUB 装到了别的磁盘。
- 开启 Secure Boot 后 GRUB 提示签名无效,需要到 UEFI 设置里关闭 Secure Boot 或给 GRUB 签名。
3.2 GRUB 接棒时到底做了什么
GRUB2 目前是绝大多数 Linux 发行版默认的 Bootloader,它的完整工作流程可以拆成两级:
第一级是引导镜像(boot.img),它被装在 MBR 或 GPT 的保护性 MBR 里。这个镜像很小,只有一个任务:找到核心镜像core.img。第二级core.img通常装在 MBR 和第一个分区之间的空隙里,它比 boot.img 大得多,包含文件系统驱动。有了它,GRUB 才可以真正去识别分区、读取/boot/grub/grub.cfg配置文件。
这里有一个很常见的误解:有人以为 GRUB 是直接读取内核文件去启动的。其实不是,GRUB 是先去读配置文件,根据配置里定义的菜单项,再去找vmlinuz-xxx内核文件和initramfs-xxx.img镜像文件。
grub.cfg里最关键的一条命令是menuentry,它定义了菜单项,在这个块内部,有两行是灵魂:
linux /vmlinuz-6.5.0-generic root=/dev/mapper/vg-root ro quiet initrd /initramfs-6.5.0-generic.imglinux这一行不仅指明内核文件路径,还通过root=参数告诉内核真正的根分区在哪。如果系统使用 LVM 或加密盘,GRUB 还得额外加载对应的模块,这就引出了升级内核后必须执行update-grub或grub2-mkconfig的原因——你不更新配置,GRUB 就不知道新内核的存在。
3.3 实战:修复一个“进不去 GRUB 菜单”的机器
有一次我拿到一台旧服务器,开机直接卡在固件界面,提示找不到启动设备。第一反应不是重装系统,而是进 UEFI 设置看启动顺序。结果发现启动优先级列表里,第一项是 U 盘,第二项是另一块没有系统的盘,真正的系统盘排到了第三位。调整顺序之后重启,GRUB 菜单就回来了。
如果连 GRUB 都彻底坏了(比如 MBR 被覆盖),正确的做法是用发行版安装 U 盘进入救援模式:
- 用安装 U 盘启动,选择“试用/救援”(不同发行版叫法不同)。
- 打开终端,执行
lsblk找到系统盘符,比如/dev/sda。 - 在救援模式下用
grub-install /dev/sda重新安装引导程序。 - 再执行
update-grub重新扫描内核并更新配置。
需要注意,如果根分区是 LVM 或 LUKS 加密盘,在救援环境里要先vgchange -ay激活卷组、或cryptsetup luksOpen解锁加密分区,否则grub-install之后还是起不来,因为 GRUB 找不到它该找的分区。
注意:
grub-install是写到磁盘引导区域,不是写某个分区。目标参数一般写整块磁盘(如/dev/sda),而不是/dev/sda1。
4. 第二棒交接:GRUB 把内核交给 initramfs——启动排障的核心战场
4.1 内核启动到底做了什么
GRUB 把内核镜像加载到内存后,会跳转过去执行。此时内核处于一个非常尴尬的状态:它自身只包含基于 x86 的通用代码,但系统的磁盘控制器、文件系统驱动、LVM 映射关系都还没加载。内核想挂载真正的根分区,但可能连硬盘都识别不了。
这就是 initramfs(初始内存文件系统)存在的意义。它是一个打包成 cpio 格式的小型根文件系统,里面包含了内核在挂载真正根分区前所需要的一切——磁盘驱动、SCSI 控制器驱动、文件系统模块、LVM 工具、加密解密工具。
启动流程的顺序是这样的:内核先解压自身,然后解压 initramfs 到内存中的临时根目录,执行里面的/init脚本(或者 systemd 作为 init),由它去识别根分区、挂载真正的根,然后通过switch_root把根目录切换到真正的根文件系统上,释放 initramfs 占用的内存。
4.2 怎样快速判断 initramfs 出了问题
最常见的一个表现是:GRUB 菜单正常,选择内核后卡了很久,然后跳出一个Kernel panic - not syncing: VFS: Unable to mount root fs...或者在类似弹出提示框。这个信息直接说明了一件事——内核找不到根分区。
原因不外乎几个:
root=参数指定的设备路径变了。比如原来用的是/dev/sda1,但你加了一块硬盘后设备名变成了/dev/sdb1。- initramfs 里缺少必要的驱动模块。比如根分区在 NVMe 固态上,但旧 initramfs 没有 nvme 驱动。
- 根分区本身损坏,或者 LVM 卷组因异常关机失活。
排查并修复的思路如下:
第一步,在 GRUB 菜单按e编辑启动项,去掉quiet和splash参数,加上rd.shell(如果基于 dracut)或break=pre-mount(如果基于 initramfs-tools)。这样启动过程中会直接给你丢一个 shell,里面可以做手工排查。
第二步,在 initramfs 的 shell 里执行lsblk查看磁盘设备名,对比/boot/grub/grub.cfg里的root=参数是否一致。
第三步,如果设备名变了,直接在 GRUB 编辑界面改root=参数,临时启动进入系统后再持久化修改。
以 Debian/Ubuntu 系为例,内核更新后重建 initramfs 的命令是:
sudo update-initramfs -u -k all如果是基于 RHEL/Fedora 的 dracut 体系:
sudo dracut --force4.3 一个典型的 LVM 根分区修复案例
我印象最深的一次故障是这样的:某台虚拟机根分区用的是 LVM,某天异常断电后,重启时一直提示找不到根卷。通过break=pre-mount进入 initramfs shell,执行lvm vgscan,发现卷组显示VG not found,再用lvm vgchange -ay强制激活,系统就能识别了。
原因也不复杂:LVM 的元数据和磁盘设备名高度绑定,异常断电可能导致设备路径变化,卷组处于 inactive 状态。解决之后,我在系统里做了两个永久性修复——一个是把根分区的root=参数改成使用 UUID 而不是设备名,另一个是把 LVM 相关工具自动激活配置确认一下。
说到这里,强烈建议所有人把启动参数里的设备路径改成 UUID。查看 UUID 的方式:
blkid例如输出PARTUUID="xxxx"或UUID="xxxx",在 GRUB 的linux行里写root=UUID=那个值。UUID 是唯一的,和设备名无关,不管磁盘顺序怎么变,它都不会变,这是我从多次磁盘顺序变动事故中总结出来的最实用的经验。
5. 第三棒:systemd 接管——从内核态跨入用户态的过程
5.1 PID 1 的真正职责:不只是跑服务
内核完成根分区挂载后,会启动/sbin/init,它会用 symlink 指向/lib/systemd/systemd。对于 systemd 来说,它接管之后要做的事情比“启动服务”多得多。它要按依赖顺序拉起 sockets、挂载点、交换分区、系统日志、多用户目标。
理解 systemd 的启动机制,必须先理解 target 的概念。target是 systemd 里的一种单元,代表一组服务的集合,可以类比为跑道上的“计时点”。我们最常接触的三个 target:
poweroff.target:关机。rescue.target:单用户模式,只有基础系统,相当于 Windows 的安全模式。multi-user.target:正常的命令行多用户模式。graphical.target:带桌面环境的目标,一般依赖 multi-user.target。
默认启动到哪个 target,是通过软链接决定的:
ls -l /etc/systemd/system/default.target如果这个软链接指向graphical.target,系统开机后就会进入图形界面,否则默认进 multi-user。
5.2 systemd 的并行化逻辑:为什么启动这么快
老派 init 是串行的,服务一个一个按顺序跑,遇到网络等待就全卡住。systemd 的核心优势在于它并行处理相互独立的服务,并且通过 socket 激活和 D-Bus 激活,把服务真正需要的时候才拉起来。
但这引出一个问题:并行启动会导致日志顺序看起来“不规律”。比如网络服务和登录管理器可能同时启动,日志里看不出严格的先后关系。初学者如果试图从journalctl -b里找“启动顺序”,会很容易陷入混乱。
实际上 systemd 的启动链分几个阶段:
- 第一阶段:启动
systemd-udevd等设备管理服务,等待所有硬件设备被 udev 接管。 - 第二阶段:挂载文件系统(
local-fs.target),包括根分区、交换分区、以及/etc/fstab里声明的挂载点。 - 第三阶段:启动基础服务(
sysinit.target、basic.target),包括系统日志、D-Bus、用户工具等。 - 第四阶段:进入 multi-user.target,按需启动网络、SSH、cron 等服务。
5.3 用 systemd-analyze 给启动过程“掐表”
如果你想知道系统启动到底花了多少时间、到底哪一步浪费了时间,不需要猜,直接看数据:
systemd-analyze会输出类似 “Kernel 12.3s, Userspace 5.1s” 的结果。其中内核部分是指从 GRUB 到内核切换到用户态的时间,用户空间部分是从 systemd 启动到达到默认 target 的时间。
进一步看每个服务的耗时:
systemd-analyze blame它会按耗时从长到短列出所有服务的启动时间。我遇到过好几次启动慢的问题,都是通过这个命令定位的,比如某个服务启动没问题但 timeout 设置过长,导致失败后还要白白等 90 秒。
再看关键链路上的时间消耗:
systemd-analyze critical-chain这条命令会展示一条从默认 target 反向回溯的依赖链,每一行的 @ 时间是相对上一层的偏移时间,+ 是服务本身的消耗时间。如果你发现某个服务在链条上被标记为@很久以前且+巨大,基本就能锁定是它拖慢了整个启动。
小技巧:某服务启动失败后 systemd 会等待一段时间再决定是否继续,这个时间默认是 90 秒(timeout)。如果某服务在 blam 列表里稳居第一,大概率不是因为它真的很慢,而是它挂了之后 systemd 在傻等超时。
5.4 卡在“却到哪里了”的日志解读法
启动过程中如果屏幕上没有出现图形界面或者登录提示符,很多人会慌。其实只需要灵活切换视角:
- 先按 Ctrl + Alt + F2(或 F3 等)尝试切换到其他虚拟控制台。如果能看到登录提示,说明系统已经起来了,只是显示服务的问题。
- 如果按了没反应,说明系统还在初始化阶段或已经损坏。
- 用
journalctl -b查看本次启动日志,配合-p err只看错误级别:
journalctl -b -p err --no-pager这条命令会列出本次开机以来所有 error 级日志,绝大多数启动问题的关键线索都会在大概 10 行之内出现。
如果你的系统卡在 systemd 初始化某个服务,可以在 GRUB 里临时追加systemd.log_level=debug启动,日志会变得非常详细,但务必结合systemd-analyze critical-chain来对照,否则 debug 日志量大到会淹没有效信息。
6. 启动排查工具箱:常用命令、应急方案和经典故障速查
6.1 每次排查都少不了的命令清单
启动过程相关的问题,绕来绕去就那么几个工具,整理成速查表,建议收藏:
| 命令/操作 | 用途 | 使用时机 |
|---|---|---|
dmesg或journalctl -k -b | 查看内核日志 | 内核阶段卡死、驱动报错 |
journalctl -b | 查看本次启动完整日志 | 任何用户态启动问题 |
systemd-analyze blame | 统计服务耗时 | 启动慢 |
systemd-analyze critical-chain | 查看关键链路 | 定位依赖链阻塞 |
lsblk | 查看磁盘设备 | 确认设备名和挂载点 |
blkid | 查看 UUID | 修改启动参数前必备 |
df -h | 查看磁盘空间 | /boot满了导致内核更新失败 |
update-grub/grub2-mkconfig | 重新生成 GRUB 配置 | 安装新内核或修改配置后 |
update-initramfs -u/dracut --force | 重建 initramfs | 内核模块变动、根分区变多 |
systemctl rescue | 进入救援模式 | 正常系统但想去排查依赖问题 |
6.2 启动卡在内核阶段的排查技巧
启动过程完全没有任何输出,屏幕一片黑,这时候要大胆怀疑是内核阶段卡住了。首先要做的是去掉quiet和splash启动参数,让内核把所有启动消息打印到屏幕上。GRUB 菜单按e进入编辑,把 quiet 和 splash 删掉,再加上loglevel=7(最高日志级别),然后 Ctrl+X 启动。
如果屏幕一直停在内核日志的某一行,注意这一行的最后内容。常见情况:
- 停在
task xxxxx blocked for more than 120 seconds.,说明某个内核线程在等待 IO,极大概率是磁盘出问题或 NFS 挂载卡死。 - 停在
BUG: soft lockup,说明 CPU 上某个内核线程死循环,优先怀疑驱动问题。 - 停在
Out of memory,说明内存不足,和启动参数或硬件配置有关。
6.3 根文件系统变成只读的典型场景与修复
启动过程中最让新手困惑的另一个问题是:系统能启动,但提示根文件系统变成只读(Read-only file system)。这通常是因为系统检测到文件系统异常,为了安全自动以只读方式挂载。
在只读环境下直接执行fsck可能不生效,因为文件系统是挂载状态。正确流程是:
- 重启,在 GRUB 菜单按
e,在 linux 行末尾加single或1以进入单用户模式。 - 如果单用户模式和 multi-user 一样繁忙,可以再加一个
emergency参数。 - 在紧急模式下,先以只读方式挂载根分区:
mount -o remount,ro /- 然后再执行文件系统检查:
fsck /dev/mapper/你的根分区这里有个我踩过的坑:如果根分区是 LVM,不能对/dev/mapper/vg-root直接执行fsck时跳过,它可能提示不认识 LVM 设备名,要看清楚实际物理分区路径。执行修复后,重启之前务必确认文件系统已经没有错误,否则修复不彻底会再次变成只读。
6.4 我常用的应急复盘法:一次起不来的经历
有一次我更新内核后,重启时卡在 GRUB 之后没有任何输出。我当时判断是 initramfs 受损,因为 update 过程被中断了。处理方式很朴素:
在 GRUB 菜单选择高级选项,进入旧内核(上一版内核)启动,然后重新执行:
sudo update-initramfs -u sudo update-grub重启后新内核恢复正常。这里的关键经验是:更新内核之后,不要立刻删除旧内核,至少保留一个已知能用的版本。这就是行业里常说的“保留可回滚点”原则,它在启动排障中救过我至少三次。
6.5 如果一切方法都无效,最终兜底方案
万不得已时,可以用 Live CD 或安装 U 盘启动系统,挂载根分区来做修复。步骤:
- 从 Live 环境启动,
lsblk找到根分区。 - 挂载根分区和 boot 分区(如果独立):
mount /dev/sda2 /mnt mount /dev/sda1 /mnt/boot- 挂载必要的虚拟文件系统:
mount --bind /dev /mnt/dev mount --bind /proc /mnt/proc mount --bind /sys /mnt/syschroot进入系统:
chroot /mnt /bin/bash- 在 chroot 环境内重新安装 GRUB:
grub-install /dev/sda update-grub这套流程几乎能解决 95% 的引导类故障,也是系统救援的标准姿势。
7. 实战侧写:从按下电源键到登录界面的完整时间线
前面讲的是阶段和原理,来串一遍完整的实战时间线,把前面的知识点串成一个完整闭环。
假设一台普通的 UEFI + systemd + LVM 根分区的机器:
- 0.000s:按下电源键,CPU 跳转到 UEFI 固件代码。
- 0.5s:UEFI 完成自检,加载 X64 引导器,扫描 ESP 分区。
- 1.0s:GRUB2 加载,显示菜单(如果配置了等待时间)。
- 1.5s:选择默认内核,GRUB 读取 vmlinuz 和 initramfs 到内存。
- 2.0s:内核接管,硬件设备开始枚举,initramfs 被解压。
- 3.0s:udev 加载存储驱动,LVM 卷组被激活,根分区准备挂载。
- 3.5s:
switch_root完成,真正的根文件系统挂载。 - 4.0s:systemd 启动,进入 basic.target。
- 4.5s:systemd-journald 启动,日志记录开始。
- 6.0s:网络服务启动,DHCP 获取地址。
- 7.0s:SSH 服务监听端口。
- 8.0s:graphical.target 或 multi-user.target 达成,显示登录提示。
如果你用systemd-analyze去测量,会发现优秀配置的系统,从固件到 login 界面 10 秒内完成非常正常。这也说明了 systemd 并行化的效果确实是实打实的。
8. 我对启动过程的理解:一套固定的排查思路
搞懂 Linux 启动过程并不是为了背流程,而是为了在面对一台永远起不来的机器时,心里有一个清晰的“接力棒地图”。机器卡在哪一步、哪一步的信息源去哪查、错误特征的背后是什么,每一样都有对应方法,这比碎星式地搜索错误码效率高得多。
我个人在实际操作中最受益的一点,是戒掉了“重启大法”的瘾。以前遇到启动问题,第一反应是重装;现在第一反应是按 Esc/Shift 进 GRUB、看一眼引导参数、进一次 initramfs shell、查一遍 journal。绝大多数问题都能定位到某一棒上。也建议所有刚接触 Linux 的朋友,找一台不重要的虚拟机,故意破坏几次引导、改错几次启动参数,亲手把它救回来。把这个流程走通,你对 Linux 的掌控感会上一个台阶。
最后一次强调那个最容易被忽略但最重要的习惯:拿到一台新机器后,第一时间记录根分区的 UUID、确认 GRUB 配置里用的是 UUID 而非设备名、确保 initramfs 是完整且最新的。这三件事做好,你大概率永远不需要使用这篇文章里的应急手段。