☰
Linux系统启动全流程解析:从BIOS/UEFI到systemd的故障排查实战
2026/10/8 2:51:02 网站建设 项目流程

干 Linux 运维这些年,每次听到"系统启动"这四个字,我的第一反应不是教科书里那张分层图,而是这些年熬夜排查过的一堆真实故障:开机卡在 GRUB 菜单、内核 panic、systemd 等某个服务等满 90 秒、fstab 写错直接进维护模式……"Linux 系统启动"看起来只是通电到登录之间短短几十秒,实际上它是整个操作系统最浓缩的一条地图,也是运维事故最高发的环节。这篇文章我打算抛开那种"从 BIOS 讲到用户态"的干巴巴套路,换成我平时排查问题的视角,把启动全流程、关键配置、常见故障和修复手段串成一条实际操作链路。不管你是刚接触 Linux 的新人,还是已经有几年经验、想根治"启动坏了就重装"这个老毛病的同行,按着这条链路走一遍,收益都会很直观。

1. 先看懂 Linux 启动全流程:你机器开机时到底在干什么

1.1 从按下电源键到引导程序的完整链路

很多人把"开机"当成一个黑盒操作,其实 CPU 通电后的每一步都是有迹可循的。用 x86 平台举例,按下电源键后,CPU 会从复位向量地址取值,跳进去执行的第一段代码不是 Linux 内核,也不是 GRUB,而是主板固件——传统 BIOS 或者现代 UEFI。

传统 BIOS 的开机过程是:先做加电自检(POST),检测内存、键盘、磁盘控制器这些基础硬件,然后按照 CMOS 里配置的启动顺序去找引导设备。找到设备后,BIOS 会读取该设备第一个扇区的前 446 字节,也就是 MBR(主引导记录)。这 446 字节里放的就是引导加载器的第一阶段代码,BIOS 无条件地把它读进内存并跳转执行。UEFI 的路径不太一样:它读取 NVRAM 里保存的启动项列表,从专用的 EFI 系统分区(ESP,一个 FAT 文件系统分区)中加载.efi引导程序。因为 UEFI 自带文件系统驱动,所以它可以直接读取分区内文件,比 BIOS 那种"裸读扇区"的方式灵活得多,也因此现代发行版几乎都默认 UEFI 引导。

这里有个常见误区:很多人以为启动顺序是"BIOS → MBR → GRUB → 内核",在纯 UEFI 环境下这套描述并不成立。UEFI 直接加载 ESP 分区里的grubx64.efi,MBR 那套只在传统 BIOS 启动模式下才用得上。判断自己机器用的是哪种模式,可以在 Linux 下看/sys/firmware/efi目录是否存在,存在即 UEFI。

1.2 谁在什么时候做什么事:启动阶段速查

我习惯把启动过程拆成四个大阶段,每个阶段的执行主体和交接点都很清晰:

阶段执行者核心任务典型结束标志
固件阶段BIOS/UEFI自检、初始化硬件、选择启动设备读取引导程序并跳转
引导阶段GRUB 等引导加载器加载内核与 initramfs 到内存跳转到内核入口
内核阶段Linux 内核初始化硬件、驱动、内存管理挂载根文件系统并启动 PID 1
用户态阶段systemd/init拉起系统所有服务和目标登录界面或命令行提示符

这四段之间的交接点是故障的高发地带。"No bootable device"说明固件没找到可引导设备;"error: file not found"大概率是 GRUB 找不到内核文件;"VFS: Unable to mount root fs"就是内核挂不上根文件系统。记住每个阶段结束的标志,排查时就能快速缩小范围——先确认卡在哪两个阶段之间,再决定往哪个方向查。

2. 引导加载器 GRUB:系统启动的第一道关口

2.1 GRUB 的两种引导路径与磁盘空间约束

GRUB 全称 GRand Unified Bootloader,是目前绝大多数 Linux 发行版默认的引导管理器。它的工作流程在不同固件环境下差异不小。

