按下电源键之后,Linux 到底经历了什么才最终出现登录界面?“Linux 开机启动流程”这个词,我在排查服务器无法启动、调优系统启动速度和带新人入门时反反复复地讲。它看似是一张大学操作系统课的目录表,实际上却是一张排查路线图:从固件到引导程序,从内核到 init,每一环都是一道关卡,任何一环出问题,机器就卡在那里不动。这篇文章把我这些年在真实故障中验证过的启动流程完整写一遍,适合刚开始接触 Linux 运维的读者,也适合做嵌入式开发或想彻底搞懂 systemd 的同学。
1. 整个启动流程的大局观
1.1 启动是一个接力赛而不是单步执行
Linux 开机启动流程如果用一句话概括,就是“接力棒依次从硬件传到固件、引导程序、内核、init 进程”的过程。完整链条可以拆成四个主要阶段:
| 阶段 | 主要角色 | 关键产物 | 职责 |
|---|---|---|---|
| 固件阶段 | BIOS / UEFI | 启动设备选择 | 初始化硬件,寻找可引导设备 |
| 引导阶段 | GRUB2 / GRUB Legacy | 加载内核与 initramfs | 解析启动配置,把控制权交给内核 |
| 内核阶段 | Linux Kernel | initramfs / rootfs | 硬件驱动初始化,挂载根文件系统 |
| init 阶段 | systemd / SysVinit | 系统服务运行 | 启动所有用户态服务,进入目标运行级别 |
我常用一个生活化的类比来理解这四个阶段:固件像是酒店前台,它只负责确认你现在能出现在房间里,并告诉你电梯在哪;引导程序是礼宾员,知道你最终要去哪一层,还帮你安排了路线;内核是酒店的工程部,负责把所有房间的电路、水路、网络全部打通;init 则是运营团队,把这些资源分配给各个业务单元,让客人真正住进来。
这个类比能解释一个关键点:越靠前的阶段,能访问的硬件和资源越少。固件甚至不需要认识文件系统,它只认固定的设备地址和启动文件;引导程序才开始具备“读取配置文件”的能力;而内核则拥有全部的硬件访问权限。理解这个递进关系,后面排查问题时会非常有帮助。
1.2 为什么不能一步到位直接加载内核
很多人会问:为什么固件不能直接把内核读进内存,还要中间夹一层引导程序?答案在于工程上的“职责分离”和“实现限制”。
固件(BIOS 或 UEFI)在设计时只负责最底层的硬件初始化和启动设备选择,它没有文件系统驱动的概念,也不关心你装的是 Linux 还是其他系统。而引导程序是各个操作系统自己维护的,天然能解析自己生态内的配置格式。如果让固件直接加载内核,固件就必须适配每一种内核的格式和特性,这对上下游都不是一件好事。
另一方面,嵌入式系统和桌面系统的需求差异也决定了必须有这一层。嵌入式设备经常直接让固件加载内核,省去 bootloader,以换取启动速度和简化设计;而桌面服务器需要多系统选择、内核参数调整、故障恢复等能力,引导程序就不得不存在。所以“直接加载”不是做不到,而是设计取舍的结果。
1.3 各发行版的差异没有想象中那么大
不同 Linux 发行版的启动流程大体一致:固件阶段基本相同,区别主要出现在引导程序和 init 的实现上。RHEL/Fedora/CentOS 系用 GRUB2 加 systemd;Debian/Ubuntu 系也是 GRUB2 加 systemd;Arch 同样;Gentoo 可能使用 GRUB2、Syslinux 或 EFISTUB;嵌入式场景则可能用 U-Boot 加自定义 init。
| 发行版/场景 | 引导程序 | init 系统 |
|---|---|---|
| RHEL / CentOS / Fedora | GRUB2 | systemd |
| Debian / Ubuntu | GRUB2 | systemd |
| Arch / openSUSE | GRUB2 | systemd |
| Gentoo | GRUB2 / Syslinux / 自定义 | systemd / OpenRC |
| 嵌入式 / Yocto | U-Boot | 自定义 init / systemd |
也就是说,只要掌握了 GRUB2 和 systemd 这两大件,就能覆盖绝大多数实际运维场景。SysVinit 在如今的新系统里基本绝迹,碰到 CTDB、LXC 等老环境才会用到,原理上差异也比较大。
2. 固件阶段:BIOS 与 UEFI 的交接
2.1 POST 自检与启动设备选择
按下电源键后的第一件事,是 CPU 开始执行固件中被映射到固定地址的代码。这里有一段所有 PC 工程师都绕不过去的流程:先执行 POST(通电自检),检查 CPU、内存、关键控制器是否存在并能正常工作。POST 通过后会进入启动设备选择阶段,固件会按照设定的顺序逐个检查可引导设备。
BIOS 时代的逻辑是从设备的第一个扇区读取引导代码。具体是读取硬盘 MBR(主引导记录)的前 446 字节,检查最后的 0x55AA 标志位,然后把控制权交给这段代码。MBR 的局限性很明显:分区表只有 64 字节,只支持 4 个主分区,而且由于使用 32 位逻辑块地址,极限只能处理约 2TB 的磁盘。这也是为什么现在的 PC 和服务器全面转向 UEFI 的重要原因。
UEFI 的逻辑完全不同。它支持从 FAT32 格式的 ESP(EFI System Partition)分区中读取.efi文件,通过 NVRAM 中保存的启动项来决定加载哪个文件。启动项不仅包含路径,还包含可选的参数,比如efibootmgr可以查看和修改启动顺序:
efibootmgr -v输出会列出编号、启动标签、设备路径等信息。如果机器启动时直接进入 UEFI Shell 而不是 GRUB 界面,大概率是 NVRAM 启动项丢失或者损坏,重建启动项是比较基础的应急操作。
2.2 Secure Boot 与内核签名
UEFI 还有一个绕不开的机制叫 Secure Boot。它通过固件内置的密钥库来校验启动文件的数字签名,防止恶意程序在系统启动早期注入。这本来是一件好事,但和 Linux 主线的自编译内核却天生有矛盾:自编译内核没有签名的密钥,Secure Boot 会拒绝加载。
我踩过这个坑。之前在对应发行版上编译了一个带特定驱动的内核,重启后直接提示Verification failed: (0x1A) Security Violation,屏幕定格在固件界面,完全不给进系统的机会。解决方式有三种:
| 方案 | 适用场景 | 操作方式 |
|---|---|---|
| 在固件中关闭 Secure Boot | 自用开发机 | 开机进入 UEFI 设置,关闭 Secure Boot |
| 使用发行版自带签名内核 | 生产环境 | 直接安装发行版仓库内的内核 |
| 注册自己的 MOK 密钥 | 需要保留 Secure Boot | mokutil --import,重启后按提示登记 |
如果你是个人电脑自编译内核,直接关闭 Secure Boot 最简单。如果是工作用的服务器,建议走 MOK 密钥这条路,不要轻易动固件安全设置。
2.3 固件阶段常见故障速查
固件阶段的故障现象往往在画面还停留在固件 logo 时就能察觉。碰到过以下几类典型问题:
- UEFI 找不到 ESP 分区:通常表现为启动时白屏或者提示
No bootable device,在固件设置里确认 SATA/NVMe 硬盘是否被发现,再做一次启动项探测。 - NVRAM 启动项失效:多见于重装 Windows 后 Windows Boot Manager 把 Linux 的启动项顶掉,用 Live USB 进系统后
efibootmgr -c重新添加。 - Secure Boot 拒绝加载:自编译内核或第三方驱动导致的签名校验失败,按上一小结的 MOK 方案处理。
这阶段排查的思路和其他阶段不太一样:能操作的工具非常少,基本只能靠固件设置界面。所以经验是,遇到启动问题先判断点是落在固件还是更后面,判断方法很粗暴:如果画面完全停在固件 logo 或直接提示没有设备,那基本都是固件层;如果固件 logo 消失后出现了 GRUB 界面或者内核日志,那已经进入了下一关。
3. 引导程序阶段:GRUB2 的配置与实战操作
3.1 GRUB2 在启动链条中的真实职责
GRUB2 是当前 Linux 世界使用最广泛的引导程序,它的核心职责可以压缩成三件事:加载内核,加载 initramfs,传递内核参数。所谓“引导”真不是把文件读进内存那么简单,它还需要解析文件系统上的配置文件,把内核和数据块从根分区或 /boot 分区中读出来,然后以正确的协议把控制权交给内核。
GRUB2 相比老 GRUB 有革命性的变化:模块化设计和运行时命令解析。它可以在启动时动态加载文件系统驱动,识别 ext4、xfs、btrfs 等格式,甚至读取 LVM 逻辑卷。这意味着只要你的根分区格式在 GRUB2 支持的范围内,它不需要像老系统那样依赖裸扇区读写就能启动内核。
也正因为这个原因,改 GRUB2 的配置文件后需要重新生成主配置,因为主配置文件grub.cfg本质上是脚本生成的结果。直接手改grub.cfg能生效,但下次更新内核时就会被覆盖,所以规范做法是改/etc/default/grub和/etc/grub.d/下的模板,再运行生成命令。
3.2 /etc/default/grub 的常用配置项
/etc/default/grub是最常修改的配置文件,里面的变量会被/etc/grub.d/中的脚本读取并嵌入生成的grub.cfg。下面是我常用的配置说明:
GRUB_DEFAULT=0 GRUB_TIMEOUT=5 GRUB_CMDLINE_LINUX_DEFAULT="quiet splash" GRUB_CMDLINE_LINUX=""GRUB_DEFAULT:默认启动第几个菜单项,从 0 计数。也可以填菜单项的唯一 id 字符串,比如GNULinux。GRUB_TIMEOUT:菜单等待时间,设置为 0 直接启动默认项,设置为-1则会一直等待手动选择。GRUB_CMDLINE_LINUX_DEFAULT:正常启动时附加的内核参数,比如quiet splash、net.ifnames=0等。GRUB_CMDLINE_LINUX:所有启动模式都附加的内核参数,包括恢复模式。
修改完配置后,需要生成新的grub.cfg。RHEL 系是grub2-mkconfig,Debian 系是grub-mkconfig,注意命令不同:
grub-mkconfig -o /boot/grub/grub.cfg如果你在 UEFI 系统上,生成后可能还需要安装更新引导项:
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=GRUB跳过重新安装这一步,只跑 mkconfig 在绝大多数场景下也能生效,但如果修改了分区布局、换了磁盘,这一条就很有必要。
3.3 开机时临时修改内核参数
这是我最依赖的排查手段之一。当系统无法正常启动时,在 GRUB 菜单界面按e进入编辑模式,找到以linux开头的行,在行尾追加你想临时加的内核参数,然后按Ctrl+X或F10启动。这个修改只对本次启动生效,不会写进配置文件。
典型应用场景:
| 场景 | 追加参数 | 效果 |
|---|---|---|
| 进入紧急救援模式 | systemd.unit=rescue.target | 进入单用户模式 |
| 绕过损坏的显示驱动 | nomodeset | 使用基本帧缓冲 |
| 强制使用指定显卡驱动 | modprobe.blacklist=nouveau | 屏蔽开源驱动 |
| 找回 rootfs 挂载失败的原因 | rd.break | 在 initramfs 中中断进入 shell |
我处理过一次服务器在更新内核后无限重启的问题,就是因为显卡驱动在 kernel 5.x 上发生了冲突。重启后在 GRUB 编辑界面加nomodeset才顺利进入系统,之后再用工具把错误驱动从 initramfs 里摘掉。这种“先临时修复再永久修复”的套路,比直接改配置要安全得多。
还要提醒一句:GRUB 编辑模式下按Ctrl+X或F10启动,不同机器的按键提示会显示在屏幕底部,实在找不到就看屏幕最后一行。第一次操作时建议在虚拟机里练一遍,因为生产机上手抖改坏一个参数虽然不会损坏数据,但会平白多花几分钟重启时间。
3.4 initramfs 与内核文件的读取顺序
GRUB2 在解析grub.cfg中的menuentry时,会依次执行多个命令。典型的一段配置大致如下:
menuentry 'GNU/Linux' { load_video set gfxpayload=keep insmod gzio insmod part_gpt insmod ext2 set root='hd0,gpt2' if [ x$feature_platform_search_hint = xy ]; then search --no-floppy --fs-uuid --set=root --hint-bios=hd0,gpt2 6a8e... fi linux /vmlinuz-linux root=UUID=6a8e... ro quiet splash initrd /initramfs-linux.img }这里有几个容易踩坑的细节:set root是 GRUB 自己的根设备概念,不是 Linux 内核的根文件系统;search --fs-uuid用 UUID 来定位分区,避免因盘符变化导致找不到文件;linux和initrd两行分别指定内核和 initramfs 文件。如果内核文件路径写错,会在 GRUB 界面直接报file not found;如果 initramfs 路径写错,内核能启动但会在加载根文件系统时卡住,两种现象要区分开。
4. 内核阶段与 initramfs 的秘密
4.1 内核自解压与核心初始化
GRUB2 把vmlinuz文件加载到内存后,控制权从引导程序转移到内核。这看起来只是一次跳转,但内核本身是一个压缩过的可执行文件。内核开始执行时会先用一段非常精简的汇编代码完成自身解压,然后切到保护模式,调用start_kernel()进入 C 语言世界。
start_kernel()之后做的最重要的事情是按顺序初始化各个子系统:内存管理、调度器、中断、定时器、驱动模型、网络协议栈等。这些初始化代码不是挨个写死的,而是通过initcall机制将初始化函数整理成多个层级,按优先级依次调用。这也是为什么内核启动日志里能看到大量[ OK ]风格的初始化消息,但顺序非常有规律。
这段初始化过程对普通运维来说不可见,因为它发生在 init 进程启动之前。你能观察到的就是内核日志被刷屏,比如[ 1.234567] ACPI: ...、[ 1.567890] EXT4-fs: ...等。建议在排查启动问题时导出这份日志保存下来,因为一旦进入 init 阶段后,旧日志就可能被滚出 systemd journal 的窗口。
4.2 initramfs 为什么必不可少
现代 Linux 发行版的根文件系统可能存在于 LVM 逻辑卷、加密分区、软件 RAID 甚至是网络文件系统上。内核自身的驱动模块体积太大,不可能全部编入内核映像,而是在系统安装时生成的临时根文件系统中,也就是 initramfs(早期也叫 initrd)。
initramfs 的本质是压缩过的 cpio 归档,内部包含的内核模块确保根文件系统的驱动能在启动早期被加载。启动时内核先挂载这个零时根,运行其中的 init 脚本,加载所需的驱动,然后通过switch_root切换到真正的根文件系统。整个过程对用户是透明的,但如果你遇到根文件系统相关的启动失败,需要深入到这里面排查。
Debian/Ubuntu 系查看和使用 initramfs 的命令是:
lsinitramfs /boot/initrd.img-$(uname -r)RHEL/CentOS 系对应的是:
lsinitrd /boot/initramfs-$(uname -r).img如果在 initramfs 里找不到某个驱动,可以使用dracut --add-drivers xxx --force(RHEL 系)或update-initramfs -u -k all(Debian 系)重新生成。注意重新生成 initramfs 时一定要指定正确的内核版本,避免生成之后引导的还是旧内核。
4.3 rd.break 调试 initramfs
当你怀疑根文件系统挂载失败时,进入 initramfs 的现场 shell 是最直接的排查方式。在 GRUB 编辑界面给linux行追加rd.break,内核会在 initramfs 阶段停下来,给你一个包含完整工具集的 shell。
在 shell 里可以做以下事情:
# 查看哪些块设备被识别 ls /dev/sd* ls /dev/nvme* # 查看 LVM 状态 lvm vgscan lvm vgchange -ay # 查看当前挂载情况 mount # 检查根文件系统分区 fdisk -l /dev/sda这个 shell 里内核模块已经加载了大部分,能直接确认是驱动缺失、LVM 无法激活、还是 root 参数写错。比如有一次我发现系统卡在挂载根文件系统的阶段,rd.break进去检查后发现是磁盘在 RAID 卡上,而 initramfs 里没有对应的驱动模块。重新生成 initramfs 时指定--add-drivers megaraid_sas后恢复正常。没有这个调试手段,这种问题只能靠盲猜。
还有一个比rd.break更简单的排查方式:在启动参数里去掉quiet和splash,让内核把完整的启动信息打出来。很多发行版默认这两个参数把日志屏掉了,去掉它们就能看见卡在哪个驱动或哪个挂载点,信息量非常直观。
5. systemd 接管:从内核到用户态服务
5.1 /sbin/init 与 systemd 的默认 target
内核完成初始化并挂载真实根文件系统后,会执行/sbin/init。在 systemd 系统中这是指向 systemd 的符号链接,而 systemd 启动后的第一个任务就是确定它的默认启动目标(target)。
在 systemd 中,target 相当于 SysVinit 的运行级别。常用 target 有:
| Target | 对应行为 |
|---|---|
| poweroff.target | 关机 |
| rescue.target | 单用户救援模式 |
| multi-user.target | 多用户文本模式 |
| graphical.target | 多用户图形界面模式 |
可以通过内核参数直接指定要进入的 target,例如在 GRUB 编辑界面加systemd.unit=rescue.target就能进入救援模式。这个技巧不需要记住任何 systemctl 命令,开机时敲一下就行。
查看当前默认 target:
systemctl get-default修改默认 target:
systemctl set-default multi-user.target如果误改了默认目标导致系统无法进入图形界面,在 GRUB 编辑界面临时指定systemd.unit=graphical.target就能恢复,然后再把默认设置改回来。
5.2 单元依赖与并行启动模型
systemd 启动系统的方式和传统脚本完全不同。传统 SysVinit 以固定顺序执行启动脚本,一行一行跑,慢得离谱;systemd 则是用单元(unit)描述依赖关系,找出可以并行的部分同时启动。
systemd 的依赖以三种关键字描述:
Requires=:强依赖,如果依赖的单元启动失败,本单元也不启动。Wants=:弱依赖,依赖的单元启动失败不阻止本单元启动。After=:排序依赖,只决定启动顺序,不决定是否启动。
举例说明:网络服务单元network.service可能声明After=network.target,意味着系统不会在网络目标完成前启动它,但这个声明本身不会去激活网络目标;而要激活网络目标,则需要某个单元声明Wants=network.target。理解这三者的区别是理解 systemd 启动行为的关键,排查服务启动顺序问题时要重点看服务的 unit 文件。
并行启动带来的直观效果是启动速度大大提升,但也带来了一个隐藏问题:你无法再用“按时间顺序读日志”的方式理解系统状态,因为日志可能来自多个并行单元。因此启用了 systemd 以后,排查服务启动问题的手段完全变了,要依赖systemctl status、journalctl -u等命令。
5.3 systemd-analyze 与启动耗时分析
如果你想优化开机速度,systemd-analyze是最快入门的工具。它从内核挂载根文件系统开始计时,直到到达默认 target,给出各部分耗时。
可以直接使用以下命令:
# 查看总耗时 systemd-analyze time # 按耗时排列所有启动单元 systemd-analyze blame # 生成矢量时序图 systemd-analyze plot > boot-plot.svgblame的输出能帮我们找出启动耗时最高的单元,比如某个服务等待硬件稳定等待了 30 秒,那它就是这个阶段启动慢的元凶。有次我优化一台服务器的启动速度,blame显示systemd-udev-settle.service等待了 50 秒,原因是这个老服务在等待某个不存在的设备。排查后确认该设备已从硬件清单中移除,直接 mask 掉这个服务,开机时间立刻缩短到原来的三分之一。
再补充一个实际经验:systemctl list-dependencies default.target可以列出默认 target 下所有依赖单元,里面的Wants=列表是可以做减法的候选对象。但不是所有单元都能随手禁用,建议按“是否影响业务”为边界,逐步验证关闭。
6. 启动过程排障实践:从现象到根因
6.1 日志的正确打开方式
启动失败的排查,第一步一定是收集日志。systemd 环境下的日志查询优先级非常高,开机阶段的输出虽然打印在屏幕上,但很多信息在进入用户态后就被覆盖了,需要靠 journald 来复盘。
# 查看本次启动的日志 journalctl -b 0 # 查看上次启动的日志(当前启动失败时看 -1) journalctl -b -1 # 只看错误级日志 journalctl -b 0 -p err # 查看某个单元的启动日志 journalctl -u network.service -b 0 # 查看内核启动环形缓冲区 dmesg有一个处理内核早期启动问题的技巧:在 GRUB 里去掉quiet,让内核打印完整日志,同时也可以在/etc/default/grub里的GRUB_CMDLINE_LINUX中加loglevel=7,让内核日志输出到控制台。这个参数会显著增加启动信息量,排查完了记得改回来,不然日常启动满屏滚动也挺闹心。
6.2 常见故障对照表
下面这份对照表是我在实际维护中反复用到的问题定位汇总,按故障现象分类。
| 故障现象 | 可能原因 | 排查命令/操作 |
|---|---|---|
开机卡在 GRUB 菜单或grub>提示符 | grub.cfg 损坏、内核文件丢失 | 用 Live USB 进入系统,重新生成 grub.cfg |
内核启动后卡在Kernel panic - not syncing | 内核模块冲突、根文件系统无法识别 | 查看 panic 之前最后的日志行,用rd.break检查 |
卡在Failed to mount /sysroot | 根文件系统挂载失败、LVM 未激活 | rd.break后手动vgchange -ay,修复 fstab |
systemd 启动超时,服务卡在started后无反应 | 单元依赖循环或者设备等待 | systemctl list-jobs查看等待中的任务 |
开机进入emergency mode | 关键本地文件系统挂载失败 | 查看/etc/fstab是否有无效条目,注释测试 |
| 图形界面黑屏但系统能通过 SSH 访问 | 显示管理器或显卡驱动问题 | 切换到multi-user.target,修复图形栈 |
Kernel panic时屏幕上最后几行信息是最值钱的,它通常会明确写清楚是哪个驱动或哪个函数触发的问题。比如常见的VFS: Unable to mount root fs,说明根文件系统没有被正确挂载,多半是 initramfs 没包含对应磁盘驱动,或者 root= 参数指向错误。
6.3 优化启动速度的几个实测建议
做完排障后顺便聊聊优化,这项工作和排障相辅相成。通过systemd-analyze blame找到耗时大的单元后,按以下优先级处理:
- 不存在的设备等待服务:
systemd-udev-settle.service和systemd-networkd-wait-online.service是最常见的元凶。确认不再需要的设备或网络栈后,systemctl mask掉它。 - 手动启动的旧服务:有些第三方软件安装后会自动设为开机自启但实际不需要,
systemctl list-unit-files --state=enabled检查清单,逐项确认能否 disable。 - fstab 中无效条目:
/etc/fstab里指向不存在分区的条目会导致进入 emergency mode,删掉无效行是启动提速最容易被忽视的手段。 - 夜间定时任务无关的扫描服务:例如某些备份软件开机扫描,确认业务不需要时禁用。
用 mask 可以杜绝服务被任何方式启动,而 disable 只是不让它开机启动,区别是 mask 使用了-> /dev/null的符号链接强制阻断。对排查时确认不该存在的单元,习惯用 mask,一劳永逸。
还有一个细节:systemd-analyze critical-chain default.target可以查看到达默认 target 的关键路径。它会把耗时累计最长的链条高亮标注,沿着这条链优化的收益是最高的,比盲目禁服务高效得多。
启动流程的第一步永远是判断故障落在哪一环。固件阶段的问题在固件界面上就能判断,引导程序阶段会停在 GRUB 界面或提示找不到文件,内核阶段会看到 panic 或挂载失败,systemd 阶段则表现为服务超时或某些 target 无法到达。每次处理启动故障时,我习惯先打开dmesg和journalctl -b -1,把启动过程的完整日志导出,再从中找异常,而不是盯着屏幕上最后几行乱猜。
在我个人的实操中,最有价值的三个排查技巧其实很简单:一是去掉quiet让日志全部打在屏幕上,二是用rd.break进入 initramfs 现场看设备识别情况,三是在 GRUB 里临时改内核参数而不是急着改配置文件。这三个技巧覆盖了我遇到的大部分启动故障场景,省下了大量重启试错的时间。这套流程今天看来基本稳定,但每次遇到新硬件、新内核版本时,仍然值得重新走一遍完整的启动链路,因为驱动冲突、固件行为差异、systemd 单元变化,总是在你想不到的地方冒出来。