SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor

Ubuntu VPS 如何固定下次启动的内核

Ubuntu 云镜像中,GRUB_DEFAULT 可能完全不起作用。本文教您读取实际菜单条目,识别供应商覆盖,并安全锁定 VPS 的下次启动内核。

决定 VPS 下次启动哪个内核

VPS 下次启动哪个内核,由一个生成的文件决定:/boot/grub/grub.cfg。您不应直接编辑该文件,而应修改其输入并重新生成。在 Ubuntu 云镜像中,其中一个输入文件来自镜像供应商。该文件可能使菜单选择失效。因此,在租用的服务器上执行 GRUB_DEFAULT=1,再执行 update-grub,可能不会产生任何变化;但在笔记本电脑安装的系统上执行相同的两个步骤却可以生效。

请按以下顺序操作。先确认您是否可以选择内核。读取每个输入文件,包括供应商添加的文件。读取生成的输出,并统计其中实际包含的条目。然后再选择锁定内核的方法。如果在只能通过 SSH 访问的机器上操作错误,您可能只能使用救援控制台。因此,本页末尾提供的方法最安全,而且通常也是正确的选择。

首先确认您可以固定的是自己的内核

uname -r
systemd-detect-virt
ls -1 /boot/vmlinuz-*

systemd-detect-virt 的输出包含 kvmqemuxen,表示您运行的是自己的内核,以下内容全部适用。输出包含 lxcopenvz,表示您的服务器共享主机的内核,因此您没有自己的引导加载程序,也没有可固定的内核。在这种情况下,uname -r 报告的版本根本不会出现在 /boot/vmlinuz-* 中,因为正在运行的内核属于主机,磁盘上的任何设置都无法更改它。

ls -1 /boot/vmlinuz-* 是您可以选择的实际内核列表。如果其中只有一行,说明上一个内核已经被删除,任何引导加载程序设置都无法将其恢复。这通常发生在 autoremove 期间。对于重要服务器,在 Ubuntu 上 清理旧内核 之前,建议先了解这一点。

您编辑的文件不是 GRUB 读取的文件

/etc/default/grub 包含普通的 shell 变量赋值。它是输入文件。/boot/grub/grub.cfg 是输出文件,开头包含 # DO NOT EDIT THIS FILE 及其生成原因。您写入输出文件的任何内容,都会在下次安装或删除内核软件包时丢失,因为软件包脚本会重新生成该文件。

cat /usr/sbin/update-grub

update-grub 是一个包装器。它会运行 grub-mkconfig -o /boot/grub/grub.cfg;该命令读取变量,运行 /etc/grub.d/ 中的每个脚本,然后写入结果。两个命令,单向处理:输入写入,生成 grub.cfg

哪些设置会覆盖您的配置:/etc/default/grub.d

grep -rn '^[^#]' /etc/default/grub /etc/default/grub.d/

第二个路径是容易被忽略的部分。grub-mkconfig 首先加载 /etc/default/grub,然后按 glob 顺序加载 /etc/default/grub.d/ 中的每个 *.cfg 文件。请阅读执行这一过程的代码:

grep -n 'default/grub' /usr/sbin/grub-mkconfig

加载过程使用普通 shell,因此最后一次赋值会生效。Ubuntu 云镜像会在该目录中附带文件,并在读取您的文件后设置超时时间、内核命令行等参数。稍后,供应商文件将 /etc/default/grub 中的 GRUB_TIMEOUT=10 覆盖为 0。上面的 grep 命令会输出您镜像中的确切赋值,因此请以这些结果为准,不要仅依据本句判断。

由此可得一个实用规则:将您自己的设置放入排序结果靠后的文件,例如 /etc/default/grub.d/99-local.cfg,而不要编辑 /etc/default/grub。这样,镜像附带的文件就无法在您的设置之后继续加载。

为什么 GRUB_FORCE_PARTUUID 会使菜单选择无效

grep -rn GRUB_FORCE_PARTUUID /etc/default/grub /etc/default/grub.d/
grep -n GRUB_FORCE_PARTUUID /etc/grub.d/10_linux
sudo grep -n 'root=PARTUUID' /boot/grub/grub.cfg