传统 BIOS/MBR 模式下,GRUB 需要分阶段加载。MBR 只有 512 字节:前 446 字节是引导代码,中间 64 字节是磁盘分区表,最后 2 字节是结束标志0xAA55。这点空间放不下任何文件系统驱动,所以 GRUB 只能把第一阶段代码(boot.img)塞进 MBR,它的唯一任务就是找到并加载核心镜像core.img。core.img通常放在 MBR 和第一个分区之间那段空闲扇区里,体积一般几十 KB,包含了 GRUB 的基础文件系统驱动。有了驱动,GRUB 才能去/boot/grub目录读取完整的配置文件和模块。我之前见过有教程直接往 MBR 里dd写东西,写着写着就把整个引导弄没了——了解这个空间约束以后,你就会明白为什么 GRUB 安装这件事不能拿十六进制编辑器乱来。

UEFI 模式下就简单多了。GRUB 以.efi可执行文件的形式放在 ESP 分区,UEFI 固件能直接识别 FAT 文件系统,于是加载整个 GRUB 成为一体操作,不再需要分阶段跳转。这也是为什么 UEFI 机器上,引导损坏后用grub-install修复时,除了指定磁盘,还要注意--efi-directory指向 ESP 分区的挂载点。我踩过最典型的坑就是把 ESP 误挂到了/boot上,结果grub-install把文件写进了跟系统文件混在一起的分区,重启后固件依然找不到启动项。

2.2 grub.cfg 里那几个字段,值得你逐行看懂

很多人不敢碰 GRUB 配置,其实核心就那么几行。以常见的 Debian/Ubuntu 为例,一个典型的菜单项长这样:

menuentry 'Ubuntu' { recordfail load_video insmod gzio insmod part_gpt insmod ext2 search --no-floppy --fs-uuid --set=root 2a4ae7a9-...-... linux /vmlinuz-5.15.0-91-generic root=UUID=2a4ae7a9-...-... ro quiet splash initrd /initrd.img-5.15.0-91-generic }

逐行解读一下:

  • insmod ext2:加载 ext2/ext3/ext4 文件系统模块。GRUB 必须能识别出/boot所在分区的文件系统,才能读取后续的vmlinuz和initrd文件。
  • search --no-floppy --fs-uuid --set=root:按文件系统的 UUID 找到/boot所在分区,并把它设为 GRUB 的根设备。用 UUID 而不是设备名,是因为/dev/sda这种设备名会随磁盘枚举顺序变化,插个 U 盘重启就可能变,UUID 则稳定得多。
  • linux /vmlinuz-... root=UUID=... ro quiet splash:指定内核文件的路径,以及传递给内核的参数。其中root=用来告诉内核真正的根文件系统设备在哪。
  • initrd /initrd.img-...:指定 initramfs 映像路径,这部分后面详聊。

实际操作中,千万不要直接手工编辑/boot/grub/grub.cfg。这个文件是发行版用脚本生成的,下次更新内核或重新部署引导时就会被覆盖。正确做法是去改/etc/default/grub(比如换内核参数、调整菜单超时时间),改完执行update-grub(RHEL 系是grub2-mkconfig -o /boot/grub2/grub.cfg)。我当时第一次改启动等待时间就踩过这个坑,手工改了 GRUB_TIMEOUT,结果内核一升级全被冲掉,又等回去。

2.3 GRUB 损坏后的急救思路

GRUB 损坏的场景太多了:双系统装 Windows 把 MBR 覆盖、开了 Secure Boot 导致 GRUB 拒绝加载、分区结构调整后路径失效。真要遇到也别慌,急救路径是固定的:

  1. 用任何 Linux 安装 U 盘或 live 环境启动到救援模式。
  2. 把原系统的根分区挂载到/mnt,如果/boot独立分区,也要一并挂上。
  3. 使用chroot切换到原系统环境,重新生成 GRUB 配置,并执行安装命令。
