Ubuntu VPS 如何固定下次启动的内核
Ubuntu 云镜像中设置 GRUB_DEFAULT 不生效。本文教您读取实际菜单项和覆盖配置,验证可用内核,并安全固定下一次启动,避免 SSH 服务器进入救援控制台。
决定 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 输出 kvm、qemu 或 xen,表示您运行的是自己的内核,以下内容全部适用。输出 lxc 或 openvz,表示您的服务器共用主机的内核,因此不存在属于您的引导加载程序,也没有可供固定的内核。在这种情况下,uname -r 报告的版本根本不会出现在 /boot/vmlinuz-* 中,因为正在运行的内核属于主机,磁盘上的任何设置都无法更改它。
ls -1 /boot/vmlinuz-* 是您实际可以选择的内核列表。如果其中只有一行,说明上一个内核已经被删除,任何引导加载程序设置都无法恢复它。这通常发生在自动删除过程中。在您要维护的服务器上 清理 Ubuntu 上的旧内核 之前,最好先了解这一点。
您编辑的文件不是 GRUB 读取的文件
/etc/default/grub 包含普通的 shell 变量赋值。它是输入文件。/boot/grub/grub.cfg 是输出文件,以 # DO NOT EDIT THIS FILE 和原因开头。您直接写入输出文件的任何内容,都会在下次安装或删除 kernel package 时丢失,因为这些 package 脚本会重新生成该文件。
cat /usr/sbin/update-grubupdate-grub 是一个封装命令。它运行 grub-mkconfig -o /boot/grub/grub.cfg;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.cfgGRUB_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 同样无法解决问题,因为默认项本身不是故障所在。您想从中进行选择的菜单根本没有生成。
要恢复完整菜单,请将供应商文件移到其他位置,并在应用更改前预览结果。不带 -o 的 grub-mkconfig 会将结果写入标准输出,不会修改磁盘上的任何内容。
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>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 rebootuname -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=10GRUB_TIMEOUT_STYLE=hidden 与 GRUB_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"从现在开始,每次引导都会增加十秒等待时间。完成后将超时时间恢复为 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使用第一条命令输出的名称,因为云镜像通常安装 virtual 或 kvm 变体,而不是 generic。如果较新的内核是在镜像刷新前后出现的,而您怀疑发行版本身在未通知您的情况下发生了变化,那么事实并非如此,因为点发行版就是将已有更新整合到新安装介质中的版本,不会给已经完成修补的服务器提供几周前未曾提供过的内容。被锁定的软件包会被 apt upgrade 跳过,并由 The following packages have been kept back: 报告;Ubuntu 上的无人值守升级也会跳过这些软件包。代价确实存在:被锁定的内核不会继续接收安全修复,因此应将其视为有明确解除日期的暂停,并使用 sudo apt-mark unhold 解除锁定。如果您避免更新内核的原因是重启会造成停机,而不是某个内核存在问题,那么VPS 上的实时内核修补可以解决这个问题。
升级前创建快照。 快照可在几分钟内恢复,无需在控制台中输入命令,也不会有引导加载程序修改只应用了一半的风险。创建快照、升级、重启、验证。如果新内核运行异常,回滚后,引导路径会完全恢复为原来的状态。
对于已经宕机的机器,使用控制台或救援镜像。 服务器无法启动后,不能在引导加载程序配置中修复问题;恢复过程也有单独的操作步骤:VPS 在内核更新后无法启动时的处理方法。
出现的问题及其提示信息
您对 /boot/grub/grub.cfg 的修改消失了。系统安装或删除了内核软件包,其维护脚本运行了 update-grub,然后根据输入文件重新生成了该文件。# DO NOT EDIT THIS FILE 头部列出了两个输入位置。请编辑这两个位置的文件。
grub-editenv: error: environment block too small。/boot/grub/grubenv 缺失或已被截断。使用 sudo grub-editenv /boot/grub/grubenv create 重新创建它,然后再次设置您的值,并通过 sudo grub-editenv list 确认。
固定的内核因 VFS: Unable to mount root fs on unknown-block(0,0) 发生 kernel panic。您固定的条目指向的内核或 initrd 已不在磁盘上。通常,这是因为软件包已被删除,但标识符仍保留在 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 读取的文件;在云镜像中,两者之间的差异正是问题所在。请先读取生成的配置。本文中的每个判断都基于该配置的实际内容。
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 检查您自己的主机上已安装的内核 flavour 名称,再使用 apt-mark showhold 验证暂停状态。代价是被暂停的内核不会接收安全修复,因此应在执行暂停前决定何时运行 sudo apt-mark unhold。