GRUB2 menu and kernel-install
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.install 和 90-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 菜单的生成:
- /usr/lib/kernel/install.d/20-grub.install
- 这个文件来自包 grub2-common-2.02-106.el8.0.2.noarch。https://git.rockylinux.org/staging/rpms/grub2/-/blob/r8/SOURCES/20-grub.install
- /usr/lib/kernel/install.d/90-loaderentry.install
- 这个文件来自包 systemd-udev-239-51.el8.x86_64。https://github.com/systemd/systemd/blob/v239-50/src/kernel-install/90-loaderentry.install
如果你观察上面所展示的菜单文件的内容,并且对比这两个脚本,那么你就会发现他们是由 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
- https://fedoraproject.org/wiki/Changes/BootLoaderSpecByDefault
- https://systemd.io/BOOT_LOADER_SPECIFICATION/