mount /dev/sda2 /mnt mount /dev/sda1 /mnt/boot mount --bind /dev /mnt/dev mount --bind /proc /mnt/proc mount --bind /sys /mnt/sys chroot /mnt # BIOS 模式 grub-install /dev/sda # UEFI 模式 grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=Ubuntu grub-mkconfig -o /boot/grub/grub.cfg

有个细节:进入 chroot 前,/dev、/proc、/sys一定要 bind 挂载,否则grub-install可能探测不到设备,甚至会把设备节点写进错误的路径。这一步不做好,后面所有操作都会跑偏。

3. 内核初始化与 initramfs:破解根文件系统的鸡生蛋问题

3.1 initramfs 到底是干嘛的

内核被 GRUB 加载以后,第一件大事就是把你的根文件系统挂载上来。但这里有个逻辑死锁:根文件系统在磁盘上,要读它需要文件系统驱动;如果磁盘是 NVMe、LVM、RAID 或加密设备,还需要对应的高级驱动;而这些驱动编译成模块后存放在根文件系统的/lib/modules目录里——内核都还没挂上根文件系统,上哪去读这些驱动?

解决这个死锁的东西就是 initramfs。它是一个迷你根文件系统,通常由发行版用 dracut 或 update-initramfs 生成,以压缩的 cpio 归档形式存放在/boot/initrd.img-*文件里。内核启动时会把这个归档解压到内存临时根目录,执行其中的/init脚本。这个脚本的角色是"过渡管家":加载好真正根文件系统需要的所有驱动模块,挂载真实根设备,然后通过switch_root或pivot_root切换到真实根,再执行真正的 PID 1 程序(现在是 systemd)。

很多人被initrd和initramfs这两个名字绕晕了。历史上 initrd 是一个块设备镜像,内核要先提供一个 RAM disk 块设备来承载它;现在绝大多数发行版用的都是 initramfs,本质是内存中的 tmpfs,不需要额外块设备。虽然文件名还叫initrd.img,但内部机制已经是 initramfs 了,用file /boot/initrd.img-...能直接看到它是 gzip/xz 压缩的 cpio 归档。我在排查启动问题时会用lsinitramfs /boot/initrd.img-...查看里面真的有哪些模块和脚本,这一步对判断"驱动是不是没打进去"非常有用。

3.2 start_kernel 之后的内核初始化:从通电到 PID 1

内核入口分两条路:底层体系结构相关的汇编代码先完成最早的页表、寄存器设置,然后进入架构无关的start_kernel()。顺着start_kernel()往下看,这个函数像极了一位新上任的管理者第一天点名:

  • setup_arch():梳理处理器架构相关的内存布局,知道这台机器有多少物理内存、内核镜像放哪。
  • trap_init()/init_IRQ():搭建中断和异常处理框架,否则内核连"被打断"的能力都没有。
  • time_init():初始化系统时钟,之后的调度、超时统计全依赖它。
  • mem_init():建立内存管理系统,为后续创建进程准备资源。
  • rest_init():创建两个关键进程——PID 1 的 init 进程和 PID 2 的 kthreadd 内核线程守护进程。

rest_init()之后的剧情就交给了用户空间。PID 1 先运行 initramfs 里的/init脚本,完成驱动加载和真实根挂载之后,通过switch_root切换到真实根,最终 exec 成/sbin/init或/usr/lib/systemd/systemd。也就是说,内核本身在启动阶段忙的事情其实相当收敛:把基础框架搭好,然后立刻把控制权交给用户空间的管理者。理解这一点以后,再看dmesg里那些密密麻麻的初始化日志,你就能根据时间线大致判断内核卡在哪一步了。

3.3 内核启动参数:调试和维护的"万能钥匙"

GRUB 菜单里可以按e临时编辑内核参数,这对现场调试极其有用。以下是我日常用得最多的几个参数:

