grub 默认启动项的修改在 RHEL/CentOS 系发行版上可以使用 grubby 工具,但使用 grub 自带的 grub-set-default 可能更合适一些,一是 Debian/Ubuntu 系发行版上没有这个工具,二是随着 BLS 配置方式1的推出,grubby 也废弃了2。
With the adoption of BLS, grubby has been largely superseded by a shell script that lives downstream in Fedora. This repo is no longer maintained.
关于 grub 默认启动项的设置或实现,这里仅以 Ubuntu 22.04 上的 grub 2.06 为例进行分析,不讨论 BLS。
grub 默认启动项
$ man grub-set-default
grub-set-default - set the saved default boot entry for GRUB
实际上只是调用了 grub-editenv 设置了一下 grub 的环境变量(/boot/grub/grubenv)而已:
$ cat /usr/sbin/grub-set-default
$grub_editenv ${grubdir}/grubenv unset prev_saved_entry
$grub_editenv ${grubdir}/grubenv unset next_entry
$grub_editenv ${grubdir}/grubenv set saved_entry="$entry"
grub-mkconfig 生成的配置文件 grub.cfg 实际上由 /etc/grub.d/ 目录下的 00_header, 10_linux 等 shell 脚本执行的结果组成的:
$ cat /usr/sbin/grub-mkconfig
for i in "${grub_mkconfig_dir}"/* ; do
case "$i" in
*)
if grub_file_is_not_garbage "$i" && test -x "$i" ; then
echo
echo "### BEGIN $i ###"
"$i"
echo "### END $i ###"
fi
;;
esac
done
$ cat /etc/grub.d/00_header
if [ "x${GRUB_DEFAULT}" = "x" ] ; then GRUB_DEFAULT=0 ; fi
if [ "x${GRUB_DEFAULT}" = "xsaved" ] ; then GRUB_DEFAULT='${saved_entry}' ; fi
if [ -s $prefix/grubenv ]; then
set have_grubenv=true
load_env
fi
set default="${GRUB_DEFAULT}"
if [ x"\${feature_menuentry_id}" = xy ]; then
menuentry_id_option="--id"
else
menuentry_id_option=""
fi
export menuentry_id_option
其中 GRUB_DEFAULT
是从 /etc/default/grub 文件中配置的,此后 grub 的核心代码读取的是 default
这个环境变量:
// grub-core/normal/menu.c
run_menu (grub_menu_t menu, int nested, int *auto_boot)
{
default_entry = get_entry_number (menu, "default");
}
menuentry_id_option
特性定义在 grub-core/normal/main.c 中,默认启用:
// grub-core/normal/main.c
static const char *features[] = {
"feature_chainloader_bpb", "feature_ntldr", "feature_platform_search_hint",
"feature_default_font_path", "feature_all_video_module",
"feature_menuentry_id", "feature_menuentry_options", "feature_200_final",
"feature_nativedisk_cmd", "feature_timeout_style"
};
GRUB_MOD_INIT(normal)
{
for (i = 0; i < ARRAY_SIZE (features); i++)
{
grub_env_set (features[i], "y");
grub_env_export (features[i]);
}
}
因此,类似如下的 grub 菜单项在解析时,就可以通过 --id
选项得到菜单项相应的 id
:
menuentry 'Ubuntu' \
--class ubuntu --class gnu-linux --class gnu --class os \
$menuentry_id_option 'gnulinux-simple-2f06ea74-37e9-46d4-b3d7-eef0eeb8e1db'
// grub-core/commands/menuentry.c
static const struct grub_arg_option options[] =
{
{"id", 0, 0, N_("Menu entry identifier."), N_("STRING"), ARG_TYPE_STRING},
}
从而在 get_entry_number
时可以根据菜单项的 title/id 得到默认菜单项(外层的 grub_show_menu
通过递归调用实现逐层的 title/id 匹配):
// grub-core/normal/menu.c
get_entry_number (grub_menu_t menu, const char *name)
for (i = 0; e; i++)
{
if (menuentry_eq (e->title, val)
|| menuentry_eq (e->id, val))
{
entry = i;
break;
}
e = e->next;
}
如果要显式指定默认菜单项,可以使用 grub-set-default 命令,同时将 /etc/default/grub 配置文件设置如下值:
$ sudo vi /etc/default/grub
GRUB_DEFAULT=saved
在指定子菜单下的菜单项时,使用 >
符号进行子菜单与菜单项之间的分隔,如果菜单项中 title/id 有 >
符号,则 title/id 中的 >
符号需要使用 >>
进行转义:
// grub-core/normal/menu.c
grub_menu_execute_entry(grub_menu_entry_t entry, int auto_boot)
for (ptr = def; ptr && *ptr; ptr++)
{
if (ptr[0] == '>' && ptr[1] == '>')
{
ptr++;
continue;
}
if (ptr[0] == '>')
break;
}
if (ptr && ptr[0] && ptr[1])
grub_env_set ("default", ptr + 1);
else
grub_env_unset ("default");
由于 >
是 shell 的重定向操作符,注意需要给参数加上引号,如:
$ sudo grub-set-default 'submemu-id>menu-id'
id 可以是数字,也可以是 title 或者 $menuentry_id_option
选项指定的 id,如果 menu-id 是数字,其中 >
右侧可以有空格,否则不能有空格(因为 get_entry_number
对数字字符串和普通字符串有不同的处理),id 如果是数字,则从 0 开始编号,并且子菜单下的菜单项从 0 开始编号并不占用父菜单的编号。
使用 grub-editenv 或者 grub-set-default(或者修改 /etc/default/grub 中的 GRUB_DEFAULT
选项然后执行 grub-mkconfig 并保存配置)选择默认启动项为子菜单项时,如果不使用 >
拼接父菜单项,则 grub-mkconfig 会报如下的警告:
$ sudo grub-mkconfig -o /boot/grub/grub.cfg
Warning: Please don't use old title `Ubuntu, with Linux 5.15.0-50-generic' for GRUB_DEFAULT, use `Advanced options for Ubuntu>Ubuntu, with Linux 5.15.0-50-generic' (for versions before 2.00) or `gnulinux-advanced-2f06ea74-37e9-46d4-b3d7-eef0eeb8e1db>gnulinux-5.15.0-50-generic-advanced-2f06ea74-37e9-46d4-b3d7-eef0eeb8e1db' (for 2.00 or later)
这只是一个警告信息,10_linux 脚本会进行相应的规避处理:
if [ x"$title" = x"$GRUB_ACTUAL_DEFAULT" ] || [ x"Previous Linux versions>$title" = x"$GRUB_ACTUAL_DEFAULT" ]; then
replacement_title="$(echo "Advanced options for ${OS}" | sed 's,>,>>,g')>$(echo "$title" | sed 's,>,>>,g')"
quoted="$(echo "$GRUB_ACTUAL_DEFAULT" | grub_quote)"
title_correction_code="${title_correction_code}if [ \"x\$default\" = '$quoted' ]; then default='$(echo "$replacement_title" | grub_quote)'; fi;"
grub_warn "$(gettext_printf "Please don't use old title \`%s' for GRUB_DEFAULT, use \`%s' (for versions before 2.00) or \`%s' (for 2.00 or later)" "$GRUB_ACTUAL_DEFAULT" "$replacement_title" "gnulinux-advanced-$boot_device_id>gnulinux-$version-$type-$boot_device_id")"
fi
echo "$title_correction_code"
这样会在生成的 grub.cfg 文件中会增加对 default
环境变量的修改操作:
if [ "x$default" = 'Ubuntu, with Linux 5.15.0-50-generic' ]; then
default='Advanced options for Ubuntu>Ubuntu, with Linux 5.15.0-50-generic';
fi;
注意到这里代码有个字符串比较的 BUG :),因此这段代码永远都不会生效。
忽略掉代码本身的 BUG,仍然需要注意的是,这个规避代码只在 grub-mkconfig 执行时才重新生成(即使是通过 grub-editenv 或者 grub-set-default 直接修改环境变量),所以想要通过 grub-editenv 或者 grub-set-default 直接修改默认启动项则应当使用 >
符号拼接的全路径来指定子菜单项。
如果要默认使用上次启动选择的菜单项,可以将 /etc/default/grub 配置文件设置如下值:
$ sudo vi /etc/default/grub
GRUB_DEFAULT=saved
GRUB_SAVEDEFAULT=true
这样会自动将选择的启动项记录进行 saved_entry
环境变量,实现与使用 grub-editenv 或者 grub-set-default 进行手工设置同样的效果(或者修改 /etc/default/grub 中的 GRUB_DEFAULT
选项然后执行 grub-mkconfig 并保存配置):
$ # grub-mkconfig_lib
save_default_entry ()
{
if [ "x${GRUB_SAVEDEFAULT}" = "xtrue" ] ; then
cat << EOF
savedefault
EOF
fi
}
$ # 00_header
function savedefault {
if [ -z "\${boot_once}" ]; then
saved_entry="\${chosen}"
save_env saved_entry
fi
}
$ # 10_linux
linux_entry ()
{
if [ x$type != xrecovery ] ; then
save_default_entry | grub_add_tab
fi
}
UEFI 双系统
如果是多 Linux 系统(如多个 Ubuntu 版本)共存,需要特别注意 /boot/efi/EFI/<dist>/ 目录下的 grub.cfg 配置文件可能只是一个中间文件,里面的配置实际指向的是其中某一个系统根分区的 /boot/grub/grub.cfg,如果没有在中间 grub.cfg 文件指向的系统下更新 grub 配置,会导致新配置无法生效(如新安装的内核无法在 grub 菜单上体现)。
BLS
在 RHEL/CentOS 系发行版上,新的 bls 配置方式34出现了:
$ man grub2-switch-to-blscfg
grub-switch-to-blscfg — Switch to using BLS config files.
$ sudo cat /boot/grub2/grub.cfg
insmod blscfg
blscfg
$ sudo ls /boot/loader/entries/
78a6894b504d4d90a92e66fc92e92d7a-0-rescue.conf 78a6894b504d4d90a92e66fc92e92d7a-5.14.0-70.13.1.el9.aarch64.conf 78a6894b504d4d90a92e66fc92e92d7a-5.19.15.conf
-
The Boot Loader Specification
https://systemd.io/BOOT_LOADER_SPECIFICATION/ ↩︎ -
Does GNU GRUB support BLS (Boot Loader Specification)?
https://unix.stackexchange.com/questions/686369/does-gnu-grub-support-bls-boot-loader-specification ↩︎ -
A gotcha with Fedora 30’s switch of Grub to BootLoaderSpec based configuration
https://utcc.utoronto.ca/~cks/space/blog/linux/Fedora30GrubBLSGotcha ↩︎
最后修改于 2022-10-23