本章承接上一章的内容。在上一章的“从源码构建内核的步骤”部分,我们已经介绍了构建内核的前三个步骤。在那里,你学习了如何下载并解压内核源码树,或者使用 git clone 获取源码(步骤 1 和 2)。接着,我们深入了解了内核源码树的结构,尤其是如何通过多种方法正确找到配置内核的起点(步骤 3)。我们甚至在内核配置菜单中添加了一个自定义菜单项。
在本章中,我们将继续构建内核,涵盖剩余的四个步骤。首先,我们将构建内核本身(步骤 4)。然后,你将学习如何正确安装构建过程中生成的内核模块(步骤 5)。接下来,我们将运行一个简单的命令,设置 GRUB(Grand Unified Bootloader)引导程序并生成 initramfs(或 initrd)镜像(步骤 6)。我们还将讨论为什么使用 initramfs 镜像以及它是如何生成的。此外,还会介绍一些有关 GRUB 引导程序(针对 x86 架构)的配置细节(步骤 7)。
到本章末尾,我们将使用新的内核镜像启动系统,并验证其是否按预期构建成功。最后,我们将学习如何为其他架构交叉编译 Linux 内核(针对 AArch 32/64,目标设备为广为人知的 Raspberry Pi)。
简而言之,本章将涵盖以下内容:
- 步骤 4 —— 构建内核镜像和模块
- 步骤 5 —— 安装内核模块
- 步骤 6 —— 生成 initramfs 镜像并设置引导程序
- 理解 initramfs 框架
- 步骤 7 —— 定制 GRUB 引导程序
- 验证我们新内核的配置
- 针对 Raspberry Pi 的内核构建
- 内核构建的一些其他技巧
技术要求
在开始之前,我假设你已经下载并解压(如有必要)并配置好了内核,因此已经有一个 .config 文件准备就绪。如果你还没有完成这些步骤,请参考第 2 章《从源码构建 6.x Linux 内核——第 1 部分》,其中详细介绍了如何进行这些操作。现在我们可以继续进行内核的构建。
步骤 4 —— 构建内核镜像和模块
从终端用户的角度来看,执行内核构建非常简单。在最简单的情况下,只需确保你处于已配置好的内核源码树的根目录,并输入 make。就这样,内核镜像和所有的内核模块(在嵌入式系统中,可能还有设备树 Blob (DTB) 二进制文件)将会被构建。可以去喝杯咖啡了!第一次构建可能需要一段时间。
当然,我们可以通过传递不同的 Makefile 目标给 make 来进行控制。在命令行输入 make help 命令,你会发现很多有用的信息。记得我们在之前已经使用过这个命令,事实上,通过它我们查看了所有可用的配置目标(如果你需要,可以回顾第 2 章《从源码构建 6.x 内核——第 1 部分》的“查看所有可用配置选项”部分)。在这里,我们用它来查看 all 目标默认构建的内容:
$ cd ${LKP_KSRC} # 记住,环境变量 LKP_KSRC 保存了我们 6.1 LTS 内核源码树的路径
$ make help
[...]
其他通用目标:
all - 构建标记为 [*] 的所有目标
* vmlinux - 构建基础内核
* modules - 构建所有模块
[...]
特定架构目标 (x86):
* bzImage - 压缩内核镜像 (arch/x86/boot/bzImage)
[...]
这里需要注意的是:执行 make all 将构建前面三个带有 * 符号的目标。它们分别是什么意思呢?让我们来看看:
vmlinux对应的是未压缩的内核镜像文件。modules目标表示所有在内核配置中标记为 m(模块)的选项将会被构建为内核模块(.ko 文件),并存放在内核源码树中(关于内核模块的具体内容和如何编写模块将在后面的两章中讨论)。bzImage是特定架构的内核镜像文件。在 x86[_64] 系统中,它是压缩后的内核镜像文件——引导程序将其加载到内存中,解压后启动系统;实际上,它就是压缩后的内核镜像文件。
一个常见的问题是:如果 bzImage 是我们用来引导并初始化系统的实际内核镜像文件,那么 vmlinux 的作用是什么?vmlinux 是未压缩的内核镜像文件,它可能非常大(尤其是在调试构建中生成内核符号时)。虽然我们从不使用 vmlinux 启动系统,但它非常重要——在内核调试过程中具有不可替代的价值(在我的《Linux 内核调试》书中对此有详细说明)。因此,请保留 vmlinux 以便于内核调试使用。
在内核使用的 kbuild 系统中,只运行 make 相当于执行 make all。
现代的 Linux 内核代码库庞大无比。最新的估计显示,最近的内核版本大约有 2500 万到 3000 万行源代码(SLOC)!因此,构建内核确实是一个非常耗费内存和 CPU 的任务。事实上,有些人甚至将内核构建用作压力测试!(你也应当意识到,在特定构建运行中,并非所有代码行都会被编译。)现代的 make 工具非常强大,支持多进程。我们可以通过 -j n 选项请求 make 生成多个进程来并行处理不同(不相关)的构建部分,从而提高吞吐量并缩短构建时间。-j n 选项中的 n 是并行运行的任务数量上限。常用的经验法则如下:
n = CPU 核心数 * 系数
在这里,系数通常是 2(在拥有数百甚至数千个 CPU 核心的高端系统上,系数可能是 1.5)。同时,这个经验法则在 CPU 核心支持多线程或使用同步多线程(SMT,即 Intel 所称的超线程技术)时才有效。
关于并行 make 的更多详细信息,请参阅 make 的 man 页面中的“PARALLEL MAKE AND THE JOBSERVER”部分(通过 man 1 make 调用)。
另一个常见问题是:如何查看系统中的 CPU 核心数?有多种方法可以实现这一点,其中一个简单的方法是使用 nproc 实用程序:
$ nproc
4
快速说明一下 nproc 和相关工具:
对 nproc 进行 strace 跟踪会发现,它实际上是通过使用 sched_getaffinity() 系统调用来实现的。我们将在第 10 章《CPU 调度器——第 1 部分》和第 11 章《CPU 调度器——第 2 部分》讨论更多关于 CPU 调度的内容。
此外,lscpu 实用程序可以显示核心数量以及更多有用的 CPU 信息。你可以在 Linux 系统上试试看。
显然,我的虚拟机配置了四个 CPU 核心,因此我们将 n=4*2=8。现在我们开始构建内核。以下输出来自我们配置了 2 GB RAM 和四个 CPU 核心的 x86_64 Ubuntu 22.04 LTS 客户系统。
记住,在构建之前,内核必须先正确配置。有关详细信息,请参考第 2 章《从源码构建 6.x 内核——第 1 部分》。
当你开始构建时,内核构建过程中可能会发出警告,尽管在这种情况下并非致命:
$ make -j8
scripts/kconfig/conf --syncconfig Kconfig
UPD include/config/kernel.release
warning: Cannot use CONFIG_STACK_VALIDATION=y, please install libelf-dev, libelf-devel or elfutils-libelf-devel
[...]
因此,我们通过 Ctrl + C 中断构建,按照输出提示安装 libelf-dev 包。在我们的 Ubuntu 系统上,执行 sudo apt install libelf-dev 就足够了。请注意,如果你按照《内核工作区设置》在线章节中的详细设置步骤(或运行了我们的 ch1/pkg_install4ubuntu_lkp.sh 脚本),这种情况是不会发生的。
由于内核构建非常消耗 CPU 和内存,在虚拟机中进行这一过程会比在原生 Linux 系统上慢得多。为了节省内存,至少可以在运行级别 3(多用户模式,带网络连接,无图形界面)启动你的虚拟机:www.if-not-true-then-false.com/2012/howto-…。
接下来我们来运行构建,并给出一些有用的小技巧:
由于内核构建过程中 CPU 和内存使用量非常高,有时在图形模式下构建时可能会出现错误;系统内存不足可能导致奇怪的故障,甚至导致用户被注销!为了避免这种情况,建议你在运行级别 3(或 systemd 通常称为 multi-user.target,即多用户模式,不含图形界面)下启动虚拟机。为此,你可以从 GRUB 菜单编辑内核命令行并添加 3 参数(我们将在步骤 7——定制 GRUB 章节中讨论这些内容)。或者,如果你已经处于图形模式(systemd 称之为 graphical.target),可以使用以下命令切换到 multi-user.target:
sudo systemctl isolate multi-user.target
此外,考虑到内存成本较低(且可能在下降),增加内存也是一个提高性能的简单快捷方法!
特别是在控制台模式下,我个人更喜欢通过 ssh 登录到虚拟机并在其中工作。
在构建过程中,通过利用 tee 工具,我们可以轻松地将标准输出和标准错误同时保存到一个文件中(tee 允许我们同时在控制台上查看输出):
$ sudo systemctl isolate multi-user.target
[...]
$ cd ${LKP_KSRC}
$ make –j8 2>&1 | tee out.txt
SYNC include/config/auto.conf.cmd
HOSTCC scripts/basic/fixdep
HOSTCC scripts/kconfig/conf.o
HOSTCC scripts/kconfig/confdata.o
HOSTCC scripts/kconfig/expr.o
LEX scripts/kconfig/lexer.lex.c
YACC scripts/kconfig/parser.tab.[ch]
HOSTCC scripts/kconfig/preprocess.o
[...]
现在,kbuild 系统应该会确保我们的新内核和配置为模块的组件都能被构建。当然,有时事情会出错。在本章的后续部分,我们将讨论几种可能的情况以及如何解决它们。
在 Ubuntu 系统上解决证书配置问题
特别是在最近的 Ubuntu 系统上,我们运行 make,内核构建看起来一切正常,直到我们(通常)遇到以下问题:
$ make ...
[...]
EXTRACT_CERTS certs/signing_key.pem
CC certs/system_keyring.o
CC arch/x86/entry/vdso/vclock_gettime.o
EXTRACT_CERTS
CC certs/common.o
CC arch/x86/entry/vdso/vgetcpu.o
make[1]: *** No rule to make target 'debian/canonical-revoked-certs.pem', needed by 'certs/x509_revocation_list'. Stop.
make[1]: *** Waiting for unfinished jobs....
CC certs/blacklist.o
[...]
不过你会注意到,make 继续执行其他并行任务——至少在它发现继续构建没有意义之前,构建过程最终会失败(尽管可能需要一段时间),最后并没有成功构建内核。
那么问题出在哪里呢?原来这是一个名为 CONFIG_SYSTEM_REVOCATION_KEYS 的内核配置项,它被添加到了最近的 5.x 内核中,检查一下:
$ grep CONFIG_SYSTEM_REVOCATION_KEYS .config
CONFIG_SYSTEM_REVOCATION_KEYS="debian/canonical-revoked-certs.pem"
至少在 Ubuntu 系统上,这个特定的配置设置似乎会导致构建失败。解决方法很简单,只需将其关闭即可。你可以通过以下命令关闭它:
scripts/config --disable SYSTEM_REVOCATION_KEYS
再用 grep 检查,你会发现该配置项已被关闭。
顺便提一下,这个问题在 Ask Ubuntu 论坛上的问答中有提到:askubuntu.com/a/1329625/2…。对于好奇的读者来说,这个配置项是在 5.13 内核中加入的,具体的提交记录可以查看:github.com/torvalds/li…。
再次运行 make 命令(你可能需要在某些提示时按下回车键);这次它应该可以成功构建了!
[...]
KSYMS .tmp_vmlinux.kallsyms2.S
AS .tmp_vmlinux.kallsyms2.S
LD vmlinux
BTFIDS vmlinux
SORTTAB vmlinux
SYSMAP System.map
MODPOST modules-only.symvers
CC arch/x86/boot/a20.o
AS arch/x86/boot/bioscall.o
CC arch/x86/boot/cmdline.o
[...]
GEN Module.symvers
LDS arch/x86/boot/compressed/vmlinux.lds
AS arch/x86/boot/compressed/kernel_info.o
CC [M] arch/x86/crypto/aesni-intel.mod.o
CC [M] arch/x86/crypto/crc32-pclmul.mod.o
[...]
LD arch/x86/boot/setup.elf
OBJCOPY arch/x86/boot/setup.bin
BUILD arch/x86/boot/bzImage
Kernel: arch/x86/boot/bzImage is ready (#3)
啊,构建完成了!
顺便一提:如果构建失败,通常要检查什么?
检查并重新检查你是否做对了所有步骤;要责怪自己而不是内核(社区或代码)!
所有必需的且最新的包都安装了吗?例如,如果内核配置项 CONFIG_DEBUG_INFO_BTF=y(我的系统有这个配置),那么它要求安装 pahole 1.16 或更高版本。
内核配置是否合理?
是否硬件问题?类似“内部编译器错误:段错误”通常表明存在硬件问题;是否分配了足够的内存和交换空间?尝试在另一个虚拟机上或更好的情况下在原生 Linux 系统上进行构建。
从头开始或重启:在内核源码树的根目录中,执行 make mrproper(小心:它会清理所有内容,甚至会删除 .config 文件),然后仔细执行所有步骤。
当所有其他方法都失败时,Google 搜索错误消息吧!
构建应该干净地运行,没有任何错误或警告。当然,有时会出现编译器警告,但我们可以忽略它们。如果你在这一步遇到编译器错误从而导致构建失败,我们要如何委婉地表达?嗯,其实没法委婉——很有可能是你的问题,而不是内核社区的错误。正如刚才提到的,请检查并重新检查每一步,如果所有方法都无效,使用 make mrproper 重新开始构建!通常,内核构建失败意味着内核配置错误(可能是冲突的随机配置)、过时的工具链版本或错误的补丁等问题。(附注:我们将在“内核构建的其他技巧”部分中提供更多更具体的建议。)
好的,我们假设内核构建步骤成功了。压缩内核镜像(对于 x86[_64],它叫做 bzImage)和未压缩的内核镜像 vmlinux 都已成功构建,如前面的输出所示——前面输出块中的最后一行确认了这一点(#3 表示我已第三次构建该内核)。在构建过程中,kbuild 系统还会继续构建所有内核模块。
小提示:如果你想知道命令执行所需的时间,可以在命令前加上 time 命令(这里是:time make -j8 2>&1 | tee out.txt)。它的确能工作,但 time(1) 工具只能提供一个非常粗略的命令执行时间估计。
如果你想获得精确的 CPU 分析和时间统计,学习如何使用强大的 perf 工具吧。你可以试试 perf stat make -j8 ... 命令。我建议你在发行版内核上试一试,因为否则你可能需要为自定义内核手动构建 perf 工具。
此外,在前面的输出中,由于我们进行了并行构建(通过 make -j8,表示最多 8 个进程并行构建),所有的构建进程会将输出写入同一个标准输出位置——控制台或终端窗口。因此,输出可能会乱序或混杂在一起。
假设一切顺利(确实应该如此),当此步骤结束时,kbuild 系统将生成三个关键文件(在众多文件中)。在内核源码树的根目录中,我们现在可以看到以下文件:
- 未压缩的内核镜像文件
vmlinux(用于调试) - 符号地址映射文件
System.map - 压缩的可引导内核镜像文件
bzImage(见下方输出)
让我们检查一下这些文件!通过给 ls 命令加上 -h 选项,可以让输出(尤其是文件大小)更加人性化:
$ ls -lh vmlinux System.map
-rw-rw-r-- 1 c2kp c2kp 4.8M May 16 16:12 System.map
-rwxrwxr-x 1 c2kp c2kp 704M May 16 16:12 vmlinux
$ file vmlinux
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=e4<...>, with debug_info, not stripped
正如你所见,vmlinux 文件非常大。这是因为它包含所有的内核符号以及额外的调试信息。(附注:vmlinux 和 System.map 文件在内核调试中非常有用;请保留它们。)file 工具展示了该镜像文件的更多细节。
实际的内核镜像文件会始终位于 arch/<架构>/boot/ 目录下。因此,对于 x86 架构,我们可以看到以下文件:
$ ls -lh arch/x86/boot/bzImage
-rw-rw-r-- 1 c2kp c2kp 12M May 16 16:12 arch/x86/boot/bzImage
$ file arch/x86/boot/bzImage
arch/x86/boot/bzImage: Linux kernel x86 boot executable bzImage, version 6.1.25-lkp-kernel (c2kp@osboxes) #3 SMP PREEMPT_DYNAMIC Tue [...], RO-rootFS, swap_dev 0XB, Normal VGA
因此,我们的压缩内核镜像 6.1.25-lkp-kernel 对于 x86_64 系统大约有 12 MB 大小。file 工具再次清晰地显示这是一个用于 x86 架构的 Linux 内核引导镜像。
顺便提一下,内核的顶级 Makefile 中有一些简单(但有用)的目标,可以用来验证内核版本字符串等信息;让我们来看一看(它位于大型 Makefile 的末尾):
cd ${LKP_KSRC}
cat Makefile
[ … ]
kernelrelease:
@echo "$(KERNELVERSION)$$($(CONFIG_SHELL) $(srctree)/scripts/setlocalversion $(srctree))"
kernelversion:
@echo $(KERNELVERSION)
image_name:
@echo $(KBUILD_IMAGE)
[ … ]
让我们试试这些目标:
$ make kernelrelease kernelversion image_name
6.1.25-lkp-kernel
6.1.25
arch/x86/boot/bzImage
$
很不错。
内核文档记录了许多可以通过设置各种环境变量在构建内核时进行的调整和切换。可以在内核源码树中的 Documentation/kbuild/kbuild.rst 文件中找到相关文档。实际上,我们将在接下来的内容中使用 INSTALL_MOD_PATH、ARCH 和 CROSS_COMPILE 等环境变量。
太棒了!我们的内核镜像和模块已经准备好了!继续阅读,了解下一步如何安装内核模块。
步骤 5 —— 安装内核模块
在上一步中,所有在内核配置中标记为 m 的选项(即所有内核模块,*.ko 文件)已经在源码树中构建完成了。然而,仅仅构建这些模块还不够:它们必须被安装到系统中的某个已知位置。此部分将介绍这些细节。
在内核源码中找到内核模块
正如你刚刚了解到的,构建内核镜像和模块的步骤生成了压缩和未压缩的内核镜像,以及所有的内核模块(根据我们的内核配置)。内核模块是具有 .ko(kernel object,内核对象)后缀的文件。这些模块非常有用,它们以模块化的方式为我们提供内核功能(我们可以随时决定将它们加载到或移出内核内存)。接下来的两章将详细介绍这一主题。
目前,我们知道上一步已经生成了所有的内核模块文件,那么让我们在内核源码树中找到它们。为此,我们可以使用 find 命令在内核源码文件夹中定位这些文件:
$ cd ${LKP_KSRC}
$ find . -name "*.ko"
./crypto/crypto_simd.ko
./crypto/cryptd.ko
[...]
./fs/binfmt_misc.ko
./fs/vboxsf/vboxsf.ko
但仅仅构建内核模块还不够;为什么呢?因为这些模块需要被安装到根文件系统中的某个已知位置,这样系统在启动时才能找到并将它们加载到内核内存中。这就是为什么我们需要执行接下来的模块安装步骤(见下文“安装内核模块”部分)。这些模块的安装位置是 /lib/modules/$(uname -r)/,其中 $(uname -r) 会返回内核版本号。
安装内核模块
执行内核模块的安装非常简单;在构建步骤之后,只需调用 modules_install 这个 Makefile 目标即可。让我们来执行它:
$ cd ${LKP_KSRC}
$ sudo make modules_install
[sudo] password for c2kp:
INSTALL /lib/modules/6.1.25-lkp-kernel/kernel/arch/x86/crypto/aesni-intel.ko
SIGN /lib/modules/6.1.25-lkp-kernel/kernel/arch/x86/crypto/aesni-intel.ko
[ … ]
INSTALL /lib/modules/6.1.25-lkp-kernel/kernel/sound/soundcore.ko
SIGN /lib/modules/6.1.25-lkp-kernel/kernel/sound/soundcore.ko
DEPMOD /lib/modules/6.1.25-lkp-kernel
$
需要注意的几点:
- 我们使用
sudo以 root(超级用户)的权限执行模块安装过程。这是必须的,因为默认的安装位置(位于/lib/modules/下)只有 root 用户可以写入。modules_install目标会将内核模块复制到/lib/modules/下的正确安装位置(输出中显示为INSTALL /lib/modules/6.1.25-lkp-kernel/<...>)。 - 接着,模块可能会被“签名”。在配置了内核模块的加密签名(
CONFIG_MODULE_SIG)的系统上(这是一个有用的安全功能,此处也是如此),SIGN步骤会对模块进行签名。 - 简而言之,当配置选项
CONFIG_MODULE_SIG_FORCE开启时(默认关闭),只有正确签名的模块才能在运行时加载到内核内存中。 - 在所有模块复制(并可能签名)完成后,kbuild 系统会运行一个名为
depmod的工具。它的任务是解析内核模块之间的依赖关系,并将其编码到一些元文件中(如果存在依赖)。
现在让我们看看模块安装步骤的结果:
$ ls /lib/modules
5.19.0-40-generic/ 5.19.0-41-generic/ 5.19.0-42-generic/ 6.1.25-lkp-kernel/
如上输出所示,系统中每个已安装的(Linux)内核都会在 /lib/modules/ 下有一个文件夹,文件夹名称就是内核的版本号。让我们看看我们新内核(6.1.25-lkp-kernel)对应的文件夹。在其 kernel/ 子目录下的各个目录中存放着刚刚安装的内核模块:
$ ls /lib/modules/6.1.25-lkp-kernel/kernel/
arch/ crypto/ drivers/ fs/ lib/ net/ sound/
顺便一提,/lib/modules/<kernel-ver>/modules.builtin 文件列出了所有安装的内核模块(它们位于 /lib/modules/<kernel-ver>/kernel/ 目录下)。
覆盖默认模块安装位置
最后一个关键点是:在内核构建过程中,我们可以指定一个自定义位置来安装内核模块,覆盖默认的 /lib/modules/<kernel-ver> 位置。通过设置环境变量 INSTALL_MOD_PATH,可以实现这一点。举个例子,我们将环境变量 STG_MYKMODS 设置为我们希望安装内核模块的位置,然后运行 modules_install 命令:
export STG_MYKMODS=../staging/rootfs/my_kernel_modules
make INSTALL_MOD_PATH=${STG_MYKMODS} modules_install
通过这种方式,所有的内核模块将会安装到 ${STG_MYKMODS}/ 目录中。如果 INSTALL_MOD_PATH 指向的是一个不需要 root 权限的目录,可能就不需要使用 sudo 了。
这种覆盖内核模块安装位置的技术在为嵌入式目标构建 Linux 内核和内核模块时特别有用。我们不能将宿主系统的内核模块与嵌入式目标的内核模块混淆,否则后果将十分严重!实际上,所有人都可能偶尔犯这种错误(我自己也犯过!);因此,虚拟机的用处——特别是快照检查点功能,让我们可以快速恢复到良好状态——显而易见!
接下来的步骤是生成所谓的 initramfs(或 initrd)镜像并设置引导程序。我们还需要清楚地了解 initramfs 镜像到底是什么,以及使用它的动机。后面的章节将详细讨论这些内容。
步骤 6 —— 生成 initramfs 镜像并设置引导程序
首先,请注意,本讨论主要针对 x86[_64] 架构,这可能是最常见的架构。不过,这里学到的概念可以直接应用于其他架构(如 ARM),尽管具体命令可能会有所不同。通常情况下,不同于 x86 架构,至少在基于 ARM 的 Linux 系统上,没有直接生成 initramfs 镜像的命令;这需要手动完成。不过,像 Yocto 和 Buildroot 这样的嵌入式构建项目确实提供了自动化生成的方法。
对于典型的 x86 桌面或服务器内核构建过程,这一步分为两个部分:
- 生成 initramfs(以前称为 initrd)镜像
- 为新的内核镜像设置 GRUB
之所以将这两个步骤放在一起,是因为在 x86 架构上,便利脚本会同时执行这两个任务,给人一种这是一条命令的感觉。
你可能会好奇,initramfs(或 initrd)镜像文件到底是什么?请参阅“理解 initramfs 框架”部分了解更多细节,我们很快就会详细讨论。
现在,让我们生成 initramfs(即初始 RAM 文件系统)镜像文件并更新引导程序。顺便说一下,现在可能也是检查点或备份虚拟机的好时机,以便在最坏的情况下,即使根文件系统损坏(不应该发生这种情况),你也可以恢复到一个良好的状态并继续工作。在 x86[_64] Ubuntu 系统上,执行这一步非常简单,只需一个命令:
$ sudo make install
INSTALL /boot
run-parts: executing /etc/kernel/postinst.d/dkms 6.1.25-lkp-kernel /boot/vmlinuz-6.1.25-lkp-kernel
* dkms: running auto installation service for kernel 6.1.25-lkp-kernel [ OK ]
run-parts: executing /etc/kernel/postinst.d/initramfs-tools 6.1.25-lkp-kernel /boot/vmlinuz-6.1.25-lkp-kernel
update-initramfs: Generating /boot/initrd.img-6.1.25-lkp-kernel
[ … ]
run-parts: executing /etc/kernel/postinst.d/xx-update-initrd-links 6.1.25-lkp-kernel /boot/vmlinuz-6.1.25-lkp-kernel
I: /boot/initrd.img.old is now a symlink to initrd.img-5.19.0-42-generic
I: /boot/initrd.img is now a symlink to initrd.img-6.1.25-lkp-kernel
run-parts: executing /etc/kernel/postinst.d/zz-update-grub 6.1.25-lkp-kernel /boot/vmlinuz-6.1.25-lkp-kernel
Sourcing file `/etc/default/grub'
Sourcing file `/etc/default/grub.d/init-select.cfg'
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-6.1.25-lkp-kernel
Found initrd image: /boot/initrd.img-6.1.25-lkp-kernel
[ … ]
Found linux image: /boot/vmlinuz-5.19.0-42-generic
Found initrd image: /boot/initrd.img-5.19.0-42-generic
[ … ]
done
注意,我们再次使用 sudo 前缀执行 make install 命令。这是因为我们需要 root 权限来写入相关文件和文件夹,它们会被写入 /boot 目录(该目录可能作为根文件系统的一部分或单独分区)。
如果我们不想将生成的 initramfs 镜像和引导程序文件保存在 /boot 中怎么办?你可以通过设置环境变量 INSTALL_PATH 来覆盖目标目录;这在为嵌入式系统构建 Linux 时经常会用到。内核文档中提到了这一点: docs.kernel.org/kbuild/kbui…。
至此,我们已经完成了:全新的 6.1 内核、所有请求的内核模块和 initramfs 镜像都已经生成并安装,GRUB 也已经更新,以反映新内核和 initramfs 镜像的存在。接下来只需重启系统,在引导时选择新的内核镜像,启动系统,登录并验证一切是否正常即可。
在本步骤中,我们生成了 initramfs 镜像。问题是,在生成该镜像时,kbuild 系统到底做了什么?继续阅读以了解更多详情。
生成 initramfs 镜像——幕后原理
当你在 x86 系统上运行 sudo make install 命令时,内核的 Makefile 调用了 scripts/install.sh 脚本。这个脚本是一个封装脚本,会循环检查所有可能的与架构相关的安装脚本,并在存在时运行它们(带有适当的参数)。具体来说,以下是可能被执行的与架构相关的脚本的位置(按顺序):
${HOME}/bin/${INSTALLKERNEL}/sbin/${INSTALLKERNEL}${srctree}/arch/${SRCARCH}/install.sh${srctree}/arch/${SRCARCH}/boot/install.sh
在 x86[_64] 系统中,arch/x86/boot/install.sh 脚本内部会将以下文件复制到 /boot 目录,文件名格式通常为 <filename>-$(uname -r)-kernel:
/boot/config-6.1.25-lkp-kernel/boot/System.map-6.1.25-lkp-kernel/boot/initrd.img-6.1.25-lkp-kernel/boot/vmlinuz-6.1.25-lkp-kernel
initramfs 镜像也在此过程中构建。在 x86 Ubuntu Linux 上,一个名为 update-initramfs 的 shell 脚本执行这一任务(实际上它是另一个脚本 mkinitramfs 的封装,后者完成实际的工作)。
那么,镜像是如何构建的呢?简单来说,initramfs 镜像其实就是一个使用所谓的新 newc 格式的 cpio 文件。cpio(copy-in, copy-out)工具是一个较老的工具,用于创建归档文件——即文件的简单集合;tar 工具就是 cpio 的一个典型用户。要从给定目录的内容(假设目录名为 my_initramfs)中构建 initramfs 镜像,可以这样做:
find my_initramfs/ | sudo cpio -o --format=newc -R root:root | gzip -9 > initramfs.img
注意,镜像通常使用 gzip 进行压缩。
镜像构建完成后,它也会被复制到 /boot 目录中,在前面的输出片段中你可以看到 /boot/initrd.img-6.1.25-lkp-kernel 文件。
如果要复制到 /boot 的文件已经存在,那么它将被备份为 <filename>-$(uname -r).old。文件名为 vmlinuz-<kernel-ver>-kernel 的文件是 arch/x86/boot/bzImage 文件的副本。换句话说,它是压缩后的内核镜像文件——引导程序将被配置为加载到 RAM 中的镜像,解压缩后跳转到其入口点,从而将控制权交给内核!
为什么它们的名字是 vmlinux(这是存储在内核源码树根目录中的未压缩内核镜像文件)和 vmlinuz?这是一个古老的 Unix 传统,而 Linux 系统也很乐意沿用这一传统:在许多 Unix 系统中,内核被称为 vmunix,因此 Linux 将其称为 vmlinux,压缩后的则称为 vmlinuz;vmlinuz 中的 z 暗示了默认的 gzip 压缩。顺便说一下,使用 gzip 压缩现代内核已经相当过时了;在现代 x86 系统中,默认使用更优(且更快)的 ZSTD 压缩,尽管文件命名约定依然保持不变。
此外,位于 /boot/grub/grub.cfg 的 GRUB 配置文件也已更新,以反映系统中已安装的新内核。
再次强调,这一切都是非常架构相关的。上面的讨论是基于在 Ubuntu Linux x86_64 系统上构建内核的过程。虽然概念上类似,但内核镜像文件的名称、位置,尤其是引导程序的细节,在不同架构和不同发行版中有所不同。
如果你愿意,可以直接跳到步骤 7——定制 GRUB 部分。如果你对 initramfs 框架感到好奇,请继续阅读。接下来的部分将更详细地介绍 initramfs(早期称为 initrd)框架的原理和动机。
理解 initramfs 框架
这仍然是一个谜团!到底什么是 initramfs(初始 RAM 文件系统)或 initrd(初始 RAM 磁盘)镜像?它的作用是什么?为什么需要它?
首先,使用这个功能是可选的——对应的内核配置指令是 CONFIG_BLK_DEV_INITRD,默认被设置为 y,因此默认启用。简单来说,对于那些在启动时无法提前知道某些信息的系统,例如引导磁盘的主机适配器或控制器类型(如 SCSI、RAID 等),根文件系统的确切文件系统类型(可能是 ext2、ext4、btrfs、f2fs 等),或者这些功能总是作为内核模块来构建的系统,我们需要 initramfs 功能。为什么呢?原因马上会清楚。正如前面提到的,initrd 现在被视为较旧的术语。如今,我们更多地使用 initramfs 来代替它。
那么,旧的 initrd 和新的 initramfs 之间究竟有什么区别?主要区别在于它们的生成方式不同。要使用当前目录内容构建(旧的)initrd 镜像,我们可以这样做:
find . | sudo cpio -R root:root | gzip -9 > initrd.img
而要使用当前目录内容构建(新的)initramfs 镜像,我们可以这样做(使用 newc 格式):
find . | sudo cpio -o --format=newc -R root:root | gzip -9 > initramfs.img
(提示:阅读后面的部分时,这些细节会更加清晰。)
为什么需要 initramfs 框架?
initramfs 框架本质上是早期内核启动和用户模式之间的一种“中介”。它允许我们在实际的(真正的)根文件系统挂载之前、在内核完成系统初始化之前运行用户空间的应用程序(或脚本)。在许多情况下,这都非常有用,下面列出了一些关键场景。关键点是,initramfs 使我们能够运行内核在启动时通常无法运行的用户模式应用程序。
从实际角度来看,这个框架允许我们做一些有趣的事情,其中包括:
- 设置控制台字体
- 自定义键盘布局设置
- 在控制台设备上打印自定义的欢迎消息
- 接受密码(用于加密磁盘)
- 根据需要加载内核模块
- 如果某些操作失败,启动一个“救援” shell
- 以及更多功能!
假设你正在开发和维护一个新的 Linux 发行版。在安装时,你的发行版用户可能会选择将他们的 SSD 磁盘格式化为 f2fs(快速闪存文件系统)文件系统。问题是,你无法提前知道用户会做出什么选择——可能会是许多文件系统中的任何一个。因此,你决定预先构建并提供各种内核模块,以满足几乎所有的可能性。当安装完成后,用户的系统启动时,内核将在这种情况下需要 f2fs.ko 内核模块来成功挂载(f2fs)根文件系统并继续操作。
当引导程序完成其工作后,Linux 内核接管系统,运行其代码以初始化并准备系统。所以,想想这个场景:内核现在已经在 RAM 中运行(概念上可以参见图 3.1 的左上角),但它即将需要的模块仍然位于次级存储设备上,即磁盘(或闪存芯片)(概念上可见于图 3.1 的右下角);更重要的是,它们尚不可访问,因为根文件系统还没有被挂载。
但等等,想一想,这里出现了经典的“先有鸡还是先有蛋”的问题:为了让内核挂载根文件系统(从而获得存储在其上的必要模块的访问权限),它需要将 f2fs.ko 内核模块文件加载到 RAM 中(因为它包含挂载和操作该文件系统所需的代码)。然而,这个文件嵌入在 f2fs 根文件系统本身中——具体来说,它位于这里:/lib/modules/<kernel-ver>/kernel/fs/f2fs/f2fs.ko(见图 3.1)。
initramfs 框架的主要目的之一就是解决这个“鸡和蛋”问题。initramfs 镜像文件是一个压缩的 cpio 归档文件(cpio 是一种由 tar 使用的平面文件格式)。正如我们在上一节提到的,update-initramfs 脚本内部调用了 mkinitramfs 脚本(至少在 x86 Ubuntu 上是这样)。这些脚本构建了一个最小的临时根文件系统,包含内核模块以及支持性基础设施,如 /etc 和 /lib 文件夹,并以简单的 cpio 文件格式存储,通常会使用 gzip 进行压缩。现在,这个文件就形成了所谓的 initramfs(或 initrd)镜像文件;它会被放置在名为 /boot/initrd.img-<kernel-ver> 的文件中。那么,这如何帮助解决问题呢?
在启动过程中,假设启用了 initramfs 功能,引导程序会多执行一步操作:在将内核镜像解压并加载到 RAM 后,它还会将指定的 initramfs 镜像文件加载到 RAM 中。现在,当内核运行并检测到 initramfs 镜像的存在时,它会解压缩该镜像,并通过其内容(通过脚本)将所需的内核模块加载到 RAM 中(见图 3.2)。
现在所需的模块已经加载到了主内存(RAM)中(以“RAM 磁盘”的格式),内核可以加载这些必要的模块(如 f2fs.ko),获取它们的功能,从而能够挂载“真实”的根文件系统并继续操作!关于启动过程(在 x86 上)和 initramfs 镜像的更多细节,可以在接下来的部分中找到。
理解 x86 启动过程的基本原理
下面是 x86[_64] 桌面(或笔记本)、工作站或服务器的典型启动过程的简要概述:
首先,系统启动有两种主要方式。第一种是传统方式(特定于 x86),通过 BIOS(基本输入输出系统)进行启动。BIOS 进行基本的系统初始化和诊断(POST——加电自检)后,会将第一个可启动磁盘的第一个扇区加载到 RAM 中,并跳转到其入口点。这就是通常所说的第一阶段引导程序,它非常小(通常只有一个扇区,512 字节);它的主要任务是将第二阶段(更大)的引导程序代码——也存在于可启动磁盘上——加载到内存中并跳转到它。
第二种更现代、更强大的启动方式是通过新的 UEFI(统一可扩展固件接口)标准。在许多现代系统中,UEFI 框架被用作更高级且更安全的启动系统方式。
那么,传统的 BIOS 和现代的 UEFI 有什么区别呢?简而言之:
- UEFI 是一个现代框架,不仅限于 x86(BIOS 仅限于 x86);例如,基于 ARM 的系统利用 UEFI 实现其著名的安全启动功能。
- UEFI 更加安全;它只允许“签名”的操作系统(它称之为“应用程序”)通过它启动(这有时会导致双系统启动问题)。
- UEFI 需要一个独立的特殊分区,称为 ESP(EFI 系统分区);此分区存放一个 .efi 文件,该文件包含初始化代码和数据,而 BIOS 则将其写入固件中(通常是在 EEPROM 芯片中)。因此,更新 UEFI 比更新 BIOS 简单得多。
- UEFI 的启动时间比传统 BIOS 更快。
- UEFI 允许运行现代的 32 位或 64 位代码,因此可以使用吸引人的图形界面;而 BIOS 只能处理 16 位代码。
- 磁盘大小:BIOS 仅支持最大 2.2 TB 的磁盘,而 UEFI 可支持多达 9 ZB(泽字节)的磁盘!
无论是使用 UEFI 还是 BIOS,一旦内核镜像被加载到 RAM 中,第二阶段引导程序代码就会接管。它的主要任务是从文件系统中加载第三阶段的引导程序到内存中并跳转到它的入口点。在 x86 系统中,通常使用 GRUB 作为引导程序(较早的一个引导程序是 LILO(Linux Loader))。
GRUB 会传递压缩的内核镜像文件(/boot/vmlinuz-<kernel-ver>)以及压缩的 initramfs 镜像文件(/boot/initrd.img-<kernel-ver>)作为参数(通过其配置文件,我们将在后续部分看到)。引导程序(简单来说)执行以下操作:
- 进行低级硬件初始化。
- 将这些镜像加载到 RAM 中,部分解压缩内核镜像。
- 然后跳转到内核的入口点。
现在,Linux 内核完全控制机器,并开始初始化硬件和软件环境。它通常不会假设引导程序已经完成的工作。不过,它依赖于 BIOS 或 UEFI 来通过 ACPI 表(或在 ARM/PPC 上通过设备树)设置诸如 PCI 地址分配和中断线分配之类的工作。
完成大部分硬件和软件初始化后,如果内核检测到 initramfs 功能已启用(CONFIG_BLK_DEV_INITRD=y),它会定位(并在需要时解压缩)RAM 中的 initramfs(或 initrd)镜像(见图 3.2)。
然后内核会将其挂载为 RAM 内部的一个临时根文件系统。
现在,我们在内存中设置了一个基本的、最小的临时根文件系统。因此,基于 initramfs 的启动脚本开始运行,执行包括将所需的内核模块加载到 RAM 中(实际上就是加载根文件系统的驱动程序,在我们的场景中包括 f2fs.ko 内核模块;再次参见图 3.2)。
当 initramfs 运行时,它首先调用 /sbin/init(这可能是一个可执行文件或脚本);除了其他日常任务外,它执行了一个关键任务:pivot-root,卸载临时的 initramfs 根文件系统,释放其内存,并挂载实际的根文件系统。由于提供该文件系统支持的内核模块已经在 RAM 中,因此这现在是可能的。
一旦成功挂载基于磁盘或闪存的实际根文件系统,系统初始化就可以继续进行。内核继续运行,最终调用第一个用户空间进程(PID 1),通常是 /sbin/init(当使用较旧的 SysV init 框架时),或者在现代系统中,更有可能通过功能更强大的 systemd init 框架调用。
然后,init 框架继续初始化系统,启动配置好的系统服务。
需要注意几点:
- 在现代 Linux 系统上,传统的(也可理解为老旧的)SysV(System Five)
init框架在很大程度上已经被一种现代的优化框架systemd(系统守护进程)取代。因此,在许多现代 Linux 系统中,包括嵌入式系统,传统的/sbin/init已经被systemd取代(或只是一个指向其可执行文件的符号链接)。systemd框架被认为是更优的,它能够微调启动过程并优化启动时间,还具有许多其他功能。关于systemd,可以在“进一步阅读”部分找到更多信息。 initramfs根文件系统的生成本书未详细介绍;官方内核文档涵盖了一些相关内容——使用初始 RAM 磁盘(initrd):docs.kernel.org/admin-guide…。- 作为生成根文件系统的简单示例,你可以查看 SEALS 项目的代码(见 github.com/kaiwan/seal…),该项目在在线章节“内核工作区设置”中提到;它有一个 Bash 脚本,可以从头开始生成一个非常简洁或框架式的根文件系统。
现在你已经了解了 initramfs 的动机,我们将在接下来的部分更深入地探讨 initramfs。继续阅读吧!
关于 initramfs 框架的更多内容
initramfs 框架的另一个重要应用是在启动加密磁盘的计算机时起作用。
你是否在使用未加密磁盘的笔记本电脑?这不是一个好主意;如果你的设备丢失或被盗,黑客可以很容易地通过使用基于 Linux 的 USB 启动盘进入系统,从而访问所有的磁盘分区。此时你的登录凭据不会对他们构成阻碍。现代发行版可以轻松地加密磁盘分区,这已经成为安装过程中的常规步骤。除了基于卷的加密(由 LUKS、dm-crypt、eCryptfs 等提供),还有一些工具可以对单个文件进行加密和解密。更多加密工具可以参考以下链接:
假设系统使用了加密的文件系统,在启动过程的早期,内核必须向用户请求密码,输入正确后才能继续解密和挂载磁盘等操作。
但是,思考一下:在没有 C 运行时环境的情况下(例如没有包含库、加载程序(ld-linux...)以及所需内核模块的根文件系统),如何运行一个 C 程序可执行文件来请求密码?
请记住,此时内核尚未完成初始化,用户空间应用程序无法运行。initramfs 框架通过在主内存中设置一个临时但完整的用户空间运行环境解决了这个问题,该环境包含必要的根文件系统、库、加载程序、内核模块、脚本等。
了解 initramfs 镜像
我们刚才提到,initramfs 镜像是一个临时但相当完整的系统,其中包含系统库、加载程序、最小所需的内核模块、一些脚本等。我们可以验证这一点吗?当然可以!我们可以查看 initramfs 镜像文件的内容。在 Ubuntu 中,可以使用 lsinitramfs 脚本(在 Fedora 中,等效的命令是 lsinitrd):
$ ls -lh /boot/initrd.img-6.1.25-lkp-kernel
-rw-r--r-- 1 root root 26M Jun 13 11:08 /boot/initrd.img-6.1.25-lkp-kernel
$ lsinitramfs /boot/initrd.img-6.1.25-lkp-kernel | wc -l
364
$ lsinitramfs /boot/initrd.img-6.1.25-lkp-kernel
.
kernel
[ ... ]
bin
[ ... ]
conf/initramfs.conf
etc
etc/console-setup
[ ... ]
etc/default/console-setup
etc/default/keyboard
etc/dhcp
[ ... ]
etc/modprobe.d
etc/modprobe.d/alsa-base.conf
[ ... ]
etc/udev/udev.conf
[ ... ]
lib64
libx32
run
sbin
scripts
scripts/functions
scripts/init-bottom
[ ... ]
usr
usr/bin
usr/bin/cpio
usr/bin/dd
usr/bin/dmesg
[ ... ]
usr/lib/initramfs-tools
usr/lib/initramfs-tools/bin
[ ... ]
usr/lib/modprobe.d/systemd.conf
usr/lib/modules
[ ... ]
usr/lib/modules/6.1.25-lkp-kernel/kernel/crypto/crc32_generic.ko
[ ... ]
usr/lib/modules/6.1.25-lkp-kernel/kernel/drivers/net/ethernet/intel/e1000/e1000.ko
[ ... ]
usr/lib/modules/6.1.25-lkp-kernel/kernel/fs/f2fs/f2fs.ko
[ ... ]
usr/sbin/modprobe
[ ... ]
var/lib/dhcp
里面有很多内容:我们截取了一部分输出,显示了一些关键部分。可以看到,这是一个包含支持运行库、内核模块、/etc、/bin、/sbin、/usr 及更多目录和工具的最小根文件系统。
initramfs(或 initrd)镜像的构建细节超出了我们在此讨论的范围。我们提到了基本原理,你可以使用以下命令生成 initramfs 镜像:
find my_initramfs/ | sudo cpio -o --format=newc -R root:root | gzip -9 > initramfs.img
我建议你研究这些脚本的内部工作原理(在 Ubuntu 上):/usr/sbin/update-initramfs,它是 /usr/sbin/mkinitramfs shell 脚本的封装。有关更多信息,请参阅“进一步阅读”部分。
此外,现代系统引入了所谓的混合 initramfs:一个 initramfs 镜像,它由早期的 ramfs 镜像与常规或主 ramfs 镜像组成。解压/打包(解压/压缩)这些镜像需要使用特殊工具。Ubuntu 提供了 unmkinitramfs 和 mkinitramfs 脚本来执行这些操作。
作为一个快速实验,让我们将我们之前生成的全新 initramfs 镜像解压到一个临时目录中。记住,这是在我们的 x86_64 Ubuntu 22.04 LTS 虚拟机上进行的操作。我们使用 tree 命令查看输出,结果为了可读性进行了截断。
步骤 7 —— 定制 GRUB 引导程序
我们现在已经完成了第 2 章《从源码构建 6.x Linux 内核——第 1 部分》中列出的第 1 到第 6 步。你现在可以重新启动系统了;不过,在此之前,确保保存并关闭所有应用程序和文件。默认情况下,现代的 GRUB 不会在重启时显示菜单,它会默认启动新构建的内核(记住,这里仅描述 x86[_64] 系统上运行 Ubuntu 的过程;默认启动的内核可能因发行版而异)。
在 x86[_64] 系统上,你可以在系统启动的早期通过按住 Shift 键进入 GRUB 菜单。不过,这一行为可能会因其他因素而异——在启用了较新 UEFI/BIOS 固件的系统上,或者在嵌套虚拟机中运行时,你可能需要使用其他方法强制显示 GRUB 菜单(试着按 Esc 键也是一种方法)。
如果我们想在每次启动时都查看并定制 GRUB 菜单,从而选择不同的内核/操作系统启动(甚至传递一些内核参数),该怎么办?这在开发和调试过程中非常有用,因此让我们看看如何实现。
定制 GRUB —— 基础知识
定制 GRUB 非常简单;我们可以作为 root 用户编辑其配置文件 /etc/default/grub。请注意以下几点:
- 这些步骤应在“目标”系统上执行(而不是在宿主机上)——在我们的例子中,是在 x86_64 Ubuntu 22.04 虚拟机中。当然,如果你在本地 Linux 上工作,也可以直接操作。
- 该过程仅在我们的 x86_64 Ubuntu 22.04 LTS 虚拟机上经过测试和验证。
那么我们在这里要做什么?我们希望 GRUB 在启动时显示其菜单,让我们可以进一步定制。以下是实现此目的的步骤:
-
首先,为了安全起见,备份 GRUB 引导程序配置文件:
sudo cp /etc/default/grub /etc/default/grub.orig -
编辑它。你可以使用
vi或你喜欢的编辑器:sudo vi /etc/default/grub -
要在每次启动时显示 GRUB 提示符,插入以下行:
GRUB_HIDDEN_TIMEOUT_QUIET=false在某些 Linux 发行版中,可能会有
GRUB_TIMEOUT_STYLE=hidden指令;只需将其更改为GRUB_TIMEOUT_STYLE=menu即可达到相同的效果。在开发和测试过程中始终显示引导菜单是有益的,但在生产环境中通常为了速度和安全会关闭此功能。 -
设置启动默认操作系统的超时时间(以秒为单位)。默认值是 10 秒;这里我们将其设置为 3 秒:
GRUB_TIMEOUT=3设置超时时间的值会产生以下结果:
0:立即启动系统,不显示菜单。-1:无限等待。
-
如果配置文件中存在
GRUB_HIDDEN_TIMEOUT指令,将其注释掉:#GRUB_HIDDEN_TIMEOUT=1 -
最后,作为 root 用户运行
update-grub程序以使更改生效:sudo update-grub上述命令通常会导致
initramfs镜像被刷新(重新生成)。完成后,你可以准备重启系统了。不过稍等一下!接下来的部分将告诉你如何修改 GRUB 配置,使其默认启动你选择的内核。
选择默认启动的内核
GRUB 默认的内核设置为编号为 0(通过 GRUB_DEFAULT=0 指令),这将确保默认启动“第一个内核”——即最近添加的内核(在超时后启动)。
但这可能不是我们想要的;举个真实的例子,在我们的 x86 Ubuntu 22.04 LTS 虚拟机上,我们可以将其设置为默认启动 Ubuntu 发行版内核。与之前一样,编辑 /etc/default/grub 文件(当然,作为 root 用户):
GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 5.19.0-43-generic"
当然,这意味着如果你的发行版更新或升级,你需要再次手动更改上述行以反映你希望默认启动的新版内核,并再次运行 sudo update-grub。
这是我们修改后的 GRUB 配置文件:
$ cat /etc/default/grub
[...]
#GRUB_DEFAULT=0
GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 5.19.0-43-generic"
#GRUB_TIMEOUT_STYLE=hidden
GRUB_HIDDEN_TIMEOUT_QUIET=false
GRUB_TIMEOUT=3
GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"
GRUB_CMDLINE_LINUX="quiet splash"
[...]
如前一节所述,别忘了:如果你在此文件中进行任何更改,务必运行 sudo update-grub 命令以使更改生效。
其他注意事项
-
你可以添加一些“美化”调整,例如通过
BACKGROUND_IMAGE="<img_file>"指令更改背景图片(或颜色)。 -
在 Fedora 中,GRUB 引导程序配置文件有所不同;运行以下命令可在每次启动时显示 GRUB 菜单:
sudo grub2-editenv - unset menu_auto_hide详细信息可以在 Fedora 维基的 Changes/HiddenGrubMenu 页面找到。
-
不幸的是,GRUB2(现在最新版本是 2)在几乎每个 Linux 发行版中实现的方式都有所不同,这导致尝试在不同发行版中进行统一配置时出现不兼容性。
一切准备就绪!
现在,我们可以重启系统,进入 GRUB 菜单,并启动新内核:
$ sudo reboot
[sudo] password for c2kp:
当系统完成关机程序并重新启动时,你很快会看到 GRUB 引导菜单(下一节还会展示一些截图)。确保通过按下任意键中断自动引导!
虽然总是有可能,但我建议不要删除原始的发行版内核镜像(以及相关的 initrd、System.map 文件等)。万一你全新的内核无法启动怎么办?(连“泰坦尼克号”都能出事!)通过保留原始镜像,我们可以有一个备用选项:从原始发行版内核启动,修复问题并重新尝试。几乎总会有一个 Recovery... 菜单选项,它至少会尝试让你登录到 root shell。接近最坏的情况,你通常可以进入基于 initramfs 的 shell(从 RAM 中工作)。
最坏的情况是,假如所有其他内核/initrd 镜像都被删除(或损坏),而你唯一的新内核无法成功启动?嗯,你可以通过 USB 启动盘进入恢复模式。稍加搜索,你会找到很多相关链接和视频教程。(附注:这是 Ubuntu 的链接,指导你通过一个简洁的图形应用创建 USB 启动盘,该应用通常预装在系统上:ubuntu.com/tutorials/c…)。
通过 GNU GRUB 引导程序启动我们的虚拟机
现在,我们的虚拟机(在此使用 Oracle VirtualBox 虚拟机管理程序)即将启动;一旦其(模拟的)BIOS / UEFI 例程完成,首先显示的就是 GNU GRUB 屏幕。(请注意,这里解释的步骤在本地 Linux 系统上也同样适用。)这是因为我们故意将 GRUB_HIDDEN_TIMEOUT_QUIET GRUB 配置指令设置为 false(如前一节所述)。请看下面的截图(图 3.4)。
截图中看到的特定样式是 Ubuntu 发行版自定义的显示样式:
现在让我们直接启动虚拟机:
- 按下任意键(除回车键外),确保在超时时间结束后默认内核不会启动(回想一下,我们将超时设置为 3 秒)。
- 如果尚未进入,请滚动到
Advanced options for Ubuntu菜单,选中它(如图 3.4 所示),然后按下回车键。 - 现在你会看到一个类似于但不完全相同的菜单,如下图 3.5 所示。对于 GRUB 检测到并可以启动的每个内核,会显示两行——一行是内核本身,另一行是该内核的特殊恢复模式启动选项:
请注意,默认将启动的内核(在我们的例子中是 6.1.25-lkp-kernel)默认带有星号(*)高亮显示。
前面的截图显示了一些“额外”的条目。这是因为在截取此截图时,我已经构建了几次 6.1.25 内核(导致之前的内核作为“.old”项被保存);另外,保持 Ubuntu 更新会导致较新的发行版内核也被安装进来。我们可以看到 5.19.0-43-generic 内核及其前一个版本。不过,这里我们可以忽略它们。
顺便一提,为了提供一些不同的视角,下面是一张基于 UEFI 系统的启动选择菜单截图(在这个特定案例中,展示的是一台 x86_64(戴尔)PC 的启动菜单,UEFI 的快捷键是 F12);注意这个图形界面非常丰富,甚至支持鼠标指针:
好吧,回到我们的 x86 Ubuntu 虚拟机和 GRUB。无论如何,只需滚动并选中感兴趣的条目——在这里,是我们崭新的 6.1 LTS 内核,菜单项为 *Ubuntu, with Linux 6.1.25-lkp-kernel(如图 3.5 所示)。在这里,它是 GRUB 启动菜单中的第一行(因为它是最近添加的可启动操作系统)。
一旦你高亮选中了上述菜单项,按下回车键,Voilà!引导程序将开始工作,解压并将内核和 initramfs(或 initrd)镜像加载到 RAM 中,并跳转到 Linux 内核的入口点,将控制权交给 Linux!
如果一切顺利(应该如此),你将成功启动到全新构建的 6.1.25 LTS Linux 内核!恭喜你任务完成!
当然,你还可以做得更多——接下来的部分将展示如何在运行时(启动时)进一步编辑和定制 GRUB 配置。这种技能有时会非常有用——例如,忘记了 root 密码?没错,你实际上可以使用这种技术绕过密码!继续阅读,了解如何操作。
试验 GRUB 提示符
你可以进一步试验;而不是在高亮选择 Ubuntu, with Linux 6.1.25-lkp-kernel 菜单项时直接按回车键,确保该行已被高亮显示,然后按下 e 键(表示编辑)。现在我们进入 GRUB 的编辑屏幕,在这里我们可以自由编辑任何我们喜欢的值。以下是按下 e 键后的截图:
这张截图是在滚动几行后截取的;仔细看——你可以在编辑框底部倒数第四行的开头看到光标(下划线型:“_”)。我特意用框标出了这一关键行,供参考。此外,磁盘 UUID 值部分已被隐藏。这是关键行,开头是缩进的关键词 linux。
这行指定了内核镜像的路径,接着是存储设备的 ID,随后是通过 GRUB 引导程序传递给 Linux 内核的内核参数列表。这里,内核镜像的路径是 /vmlinuz-6.1.25-lkp-kernel,路径从 / 开始,因为 /boot 是一个独立的分区。
你可以在这里做一些简单的试验:
练习 1:
作为一个简单的练习,在高亮显示以 linux 开头的关键菜单行后,向右滚动并删除该条目中的 quiet 和 splash 字样,然后按 Ctrl + X 或 F10 来启动。这次,美丽的 Ubuntu 启动画面不会出现;你会直接进入控制台,看到所有的内核消息(通过常用的 printk() 相关 API 发出)快速滚过,之后还能看到 systemd 的控制台消息等。
接下来,一个常见问题是:如果忘记密码,无法登录怎么办?好吧,有几种方法可以解决这个问题。一个方法是通过引导程序重置密码;以下是一个简单的学习练习,教你如何操作。
练习 2:
像之前一样进入 GRUB 菜单,找到相关的内核菜单项,按 e 进行编辑,滚动到以 linux 开头的行,在这行末尾追加 single(或数字 1),使其看起来像这样:
linux /vmlinuz-6.1.25-lkp-kernel root=UUID=<...> ro quiet splash $vt_handoff single
现在,当你启动时,内核会进入单用户模式,并为你提供一个带有 root 权限的 shell。系统可能会提示你按回车键进行维护,请按提示操作。然后运行 passwd <用户名> 命令来更改或重置密码。退出这个 root shell 后,系统会继续正常启动过程。
引导进入单用户模式的具体步骤因发行版而异。在 Red Hat/Fedora/CentOS 上,GRUB2 菜单中的编辑方式略有不同。你可以在“进一步阅读”部分找到如何在这些系统上设置的链接。
这也让我们学到了关于安全性的一些知识,不是吗?当可以无需密码访问引导菜单(甚至 BIOS/UEFI)时,系统就被认为是不安全的!实际上,在高度安全的环境中,甚至对控制台设备的物理访问也必须受到限制。
很好,到现在为止,你已经学会了如何定制 GRUB,并且我预计你已经启动了全新的 6.1 Linux 内核!但我们不能仅仅假设如此;我们需要验证内核确实是我们想要的那个,并且按照计划进行了配置。
验证我们新内核的配置
好了,回到我们的讨论。我们现在已经启动到了新构建的内核。但是,请不要盲目假设一切正常;我们需要实际验证这一点。
经验方法总是最好的。在本节中,我们将验证我们确实运行的是我们刚刚构建的(6.1.25)内核,并且该内核的配置确实如我们预期的那样。首先,我们查看内核版本:
$ uname -r
6.1.25-lkp-kernel
确实如此,我们现在运行的是 Ubuntu 22.04 LTS,并且内核是我们定制构建的 6.1.25 LTS!uname 工具的其他变体显示了我们的硬件和操作系统如计划中的那样:我们在运行 GNU/Linux 的 x86_64 系统上:
$ uname -m ; uname -o
x86_64
GNU/Linux
接着,回想一下我们在第 2 章《从源码构建 6.x Linux 内核——第 1 部分》的“使用 make menuconfig UI 的示例”一节中作为练习对内核配置所做的一些小改动。
现在我们检查这些更改的配置项是否已生效。以下是我们更改的配置项及其对应的名称:
- CONFIG_LOCALVERSION: 上面
uname -r的输出清楚显示了内核版本的localversion(或-EXTRAVERSION)部分已经设置为我们想要的-lkp-kernel。 - CONFIG_IKCONFIG: 这一选项应该已经启用(参见上一章);它允许我们通过
/proc/config.gz伪文件查询当前内核的配置。 - CONFIG_HZ: 我们将此选项设置为 300 作为实验。
现在我们通过内核的 extract-ikconfig 脚本检查这些配置(记住要将 LKP_KSRC 环境变量设置为 6.1 内核源码树目录的根位置):
$ ${LKP_KSRC}/scripts/extract-ikconfig /boot/vmlinuz-6.1.25-lkp-kernel
#
# Automatically generated file; DO NOT EDIT.
# Linux/x86 6.1.25 Kernel Configuration [...]
CONFIG_HZ_300=y
[...]
成功了!我们可以通过 scripts/extract-ikconfig 脚本查看整个内核配置(因为启用了 CONFIG_IKCONFIG[_PROC] 配置项)。我们还可以使用该脚本来 grep 出之前我们在第 2 章《从源码构建 6.x Linux 内核——第 1 部分》中修改的其他配置项:
$ scripts/extract-ikconfig /boot/vmlinuz-6.1.25-lkp-kernel | grep -E "LOCALVERSION|CONFIG_HZ"
CONFIG_LOCALVERSION="-lkp-kernel"
# CONFIG_LOCALVERSION_AUTO is not set
# CONFIG_HZ_PERIODIC is not set
# CONFIG_HZ_100 is not set
# CONFIG_HZ_250 is not set
CONFIG_HZ_300=y
# CONFIG_HZ_1000 is not set
CONFIG_HZ=300
仔细查看上面的输出,可以看到我们的配置确实达到了预期。新内核的配置设置与我们在第 2 章中期望的一致。
另外,由于我们启用了 CONFIG_IKCONFIG_PROC 选项,我们还可以通过查看 /proc/config.gz 中的内核配置(通过压缩文件系统的入口)来完成相同的验证:
gunzip -c /proc/config.gz | grep -E "LOCALVERSION|CONFIG_HZ"
到这里,内核构建完成了!太棒了。我建议你再次参考第 2 章《从源码构建 6.x Linux 内核——第 1 部分》的“从源码构建内核的步骤”部分,回顾整个过程的步骤概览。好了,接下来进入一个非常有趣的部分:我们将学习如何为树莓派板子交叉编译内核。
为树莓派构建内核
树莓派是一款流行且相对廉价的单板计算机(SBC),非常适合用于实验和原型开发。爱好者、业余 tinkerer 以及部分专业人士都发现它在学习嵌入式 Linux 时非常有用,特别是因为它有强大的社区支持(提供了很多问答论坛)和出色的技术支持。(你可以在在线章节《内核工作区设置》中,树莓派实验一节中找到对树莓派的简要讨论和图片。顺便说一下,有几款著名的树莓派克隆产品,比如 Orange Pi,这些设备运行良好,本文中的讨论同样适用于它们。)
构建树莓派 4 Model B(64 位)设备内核的两种典型方式:
- 在功能强大的宿主系统上构建内核,通常是在运行 Linux 发行版的 x86_64(或 Mac)台式机或笔记本上。
- 直接在目标设备上构建内核。
我们将遵循第一种方法——速度更快,而且被认为是执行嵌入式 Linux 开发的正确方式。(如果你想了解如何直接在目标设备上构建内核,可以参考此链接。)
我们假设(像往常一样)在 x86_64 Ubuntu 22.04 LTS 虚拟机上运行。那么,现在宿主系统其实是一个来宾 Linux 虚拟机!同时,我们的目标是为 AArch64 架构构建内核,以充分利用其功能(而非 32 位)。
在虚拟机中执行大规模下载和内核构建并不是理想的选择。根据主机和虚拟机的性能和内存,构建可能需要较长时间,速度可能比在本地 Linux 系统上慢两倍。不过,只要确保虚拟机有足够的磁盘空间(并且主机系统实际上有这些空间),这种方法是可行的。
为了为目标设备(AArch64 树莓派 4 目标)构建内核或任何组件,我们需要使用 x86_64 到 AArch64 的交叉编译器。这意味着还要安装一个合适的交叉工具链来进行构建。
在接下来的几节中,我们将工作分为三个步骤:
- 克隆树莓派内核源码树
- 安装 x86_64 到 AArch64 的交叉工具链
- 配置并构建树莓派 AArch64 内核
让我们开始吧!
第一步 —— 克隆树莓派内核源码树
我们选择一个暂存文件夹(即构建发生的地方)来存放树莓派内核源码树和交叉工具链,并将其分配给环境变量(避免硬编码路径):
-
设置工作区。我们设置环境变量
RPI_STG(不必一定用这个名字,只要选择一个合理的名字并保持一致)作为暂存文件夹的位置——这是我们执行工作的地方。你可以根据自己的系统选择合适的值(我使用的是~/rpi_work):export RPI_STG=~/rpi_work mkdir -p ${RPI_STG}/kernel_rpi请确保你有足够的磁盘空间:树莓派的 Git 内核源码树大约需要 1.7 GB,而工具链只占 40 MB 左右。你至少需要 3 到 4 GB 的工作空间。如果将镜像构建为 Deb 包,还需要至少 1 GB 的磁盘空间(最好预留 7 到 8 GB 的空间)。
-
下载树莓派的内核源码树(我们从树莓派的 GitHub 仓库克隆它):
cd ${RPI_STG}/kernel_rpi git clone --depth=1 --branch=rpi-6.1.y \ https://github.com/raspberrypi/linux.git内核源码树将被克隆到名为
linux/的目录下(即${RPI_STG}/kernel_rpi/linux)。这需要一些时间。注意上面命令的几个点:- 我们选择的树莓派内核分支不是最新的(撰写时最新的是 rpi-6.8.y 系列),而是 6.1 内核;这是可以接受的(6.1.y 是 LTS 内核,且与我们的 x86 内核一致!)。
- 我们传递了
--depth参数并设置为 1,以减少下载和解压负担。
-
树莓派内核源码树已安装。我们简单验证一下:
cd ${RPI_STG}/kernel_rpi/linux head -n5 Makefile # SPDX-License-Identifier: GPL-2.0 VERSION = 6 PATCHLEVEL = 1 SUBLEVEL = 34 EXTRAVERSION =我们得到的是 6.1.34 树莓派内核端口。(我们在 x86_64 上使用的内核版本是 6.1.25;这个轻微的差异是可以接受的。同时,到你操作时,发布版本(y=34)可能会改变,这也没问题。)
第二步 —— 安装 x86_64 到 AArch64 的交叉工具链
现在是时候在宿主系统上安装合适的交叉工具链了(回想一下,它是我们的 x86 Ubuntu 虚拟机),用于执行实际构建。可用的工具链有很多种。这里,我将采用最简单且最佳的方式获取和安装合适的工具链。(顺便说一下,书的第一版介绍了另一种更复杂的方法,通过树莓派的 "tools" GitHub 仓库;但现在已经不推荐使用,所以我们不再深入探讨。)
现代发行版通常提供现成的交叉构建(或交叉编译)包!要查看它们的子集(只显示用于各种架构的 GCC 编译器包):
sudo apt install gcc-<TAB><TAB>
这将显示 398 个可能的选项。你还需要 binutils 包,因此我们同时安装相关工具:
sudo apt install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu
交叉工具通常安装在 /usr/bin/ 下,因此它们已经在你的 PATH 中;你现在可以直接使用它们。比如,检查 AArch64 GCC 编译器的位置和版本信息,如下所示:
图 3.8 显示了工具链中的工具!注意它们都安装在 /usr/bin/ 下,更重要的是,它们都以 aarch64-linux-gnu- 为前缀。这被称为交叉编译器或工具链前缀,通常被放置在名为 CROSS_COMPILE 的环境变量中(稍后我们将详细讨论这一点)。
交叉编译器前缀有什么意义吗?是的,这遵循了一个命名约定,其形式如下:
MACHINE-VENDOR-OS-
或
<CPU>-<MANUFACTURER>[-<KERNEL>]-<OS>-
第一种形式称为“目标三元组”,这也是我们的交叉工具链前缀(aarch64-linux-gnu-)所使用的形式(因为前缀中有三个组成部分,而不是四个)。在这里,MACHINE 是 aarch64(当然,这意味着目标 CPU 是 64 位 ARM),VENDOR 被简单地设置为 linux,而 OS 字段设置为 gnu(尽管不完美,你可以将 VENDOR 字段视为空,而将 OS 字段视为 linux-gnu)。
此外,请记住,此工具链适用于为 AArch64(ARM 64 位)架构构建内核,而不是用于 32 位。如果你打算为 32 位系统构建内核,则需要安装 x86_64-to-AArch32 工具链,命令为:
sudo apt install gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf
工具链包名称中的 hf 后缀代表“硬浮点”(hard float),因为树莓派的目标处理器使用硬浮点。
如前所述,还可以使用其他工具链。例如,在 ARM 开发者网站上提供了多种适用于 GNU/Linux、Windows 和 macOS 主机的 ARM 开发工具链,链接为:ARM-GNU 工具链下载。
第三步 —— 配置并构建树莓派 AArch64 内核
现在我们来配置树莓派 4 的内核。在开始之前,务必牢记以下几点:
- ARCH 环境变量:必须将
ARCH设置为将要为其交叉编译软件的 CPU 架构(即编译后的代码将在该 CPU 上运行)。ARCH的具体值是内核源码树中arch/目录下的目录名。例如,ARM-32 应设置为arm,AArch64 应设置为arm64,PowerPC 应设置为powerpc,OpenRISC 应设置为openrisc。 - CROSS_COMPILE 环境变量:必须将
CROSS_COMPILE设置为交叉编译器(工具链)的前缀。通常,它是工具链中每个工具前面的几个通用字母。在我们的例子中,所有工具链工具(如 C 编译器gcc、链接器、C++、objdump等)都以aarch64-linux-gnu-开头(见图 3.8),因此我们将CROSS_COMPILE设置为该前缀。注意,Makefile 总是以${CROSS_COMPILE}<工具名>的形式调用工具,因此会调用正确的工具链可执行文件。这意味着工具链目录应在PATH变量中。
配置树莓派内核的步骤
-
进入树莓派内核目录并设置默认内核配置:
cd ${RPI_STG}/kernel_rpi/linux make mrproper KERNEL=kernel8 make ARCH=arm64 bcm2711_defconfig上述命令的结果是生成了默认的内核配置文件
.config。关于bcm2711_defconfig配置目标的解释:这在第 2 章《从源码构建 6.x Linux 内核——第 1 部分》中的“典型嵌入式 Linux 系统的内核配置”部分中有所提及。我们必须确保使用适当的、针对特定板卡的内核配置文件作为起点。bcm2711_defconfig是树莓派 3、3+、4、400、Zero 2 W 以及树莓派计算模块 3、3+ 和 4 的 Broadcom SoC 的正确内核配置文件,它会生成默认的 64 位构建配置。(重要提示:如果你为其他类型的树莓派设备构建内核,请参阅树莓派文档。)
KERNEL=kernel8表示该处理器是基于 ARMv8 的 64 位处理器。 -
如果需要进一步定制板卡的内核配置,可以使用以下命令:
make ARCH=arm64 menuconfig如果不需要定制,可以跳过这一步,继续进行后续操作。记住,如果要微调内核配置,首先通过第 1 步正确生成配置,然后再执行这一步。
-
构建(交叉编译)内核、内核模块和设备树二进制文件(DTBs):
make -j8 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- all根据你的构建主机调整
-jn的值。all目标会构建哪些内容呢?答案是:它会构建内核顶层 Makefile 中带有*前缀的所有目标。可以通过以下命令查看:$ make ARCH=arm64 help | grep "^*" * vmlinux - Build the bare kernel * modules - Build all modules * dtbs - Build device tree blobs for enabled boards * Image.gz - Compressed kernel image (arch/arm64/boot/Image.gz) -
构建完成后,生成的许多文件中,有以下与我们相关的文件:
$ ls -lh vmlinux System.map arch/arm64/boot/Image* arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dtb -rw-rw-r-- 1 c2kp c2kp 54K Jun 21 13:02 arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dtb -rw-rw-r-- 1 c2kp c2kp 22M Jun 21 13:24 arch/arm64/boot/Image -rw-rw-r-- 1 c2kp c2kp 7.9M Jun 21 13:24 arch/arm64/boot/Image.gz -rw-rw-r-- 1 c2kp c2kp 3.6M Jun 21 13:24 System.map -rwxrwxr-x 1 c2kp c2kp 237M Jun 21 13:24 vmlinux文件
Image是Image.gz(压缩内核镜像)的未压缩版本,后者是可用于启动的内核镜像文件。vmlinux是实际的未压缩内核镜像,保留它以用于内核调试!另外,文件arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dtb是该目标平台的设备树二进制文件(DTB)。我们可以使用file命令进一步验证vmlinux镜像:$ file ./vmlinux ./vmlinux: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), statically linked, BuildID[sha1]=03b7<...>, with debug_info, not stripped
在这里,我们展示了如何为与构建主机不同的架构(即交叉编译)配置并构建 Linux 内核。关于如何将内核镜像、模块和 DTBs(包括覆盖)放置在 microSD 卡上的细节,这里不再深入讨论。你可以参考树莓派内核构建的完整文档。
到此为止,我们已经完成了树莓派内核交叉编译的实验。我们将在本章结尾分享一些其他有用的小提示。
内核构建的一些实用提示
我们用一些提示来结束本章关于从源码构建 Linux 内核的讨论。原因很简单:事情并不总是能如愿顺利进行。以下各小节包含了你需要注意的一些重要提示。
内核与根文件系统的解耦
对于新手来说,经常会感到困惑的一点是:一旦我们配置、构建并从新的 Linux 内核启动后,根文件系统和任何已挂载的文件系统仍然与原来的(发行版或自定义)系统完全相同。唯一改变的是内核(以及 initramfs 镜像)。
这是完全有意为之的,因为 Unix 系统中内核与(实际的)根文件系统之间的耦合非常松散。根文件系统包含了所有的应用程序、系统工具和库,因此我们可以为同一个基础系统提供多个内核(也许适用于不同的产品版本)。
最低版本要求
要成功构建内核,必须确保构建系统中工具链和其他工具的版本满足最低要求。内核文档中明确列出了这些最低版本要求,详见:Minimal requirements to compile the kernel。
例如,截至本文撰写时,gcc 的推荐最低版本为 5.1,Clang(一个强大的 Linux 编译器)的最低版本为 11.0.0,make 的最低版本为 3.82(还有很多其他要求,建议仔细查看)。
为其他站点构建内核
在本书的内核构建演练中,我们在某个系统上构建了 Linux 内核(这里是 x86_64 虚拟机),并在同一系统上启动了新构建的内核。如果你需要为其他站点或客户设备构建内核,虽然可以手动在远程系统上放置相关文件,但更简单且正确的方式是将内核及相关文件(initrd 镜像、内核模块集、内核头文件等)打包为常见格式的包(如 Debian 的 deb、Red Hat 的 rpm 等)。
在内核的顶层 Makefile 中有多个打包目标。你可以通过以下命令查看相关选项(只显示相关目标):
$ make ARCH=arm64 help
Kernel packaging:
rpm-pkg - Build both source and binary RPM kernel packages
binrpm-pkg - Build only the binary kernel RPM package
deb-pkg - Build both source and binary deb kernel packages
bindeb-pkg - Build only the binary kernel deb package
snap-pkg - Build only the binary kernel snap package
dir-pkg - Build the kernel as a plain directory structure
tar-pkg - Build the kernel as an uncompressed tarball
targz-pkg - Build the kernel as a gzip compressed tarball
例如,使用前面介绍的配置(为 AArch64 树莓派 4 目标构建),我们可以通过以下命令将内核及相关文件打包为 Debian 包:
$ make -j8 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- bindeb-pkg
构建过程中,会创建一个名为 debian 的文件夹,里面包含了打包所需的所有文件。实际生成的包文件会存储在内核源码目录的上一级目录中。以下是上面命令运行后生成的 deb 包文件:
dpkg-deb: building package 'linux-headers-6.1.34-v8+' in '../linux-headers-6.1.34-v8+_6.1.34-v8+-2_arm64.deb'.
dpkg-deb: building package 'linux-libc-dev' in '../linux-libc-dev_6.1.34-v8+-2_arm64.deb'.
dpkg-deb: building package 'linux-image-6.1.34-v8+' in '../linux-image-6.1.34-v8+_6.1.34-v8+-2_arm64.deb'.
dpkg-deb: building package 'linux-image-6.1.34-v8+-dbg' in '../linux-image-6.1.34-v8+-dbg_6.1.34-v8+-2_arm64.deb'.
这确实非常方便!现在,你可以通过简单的 sudo dpkg -i <包名> 命令在任何其他匹配的系统(在 CPU 和 Linux 版本方面相同)上安装这些包,从而安装新内核及其头文件和相关文件。
观察内核构建过程
如果你希望在内核构建过程中看到详细信息(例如执行的命令和脚本、GCC 编译器标志等),可以给 make 命令传递 V=1 的详细选项开关。以下是在为树莓派 4 构建内核时打开详细模式的部分输出(为了便于阅读,我添加了换行符和续行字符):
$ make -j8 V=1 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- all
[ … ]
aarch64-linux-gnu-gcc -Wp,-MMD,drivers/net/ethernet/realtek/.r8169_main.o.d
-nostdinc \
-I./arch/arm64/include -I./arch/arm64/include/generated -I./include -I./arch/arm64/include/uapi -I./arch/arm64/include/generated/uapi -I./include/uapi
-I./include/generated/uapi -include ./include/linux/compiler-version.h -include ./include/linux/kconfig.h -include ./include/linux/compiler_types.h \
-D__KERNEL__ -mlittle-endian -DCC_USING_PATCHABLE_FUNCTION_ENTRY
-DKASAN_SHADOW_SCALE_SHIFT= -fmacro-prefix-map=./= \
-Wall -Wundef -Werror=strict-prototypes -Wno-trigraphs -fno-strict-aliasing
-fno-common -fshort-wchar -fno-PIE -Werror=implicit-function-declaration
-Werror=implicit-int -Werror=return-type -Wno-format-security \
[ … ]
这种详细信息可以帮助调试构建失败的情况,或者学习所使用的编译器选项!(此时 GCC 的手册页会派上用场!)
此外,你还可以使用 tee 命令将输出同时显示在终端窗口并重定向到文件,例如,保存到 out.txt 文件,同时在屏幕上看到输出:
make -j8 V=1 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- all 2>&1 | tee out.txt
另外,使用 tee -a <文件名> 可以确保将输出追加到指定文件,而不是覆盖它。
内核构建的快捷 Shell 语法
假设已经安装了所需的包并完成了内核配置步骤(我们以 x86 为例),可以使用以下示例中的快捷 Shell 语法用于非交互式构建脚本:
time make -j8 [ARCH=<...> CROSS_COMPILE=<...>] all && \
sudo make modules_install && \
sudo make install
其中,&& 和 || 是 Shell(通常是 Bash)的条件执行语法:
cmd1 && cmd2意味着:只有当cmd1成功时才执行cmd2。cmd1 || cmd2意味着:只有当cmd1失败时才执行cmd2。
此外,你可以查看书中提供的用于 x86_64 内核构建的简化 Bash 脚本:kbuild.sh。
处理缺少的 OpenSSL 开发头文件
在一次 x86_64 Ubuntu 系统的内核构建中,出现了以下错误:
[...] fatal error: openssl/opensslv.h: No such file or directory
这只是由于缺少 OpenSSL 开发头文件。内核文档中提到了这一点,从 4.3 版(如果启用了模块签名,则为 3.7 版)开始,内核构建需要安装 OpenSSL 开发包。因此,ch1/pkg_install4ubuntu_lkp.sh 安装了 libssl-dev 包;对于 Fedora 系列发行版,等效包为 openssl-devel。
此外,此 Q&A 也展示了如何通过安装 libssl-dev 包(或其等效包)解决问题。
如何检查哪些发行版内核已安装?
有时,了解系统上安装了哪些内核包(通常是发行版提供的包)非常重要。可能是因为你想卸载旧内核以回收磁盘空间(例如 /boot 分区空间不足)。在 Debian/Ubuntu 系统上,可以使用以下命令查看:
dpkg --list | grep linux-image
在 Red Hat/Fedora/CentOS 系统上,可以使用:
dnf list installed kernel
这只会显示基于包管理器安装的内核,不包括手动构建的内核!
处理内核启动失败
如果内核构建和 initramfs 生成等步骤一切顺利,但内核启动失败,显示类似于以下的内核恐慌信息:[Kernel panic - not syncing: Attempted to kill init! ...] 或 [Kernel panic - not syncing: No working init found. ...],问题通常是内核配置错误、内核命令行参数问题(特别是根设备参数),或者由于某种原因无法挂载实际的根文件系统。官方内核文档提供了一些解决提示:init 文档。
最后提示
如果遇到构建或启动错误,你可以复制错误消息,搜索相关解决方案。你可能会惊讶于经常能找到有效的帮助。至此,我们完成了内核构建章节的讲解!
总结
本章以及前一章详细介绍了配置和从源码构建 Linux 内核所需的必要准备工作。
在本章中,我们从实际的内核(和内核模块)的构建过程开始。构建完成后,我们展示了如何将内核模块安装到系统上。接下来,我们讨论了生成 initramfs(或 initrd)镜像的实际操作,并解释了使用它的动机。
内核构建的最后一步是对引导加载程序的简单定制(这里我们只关注了 x86 的 GRUB)。随后,我们展示了如何通过新构建的内核引导系统,并验证其配置是否如预期所示。作为实用补充,我们还展示了如何为其他处理器(在本例中为 AArch64)交叉编译 Linux 内核。最后,我们分享了一些额外的提示,以帮助你在内核构建过程中取得成功。
如果你还没有尝试,我们建议你仔细回顾并尝试本文提到的步骤,构建属于你自己的定制 Linux 内核。
恭喜你完成了从头开始的 Linux 内核构建!对于实际项目(或产品),你可能不会执行每个内核构建步骤,因为通常会有专门的硬件平台/支持包(BSP)团队负责这个部分;另一个原因——尤其是在嵌入式 Linux 项目中——是可能会使用类似 Yocto(或 Buildroot)这样的 Linux 构建框架(或者使用 SoC 供应商提供的定制内核)。这些工具通常会处理构建的机械部分(尽管 Yocto 的学习曲线并不平缓)。不过,能够根据项目需求配置内核的能力依然需要你掌握的知识和理解。
接下来的两章将带你进入 Linux 内核开发的世界,展示如何编写你的第一个内核模块。