参数作用典型场景
quiet隐藏绝大部分内核日志正常启动,配合splash显示开机动画
loglevel=7打开 debug 级别内核日志内核阶段卡住时,看最后的输出
single/s进入单用户模式忘记 root 密码,需要重置密码
rd.break在 initramfs 执行早期中断,进入急救 shell解密 LUKS 失败、根驱动加载异常
systemd.unit=emergency.target直接进入 systemd 紧急目标某个服务反复挂掉导致无法正常进系统
panic=10内核 panic 后 10 秒自动重启无人值守机器,避免一直挂死

拿忘记 root 密码举例:在 GRUB 菜单选中内核,按e编辑,在linux那一行末尾加上rd.break,然后按引导继续。系统会停在 initramfs 阶段的 shell 里,这时根文件系统可能还没完整挂载,你需要用mount -o remount,rw /sysroot把真实根以可写方式挂到/sysroot,再chroot /sysroot,就能执行passwd重置密码了。注意这要求sysroot在 initramfs 中已经被挂载,实际操作中如果挂载点不对可以先看ls /sysroot里的内容再决定。用完rd.break后记得把参数删掉,否则每次开机都会停在 shell。我的习惯是:临时调试参数只加到 GRUB 的临时编辑,确认有效后,再写进/etc/default/grub的GRUB_CMDLINE_LINUX里持久化。

4. systemd 接管:从内核态到用户态的切换艺术

4.1 systemd 凭什么比旧 init 快

早期 SysV init 的启动方式是典型的串行执行:按脚本编号 S01、S02、S03……一项接一项跑,后面的服务必须等前面的完成。这种方式逻辑简单,但效率极低,而且脚本之间真正的依赖关系被数字顺序掩盖了——你在 S40 放数据库、S41 放网站服务,如果两台机器上的脚本顺序排布不同,启动结果可能完全不同。

systemd 的核心理念是把"执行顺序"换成"依赖关系":每个服务单元通过After=、Requires=、Wants=声明自己依赖谁。systemd 启动时先读取所有单元,解析成一个依赖关系图,然后从图里找没有依赖冲突的节点,并行拉起。依赖关系允许并行,没有依赖关系的服务也可以并行,所以整个用户态阶段的时间被大幅压缩。这也就是为什么同样的硬件,RHEL 7 及以上开机比 RHEL 6 明显更快。

除此之外,systemd 还支持 socket 激活和 D-Bus 激活。比如一个服务平时没人访问,就没有必要在开机时立刻启动,systemd 可以先监听它对应的 socket,等第一个请求到来时再把服务进程拉起来。这种"按需启动"看着有点绕,但在减少开机服务数方面非常有效,也是现代桌面系统启动迅速的原因之一。

4.2 Target 单元:不是运行级别,而是状态集合

很多人习惯把 systemd 的 target 对应成 SysV 的运行级别,这个理解方向对,但细节不同。SysV 的运行级别是单一的数字,systemd 的 target 是"一组单元的集合",进入某个 target 意味着把属于它的所有单元都拉起来。常用对应关系如下:

systemd Target说明类比 SysV 运行级别
poweroff.target关机0
rescue.target单用户救援模式,只挂载必要文件系统1
multi-user.target多用户字符界面3
graphical.target图形界面,在 multi-user 上叠加显示管理器5
reboot.target重启6

日常高频操作里,最实用的命令是切换默认启动目标。比如服务器装了图形界面想省掉,不一定要卸载桌面套件,直接执行systemctl set-default multi-user.target,下次开机就只进字符界面。想临时切换救援模式则执行systemctl isolate rescue.target。需要注意的是,isolate会停掉当前 target 里不属于新 target 的单元,操作前留意是否有未保存的数据。

4.3 用 systemd-analyze 定位启动慢的关键环节

系统开机慢,第一反应先别瞎猜服务,用 systemd 自带的性能分析工具把数据拉出来:

systemd-analyze blame | head -20 systemd-analyze critical-chain

blame按耗时从高到低列出每个单元的启动时间,适合快速锁定"谁最拖"。critical-chain则展示从启动到当前目标的关键路径,能看出整条链路里哪个环节最慢。我常见的一种输出类似这样:

