又到了RH134的知识点整理时间。第10章"控制启动过程"这节,很多人嫌杂:一会儿systemd,一会儿GRUB,一会儿又是target,背完就忘。但说句实在话,想搞懂Linux系统到底是怎么从按下电源键走到登录界面的,这一章反而是最值得反复琢磨的内容。这篇文章我不打算按教材顺序机械罗列,而是用一条完整的启动链条把散落的知识点串起来,再加上我自己在真实机器上反复试过、踩过的坑。如果你正在备考相关认证,或者刚接触RHEL系系统、遇到启动类故障不知道怎么下手,这篇应该对你有用。
1. 启动链条拆解:固件、GRUB、内核与systemd的交接
1.1 POST与引导设备选择:从BIOS到UEFI
运维做久了,总会遇到一次"服务器开机黑屏""卡在某个logo"的场面。要定位这类问题,必须先搞清楚启动的最开始几十秒发生了什么。
机器通电后的第一阶段是做硬件自检(POST),CPU、内存、磁盘控制器这些基础硬件都要过一遍。之后固件会按预设的设备顺序找可引导设备。老一代的BIOS是通过查找硬盘主引导记录(MBR)末尾的启动标志来定位引导代码;现在的主流是UEFI,它直接去读取EFI系统分区(ESP)里的.efi引导文件,比如grubx64.efi。UEFI的好处是更规范、支持GPT分区、启动速度也快一些。
这里有个很常见的现象:一台机器装了双系统,结果开机总是直接进另一个,其实不一定是系统坏了,很可能是固件引导顺序里排在前面的是另一个盘。我之前给一台测试机排障,就是插着U盘没拔,导致UEFI启动顺序里USB排第一,机器每次开机都卡在引导选择界面。拔掉U盘,问题立刻消失。所以遇到"开机不进系统"的第一件事,不是重装系统,而是先进固件界面看启动顺序。
1.2 GRUB2到底加载了什么
固件找到引导程序之后,控制权交给GRUB2。RH134这章重点就是要理解GRUB2的工作:它本身是一个小型操作系统,负责呈现启动菜单、选择内核、并把内核和初始RAM磁盘加载进内存。
GRUB2的主配置文件在/boot/grub2/grub.cfg。这个文件一般不建议手动改,它是由grub2-mkconfig命令根据/etc/default/grub和/etc/grub.d/目录下的脚本自动生成的。如果你直接手改了grub.cfg,下次内核更新或者重新生成配置时,你的改动很可能被覆盖,白忙活一场。
grub.cfg里的每个菜单项都带着一组关键信息:内核文件vmlinuz-xxx、初始RAM磁盘initramfs-xxx.img,以及一串内核命令行参数。其中root=UUID=xxx用来指定根文件系统所在分区。我建议大家在排障时先学会看这个文件,它能告诉你系统每次开机到底选了哪个内核、传了什么参数,这些信息在后续排查启动问题时非常有用。
1.3 initramfs存在的理由
很多新手会问:内核为什么不直接挂载根文件系统,非要先加载一个initramfs?这其实是个经典的"鸡生蛋、蛋生鸡"问题。
内核要挂载真正的根文件系统,就需要磁盘控制器驱动和文件系统驱动。但这些驱动文件放在哪里?通常就在根文件系统里。不加载驱动就找不到根,找不到根就加载不了驱动。initramfs就是用来解开这个死结的:它是一份压缩的小型根文件系统,里面预先塞进了开机挂载真实根所需的驱动模块、udev规则和一些基础工具。
启动时内核先解压initramfs到内存,以它为临时根跑起来,然后在里面加载真正的磁盘驱动和文件系统模块,最后把真实的根文件系统挂载到/sysroot,再做一次根切换,转向真实的根环境继续启动。所以,如果系统突然换了新的磁盘控制器,或者根文件系统所在的文件系统类型需要特殊模块,甚至只是initramfs损坏,都可能卡在这一步。重建initramfs的命令其实很简单,比如说用dracut -f /boot/initramfs-$(uname -r).img $(uname -r),这类操作在救援场景中经常用到。
1.4 systemd接管后的第一个动作
根切换完成后,内核执行/sbin/init。在现代RHEL系系统上,/sbin/init是指向systemd的符号链接,所以真正接管用户空间的是systemd,它从此成为进程号唯一为1的进程。
systemd接管后的工作,不再是像SysV时代那样一个脚本一个脚本地顺序执行,而是把所有开机任务抽象成"单元"(Unit),根据单元之间的依赖关系构建一张启动图,然后并行启动互不冲突的任务。这也是为什么现在开机速度普遍比老系统快很多。
在这一步,判断启动卡在哪里的常用手段有两种:一是去掉内核参数里的rhgb和quiet,让系统把启动日志直接打到屏幕上;二是等系统勉强起来或进入救援环境后,用journalctl -b -1查看上一次启动的完整日志。前者适合直观观察卡点,后者适合事后复盘分析。两者结合,基本能覆盖大部分启动排障场景。
2. systemd单元与依赖:为什么服务能"同时"启动
2.1 单元文件的结构与优先级
把systemd的启动逻辑拆开看,核心就在单元文件。单元文件通常放在两个位置:/usr/lib/systemd/system/是软件包安装时自带的,属于"出厂配置";/etc/systemd/system/是管理员自己创建的,或者用来覆盖出厂配置。RHEL系的规则是/etc下的同名单元优先级更高。
那日常维护时想改一个服务的行为,应该怎么改?我强烈建议不要整体复制单元文件到/etc下,而是用override配置目录:在/etc/systemd/system/对应服务名.service.d/下创建一个override.conf,只写需要覆盖的字段。好处很明显,软件包升级后,上游单元文件的修复和安全更新仍然能通过/etc下未覆盖的部分生效,不会因为本地整体文件把新改动全部挡住。
单元文件内部结构分为几段。每个单元通用的是[Unit]段,里面写Description描述、After/Before顺序、Wants/Requires依赖;服务单元有[Service]段,定义ExecStart启动命令、ExecStop停止命令、Restart重启策略;[Install]段则决定服务在什么target下被enable,最常用的就是WantedBy=multi-user.target。
2.2 After和Requires:顺序与依赖必须分开看
单元文件里最容易被搞混的就是After/Before与Wants/Requires这两组字段。我见过不少运维把After当成"必须等这个服务启动成功",实际上完全不是一回事。
After和Before只定义启动顺序,不意味着对方一定要启动。比如A设置了After=B,意思是如果B在当前启动集合里,那就先启动B再启动A;但B哪怕启动失败了,A还是会照常启动,因为没有硬性依赖关系。Wants和Requires才是定义依赖的:Requires是硬依赖,被依赖方失败,依赖方也会失败;Wants是软依赖,被依赖方失败,依赖方不受影响。
所以一个严谨的服务单元,既要表达"我依赖它",又要表达"我需要的顺序",那就要同时写After和Wants/Requires。拿典型的Web服务来说,它需要网络就绪才能正常监听端口,单元文件里通常就有After=network-online.target,同时network-online.target又依赖网络管理服务,systemd靠着这串依赖图就能自动决定谁先谁后。
2.3 systemctl常用操作与"为什么服务又活了"
理解单元文件之后,systemctl的命令就很好串起来了。start/stop/restart只影响当前运行状态,不影响下次开机;enable/disable控制的是开机是否自启,不影响当前状态;还有一个常被忽略但非常实用的mask/unmask,会把服务彻底屏蔽,相当于把单元文件指向/dev/null,就算别的服务声明了Wants依赖它,systemd也不会把它拉起来。
我调过的一个内部系统案例就是栽在mask没用好上。当时有个服务A在单元里Wants了服务B,运维想把B停掉,执行了systemctl disable B,当时看着是停了,结果下次开机B又出现在进程列表里。原因就是disable只去掉了B自身的自启设置,但服务A的依赖仍然会把B拉起。后来改成systemctl mask B,B才真正"死透"。所以判断服务为什么"复活",先看谁在依赖它,再看自己的enable状态。
2.4 用journalctl回看启动过程
排障启动问题时,只看当前运行状态的journal是不够的。系统每次启动的日志都按boot分区保存,几个实用命令值得记下来:
- journalctl -b 查看本次启动日志。
- journalctl -b -1 查看上一次启动日志,这在系统起不来、只能进救援环境时特别有用。
- journalctl -u xxx.service 只看某个服务单元的日志。
- systemctl list-units --failed 直接列出启动失败的服务。
有一次我处理一台启动慢且中途卡住的机器,就是在rescue环境里用journalctl -b -1找到了一个挂载单元反复超时,顺着日志里的设备路径检查,才发现是fstab里一个不再存在的旧分区配置没删干净。这种问题如果不看上次启动日志,单靠肉眼很难定位。
3. target目标单元:运行级别的后现代替代品
3.1 默认target与运行时切换
SysV时代有个"运行级别"(runlevel)的概念,0到6各有含义。现代RHEL系已经用target取代了这套机制。target本质上是一组单元的集合点,类似一个分组标签,把开机时该启动的一堆服务聚合在一起。比如multi-user.target会触发网络、SSH、cron等服务;graphical.target又在multi-user的基础上追加图形桌面相关的服务。
查看系统默认target用systemctl get-default,修改默认target用systemctl set-default。比如我想让一台带桌面的服务器开机直接进命令行,就执行systemctl set-default multi-user.target,下次开机就不会再进入图形界面了。
运行时临时切换target用systemctl isolate,比如systemctl isolate multi-user.target可以在不重启的情况下直接退出图形界面进入命令行。但注意isolate并不是每个target都能用,只有单元文件里声明了AllowIsolate=yes的target才支持,否则会拒绝执行。这个设计是为了避免误操作把一个正常运行的target切碎。
3.2 runlevel N与target的对应关系
尽管运行级别已经退出历史舞台,系统还是保留了兼容映射,方便老脚本和习惯runlevel的运维。映射关系是这样:
- runlevel3.target 对应 multi-user.target
- runlevel5.target 对应 graphical.target
- runlevel1.target 对应 rescue.target
- runlevel0.target 对应 poweroff.target
- runlevel6.target 对应 reboot.target
在旧服务脚本里用chkconfig管理开机启动的服务,到了systemd时代,就把WantedBy写在对应target下。比如一个服务希望开机进入字符界面时启动,单元文件里写WantedBy=multi-user.target,然后执行systemctl enable,就会在multi-user.target.wants目录里生成一个软链接。所以搞懂target之间的依赖,就搞懂了服务是怎么被"拉"起来的。
3.3 依赖链排查:list-dependencies的用法
target和target之间、target和服务之间经常有依赖。查询这些关系有一个很顺手的命令:systemctl list-dependencies。默认只列出一级依赖,想看完整链条就加--all参数。
比如systemctl list-dependencies graphical.target,输出里就会包括multi-user.target、NetworkManager.service、sshd.service等一大串。反过来,当一个服务无法启动时,也可以用systemctl list-dependencies xxx.service查看它依赖了谁,再逐个检查被依赖单元的状态。
我排障遇到过一次诡异现象:systemctl status显示sshd没起来,但直接执行systemctl start sshd又能成功。最后用list-dependencies一查,发现sshd单元里有个After=network.target,而network.target在这个系统上被一个自定义服务拖住了,导致sshd在启动序列里一直排队等。问题不在sshd本身,而在依赖链上游。这类经验说明,排障不要只盯着出问题的服务本身,往依赖链上下游多看两级往往更快。
4. 起不来机器时的求生路线:救援模式与重置密码
4.1 rescue与emergency:什么样的故障选哪个
系统启动到一半挂了,进不了正常登录界面,这时候先别急着重装。RHEL系提供了两个特殊target:rescue.target和emergency.target。
rescue.target可以理解为"最小可用环境":它会尽量挂载根文件系统为可读写,尝试挂载其他文件系统,并启动少量基础服务。这个模式适合大多数启动故障,比如修复fstab写错、重建initramfs、重置root密码。emergency.target则更极端,只挂载根文件系统为只读,几乎不启动任何服务,适合根文件系统本身都报错、基础挂载都无法完成的场景。
选择标准其实很简单:如果是某个服务坏了导致进不了系统,优先rescue,因为该有的工具依赖都在;如果是根分区挂载失败、文件系统受损,才考虑emergency。能进rescue就别先进emergency,后者环境太简陋,能做的事有限。
4.2 从GRUB菜单手动进入救援target
实际操作上,物理机和虚拟机从GRUB进入救援模式的过程是这样:
- 开机出现GRUB菜单时,选中默认的内核条目,按e进入编辑界面。
- 找到以linux或linuxefi开头的行,这是内核启动参数所在行。
- 在该行末尾追加systemd.unit=rescue.target(或者更简洁的single,不过single最终也会指向rescue)。
- 按Ctrl-x启动,系统会进入带root shell的救援环境。
- 输入root密码后,就可以执行修复操作了。
这里要提醒一个容易翻车的地方:RHEL 8/9在UEFI方式下,该行通常显示为linuxefi而不是linux。追加参数要注意位置,必须加在这一行的末尾,别换成下一行,否则GRUB解析配置失败会直接回到菜单,什么也进不去。
4.3 重置root密码的完整流程与常见翻车点
root密码忘了,或者root被意外锁定,这是每个管理员早晚会遇到的事。RH134这章讲的方法非常实战,步骤看起来不多,但每一步都有坑。
先进入GRUB编辑界面,找到linux/linuxefi开头那一行,在末尾追加rd.break,然后按Ctrl-x启动。rd.break的意思是让内核在initramfs阶段停下,这时候根文件系统已经被挂载到/sysroot了,但是我们还没进入真正的根环境。
接着执行mount -o remount,rw /sysroot,把根重新挂载成可写。为什么要这一步?因为启动早期根是只读挂载的,如果不先改成可写,下一步进去之后什么都改不了。然后执行chroot /sysroot,进入真实的根文件系统,执行passwd root给root设置新密码。
这里还有关键一步:在SELinux启用的系统上,建议在chroot环境里执行touch /.autorelabel。如果不做这一步,下次开机时SELinux可能因为/etc/shadow的安全上下文标签不对,直接拒绝认证服务读取这个文件,结果就是密码改了,却仍然无法登录。之所以要touch这个标记文件,是为了触发系统在下次启动时对文件系统做一次安全上下文重标。
做完密码修改后,exit退出chroot,再exit退出挂载,系统会自动继续启动流程。重启后就可以用新密码登录了。
4.4 救援完成后的善后操作
救援模式修完,别直接重启了事。正确做法是确认修复效果和清理现场。
如果是通过systemd.unit=rescue.target进入的,在救援shell里输入exit,系统通常会自动继续完成启动流程。趁这个机会观察启动日志,看之前报错的服务是否恢复正常。用journalctl -b -1看一眼上次启动的报错是否还在,比什么都靠谱。
另外,如果之前为了修问题改过GRUB参数,比如追加了rd.break或者systemd.unit=rescue.target,这些只是临时参数,重启后就不会再带上了,通常不用专门清除。但如果确实把参数写进了/etc/default/grub,那一定要记得改回来,再重新生成grub.cfg。我见过有人排查完问题忘了清理永久参数,导致后面每次开机都进救援模式,白白折腾一上午。
5. 内核启动参数与GRUB配置:想改启动行为先懂这套流程
5.1 看懂常用内核启动参数
RH134这章有一半内容其实是在教读者认识内核参数。常见参数的用途值得整理一下:
- rhgb和quiet:默认会加上的两个参数,让开机过程显示图形化的logo并隐藏细节日志。排障时通常要临时去掉,才能直观看到启动卡在哪一步。
- console=ttyS0:把启动日志输出到串口设备。适合服务器通过串口控制台管理的场景,但没配串口的机器不要乱加。
- systemd.unit=rescue.target / emergency.target:临时指定启动target。
- rd.break:在initramfs阶段中断,进入root shell,适合重置密码等操作。
- selinux=0 enforcing=0:临时关闭SELinux,一般用来判断"是不是SELinux策略导致服务起不来",定位完就要改回去。
这些参数本身不复杂,但要注意区分"临时生效"和"永久生效"。临时生效就是在GRUB编辑界面里改一下,只对本次启动有效;永久生效就要改配置文件并重新生成grub.cfg。
5.2 修改GRUB配置的正确姿势
永久修改内核启动参数的标准流程是这样的:
- 编辑/etc/default/grub,找到GRUB_CMDLINE_LINUX="..."这一行。
- 在双引号内加入或删除需要的参数。
- 执行grub2-mkconfig -o /boot/grub2/grub.cfg重新生成主配置。
- 如果是UEFI引导,部分RHEL版本还要执行同样的命令输出到/boot/efi/EFI/redhat/grub.cfg。
这里最大的坑是漏掉第3步。很多人改了/etc/default/grub就觉得完事了,重启后参数没变就开始怀疑GRUB坏了,其实只是没有重新生成配置。grub.cfg是个静态文件,不会自动感知default/grub的修改。这个套路要记牢:任何GRUB配置变更,最终都要落到grub.cfg才算数。
还有一个小细节:生成配置时会扫描系统里的内核版本,如果你同时装了多个内核,grub.cfg里会列出多个菜单项。想调整开机默认选中哪个,可以用grub2-set-default指定,或者用grub2-editenv list查看当前的saved_entry。这些零碎命令考试不一定会考,但排查时确实常碰到。
5.3 实战演练:命令行启动与关闭图形界面
最后把前面所有知识点串成一个实际场景。假设有一台带图形桌面的测试服务器,想改成开机直接进命令行,同时排障期间想看到完整启动日志。
第一步,执行systemctl set-default multi-user.target,把默认target从graphical.target改成multi-user.target,这样开机就不会再进入图形桌面了。
第二步,编辑/etc/default/grub,在GRUB_CMDLINE_LINUX里去掉rhgb和quiet。如果你的系统当前没加这两个参数,那就不用动,保证屏幕上能输出详细日志就行。
第三步,执行grub2-mkconfig -o /boot/grub2/grub.cfg重新生成配置。这一步不能省。
第四步,重启验证。重启后系统会直接进入命令行登录界面,而且开机过程中会一条条打印systemd启动服务的状态。如果某个服务启动失败,屏幕上一眼就能看到红字提示。
等排障结束,想把安静的图形开机体验找回来,再把rhgb quiet加回去,重新生成grub.cfg即可。整个过程看起来很简单,但把target切换、GRUB参数修改、配置重新生成这三个知识点全部串起来了,练一次比背十遍命令都管用。