GRUB_FORCE_PARTUUID 会让生成器根据分区 UUID 查找根文件系统,并将其直接写入内核命令行,形式为 root=PARTUUID=...;启动时不再搜索文件系统 UUID。镜像供应商设置该变量,是因为这样可以让同一个磁盘镜像在未用于构建它的硬件上可靠启动。第二条 grep 命令显示了 /etc/grub.d/10_linux 中处理该变量的代码。该脚本位于您自己的磁盘上,是判断镜像行为的权威依据。

这里的关键是结果:在这条路径中,生成器会写入一个直接启动项,而不是完整列出已安装的内核。请统计最终生成的条目数。

sudo grep -cE '^\s*(menuentry|submenu) ' /boot/grub/grub.cfg
sudo grep -nE '^\s*(menuentry|submenu) ' /boot/grub/grub.cfg

如果数量为 1,就没有第二个条目可供选择,因此 GRUB_DEFAULT=1 指向了一个不存在的条目。GRUB 无法解析该条目,于是会启动第一个条目,也就是您原本想避免启动的新内核。grub-set-default 也无法解决问题,因为默认项并不是故障所在。您要选择的菜单根本没有生成。

要恢复完整菜单,请先将供应商文件移到其他位置,然后预览结果,再决定是否应用。单独使用 grub-mkconfig 且不带 -o 时,结果会写入标准输出,不会修改磁盘上的任何内容。

sudo grub-mkconfig 2>/dev/null | grep -cE '^\s*(menuentry|submenu) '
sudo mkdir -p /root/grub-backup
sudo mv /etc/default/grub.d/<the file your grep named> /root/grub-backup/
sudo grub-mkconfig 2>/dev/null | grep -cE '^\s*(menuentry|submenu) '

如果数量从 1 增加到多个,说明移除强制设置后条目才会出现。此时尚未写入任何内容。如果第二次统计的结果不正确,请将该文件放回原处,因为强制设置的 PARTUUID 是供应商镜像定位根文件系统的方式;移除它会让系统改用搜索路径。实际运行 update-grub 前,请先创建快照。

如果您的唯一目标是应对一个有问题的内核,请在此停止,并使用后文更安全的选项。在远程服务器上重建启动菜单,只为避开一次升级,风险超过了问题本身的价值。

为什么不应固定菜单项编号

GRUB_DEFAULT接受编号、标题或标识符。编号从 0 开始,对顶层菜单项计数。嵌套菜单项使用 > 作为分隔符,因此 GRUB_DEFAULT="1>2"表示位于索引 1 的子菜单中、索引为 2 的菜单项。

索引会发生变化。10_linux按新旧顺序列出内核,因此安装新内核会使每个旧菜单项的索引加 1,删除内核则会使它们的索引减 1。您之前设置的 1>2仍然可以解析,但它现在指向另一个内核。系统不会报错,也不会发出警告;您要到重启后才会发现问题。

标识符不会变化,因为每个标识符都包含内核版本。读取标识符:

sudo awk -F"'" '/menuentry_id_option/ {print $2, "==>", $4}' /boot/grub/grub.cfg

忽略输出开头的几行。这些行是文件头中定义的变量。之后,左侧是读者看到的标题,右侧是传递给工具的标识符。对于子菜单中的菜单项,按该顺序将子菜单标识符和菜单项标识符用 > 连接起来,与数字形式完全相同。

使用 grub-reboot 临时启动上一个内核

对于远程服务器,一次性选择是正确做法,因为它会自动恢复。grub-reboot 会将 next_entry 写入 /boot/grub/grubenv。GRUB 读取该变量后会清除它,并在启动任何内容之前保存清除后的值。因此,即使某个内核导致系统崩溃,下一次启动也不会再次尝试该内核。系统只会尝试一次,然后自动恢复为正常默认设置。

首先确认生成的配置确实会读取该变量:

sudo grep -n -B2 -A5 'next_entry' /boot/grub/grub.cfg

您应看到一个 load_env 行,以及一个将 default 设置为 next_entry 的代码块。如果 grep 没有输出任何内容,说明您的镜像启动时不会读取 grubenv,因此 grub-reboot 虽然会在 shell 中被接受,但会被引导加载程序忽略。这与上一节中的强制直接启动路径相同,只是出现在了另一个位置。

sudo grub-reboot '<the identifier you copied>'
sudo grub-editenv list

