按下电源键到屏幕上出现登录提示,这短短几秒钟里,Linux 其实走完了一段相当复杂的接力赛。我最初真正搞懂 Linux 开机启动流程,不是从文档里读来的,而是某台服务器某天重启后直接停在 GRUB 救援提示符,满屏只有一个rescue>,整个办公室都在等我把机器“捞”回来。当时我也只会敲ls看看哪个分区还能访问,根本不明白 GRUB 自己报出来的磁盘路径其实已经把根因说得明明白白。后来我把整个启动链路拆成四段,固件、引导加载器、内核、用户态进程,每一段的职责边界一旦清楚,排错效率完全不一样。这篇文章就把整条链路完整讲透,说清楚每一步为什么要这样设计,以及遇到常见故障时该怎么动手处理。
1. 从按下电源到引导加载器:固件阶段发生了什么
很多人习惯把“开机流程”从 GRUB 菜单出现那一刻算起,但真正的起点是主板上电之后固件先接管。这一步虽然短暂,却是后面所有环节的地基,因为固件决定了引导设备从哪找、按什么顺序找、以哪种模式启动。
1.1 固件自检与引导设备发现
电源接通后,CPU 先执行的不是操作系统代码,而是主板固件中固化的一段程序。以传统 BIOS 为例,这段程序会先做上电自检,也就是 POST,检查内存、显卡、键盘控制器这些基础硬件是否在线。POST 通过之后,BIOS 按照用户配置的启动顺序,逐个尝试可引导设备,比如 U 盘、光驱、硬盘、网络启动。传统 BIOS 访问硬盘的方式非常原始,通过中断调用直接读取某个扇区的数据,而且默认只读磁盘最开头的 512 字节,也就是 MBR 主引导记录。
MBR 的结构值得记住:前 446 字节存引导代码,接着是 64 字节的分区表,最后两个字节是固定的0x55AA结束签名。BIOS 找到硬盘后,把 MBR 读到内存,检查尾部签名是否正确,然后跳转到引导代码位置执行。这就是传统 BIOS 模式下一个非常刻板的约定:它不关心分区的具体格式,也不关心操作系统是什么,只负责找到一处可以执行的位置。所以很多人把 BIOS 阶段理解为“对暗号”阶段,引导代码就是那句暗号,对上了,后面就交给别人了。
1.2 UEFI 与传统 BIOS 在启动路径上的核心差异
近些年的机器基本都默认 UEFI 启动模式,它和传统 BIOS 有本质区别。UEFI 固件本身就是一个完整的运行环境,它不再盲目读取磁盘第一个扇区,而是从 NVRAM 中读取启动条目,找到 FAT 格式的 EFI 系统分区,也就是 ESP,然后加载该分区里的.efi可执行文件。以常见的 GRUB 为例,UEFI 固件会直接加载类似grubx64.efi这样的文件,将其当作一个标准程序运行。
UEFI 带来的另一个优势是支持 GPT 分区表和超大磁盘,传统 BIOS 的 MBR 方案在 2TB 以上磁盘和超过四个主分区时会非常吃力。很多人在老旧机器上装新系统时遇到问题,往往就是误以为 UEFI 和 BIOS 是“同一个东西”,实际上它们的启动文件路径、分区要求、恢复方法完全不同。
注意:如果机器支持 UEFI,安装系统时尽量保持 UEFI 模式统一。Windows 和 Linux 混装时更容易出现启动项覆盖问题,这种覆盖往往是 NVRAM 里的条目被改写导致的,跟分区本身没关系。排查这类问题时,可以在 Linux 里用
efibootmgr查看和管理启动条目。
固件阶段结束后,机器已经找到了第一个可执行程序,也就是引导加载器。Linux 世界里绝大多数发行版默认使用 GRUB2,接下来重点看它。
2. GRUB2 是怎么把内核“领”进内存的
GRUB2 的整个设计思路可以概括为“小步接力”:第一段代码非常小,小到只能完成一件事情,就是把下一段代码找到并加载起来;下一段代码又稍微大一点,大到能读文件系统,然后才去读配置文件、展示菜单、加载内核。这种分层设计不是拍脑袋,而是古老存储器容量限制留下的合理遗产。
2.1 GRUB 的三块拼图与配置文件流转
传统 BIOS 模式下,GRUB2 的核心由两部分组成:写在 MBR 前 446 字节里的boot.img,以及紧随其后存放在磁盘间隙或者其他位置里的core.img。boot.img做的事情非常单调,根据固定的位置信息找到core.img,然后跳过去执行。core.img则自带文件系统驱动,能够读取/boot分区里的配置文件。为什么不把文件系统驱动直接塞进 MBR?因为 MBR 只有 512 字节,连一个最小的文件系统驱动都放不下。至于 UEFI 模式,固件直接加载整个 GRUB 可执行文件,就不需要boot.img和core.img这种物理层面上的拼接了。
真正决定 GRUB 行为的,是配置文件。在 RPM 系发行版里通常叫/boot/grub2/grub.cfg,在 Debian 系里可能是/boot/grub/grub.cfg。这个文件内容很长,而且无论如何都不建议手工编辑,因为它是通过脚本和模板自动生成的。生成入口一般是/etc/default/grub以及/etc/grub.d/目录下的若干脚本。修改启动参数、调整默认启动项、修改菜单超时时间,都应当改/etc/default/grub,再重新生成配置文件。
# RPM 系 grub2-mkconfig -o /boot/grub2/grub.cfg # Debian 系 update-grub我自己踩过一个坑,直接在grub.cfg里改了一处菜单标题,升级内核重新运行生成命令后,改的内容全部被覆盖,还得重新找回来。把grub.cfg当作只读生成物,把所有自定义需求放到/etc/default/grub和/etc/grub.d/里去,才不会出现这种反复劳动。
2.2 内核选择与启动参数如何拼接
GRUB 菜单里每一行对应一个menuentry,每个条目里会指明三样东西:内核镜像vmlinuz-xxx、内存文件系统initramfs-xxx、以及内核启动参数。正常维护时,GRUB 会扫描/boot目录,把检测到的内核全部加入菜单,包括旧内核。这是很有价值的保留机制,一旦新内核启动异常,还能回退到上一个可用内核。
内核启动参数的拼接值得认真理解。以最常见的写法为例:
linux /boot/vmlinuz-6.1.0 root=UUID=xxxx ro quiet这里的root=告诉内核真实的根文件系统在哪里,ro表示先以只读方式挂载根文件系统,quiet则减少内核日志输出。拼接逻辑是:/etc/default/grub里的GRUB_CMDLINE_LINUX中定义的内容会固定加入每一个菜单条目,而GRUB_CMDLINE_LINUX_DEFAULT定义的内容通常只加在默认启动项上。所以想调试时不希望quiet干扰日志,但又不想永久删除,最干净的方式是进入 GRUB 菜单后按e键临时编辑当前条目,只对这一次启动生效。
另外两套生成菜单时的细节也容易忽略:一是保留了旧内核的启动项,这需要额外的磁盘空间;二是菜单里可能还有内存测试之类的条目。生产环境里,除非有明确需求,否则我不建议为了省空间把所有旧内核全删光,至少保留一个最近可用且确认没问题的内核。
2.3 启动菜单临时编辑与恢复模式入口
GRUB 菜单按e进入临时编辑模式,这个功能在故障恢复时非常救命。编辑界面里能看到当前条目加载的每个文件、传递的每条参数。常见的操作包括删掉quiet以观察完整内核日志、在命令行末尾加上systemd.unit=emergency.target进入紧急模式、加上rd.break在 initramfs 阶段卡住以便重置密码等等。按Ctrl+x或F10启动当前编辑好的条目。
实操心得:临时编辑只影响当前这一轮启动,重启后自动恢复原状。因此适合测试性操作,确认有效后再考虑写进配置文件,避免把整个系统永久改坏。
从 GRUB 菜单进入紧急模式或救援模式,是个非常安全的排查路径,因为此时文件系统还没有真正挂载,错误不会被放大。后面第 5 章讲具体故障时会再次用到这些入口。
3. 内核初始化与 initramfs:为什么系统需要“临时根”
GRUB 把内核镜像和 initramfs 镜像加载进内存之后,执行权交给内核。内核是一段极其精炼的程序,它首先要确认自己运行在哪种 CPU 模式下,接着解压自身、初始化内存管理、建立基本的中断和异常处理机制。这个过程快到以毫秒计,但信息量极大。
3.1 内核自解压后先做的事情
现代发行版的vmlinuz文件大多是压缩过的内核映像,也就是自解压内核。GRUB 把文件读入内存后,内核开头的解压代码会将真实内核引导到正确的内存位置,然后开始底层初始化。这时候内存管理、调度器、基础驱动框架逐一启动,但还没有任何用户态进程。紧接着,内核需要找到根文件系统,问题恰恰出现在这里。
内核本身对存储设备的支持并不完整。绝大多数发行版的内核把磁盘控制器驱动、文件系统驱动、LVM 逻辑卷支持、RAID 支持都做成了可加载模块,而不是直接编译进内核。如果只有vmlinuz,内核面对一块 NVMe 固态硬盘时可能连设备都认不出来,更不用说挂载上面的ext4文件系统了。这就是 initramfs 存在的根本原因。
3.2 initramfs 的工作原理与真实根文件系统交接
initramfs 本质上是一个打包的微型用户空间,里面包含了内核在这个阶段可能用到的一整套工具和驱动模块。它可能是cpio归档,也可能是压缩后的归档文件,典型名称是initramfs-xxx.img。GRUB 把 initramfs 一起加载到内存后,内核会把它当作临时的根文件系统挂载起来,并执行其中的/init脚本。
这段脚本承担的任务非常明确:加载磁盘控制器驱动,启动设备管理器扫描所有存储设备,唤醒 LVM 和 MD 设备,必要时等待加密设备解锁,然后根据root=参数找到真正的根文件系统,尝试挂载到/sysroot目录。挂载成功之后,脚本会做一次关键操作,把当前运行的临时环境切换到真正的根文件系统上。传统做法叫pivot_root,更现代的实现叫switch_root。切换完成后,内核真正启动/sbin/init,也就是用户态的第一个进程,在现代 Linux 发行版上几乎都是 systemd。
为什么不能省略 initramfs、直接让内核挂载根文件系统?用生活类比解释就是:你手里的钥匙能开目的地房间的门,但出发之前你还需要先穿过后勤仓库,找到那把钥匙、找到去房间的路线图,然后才能出发。initramfs 就是那个后勤仓库,里面装着这个阶段必需的“钥匙和地图”。把全部驱动编译进内核当然可以绕开它,但代价是内核体积爆炸、内存占用升高、维护成本变大,所以并不推荐。
3.3 常用内核参数速查与启动调试手段
为了排查方便,这里把出现频率最高的几个参数功能整理出来。
| 参数 | 作用 | 典型场景 |
|---|---|---|
root=UUID=xxxx | 指定根文件系统 | 系统默认配置 |
root=/dev/sda1 | 指定根设备节点 | 临时调试 |
ro/rw | 根文件系统只读 / 可写 | 默认只读 |
quiet | 减少内核日志输出 | 正常启动隐藏刷屏 |
rd.break | 在 initramfs 阶段进入 shell | 重置密码、调试 initramfs |
rd.shell | 失败时进入 initramfs 内的 shell | initramfs 排错 |
systemd.unit=rescue.target | 启动到救援目标 | 进入单用户环境 |
systemd.unit=emergency.target | 启动到紧急目标 | 维修系统基本环境 |
在 GRUB 启动菜单编辑界面里追加这些参数,是最快捷的调试方式。如果启动过程中发现卡死,先把quiet删掉,观察内核打印停在哪一步,定位思路会清晰很多。这个习惯在我处理启动异常时帮了很大忙,因为大多数启动卡顿都有明确的前一条日志告诉你“它正要做什么”。
4. systemd 接管之后:用户空间是怎么组织起来的
当内核完成根文件系统切换,最终执行的第一个用户态程序 PID 1 就是 systemd。从这个节点开始,启动流程从“硬件驱动、底层初始化”进入“服务编排、依赖管理”阶段。
4.1 PID 1 与 systemd 单元模型
PID 1 是整个用户空间的祖先进程。所有其他进程都直接或间接由它派生,或者至少由它领养。systemd 通过单元文件来描述系统中的每一项资源,单元可能是服务、挂载点、套接字、设备、目标。单元文件里最关键的是声明依赖关系:Requires表示硬依赖,Wants表示软依赖,After控制启动顺序,Before则反过来约束。
举个例子,网络服务通常要求网卡先就绪,那么网络服务的单元会声明After网络设备单元,但它不一定用Requires强制要求网卡必须可用,因为断网环境下网络服务本身也可以正常启动并等待设备出现。这种松耦合设计让启动过程能够并行开展:相互之间没有依赖的服务可以同时启动,不需要像脚本式启动那样呆板地一行一行顺序执行。
4.2 默认启动目标与依赖解析流程
systemd 启动时,会读取一个默认目标。所谓目标,类似传统概念里的“运行级别”,只不过它是通过依赖关系组成的单元集合。桌面系统的默认目标通常是graphical.target,服务器常见的默认目标是multi-user.target,也就是不带图形界面、只提供文本终端的多用户环境。目标之间层层依赖,graphical.target依赖multi-user.target,后者又依赖basic.target、sysinit.target。
可以通过以下命令切换默认目标:
# 查看当前默认启动目标 systemctl get-default # 设为多用户文本模式 systemctl set-default multi-user.targetsystemd 并不是等所有依赖解析完才开始干活,而是按照单元依赖图尽量并行推进。比如同时启动多个服务、同时挂载多个本地文件系统,能并行的绝不阻塞。这也解释了为什么 systemd 时代开机明显比 SysV init 时代快,因为脚本时代最容易出现的问题就是“某一个初始化步骤在傻等前一步”,而 systemd 的依赖机制大幅削减了这种浪费。
4.3 用 systemd-analyze 定位启动时间瓶颈
调试启动速度,我几乎总是先打开两个命令。
systemd-analyze systemd-analyze blame第一个会给出内核阶段耗时、用户空间阶段耗时,以及整体耗时;第二个会把每个服务从启动到运行完成所花的时间从高到低排出来。紧接着再用:
systemd-analyze critical-chain能看到一条启动关键路径,也就是“一路上都必须完成的环节”。我在实际维护中发现了一个规律:开机慢往往不是某一个服务真的花了十几秒,而是某个等待操作把默认超时时间拉到了 90 秒。比如某个启动时等待网络就绪的服务,在网络环境异常的机器上会反复重试,最终拖慢整条启动链。定位到具体的单元后,再决定是调整超时参数、关闭等待还是干脆禁用该服务,这个顺序特别重要,直接上来就禁用一坨服务,反而可能把真正的问题掩盖了。
查看整个启动过程的日志也是基本功:
journalctl -b journalctl -b -p err-b表示这次启动,-p err只显示错误级别。如果系统启动失败,先用这条看有没有明显报错。查看上一次失败的启动日志可以用journalctl -b -1,这在重启后抓线索时太实用了。
5. 启动失败场景排查实录
前面讲的都是正常链路,但真正让人“学到东西”的往往是故障现场。我按经验把最常遇到的几类启动问题整理成排查思路,每一条都有明确的第一动作。
5.1 开机卡在 GRUB 救援模式怎么办
如果在 GRUB 菜单出现之前就看到grub rescue>提示符,说明boot.img或者core.img没有正常加载,或者配置文件路径丢失。此时系统是活的,只是一个引导程序找不到自己的文件。当务之急是让当前会话知道 GRUB 的配置文件和模块在哪。
# 先确认当前磁盘分区是否可见 ls # 假设看到 (hd0,msdos1) 是 /boot 所在分区 set root=(hd0,msdos1) set prefix=(hd0,msdos1)/boot/grub2 # 加载正常模块并进入菜单 insmod normal normal不同发行版的 GRUB 目录名略有差异,常见的有/boot/grub和/boot/grub2。进入菜单后再进一步修复引导,比如重新安装 GRUB。要理解这条命令的每一步:set root告诉 GRUB,哪个分区是真正的根分区,set prefix告诉 GRUB 到哪里找配置文件和模块,insmod normal加载显示菜单的模块。我遇到过不少次 rescue> 提示,绝大多数都能靠这套手动指路流程把系统救回来。
5.2 根文件系统找不到的排查思路
启动过程中如果打印类似VFS: Unable to mount root fs或者Kernel panic - not syncing,说明内核已经接手,但无法挂载真正的根文件系统。常见原因有三类:root=参数指向的设备变了、磁盘驱动没进 initramfs、或者根分区本身损坏。
首查root=传的是什么。如果写的是/dev/sda1这种设备节点,一旦磁盘顺序变化,比如新增了一块硬盘、调整了 BIOS 启动顺序,系统可能就找错位置。更稳的方式是使用 UUID。用blkid查看分区的 UUID,然后在 GRUB 编辑菜单里临时改成:
root=UUID=正确的UUID如果 UI 不变时确认 UUID 没写错,仍是同样报错,就要考虑 initramfs 缺驱动。常见于更换了硬盘控制器,比如从 SATA 盘换到 NVMe,把新驱动补进 initramfs 后重建即可。
# RPM 系 dracut -f # Debian 系 update-initramfs -u顺带提醒,/boot分区如果空间不足,内核或 initramfs 可能写到一半断掉,旧内核被清理时又可能导致文件不完整。这也是比较隐蔽的根因,建议定期看下/boot空间余量。
5.3 root 密码遗忘的恢复流程
忘记 root 密码不需要重装系统。关键是进入一个可以挂载根文件系统的环境。最常见的方法是:重启进入 GRUB 菜单,按e编辑内核启动行,在linux行末尾加rd.break,然后启动。系统会停在 initramfs 阶段,根文件系统尚未切换,这时候用如下流程修改密码:
# 把根文件系统重新挂载为可写 mount -o remount,rw /sysroot # 切换进系统环境 chroot /sysroot # 修改密码 passwd # 退出并重启 exit reboot如果不用rd.break,也可以加systemd.unit=emergency.target,在紧急模式下同样可以执行以上挂载、切换操作。核心原则是:在真正的根文件系统还没被 systemd 全面接管前动手,避免复杂依赖,减少风险。
5.4 常见启动问题速查表
| 现象 | 常见原因 | 第一动作 |
|---|---|---|
卡在grub rescue> | GRUB 找不到配置文件或模块 | 手动set root/set prefix加载 |
显示VFS: Unable to mount root fs | root 设备变化或驱动缺失 | 检查root=参数,重建 initramfs |
| 启动进入 emergency mode | /etc/fstab配置错误 | 查看journalctl -xb,修复挂载项 |
| 黑屏且光标闪烁 | 显卡相关驱动问题 | 启动时临时加nomodeset |
| 启动后网络服务等待很久 | 网络等待超时 | 定位systemd-analyze blame里的可疑单元 |
| 一键重启后找不到 Linux 启动项 | NVRAM 或 ESP 被改写 | 用efibootmgr检查启动条目并调整 |
进入 emergency mode 时,系统通常会先尝试以只读方式挂载根文件系统,然后提供一个极小的 shell。这期间fsck可以手动执行,但要在确认分区没有被占用的情况下进行。排查/etc/fstab时,我习惯先备份,再逐一检查每个条目,尤其是挂载点是否存在、UUID 是否仍然匹配。
6. 启动提速与日常维护的个人经验
排查故障是“救火”,日常维护则是“防火”。启动速度和稳定性其实都源于平时养成的几个习惯。
6.1 先找到瓶颈再动刀:不要盲目裁剪
优化启动时间的第一步永远是测量,而不是凭感觉删服务。执行systemd-analyze blame之后,我通常会把耗时超过一秒的服务先列出来,再挨个确认它到底是在干什么。有一个典型情况是服务本身启动很快,但带了一个After=network-online.target依赖,而network-online.target要等待网络配置完成。在有 DHCP 的慢速网络环境里,这一等待就可能让启动慢几十秒。
这类问题更合理的解法是调整服务的等待策略,而不是直接禁用网络服务。比如某些机器不需要远程管理的本地工作站,可以考虑不把关键服务绑定到“网络完全就绪”的目标上。每台机器的用途不同,适合别人的优化方案不一定适合你,先看懂依赖链路再动手,才是保持长期稳定的关键。
6.2 服务裁剪要克制:稳定性优先于启动速度
很多新手喜欢一上来就把看不懂的服务全部禁用,省下的几秒启动时间确实可观,但如果禁用的正好是硬件相关服务,后续插拔外设、调整电源策略时就会出现莫名其妙的问题。我的原则是:接近默认配置,只关掉明确已知无用的服务;如果某个服务只是启动慢,优先怀疑它的等待逻辑,而不是一刀切。
另外,systemd里可以调整全局默认超时时间。比如反复遇到启动阶段等待 90 秒的情况,可以在/etc/systemd/system.conf里设置DefaultTimeoutStartSec=30s,把超时上限压到合理范围,这样单元异常时不会傻等太久。但这只是让失败更快暴露,并不能让服务本身变快,最终还是要解决根因。
6.3 维护习惯决定启动稳定性
这几年处理启动故障,我最大的体会是:很多严重问题其实是由几次“小事”叠加出来的,比如/etc/fstab里用过时分区路径、升级内核后不测试就删掉旧内核、/boot空间常年不清理。值得坚持的习惯我总结成几条供参考。
第一,修改/etc/fstab前先备份,并尽量使用 UUID 而不是设备名。第二,内核更新后先重启验证新内核可用,再清理旧的,保留最近一两个可用内核作为回退备份。第三,每次配置 GRUB 或 initramfs 相关操作后,确认配置文件重新生成成功。第四,定期用journalctl -b -p err扫一眼历史错误,很多小问题是在日志里提前暴露的。
实操心得:我见过最可惜的一次事故,是系统在升级时断电,
/boot里的核心文件写坏,重启后直接卡死。后来我养成了备份重要引导配置的习惯,把/etc/default/grub、/etc/fstab这类关键文件定期归档。备份文件不占多少地方,但在故障现场,能帮你省下整套排错时间。
启动流程越复杂,越需要在平时把“为什么每一步要这样做”想清楚。我自己刚接触 Linux 时,遇到开机问题只会照着搜索引擎里的命令复制粘贴,效果时好时坏。后来把固件阶段、GRUB 阶段、内核阶段、systemd 阶段各自的职责拆清楚,再遇到报错,我不急着查“这条报错怎么解决”,而是先想“现在执行到哪一步了、这一步期望拿到什么、实际发生了什么”。这个思路上的转变,比记住任何一条具体命令都更有价值。希望这篇梳理也能给你提供一个类似的排查框架。