上个月给一批虚拟服务器升级内核,从5.10的某个小版本升到了新小版本。命令执行得很顺利,下载、安装、重启一气呵成。结果十几台机器重启完,uname -r一敲,全还是旧内核版本。那一刻我意识到,在欧拉25.09系统里,“新内核装好了”和“新内核成为第一启动项”完全是两件事。
欧拉25.09的引导器是GRUB2,它默认启动哪个内核,由/boot/grub2/grub.cfg里的 default 项和grubenv里的saved_entry共同决定,而不是“哪个版本新就引导哪个”。内核包安装时只是往 /boot 目录和 grub 菜单里增加了一个候选启动项,默认启动项并不会自动跟着新内核走。
这篇文章想把这件事彻底讲透。系统里有多个内核、需要精确指定默认启动版本的场景太常见了:生产环境内核升级要分阶段验证、业务软件依赖特定内核的 kabi 接口、RT 实时内核作为主力、排查故障时需要临时回滚旧内核,全都离不开启动项顺序的手动配置。我会从底层原理拆解到具体命令实操,再把我实际踩过的几个坑完整复盘一遍,你可以直接照着操作。
1. 默认启动项为什么不能想当然:一次升级后仍旧内核启动的真实场景
1.1 从一段平淡无奇的升级经历说起
先还原那个让我印象深刻的场景。某天下午,我把一台测试机从软件源里升级内核,新内核的 RPM 包安装完成后,我习惯性地敲了reboot。大概半分钟后机器回来,我执行uname -r,看到的还是旧版本号。第一反应是升级没生效,于是又用rpm -qa | grep kernel查了一遍,新内核的包明明已经装好了,ls /boot/vmlinuz-*也能看到新内核文件,这就让人有点迷惑。
后来才反应过来,引导层决定启动哪个内核,根本不是“哪个新就用哪个”。具体来说,内核安装包的 post-install 脚本会检测当前系统的 grub 菜单并重新生成 grub.cfg,往菜单列表里插入新内核对应的 menuentry,但默认项的处理逻辑是保留系统当前的 default 设定。也就是说,如果之前配置的是set default="${saved_entry}",而saved_entry还停留在旧内核的名称上,那新内核虽然出现在菜单第二项、第三项,默认选中的仍然是旧内核。
这个逻辑本身是合理的,它给系统保留了版本切换的自主权。但对刚接触多内核管理的人来说,确实容易产生“装完新内核就应该自动用新内核”的错觉。本文后面所有操作,本质上都是在把这种选择权拿回自己手里。
1.2 GRUB2的启动顺序到底由谁决定
要搞懂怎么改,先得搞懂 GRUB2 的启动顺序由谁决定。核心文件是/boot/grub2/grub.cfg,里面每个 menuentry 就是一个可启动项。开机时 GRUB 先读 grub.cfg,找到set default=...这一行,确定默认选中哪一个菜单项。常见写法有三种:
set default="0":硬编码选第一个菜单项。set default="openEuler (5.10.0-153.xx) 25.09":按菜单名称指定。set default="${saved_entry}":从 grubenv 里的 saved_entry 变量读取。
第三种写法最常见,因为它配合“记录上次启动项”的功能使用,灵活性最好。而grub2-set-default命令,本质上就是修改/boot/grub2/grubenv里的saved_entry;grubby则是直接操作 grub.cfg 和 grubenv 中对应的默认项配置。
这个机制可以用一个生活类比来记:GRUB2 是小区门卫,菜单里的内核候选是住户,saved_entry是门卫手里的默认放行名单。新住户搬进来(安装新内核),门卫并不会自动把名单换成新住户,只有你明确告诉他“以后默认叫这个住户”,他才会照做。这个类比虽然简单,但能解释绝大多数“改了不生效”的疑问——你很可能只是改了某一处配置,而门卫实际读的是另一处。
1.3 哪些场景必须手动干预启动顺序
不是每台机器都要手动设置启动顺序,单内核环境的服务器完全不用操心。但下面几类场景一定会用到。
第一,生产环境分批次内核升级。典型流程是先让一两台测试机把新内核设为首选,观察运行状态稳定之后,再逐步扩大到其他机器。如果默认启动项不跟着新内核走,批量切换就无从谈起。
第二,业务软件与内核版本强绑定。某分布式存储的驱动模块、某高性能网卡驱动、某数据库的安全组件,都可能在安装时检测内核版本,或者依赖某个具体内核版本的 kabi 接口。这些业务系统要求机器重启后必须精确回到那个内核版本,其他版本一概不能引导。
第三,实时系统与普通内核共存。需要低延迟响应时,RT 内核是主力,普通内核只能当备用。机器重启后必须默认进 RT 内核。
第四,故障排查与回滚。新内核引发 panic 或者业务异常,需要快速回到上一个已知可用的版本。这时默认启动项必须能一键指向备份内核。
这些场景的共同点是系统里永远不止一个内核,但默认启动项必须精确命中其中一个。下面就从查看现状开始,一步步把配置做起来。
2. 操作前先确认三件事:看内核、看菜单、看当前默认值
2.1 摸清系统里装了几个内核
不管你想设置哪个内核为第一启动项,第一步都应该是把系统里的内核家底摸清楚。我常用的命令是三个组合:
rpm -qa | grep kernel | sort ls /boot/vmlinuz-* uname -r第一条列出所有与内核相关的包,会看到 kernel、kernel-core、kernel-modules、kernel-tools 等一批。数量看着不少,但真正决定启动项的只有 vmlinuz 文件。第二条列出 boot 目录下所有内核文件,这是最直观的版本清单。第三条显示当前运行的内核版本。
三条命令都跑一遍,你就能知道“系统里装了几个候选内核、现在跑的是哪个”。这一步花不了半分钟,但真的能避免后面改错目标。我把它们固定成一条组合命令,需要时直接执行:
echo "--- 已装内核包 ---" && rpm -qa | grep kernel | sort && echo "--- boot目录内核文件 ---" && ls /boot/vmlinuz-* && echo "--- 当前运行内核 ---" && uname -r2.2 整理GRUB启动菜单的真实顺序
RPM 包清单只是候选名单,真实生效的启动菜单要看 grub.cfg。用 awk 从 grub.cfg 里提取所有 menuentry 的名称和顺序:
awk -F\' '/menuentry / {print $2}' /boot/grub2/grub.cfg输出大致是这样:
openEuler (5.10.0-153.xx) 25.09 openEuler (5.10.0-136.xx) 25.09 openEuler (5.10.0-136.xx with kdump) 25.09注意一个细节:同一个内核版本可能对应多个菜单项,比如with kdump、with debug,它们实际是不同的引导参数组合。所以描述“启动哪个内核”时,更准确的说法是“启动哪个 menuentry”。菜单项名称差一个后缀,实际引导出来的参数集就不一样。
这份菜单清单是你后面所有设置的基础。无论用grub2-set-default按名称匹配,还是用grubby按内核文件定位,都以这份清单为参照。我建议把这份输出保存到一个临时文件里,后续操作时反复对照。
2.3 确认当前默认启动项是谁
动手改之前,还得知道当前默认启动的到底是哪一项。查看 grubenv 中的记录:
grub2-editenv list输出可能是:
saved_entry=openEuler (5.10.0-136.xx) 25.09这说明默认启动项由 grubenv 里的saved_entry控制。grubenv 是一个持久化的环境变量文件,专门用来保存 GRUB 运行时的状态。
如果输出里没有 saved_entry 这一条,那默认项就看 grub.cfg 里的set default行:
grep '^set default' /boot/grub2/grub.cfg这一行的内容决定了默认启动逻辑。看到set default="0"就是固定第一个菜单项;看到set default="${saved_entry}"则是从 grubenv 读取。
还有个容易忽略的点:UEFI 引导模式下,grub.cfg 的实际路径可能是/boot/efi/EFI/openEuler/grub.cfg,而 BIOS 模式是/boot/grub2/grub.cfg。大多数传统服务器走 BIOS 或者 Legacy 方式,但如果你管理的是 UEFI 启动的机器,查看和生成配置时要注意路径差异。可以用find /boot -name grub.cfg来定位真实文件。
提示:本章的确认工作不是可选项。我见过太多人跳过这一步直接执行设置命令,结果把默认项配到了错误的目标上,重启后才傻眼。
3. 改默认启动项的三类主流操作与各自适用场景
3.1 grub2-set-default:按菜单名称指定
这是最直观的命令,语义直白:设置当前系统的默认启动项。
grub2-set-default "openEuler (5.10.0-153.xx) 25.09"引号里的内容必须和 grub 菜单里的 menuentry 名称完全一致,大小写、空格、括号、版本号都不能差。执行成功后,用grub2-editenv list验证,能看到saved_entry被改成了目标菜单项。
这条命令的优势是直接用菜单名称定位,人类可读性好。缺点是你的操作对象是“名称”,一旦菜单里有多个相似名称,或者名称写错,它会在 grubenv 里写入一个无法精确匹配的条目,启动时 GRUB 可能自动回退到第一个菜单项。所以用这条命令之前,最好把 grub.cfg 里的菜单名称完整复制出来,不要手敲。
3.2 grubby:按内核文件指定
grubby 是系统级引导配置工具,它不关心菜单显示名称,而是直接操作内核文件和启动条目。查看所有启动项的完整信息:
grubby --info=ALL输出会列出每个启动项的信息,包括 index、title、kernel、initrd 等字段。想设置某个内核文件为默认项,直接用文件路径:
grubby --set-default=/boot/vmlinuz-5.10.0-153.xxgrubby 会根据这个 vmlinuz 文件找到它对应的菜单项,并调整默认配置。还可以按菜单索引设置:
grubby --set-default-index=1在脚本化批量管理时,grubby 往往比 grub2-set-default 更可靠,因为它以内核文件路径为锚点,不依赖菜单名称的格式稳定性。我自己的习惯是:交互式操作用 grub2-set-default,写脚本或批量操作时用 grubby。
3.3 /etc/default/grub文件级配置
如果你的系统要求长期稳定地把某个内核作为默认启动项,推荐走文件配置路线。打开/etc/default/grub:
vim /etc/default/grub找到GRUB_DEFAULT这一行,当前值可能是saved,也可能是0。改成目标内核的数字编号或完整菜单名称:
GRUB_DEFAULT=2或者更稳妥的写法:
GRUB_DEFAULT="openEuler (5.10.0-153.xx) 25.09"改完不等于生效,必须重新生成 grub.cfg。这里先说清不同写法的后续影响:GRUB_DEFAULT=saved表示始终读取 grubenv 中的 saved_entry,适合配合 grub2-set-default 做动态切换;写死数字表示固定第几个菜单项,简单但脆弱,一旦内核顺序调整就跟着错;写菜单名称最稳定,即使菜单顺序变了,名称不变就不会错,缺点是新内核升级后名称也需要同步更新。
3.4 生成配置与验证
改完文件必须执行配置生成,让/etc/default/grub的改动真正落到 grub.cfg 里:
grub2-mkconfig -o /boot/grub2/grub.cfg这条命令会扫描 /boot 目录下的 vmlinuz 和 initramfs,结合/etc/default/grub里的选项和/etc/grub.d/下的脚本,重新生成一份完整的 grub.cfg。
验证分两步。第一步看默认项是否对准目标:
grub2-editenv list grep '^set default' /boot/grub2/grub.cfg第二步确认目标内核的菜单项确实存在于 grub.cfg 中:
awk -F\' '/menuentry / {print $2}' /boot/grub2/grub.cfg | grep '5.10.0-153'注意:grub2-mkconfig 在 UEFI 系统上有时需要指定输出路径,比如
/boot/efi/EFI/openEuler/grub.cfg。不确定平台的话,先用grub2-mkconfig --help看一下帮助,或者直接查当前系统 grub.cfg 的真实路径再动手。
我把三类方式整理成一张表,方便对照选型:
| 方式 | 适用场景 | 优势 | 注意事项 |
|---|---|---|---|
| grub2-set-default | 临时切换、交互式操作 | 命令直接,改 grubenv 即时生效 | 菜单名称必须完全一致 |
| grubby | 按内核文件定位、脚本批量操作 | 不依赖菜单名称格式,稳定 | 需先确认内核文件路径 |
| /etc/default/grub | 长期固定默认项 | 配置持久化,重启后不被覆盖 | 必须配合 grub2-mkconfig 执行 |
4. 实测替换过程:从修改配置到重启确认的完整链路
4.1 一个完整实例:把RT内核设为默认启动
空讲不如走一趟。某台测试机上同时装了普通内核和 RT 实时内核,目标是把 RT 内核设成默认启动项。整个过程分四步。
第一步定位。执行uname -r确认当前内核,ls /boot/vmlinuz-*看候选版本,grub2-editenv list记录当前默认值。
第二步设置。因为 RT 内核的菜单名称可能带着rt或者preempt_rt这类标识,名称又长又容易记岔,我用 grubby 按内核文件路径设置更省心:
grubby --set-default=/boot/vmlinuz-5.10.0-153.rt.xx第三步确认。执行grub2-editenv list,输出里saved_entry已经变成 RT 内核对应的 menuentry。
第四步重启验证:
reboot机器起来后马上uname -r,输出5.10.0-153.rt.xx,和预期一致,RT 内核成功成为第一启动项。整个过程从执行到确认不超过两分钟,真正耗时间的反而是前期判断哪个菜单项对应哪个内核文件。
4.2 翻车现场一:菜单里两个几乎一样的内核项
最近同事就栽在这个坑上。他执行grub2-set-default,明明指定了一个版本号,重启后 grub 默认选中的却是另一个。我上去排查,发现 grub.cfg 里同一个版本号其实对应两个 menuentry:一个是标准引导,另一个带with kdump后缀。两个项的名称只有尾部几个字符不一样,而他复制的名称恰好少了一小块。
排查命令还是那几条。用grubby --info=ALL把每个 menuentry 对应的 kernel 文件路径列出来,才发现同名不同质。
这类问题的根治办法是用grubby --set-default=/boot/vmlinuz-xxx,以文件路径为锚点。文件名在 boot 目录里是唯一的,不会出现两个 vmlinuz 同名的情况。从这次之后,我给自己用的操作脚本一律改成 grubby,再也没有遇到过菜单名称匹配出错的问题。
4.3 翻车现场二:内核可以启动但外部模块加载失败
把内核设为第一启动项只是第一步,内核能不能正常干活,还得看外部模块兼容性。
有一次我把某台机器切到新内核,启动顺序没问题,系统也正常起来了,但业务方很快就反馈异常。查看 dmesg 和 journald 日志,发现某网卡驱动模块加载失败,错误信息直指内核 kabi 接口不匹配。这台机器上的业务是某分布式存储集群,网卡驱动是硬件厂商后装的,对内核版本有严格绑定。新内核临时启动可以,但驱动模块加载不了,网络就断了,存储节点在集群里基本处于不可用状态。
我的处理方式:先grub2-set-default切回旧内核,等业务恢复正常,然后去硬件厂商确认该网卡驱动支持的内核版本范围,再规划下一步升级路径。这次事故给我的经验是:切换内核之前,把机器上所有外部安装的驱动、数据库、安全组件梳理一遍,凡是和内核版本绑定较深的,都要先确认兼容性再行动。
4.4 翻车现场三:重启后默认项悄悄回到老内核
比同名菜单更隐蔽的坑是:配置时一切正常,saved_entry 也改对了,重启后却回到老内核。我第一次遇到时一度怀疑 grubenv 被什么机制重置了,后来排查才发现,问题根源在于配置来源不一致。
在欧拉25.09里,grubenv 的 saved_entry 确实被改成了新内核,但 grub.cfg 里的set default这一行写的不是${saved_entry},而是一个写死的数字。这个数字正好对应旧内核的菜单顺序。于是重启时 GRUB 只看 grub.cfg 的 default 行,根本不关心 grubenv 被改成了什么样。
这个坑的排查顺序我再理一遍:
grep '^set default' /boot/grub2/grub.cfg,看它到底引用 saved_entry 还是写死数字。grub2-editenv list看 saved_entry 当前值。- 如果 default 行是数字,用
grubby --set-default-index或改/etc/default/grub来修正,不要只依赖 grub2-set-default。
配置来源统一之后,重启再没出过问题。
4.5 救援补救:GRUB启动失败时的命令行操作
最坏的情况是改完默认启动项之后,重启进入不了系统,grub 菜单直接卡住或者报错。这时候还有最后一招:GRUB2 的命令行和编辑模式。
如果 grub 菜单还能显示,直接按e进入编辑模式,找到以linux开头的行,改成你希望引导的内核和根分区:
linux /vmlinuz-5.10.0-136.xx root=/dev/mapper/root ro initrd /initramfs-5.10.0-136.xx.img按Ctrl+X引导进入系统。
如果 grub 菜单直接黑了,要在 grub 提示符下用ls列出分区,找到存放 vmlinuz 的分区,再手动指定内核文件和 initramfs 来引导。这些操作在系统手册里都有,但真到机器起不来的那天,人很容易手忙脚乱。所以我建议把这条救援流程提前写到本地笔记里,出了问题照着敲就好。
进系统后不要急着重复之前的错误配置,先恢复默认项到已知可用的内核,重新生成 grub.cfg,确认无误后再做下一步调整。
5. 多内核长期共存的几条经验
5.1 保留内核版本要克制
多内核共存是常态,但保留多少版本需要克制。boot 分区空间一般不会成为瓶颈,问题在于内核版本囤积会让 grub 菜单越来越长。菜单项多了,默认项配置的错误率会上升,重启时需要人工确认的场景也会增加。
我个人的习惯是保留最近两到三个稳定内核版本:当前正式使用一个,回滚备用一个,再留一个应急的旧版本。这样既能覆盖大多数故障场景,又不会让配置复杂度失控。如果你发现系统里已经堆了七八个内核,建议先按下一节的方式清理一轮,再考虑默认项的设置。
5.2 卸载旧内核的正确姿势
不需要的内核应该清理,但清理时要讲顺序。第一步确认当前运行的内核不在清理列表里:
uname -r第二步卸载旧内核包:
dnf remove kernel-5.10.0-136.xx卸载命令会同步删除对应的 vmlinuz 和 initramfs 文件,并尝试更新 grub.cfg。为了保险,卸载完成后手动再生成一次配置:
grub2-mkconfig -o /boot/grub2/grub.cfg然后检查 grub 菜单,确认被卸载版本对应的 menuentry 已经消失。再检查 grubenv 和/etc/default/grub里的默认项,确保它们指向的内核仍然存在。
提示:如果默认项指向了一个已被卸载的内核,重启后 GRUB 会从第一个可用菜单项启动。这多数时候能兜底,但可能不是你想要的。所以清理前务必检查默认项。
5.3 变更前的备份与回滚思路
内核启动项配置的变更,单看每一步都不复杂,但生产环境里还是要有回滚预案。我的做法是先备份当前配置:
cp /boot/grub2/grub.cfg /boot/grub2/grub.cfg.bak.$(date +%F) grub2-editenv export > /boot/grub2/grubenv.bak.$(date +%F)如果切到新内核后发现问题,只要能进 grub 菜单,就先选择备用的旧内核启动项把机器带起来,然后恢复备份的 grub.cfg 和 grubenv,重新生成配置,再重启。
这套思路其实和软件发布的灰度策略一个逻辑:先保证能回退,再往前推进。配置启动项这种看似低风险的操作,在规模化的服务器环境里也值得当成一次发布来对待。我后来把所有服务器的内核切换都纳入了这个流程之后,再没有因为配置启动项这件事在半夜爬起来救过机器。