此时,grub-editenv list 应输出一个 next_entry= 行,其中包含您传入的完整值。在浏览器标签页中打开云服务商的控制台,然后重启并检查结果。

sudo reboot
uname -r

如果 uname -r 报告的是较旧版本,说明固定版本已生效。如果报告的是新版本,说明标识符未解析成功,或者未读取 grubenv。无论哪种情况,机器都已启动,这正是使用一次性启动方式的目的。

使用 GRUB_DEFAULT=saved 使选择保持不变

GRUB_DEFAULT=saved 会从 grubenv 中的 saved_entry 获取默认项;您可以使用 grub-set-default 设置该值。安装内核后该设置仍会保留,因为 update-grub 会重写 grub.cfg,但不会修改 grubenv

echo 'GRUB_DEFAULT=saved' | sudo tee /etc/default/grub.d/99-local.cfg
sudo update-grub
sudo grub-set-default '<the identifier you copied>'
sudo grub-editenv list
sudo grep -n 'set default' /boot/grub/grub.cfg

最后一条命令必须输出 set default="${saved_entry}"。如果输出 set default="0",说明在您的文件之后加载的某个配置将 GRUB_DEFAULT 恢复成了字面值。因此,请再次列出 /etc/default/grub.d/,并确认 99-local.cfg 的排序确实位于最后。

GRUB_SAVEDEFAULT=true 是另一个设置,很容易与当前设置混淆。它会将刚刚启动的条目保存为新的默认项,因此默认项会跟随上一次成功启动的内核。在服务器上,这意味着无人值守重启可能会静默改变您固定的内核。除非您确实需要此行为,否则请将其关闭。

按标识符固定内核仍有一种情况会失效。删除该标识符所指向的内核后,标识符将无法解析,系统会退回到第一个条目。因此,请同时保留对应的软件包,或确保该内核不会被 autoremove 删除。

将菜单显示在云服务商控制台中

交互式选择需要在屏幕上显示菜单,而云镜像会将其隐藏。将以下内容写入排序最后的文件,然后运行 sudo update-grub

GRUB_TIMEOUT=10
GRUB_TIMEOUT_STYLE=menu
GRUB_RECORDFAIL_TIMEOUT=10

GRUB_TIMEOUT_STYLE=hiddenGRUB_TIMEOUT=0 一起使用时完全不显示任何内容,因此监控控制台的人员会看到内核消息立即开始,并认为引导加载程序被跳过了。GRUB_RECORDFAIL_TIMEOUT 是引导未完成后使用的独立超时时间。云镜像也将其设置为 0,因此刚刚启动失败的服务器仍不会停止并等待您的操作。

如果服务商提供的是串行控制台而不是图形控制台,但您仍然看不到任何内容,说明 GRUB 正在向您无法查看的终端写入内容。请同时添加以下两行,因为第一行选择输出,第二行配置端口:

GRUB_TERMINAL="console serial"
GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"

从现在开始,每次启动都会增加 10 秒。完成后,将超时时间改回 0。

编辑引导加载程序的更安全方案

对于只能通过 SSH 访问的机器,修改引导加载程序输入是本页中风险最高的选项。还有成本更低的解决方案,而且通常能解决真正的问题。

锁定内核软件包。 如果目标是“不要给我安装更新的内核”,应将此要求告知软件包管理器,而不是告知引导加载程序。

apt list --installed 2>/dev/null | grep -E '^linux-(image|headers|generic|virtual|kvm|aws|azure|gcp|oracle)'
sudo apt-mark hold linux-image-virtual linux-headers-virtual
apt-mark showhold

请使用第一个命令输出的名称,因为云镜像通常安装的是 virtualkvm 变体,而不是 genericapt upgrade 会跳过已锁定的软件包,并通过 The following packages have been kept back: 报告这一情况;Ubuntu 上的无人值守升级也会跳过这些软件包。这样做确实有代价:锁定的内核不会再接收安全修复,因此应将其视为带有截止日期的暂停,并使用 sudo apt-mark unhold 解除锁定。如果阻止内核更新的原因是重启会造成停机,而不是某个内核存在问题,那么应改用VPS 上的在线内核修补

在升级前创建快照。 快照可在几分钟内恢复,无需通过控制台输入,也不会出现引导加载程序修改只应用了一半的情况。创建快照、升级、重启、验证。如果新内核运行异常,回滚后,引导路径将完全恢复原状。