graphical.target @3.541s └─multi-user.target @3.541s └─network-online.target @3.540s └─NetworkManager-wait-online.service @118ms 3.422s

每次看到NetworkManager-wait-online.service占着几秒钟,我都想吐槽——这个服务的意义是等待网络完全就绪,但对很多普通客户端来说,开机网络根本没有硬性依赖。如果机器上网络环境稳定,systemctl mask NetworkManager-wait-online.service能立省几秒甚至几十秒的启动时间。注意这里用的是mask而不是disable,mask 相当于把这个服务彻底屏蔽,连手动启动都不行;想恢复就systemctl unmask NetworkManager-wait-online.service。这俩命令的区别我最初也搞混过,disable 只是取消开机自动启动,mask 是直接禁止拉起。

5. 启动故障排查:典型案例与修复实录

5.1 开机卡住的通用排查路径

开机卡住是最常见的故障,但"卡住"这个词太笼统。我习惯先回答一个问题:到底卡在哪个阶段?判断方法很简单——看屏幕上的动态:

  • 还没出现任何引导信息就停在品牌 logo 或黑屏:固件或硬件层面,先查 BIOS/UEFI 设置、启动顺序、内存、磁盘。
  • 能看到 GRUB 菜单但选完删除后黑屏:引导阶段问题,检查 GRUB 配置、内核文件是否存在。
  • 黑色屏幕上滚动大量内核日志然后停住:内核或 initramfs 阶段,日志的最后一行通常是线索。
  • 启动动画结束或输出几行 systemd 信息后长时间停滞:用户态服务问题,大概率是某个服务在超时等待。

定位阶段之后,内核阶段的问题,把 GRUB 菜单里的内核参数暂时改成loglevel=7,观察内核最后输出的几行内容。systemd 阶段的问题,在 GRUB 里临时加systemd.unit=rescue.target进入救援模式,用journalctl -xb翻启动日志。我处理过最典型的卡住案例是"Reached target Multi-User System"之后无故冻住,最后用journalctl -xb发现/etc/fstab里一个不存在的挂载点让 systemd 死等 90 秒,直到超时才放弃。

5.2 fstab 配置错误导致进入维护模式的完整修复

fstab 是 Linux 启动故障排名前三的元凶。它的格式是六列:设备、挂载点、文件系统类型、挂载选项、dump 备份标记、fsck 检查顺序。只要有一行写错设备 UUID,启动时系统就会尝试等待对应设备出现,等不到就进入 emergency 模式,屏幕上出现 "Give root password for maintenance" 或提示输入 root 密码进行维护。

我记得有一次紧急处理远程服务器,用户反馈开机后进不了系统,只停在维护提示。排查路径如下:

  1. 输入 root 密码进入维护 shell,执行mount -o remount,rw /让根文件系统可写。
  2. cat /etc/fstab找到可疑的那一行——某个 UUID 开头的条目,用blkid对照真实设备的 UUID,确认 UUID 确实不存在。
  3. 用注释符#把错误行临时注释掉,保存重启。
  4. 确认系统能正常进入后,如果是笔误,改正 UUID;如果是设备已经移除,直接把那一行删除。
  5. 如果错误的挂载写入被固化进 initramfs,再执行update-initramfs -u重新生成 initramfs。

过程中最容易犯的错是直接对根文件系统执行fsck,而这一步在根已经被挂载的状态下是不该做的。对已挂载的文件系统做 fsck 轻则工具拒绝执行,重则造成文件系统元数据损坏。要 fsck,就得从 live U 盘启动或进入 initramfs 的rd.breakshell,在根未挂载或只读挂载的前提下操作。

5.3 服务启动超时拖慢开机的实战记录

有时候系统最终能进,但得等上很长一段时间,这通常不是"起不来"问题,而是"某一个服务起不动"。我用systemd-analyze blame定位过一次典型的 90 秒延迟,罪魁是一个负责挂载网络共享的服务,它依赖的网络地址迟迟不可达。系统显示a start job is running for ... (90s / no limit),等满 90 秒才跳过。

