系统里装了好几个内核版本,结果每次开机都要在GRUB菜单里盯着倒数计时,手忙脚乱地选内核,这种经历我太熟悉了。被逼到这个份上,多半是因为新内核和NVIDIA驱动打架、某个硬件模块只在旧内核下工作正常,或者跑实时内核(PREEMPT_RT)需要锁定版本。不管你是哪种情况,解决思路都落在同一个地方:GRUB配置。这篇文章我会把Ubuntu下修改GRUB默认启动内核的几种方案、底层原理和实操中踩过的坑一次性讲透,从查看菜单到调整配置再到验证,照着做就行。
1. 为什么需要修改默认启动内核
1.1 常见的多内核共存场景
先说说我在实际中遇到最多的情况。Ubuntu每次系统更新都会自动升级内核版本,默认会在GRUB菜单里保留旧内核,方便出问题时回退。但这样一来,GRUB菜单里往往有好几行"Ubuntu"和"Advanced options for Ubuntu",默认启动的是最新内核。
什么情况下你需要手动修改默认内核?根据我的经验,下面几类场景最常见:
- 新内核和显卡驱动不兼容。装了NVIDIA专有驱动后某次内核升级,驱动模块编译失败,开机进桌面直接黑屏或分辨率异常,把默认内核指回旧版本是最快的止损方式。
- 工控机、服务器需要固定内核版本。跑采集卡、运动控制卡、EtherCAT主站这类依赖内核模块的应用,必须用经过验证的内核版本,不能放任内核自动升级。这也是"实时内核"场景下最标准的操作。
- 新内核存在回归问题。某些内核版本在特定硬件平台上的休眠唤醒、网络驱动、USB控制器方面会有反复,这时手动锁定旧内核是比较理性的选择。
- 测试内核。你想试用候选版本,但希望系统默认还是走稳定内核,出问题随时回退。
提示:在修改默认内核之前,我强烈建议先通过GRUB菜单手动启动一次目标内核,确认它在你的机器上工作正常,再把它设为默认。跳过这一步,很容易把自己锁在门外。
1.2 GRUB在启动流程中的角色
GRUB(GRand Unified Bootloader)是Linux系统中最常用的引导加载程序。它的职责简单说就是:按下电源键之后,BIOS/UEFI从磁盘上把GRUB代码加载进来,GRUB读取/boot/grub/grub.cfg配置文件,然后把内核镜像(vmlinuz)和initrd加载到内存,接着把控制权交给内核。整个引导过程中,GRUB菜单项决定了加载哪个内核、以什么参数加载。
这里有一个很多新手会混淆的点:/etc/default/grub是用户编辑的模板配置,而/boot/grub/grub.cfg是GRUB启动时真正读取的运行时配置。我们每次运行update-grub或grub-mkconfig,系统会把/etc/default/grub里的设置、/etc/grub.d/目录下的脚本,以及当前系统中已安装的内核信息合并起来,重新生成一份grub.cfg。所以修改默认内核的完整链路是:改/etc/default/grub → 执行update-grub → 重启。这条链路是整个操作的主线,后边所有方案都围绕它展开。
2. 动手前的准备:看清系统现状
2.1 确认当前内核与候选内核
操作之前,先搞清楚系统里都有哪些内核。我的习惯是先看当前正在用的:
uname -r输出类似5.15.0-91-generic,这就是当前内核版本。然后看系统里安装了哪些内核镜像包:
dpkg --list | grep linux-image如果你看到linux-image-5.15.0-91-generic、linux-image-5.15.0-92-generic这些条目,就说明系统里装了多个内核。另外还要注意linux-headers包,那是编译内核模块时用的头文件,一般不影响启动顺序,但删除内核时建议一起清理。
也可以查看/boot目录:
ls -lh /boot/vmlinuz-*这个命令会把所有内核镜像文件列出来,附带文件大小和修改时间,看起来更直观。我个人更习惯用dpkg来查,因为dpkg能同时告诉你内核包的状态(ii表示已安装)和架构,这个细节对于需要确认软件包来源、准备做双系统清理的人来说非常实用。
2.2 查看GRUB菜单的真实顺序
修改默认内核,最关键的一步是搞清楚GRUB菜单里每一项的真实排列。GRUB菜单项的序号从0开始,第一个菜单项是0,第二个是1,以此类推。
查看菜单项最可靠的方式是直接读grub.cfg:
grep -E "^menuentry|^submenu" /boot/grub/grub.cfg这条命令会把所有顶层菜单项和子菜单项列出来。Ubuntu的GRUB菜单结构通常长这样:
- Ubuntu(顶层菜单项)
- Advanced options for Ubuntu(顶层子菜单)
- Ubuntu, with Linux 5.15.0-92-generic
- Ubuntu, with Linux 5.15.0-92-generic (recovery mode)
- Ubuntu, with Linux 5.15.0-91-generic
- Ubuntu, with Linux 5.15.0-91-generic (recovery mode)
- ...
注意,Ubuntu默认的第一项"Ubuntu"实际上是指向"Advanced options for Ubuntu"子菜单里最新内核那条的,而不是一个简单的独立菜单项。这一个细节在后边用序号方式设置默认值的时候特别容易踩坑。
还有个更便于脚本处理的命令:
awk -F\' '/menuentry |submenu / {print $1$2}' /boot/grub/grub.cfg这条命令用单引号作为分隔符,把每个菜单项类型和完整名称提取出来,输出结果更方便人工对照。前后两条命令建议都试一下,选择自己看着顺眼的方式。
2.3 理解GRUB配置体系
修改默认启动项,日常接触的核心是/etc/default/grub这个文件。打开看一下:
sudo cat /etc/default/grub典型的内容是这样:
GRUB_DEFAULT=0 GRUB_TIMEOUT_STYLE=hidden GRUB_TIMEOUT=10 GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian` GRUB_CMDLINE_LINUX_DEFAULT="quiet splash" GRUB_CMDLINE_LINUX=""这几个变量里,跟本文主题关系最密切的是GRUB_DEFAULT,它控制默认启动哪个菜单项。GRUB_TIMEOUT控制菜单等待时间,GRUB_TIMEOUT_STYLE=hidden表示不显示菜单,直接按超时选择默认项。
理解了GRUB_DEFAULT的几种取值方式,后面才能真正"按需修改":
- 数字:GRUB_DEFAULT=0代表第一个菜单项
- 菜单项名称:GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 5.15.0-91-generic"
- saved:配合GRUB_SAVEDEFAULT=true,用grub-set-default来指定
对于大多数固定场景,我建议优先用菜单项名称或saved方式;数字方式只在临时改动时用。
3. 修改默认内核的几种实用方案
3.1 方案一:GRUB_DEFAULT指定菜单项序号
最直接的思路:GRUB_DEFAULT=0默认是第一个菜单项,那我改成目标内核在菜单里的序号不就行了?
比如上面的例子,我想默认启动5.15.0-91-generic,它在子菜单里的序号是2(因为第0项是5.15.0-92,第1项是它的recovery mode,第2项才是5.15.0-91)。那么:
GRUB_DEFAULT="1>2"这里1表示第二行菜单"Advanced options for Ubuntu"子菜单的序号,2表示子菜单内第三条,也就是5.15.0-91-generic。中间的大于号是分隔符,用来表示进入子菜单。
注意:这个写法有个隐藏的坑。如果你只写一个数字2,GRUB会把它解释为顶层菜单的第三项,而顶层一般只有"Ubuntu"和"Advanced options for Ubuntu"两项,根本不指向你想启动的实际内核。所以在有子菜单的情况下,"父菜单序号>子菜单序号"是必须完整的写法,序号之间的符号是英文半角的大于号>,不要写成别的字符。
这种用序号的方式最大的问题就是可读性差,而且一旦内核升级,GRUB菜单项的排列顺序变化,序号就乱了。所以对于要长期保持的配置,我不太推荐用序号方式,它更适合临时改一下、确认没问题就改回来的场景。
3.2 方案二:通过菜单项名称锁定内核
我更推荐的方式是用菜单项的完整名称来指定默认内核。在/etc/default/grub里这样写:
GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 5.15.0-91-generic"这里的>用来分隔顶级菜单和子菜单,后面的字符串必须和grub.cfg里的menuentry名称完全一致。用名称锁定有一个好处:即使系统里又装了一个新内核,只要目标内核还在,这个字符串就不会变,默认项不会发生漂移。
有个小技巧:如果你嫌手敲名称容易拼错,可以先从grub.cfg里把名称复制出来:
grep "menuentry " /boot/grub/grub.cfg | cut -d "'" -f2把输出里那一长串带"with Linux"的文本直接粘到配置文件里,再手动加上前面"Advanced options for Ubuntu>"前缀即可。注意双引号内不要多出空格,也不要手动加引号之外的特殊字符,否则GRUB匹配不到会回退到第一个菜单项。
3.3 方案三:saved方式配合grub-set-default
第三种方式适合"我可能频繁切换不同内核版本"的场景。原理很简单:GRUB会把"上次选择启动的菜单项"记录下来,以后默认启动上次选择的那一项。具体配置:
/etc/default/grub里设置两行:
GRUB_DEFAULT=saved GRUB_SAVEDEFAULT=true然后更新配置:
sudo update-grub重启之后,GRUB菜单会记住你开机时手动选择的内核,并把它的序号写入/boot/grub/grubenv文件,下一次开机默认启动的就是它。如果你想不通过菜单,直接在命令行里指定默认项,可以用:
sudo grub-set-default "Advanced options for Ubuntu>Ubuntu, with Linux 5.15.0-91-generic"这个命令会把默认项写入grubenv,效果等同于手动在菜单里选了那一项。saved方式的优点在于灵活,特别适合调试内核的场景:你可以从旧内核启动,测试完再手动引导新内核,系统会记住你最后一次的选择。缺点是你必须在每次重启后注意确认GRUB菜单里选对了项,不然默认项可能被意外修改。
3.4 方案四:临时切换用grub-reboot
有一种更"轻"的操作,不需要修改任何配置文件,只需要这一次开机用某个内核,之后还是用原来的默认项。执行:
sudo grub-reboot "Advanced options for Ubuntu>Ubuntu, with Linux 5.15.0-91-generic" sudo rebootgrub-reboot只会影响下一次启动,不会修改持久化的默认配置。它的实现原理是把目标菜单项写入grubenv中的next_entry变量,GRUB在启动时会优先读取next_entry,用完即清。这个命令在我处理"新内核可能有问题,先试一次旧内核"的场景里特别好用,相当于给系统做了一个单次启动的快照切换。
我个人在调试内核参数、做硬件兼容性验证时,最常用的其实是这个命令。因为不需要改配置文件,也不用担心改完忘了改回来,非常适合"只试一次"的场景。
四种方案的对比,我整理成一张表:
| 方案 | 配置方式 | 适用场景 | 缺点 |
|---|---|---|---|
| 序号 | GRUB_DEFAULT="1>2" | 临时快速指定 | 可读性差,内核升级后易偏移 |
| 名称 | GRUB_DEFAULT="Advanced options for Ubuntu>..." | 长期固定 | 名称必须精确匹配 |
| saved | GRUB_DEFAULT=saved + grub-set-default | 频繁切换 | 默认项会跟随手动选择变化 |
| grub-reboot | 命令行指定单次 | 临时试用 | 仅影响下一次启动 |
4. 刷新配置与启动验证
4.1 update-grub到底做了什么
不管用哪种方案修改了/etc/default/grub,最后一步都需要刷新GRUB配置。在Ubuntu上就是执行:
sudo update-grub这个命令是一个脚本封装,底层调用的是grub-mkconfig -o /boot/grub/grub.cfg。它会扫描/etc/grub.d/目录下的脚本(00_header、10_linux、30_os-prober等),结合/etc/default/grub里的参数,重新生成grub.cfg。简单理解就是:模板配置 + 检测逻辑 = 最终配置,也就是说你改完模板之后,必须让系统重新"翻译"一遍,grub.cfg才会带上你的新设置。
执行update-grub后,观察输出有没有出现你期望的内核名称。它通常会打印类似:
Found linux image: /boot/vmlinuz-5.15.0-92-generic Found initrd image: /boot/initrd.img-5.15.0-92-generic Found linux image: /boot/vmlinuz-5.15.0-91-generic Found initrd image: /boot/initrd.img-5.15.0-91-generic done如果你改了GRUB_DEFAULT但看不到任何"found"输出,说明update-grub没有正确执行,多半是权限不够、磁盘空间不足或其他问题。另外注意update-grub需要root权限,别漏了sudo。
有一点要提醒:如果GRUB_DEFAULT指向的菜单项名称在当前系统里不存在,update-grub不会报错,它只是照常生成配置,重启时GRUB找不到对应菜单项,会静默回退到第一个可启动项。所以刷新完配置后,最好再看一眼grub.cfg内容,确认目标内核确实在菜单里。
4.2 重启后如何验证生效
完成配置后,重启系统:
sudo reboot重启后进入系统,我建议第一件事就是确认当前到底用了哪个内核:
uname -r如果你看到的就是目标内核版本,说明配置生效了。有时候你以为改对了,但uname -r出来还是老内核,这时候不要慌,我遇到过好多次,原因基本集中在几个点:
- 没执行update-grub,grub.cfg还是旧的
- 修改/etc/default/grub时语法写错了,比如引号没闭合、>前后多了空格
- GRUB_DEFAULT的值指向了不存在的菜单项名称
- UEFI机器上出现了多个GRUB引导项,BIOS实际引导的是另一个EFI分区里的GRUB
最后一种情况在双系统、多硬盘环境下特别常见。你改的可能是某个磁盘上的GRUB,但主板实际引导的是另一个EFI分区里的GRUB。排查方法很简单:开机时手动进入BIOS/UEFI设置界面,确认默认引导项到底是哪个,或者用efibootmgr命令查看当前引导顺序:
efibootmgr这个命令会显示EFI引导项的列表,你可以看到当前BootOrder指向哪个设备。如果系统没有efibootmgr,可以用apt install efibootmgr安装。如果你发现引导顺序里排在最前面的不是Ubuntu的grubx64.efi,那修改/etc/default/grub大概率不会生效,得先把引导顺序调整到正确的EFI分区。
4.3 配置撤销或回滚
改完配置后如果发现不对劲,恢复原状也简单。编辑/etc/default/grub,把GRUB_DEFAULT改回0,然后:
sudo update-grub如果你不确定改之前是什么值,Ubuntu默认就是GRUB_DEFAULT=0。还有一种更稳妥的方式:改配置文件之前先备份:
sudo cp /etc/default/grub /etc/default/grub.bak出问题时恢复备份再update-grub即可。不要小看这一步,在生产机器上操作时,备份一下配置文件成本几乎为零,但真遇到配置写错、还要现场救急的时候,这一条能帮你省下不少时间。
5. 常见坑与排查经验
5.1 GRUB菜单序号偏移问题
用序号方式配置默认内核,最让人头疼的就是"内核升级后序号乱了"。比如你设置了GRUB_DEFAULT="1>2",本来指向前一个内核。结果某天apt自动升级内核,系统装了一个5.15.0-93,菜单排列变成了:
- Ubuntu
- Advanced options for Ubuntu
- 5.15.0-93
- 5.15.0-93 (recovery mode)
- 5.15.0-92
- 5.15.0-92 (recovery mode)
- 5.15.0-91(你要锁定那个)
原来序号2对应的是5.15.0-91,现在变成了5.15.0-92,你的默认项"悄悄"变成了另一个新内核,而你自己可能过了很久才发现。所以我前面才反复强调:如果需要长期固定,用菜单项名称而不是序号。
判断自己是不是踩了序号偏移的坑,最简单的方法是执行update-grub后,重新检查一遍grub.cfg里的菜单顺序,和自己配置文件里的序号对照一下。这个习惯我基本已经养成了,每次升级内核后都会顺手看一眼。
5.2 内核升级后默认项被重置
这个问题在20.04之后的Ubuntu上其实已经有改进了,但偶尔还是会遇到。原因通常是/etc/default/grub里的GRUB_DEFAULT=saved配合GRUB_SAVEDEFAULT=true时,GRUB会默认把"最后一次手动选择"当作默认。如果你在某个时间点手滑在菜单里选了一次别的内核,之后默认项就会跟着变。
避免办法有两种:一是用固定的菜单项名称,别用saved;二是在确认默认项正确之后,重新执行一次grub-set-default把grubenv里的记录写死。我个人更推荐第一种,省心。
5.3 GRUB菜单不出现的处理
有同学改完配置,发现开机直接进系统,GRUB菜单一闪而过甚至完全看不到,想手动选内核都没机会。这是因为/etc/default/grub里有这么一行:
GRUB_TIMEOUT_STYLE=hidden GRUB_TIMEOUT=0GRUB_TIMEOUT_STYLE=hidden会隐藏菜单,GRUB_TIMEOUT=0表示等待时间为0,系统会直接用默认项启动。如果你需要经常切内核,建议把GRUB_TIMEOUT改成3或5,并且把GRUB_TIMEOUT_STYLE设为menu。注意这两个变量必须配套修改,只改一个不会生效。
另外有一个救命技巧:即使GRUB菜单被隐藏,开机时按住Shift键(传统BIOS模式)或反复按Esc键(UEFI模式)也能把菜单呼出来。当你把默认项改错、系统都快启动不起来的时候,这个技巧能救急。
5.4 清理旧内核的正确姿势
修改默认内核之后,很多人会顺手想把旧内核删掉,腾出/boot空间。但删除内核一定要小心,别把当前正在使用的内核删了。我的建议是先确认当前内核:
uname -r然后删除不需要的内核镜像:
sudo apt remove --purge linux-image-5.15.0-91-generic linux-headers-5.15.0-91-generic如果想更省事,也可以:
sudo apt autoremoveautoremove会清理不再需要的旧内核包,但有的时候它也会把你还想保留的内核一并清掉,所以执行前先看一下它将移除的包列表,确认没有当前正在运行的版本再继续。删除完成后记得:
sudo update-grub让GRUB菜单同步更新。这个顺序很重要,如果你先删内核再update-grub,没问题;但如果先update-grub再删内核,grub.cfg里就会残留指向已删除内核的菜单项,启动时GRUB会报错。
5.5 遇到grub rescue模式怎么办
聊到GRUB相关的坑,就不得不提让无数人头疼的grub rescue模式。屏幕上一行"grub minimal bash like line editing is supported",说明GRUB找不到正常的配置文件或内核镜像,进入了最小化救援环境。这个情况我见过太多次了,常见原因有两个:
一是删内核删出了事,比如不小心把包含当前内核的vmlinuz文件从/boot里删掉了,或者操作时断电导致grub.cfg损坏。
二是双系统安装或Ubuntu重装时,GRUB安装位置不对,导致引导记录指向了不存在的分区。
如果是第一种情况,你大概率需要从Ubuntu安装U盘启动,进入Live环境后chroot到系统里,重新安装GRUB。下面是核心步骤(前提是你能在Live环境中挂载系统分区):
sudo mount /dev/sda2 /mnt sudo mount /dev/sda1 /mnt/boot/efi sudo mount --bind /dev /mnt/dev sudo mount --bind /proc /mnt/proc sudo mount --bind /sys /mnt/sys sudo chroot /mnt grub-install /dev/sda update-grub exit sudo reboot这里的/dev/sda2要替换成你的根分区,/dev/sda1是EFI分区(UEFI模式下必须有),/dev/sda是目标磁盘。如果你的/boot是独立分区,还需要在chroot前额外挂载sudo mount /dev/sdXX /mnt/boot。这个操作能重建GRUB引导,一般都能救回来。
如果是第二种情况,麻烦在于"哪个磁盘才是真正的启动盘"。我的排查思路是:先查看当前系统的分区布局,确认系统安装在哪个硬盘上,然后用grub-install把GRUB装到对应磁盘的EFI分区里,最后检查主板启动顺序。
注意:grub-install是风险较高的操作,在双系统和多硬盘环境里尤其要小心。如果你不确定某个分区是根分区还是EFI分区,先执行lsblk -f看清楚,不要在没确认的情况下乱挂载、乱安装。
5.6 多系统环境下GRUB的默认项
既然要聊双系统安装Ubuntu,我也顺带提一句。双系统环境下,GRUB菜单往往会多出Windows Boot Manager这一项(由30_os-prober脚本检测)。如果你想修改默认启动的系统,依然可以用GRUB_DEFAULT来指定,比如:
GRUB_DEFAULT="Windows Boot Manager"但Windows Boot Manager在不同主板上的显示名称可能有差异,有的显示成"Windows Boot Manager (on /dev/sda2)",这里的名称同样要和grub.cfg里的menuentry完全一致。多系统环境下建议先用grep命令把真实菜单名称抓出来,再填到配置里。
另外,双系统环境下如果网络、硬件和某个内核版本有兼容问题,锁定Linux内核同样用第3节的方法,逻辑完全一致。
写到这里,修改GRUB默认启动内核的几种方案、底层原理和常见坑基本都覆盖到了。我个人在实际操作中的体会是:能靠菜单项名称锁定就尽量别用序号,能分清楚"永久修改"和"单次切换"这两类需求,就不会在GRUB配置上反复折腾。最后再分享一个小技巧:每次update-grub之后,随手执行一下grep menuentry /boot/grub/grub.cfg,扫一眼菜单顺序,几秒钟的功夫,能避免很多次重启后才发现选错内核的尴尬。这个事儿多操作两次就熟了,剩下的就是自己动手试一次。