BLS: Boot Loader Specification

在新的发行版上,普遍采用了 GRUB2 (以下简称 GRUB) 作为 boot loader。GRUB2 现在采用了 Boot Loader Specification (简称 BLS) 来管理启动菜单: https://systemd.io/BOOT_LOADER_SPECIFICATION/

在非 UEFI Secure 启动的情况下,启动菜单直接使用文本文件保存,方便维护。你可以在一个使用该标准的系统上查看这些文件,例如 Rocky Linux 8.5:

# ll /boot/loader/entries/
total 12
-rw-r--r--. 1 root root 405 May 10 13:54 a0284538aa5b498cb38b8e530b2a6be4-0-rescue.conf
-rw-r--r--. 1 root root 353 May 10 13:54 a0284538aa5b498cb38b8e530b2a6be4-4.18.0-348.el8.0.2.x86_64.conf

# cat a0284538aa5b498cb38b8e530b2a6be4-4.18.0-348.el8.0.2.x86_64.conf
title Rocky Linux (4.18.0-348.el8.0.2.x86_64) 8.5 (Green Obsidian)
version 4.18.0-348.el8.0.2.x86_64
linux /vmlinuz-4.18.0-348.el8.0.2.x86_64
initrd /initramfs-4.18.0-348.el8.0.2.x86_64.img $tuned_initrd
options $kernelopts $tuned_params
id rocky-20211114010422-4.18.0-348.el8.0.2.x86_64
grub_users $grub_users
grub_arg --unrestricted
grub_class kernel

Machine ID

Machine ID 是一个在系统安装的时候生成的 UUID,用于表示系统的唯一性。它存放在 /etc/machine-id 文件中

# cat /etc/machine-id
a0284538aa5b498cb38b8e530b2a6be4

可以查看 man 手册获取更多信息: man machine-id

GRUB 菜单的生成

GRUB 菜单是在 kernel 安装的时候生成的,在 kernel 卸载的时候删除的,这整个过程是由 kernel-install 命令来完成的。在 RHEL 8 及衍生系统上,这个程序由 systemd-udev 包来提供。

kernel-install

这个程序只有两个功能,添加内核,和删除内核:

# kernel-install --help
Usage:
        /usr/bin/kernel-install add KERNEL-VERSION KERNEL-IMAGE
        /usr/bin/kernel-install remove KERNEL-VERSION

它本身是通过一系列的脚本来实现的,这些脚本存放在:

  • /usr/lib/kernel/install.d/
  • /etc/kernel/install.d/

默认脚本存放在 /usr/lib/kernel/install.d 目录下:

[root@overlord-sz3 install.d]# cd /usr/lib/kernel/install.d/
[root@overlord-sz3 install.d]# ll
total 40
-rwxr-xr-x. 1 root root 7120 Nov 14  2021 20-grub.install
-rwxr-xr-x. 1 root root 2252 Nov  9  2021 20-grubby.install
-rwxr-xr-x. 1 root root  368 Jun 22  2018 50-depmod.install
-rwxr-xr-x. 1 root root 1657 Nov  9  2021 50-dracut.install
-rwxr-xr-x. 1 root root 3338 Nov  9  2021 51-dracut-rescue.install
-rwxr-xr-x. 1 root root  791 Oct 13  2021 60-kdump.install
-rwxr-xr-x. 1 root root 1975 Nov  9  2021 90-loaderentry.install
-rwxr-xr-x. 1 root root  989 Jul 22  2021 92-tuned.install
-rwxr-xr-x. 1 root root  454 Nov 14  2021 99-grub-mkconfig.install

这些都是 shell 脚本,主要是完成一些配置。例如 20-grub.install90-loaderentry.install 这两个脚本就是完成 grub 相关的一些配置。

另外,kernel-install 对于这些脚本返回值做了一个特殊的规定:

An executable should return 0 on success. It may also return 77 to cause the whole operation to terminate (executables later in lexical order will be skipped).

所以,如果一个脚本返回了 77,那么后续的都会被跳过。

生成菜单的脚本

在 RHEL 8 及其衍生系统中,默认安装的 kernel-install 脚本里有两个脚本会负责 GRUB 菜单的生成:

如果你观察上面所展示的菜单文件的内容,并且对比这两个脚本,那么你就会发现他们是由 20-grub.install 这个文件生成的。为什么嗯?

这个是两个脚本的实现问题,首先 20-grub.install 脚本会被先执行,并且生成了 entries 目录中的文件。等到 90-loaderentry.install 被执行时,它有一个判断如下:

if ! [[ -d "$BOOT_DIR_ABS" ]]; then
    exit 0
fi

这个 BOOT_DIR_ABS 如果不是一个目录,就不会执行。这个变量如何定义呢?它在这里定义:https://github.com/systemd/systemd/blob/v239-50/src/kernel-install/kernel-install#L92

if ! [[ $MACHINE_ID ]]; then
    BOOT_DIR_ABS=$(mktemp -d /tmp/kernel-install.XXXXX) || exit 1
    trap "rm -rf '$BOOT_DIR_ABS'" EXIT INT QUIT PIPE