处理这种服务超时有几个思路:

  • 如果服务确实没必要在开机时执行,systemctl disable或mask掉。
  • 如果服务要做但网络条件差,在它的单元文件里调整TimeoutStartSec和TimeoutStopSec,缩短不合理的等待时长。
  • 对网络挂载类需求,fstab 条目上加_netdev或x-systemd.automount,让 systemd 在该设备真正被访问时才去挂载,而不是开机时死等。

不要一上来就乱删服务,先systemctl status看服务的实际状态,journalctl -u 服务名看具体日志。很多超时的根因是上游依赖(网络、其他服务)没就绪,单纯延长本服务超时只是治标。如果服务彻底不需要,mask才是最干净的方案,这样连依赖它的其他单元也不会因为等待它而被拖慢。

6. 从启动流程延伸:优化省时与操作习惯

6.1 启动性能优化:实测有效的几个方向

服务端和桌面端对"开机快"的需求不同,但优化方向大体一致。我试过综合应用以下手段,普通 x86 机器启动耗时能肉眼可见地缩短:

  • 精简内核模块:普通桌面机器的 initramfs 里经常塞了很多用不到的驱动。Debian/Ubuntu 上可以修改/etc/initramfs-tools/initramfs.conf里的模块列表,用update-initramfs -u重新生成,让 initramfs 更小、加载更快。RHEL 系则用 dracut 的--omit-drivers选项。注意留出余地,别把磁盘控制器驱动也剔出去。
  • mask 掉确定无用的服务:上面提到的 NetworkManager-wait-online、不需要的 bluetooth、打印服务等,mask省下的是排队等待时间。
  • 默认目标切到multi-user.target:如果服务器不需要图形界面,这一步省掉的是整个显示管理器及其依赖。
  • 检查并修复 fstab 里的异常等待项:任何无法立刻满足的挂载需求都会让 systemd 傻等 90 秒,这条最不起眼但收益最大。
优化项节省时间参考风险等级
mask 无用网络等待服务2~30 秒低
精简 initramfs 模块0.5~3 秒中
切换到 multi-user.target3~10 秒低
修复 fstab 异常等待最多 90 秒低

6.2 这些操作习惯,能帮你少踩启动的坑

以我这些年修过的机器为教训,有几个习惯真的能救命:

  • 改任何系统关键文件(fstab、grub 配置、内核参数)之前,先备份:cp /etc/fstab /etc/fstab.bak花不了 1 秒,但能让你在恢复现场时从容十倍。
  • 远程机器上做引导相关操作前,反复确认。修改 grub 配置、重新安装引导、切换内核后,不要立刻远程重启,先再想一遍有没有哪个环节可能让你连不上机器。真做过一次因为改 GRUB 导致远程机器失联的事,此后我所有引导操作都先在维护窗口期执行,并确保能通过带外管理接口或现场访问。
  • 升级内核后保留旧内核不要立即删除。发行版默认保留多个内核版本,启动菜单里可以按高级选项进入旧内核。一旦新内核有驱动或模块问题导致起不来,旧内核就是最可靠的回滚通道。等新内核稳定运行两周以上,再用apt purge或dnf remove清理旧内核。
  • 学会看日志而不是遇到启动故障就重装。journalctl -xb、dmesg、systemctl status、systemd-analyze这四个工具组合使用,能覆盖 90% 以上的启动问题定位。重装系统往往意味着数据丢失和时间成本,而排查流程一旦熟练,多数问题半小时内能找到思路。

这几年我最大的体会是,Linux 启动流程不是一个背完就忘的知识点。每次遇到启动故障,回头对照这条链路查一遍,你可能又对内核、对 systemd、对文件系统多了一层理解。别怕启动起不来,怕的是没有任何排查路径。把这条链路刻进脑子里,下次再看到屏幕上那些滚动日志或 systemd 的报错,你会比之前任何时候都镇定——因为你已经知道是谁在执行、可能卡在哪一步、以及下一步该看什么日志了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询