对于已经无法启动的服务器,使用控制台或救援镜像。 服务器无法启动后,不能在引导加载程序配置中修复问题;恢复过程需要单独执行:VPS 在内核更新后无法启动时的处理方法

出现的问题及对应消息

您对 /boot/grub/grub.cfg 的修改消失了。 系统安装或删除了 kernel package,其维护脚本在 update-grub 运行,并根据输入文件重新生成了该文件。# DO NOT EDIT THIS FILE header 列出了两个输入位置。请修改这两个位置。

grub-editenv: error: environment block too small /boot/grub/grubenv 缺失或已截断。使用 sudo grub-editenv /boot/grub/grubenv create 重新创建,然后再次设置您的值,并使用 sudo grub-editenv list 确认。

固定的 kernel 因 VFS: Unable to mount root fs on unknown-block(0,0) 发生 panic。 您固定的条目指向的 kernel 或 initrd 已不在磁盘上。通常,这是因为删除了 package,但标识符仍保留在 grubenv 中。恢复方法是从控制台启动一个可用条目,然后清除过期值。

重启后,uname -r 没有按预期变化。 按以下顺序检查三项:grub-editenv list 是否仍显示您的值,还是该值已被使用;您设置的标识符是否出现在当前 grub.cfg 中;grub.cfg 是否包含读取您所设置变量的 set default 行。这三项中的一项每次都会解释问题。

系统崩溃后,菜单自行出现。 GRUB 会在 grubenv 中将启动失败记录为 recordfail=1,因此在下一次启动时强制显示菜单,以便人工介入。确认机器恢复正常后,使用 sudo grub-editenv /boot/grub/grubenv unset recordfail 清除该状态。


值得记住的一句话是:您编辑的文件不是 GRUB 读取的文件。在 cloud image 中,两者之间的差异正是问题所在。请先读取生成的配置。本文中的每个判断都基于该配置的实际内容。

FAQ

为什么设置 GRUB_DEFAULT=1 不会改变 VPS 启动的内核?

因为在 Ubuntu 云镜像中,生成的 /boot/grub/grub.cfg 通常只包含一个启动项,因此索引 1 没有对应项,GRUB 会回退到第一个启动项。使用 sudo grep -cE '^\s*(menuentry|submenu) ' /boot/grub/grub.cfg 确认这一点。计数为 1 就说明了原因。根因是 GRUB_FORCE_PARTUUID。镜像供应商在 /etc/default/grub.d/ 下的文件中设置了该项,使生成器直接启动,而不是构建已安装内核的完整列表。使用 grep -rn GRUB_FORCE_PARTUUID /etc/default/grub /etc/default/grub.d/ 查找该文件。

如何只启动上一个内核一次?

使用从您自己的 grub.cfg 中复制的标识符运行 sudo grub-reboot '<identifier>',然后在提供商控制台已打开的情况下重启。GRUB 会在启动前清除 next_entry,因此该选择只对本次启动尝试生效;如果内核发生 panic,也不会再次尝试该内核。使用 sudo grub-editenv list 确认该值已写入。在依赖此方法前,先运行 sudo grep -n next_entry /boot/grub/grub.cfg,因为配置未加载 grubenv 的镜像会静默忽略该命令。

应按条目编号还是标识符固定内核?

应使用标识符。条目编号是列表中的位置,而 10_linux 会按从新到旧的顺序重建列表。因此,安装或删除任何内核都会改变编号;过期的 1>2 仍可能解析到一个真实但错误的条目,而且不会输出警告。标识符包含内核版本,因此要么匹配目标内核,要么解析失败。使用 sudo grep -n menuentry_id_option /boot/grub/grub.cfg 列出标识符,并复制每个条目行后面的带引号字符串。

暂停内核软件包更新是否比修改引导加载程序更安全?

对于通常的目标,是的。sudo apt-mark hold linux-image-virtual linux-headers-virtual 会阻止更新版本的内核到达系统,因此启动路径不会改变,也无需依赖您可能无法访问的控制台执行修改。先使用 apt list --installed 检查您自己服务器上已安装的软件包变体名称,再使用 apt-mark showhold 验证暂停状态。代价是暂停更新的内核不会获得安全修复,因此应在执行暂停前确定何时运行 sudo apt-mark unhold