elif [[ -d /efi/loader/entries ]] || [[ -d /efi/$MACHINE_ID ]]; then
    BOOT_DIR_ABS="/efi/$MACHINE_ID/$KERNEL_VERSION"
elif [[ -d /boot/loader/entries ]] || [[ -d /boot/$MACHINE_ID ]]; then
    BOOT_DIR_ABS="/boot/$MACHINE_ID/$KERNEL_VERSION"                           # ------------- This line
elif [[ -d /boot/efi/loader/entries ]] || [[ -d /boot/efi/$MACHINE_ID ]]; then
    BOOT_DIR_ABS="/boot/efi/$MACHINE_ID/$KERNEL_VERSION"
elif mountpoint -q /efi; then
    BOOT_DIR_ABS="/efi/$MACHINE_ID/$KERNEL_VERSION"
elif mountpoint -q /boot/efi; then
    BOOT_DIR_ABS="/boot/efi/$MACHINE_ID/$KERNEL_VERSION"
else
    BOOT_DIR_ABS="/boot/$MACHINE_ID/$KERNEL_VERSION"
fi

/boot/$MACHINE_ID/$KERNEL_VERSION 这个形式,是 BLS 规范定义的新的形式,即每个系统的启动项都放在自己的独立目录中。因为现在 RHEL 8 及其衍生版本还未用到这个规范,所以他们的 GRUB 菜单项就不是用 90-loaderentry.install 这个脚本生成的。

菜单文件

一个菜单文件的规范涉及两个部分:文件名和内容。

一般来说,文件名类似:a0284538aa5b498cb38b8e530b2a6be4-4.18.0-348.el8.0.2.x86_64.conf。这个文件名主要的限制是必须跨系统唯一,所以一般使用如下格式: {machine_id}-{kernel_version}.conf

文件的内容可能如下:

title Rocky Linux (4.18.0-348.el8.0.2.x86_64) 8.5 (Green Obsidian)
version 4.18.0-348.el8.0.2.x86_64
linux /vmlinuz-4.18.0-348.el8.0.2.x86_64
initrd /initramfs-4.18.0-348.el8.0.2.x86_64.img $tuned_initrd
options $kernelopts $tuned_params
id rocky-20211114010422-4.18.0-348.el8.0.2.x86_64
grub_users $grub_users
grub_arg --unrestricted
grub_class kernel

每行第一个空格之前的就是 key,后面则是 value。其中,title 就是展示在菜单上的内容。这个 title 就是纯文本,所以你可以随意修改。

20-grub.install 文件可以看出,这里的 title 的内容会使用到来自 /etc/os-release 中的 NAME 变量和 VERSION 变量。

# cat /etc/os-release
NAME="Rocky Linux"
VERSION="8.5 (Green Obsidian)"
ID="rocky"
ID_LIKE="rhel centos fedora"
VERSION_ID="8.5"
PLATFORM_ID="platform:el8"
PRETTY_NAME="Rocky Linux 8.5 (Green Obsidian)"
ANSI_COLOR="0;32"
CPE_NAME="cpe:/o:rocky:rocky:8.5:GA"
HOME_URL="https://rockylinux.org/"
BUG_REPORT_URL="https://bugs.rockylinux.org/"
ROCKY_SUPPORT_PRODUCT="Rocky Linux"
ROCKY_SUPPORT_PRODUCT_VERSION="8"

grub_ 开头的 key,是 GRUB 平台实现的:

  • On grub platforms, the following grub-specific keywords have been implemented:
    • the BLS filename is also used for menuentry’s –id parameter, so you can use it in saved_entry
    • grub_hotkey - same as grub’s “–hotkey” menuentry parameter
    • grub_users - same as grub’s “–users” menuentry parameter; used for password protection
    • grub_class - same as grub’s “–class” menuentry paramter
    • grub_arg - passes extra arguments to menuentry

这些是兼容原来 GRUB 的参数:https://www.gnu.org/software/grub/manual/grub/grub.html#menuentry

默认启动项

你可以使用 grub2-set-default 命令修改默认的启动项。

此外,我们还可以在安装了一个新的 kernel 之后,让 kernel-install 自动将这个新的 kernel 设置为默认 kernel。这个行为的配置,是在文件 /etc/sysconfig/kernel 文件中控制的:

# cat /etc/sysconfig/kernel
# UPDATEDEFAULT specifies if kernel-install should make
# new kernels the default
UPDATEDEFAULT=yes

# DEFAULTKERNEL specifies the default kernel package type
DEFAULTKERNEL=kernel-core

这个注释已经解释得很清楚了。这部分逻辑的实现,也是在 20-grub.install 中实现的,调用的是 grub2-editenv 命令:https://git.rockylinux.org/staging/rpms/grub2/-/blob/r8/SOURCES/20-grub.install#L134

顺便说一下,如果你安装的 kernel 采用了另外一个名字来打包,比如 elrepo 的 kernel-lt,那么修改 DEFAULTKERNEL 这行即可。

Reference


知识共享许可协议本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。