Grub 默认启动项

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

  1. The Boot Loader Specification
    https://systemd.io/BOOT_LOADER_SPECIFICATION/ ↩︎

  2. grubby
    https://github.com/rhboot/grubby ↩︎

  3. Does GNU GRUB support BLS (Boot Loader Specification)?
    https://unix.stackexchange.com/questions/686369/does-gnu-grub-support-bls-boot-loader-specification ↩︎

  4. 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