5. Linux内核如何启动
你现在已经了解了Linux系统的物理和逻辑结构,知道了什么是内核,以及如何处理进程。本章将教你内核如何启动(或引导)。换句话说,你将学习内核如何进入内存以及它直到第一个用户进程启动之前都做了些什么。
引导过程的一个简化视图如下:
- 机器的BIOS或引导固件加载并运行一个引导加载程序。
- 引导加载程序在磁盘上找到内核映像,将其加载到内存中,并启动它。
- 内核初始化设备及其驱动程序。
- 内核挂载根文件系统。
- 内核启动一个名为init的程序,其进程ID为1。这一点是用户空间启动的起点。
- init启动系统其余部分的各种进程。
- 在某个时刻,init启动一个允许你登录的进程,通常在引导序列的末尾或接近末尾。
本章涵盖前几个阶段,重点关注引导加载程序和内核。第六章通过详细介绍systemd(Linux系统上最广泛使用的init版本)来继续用户空间启动的内容。
能够识别引导过程的每个阶段对于修复引导问题以及从整体上理解系统来说将会非常有价值。然而,许多Linux发行版的默认行为常常使得识别前几个引导阶段变得困难甚至不可能,因为它们进行得很快;你通常只能在它们完成并登录之后才能好好观察一番。
5.1 启动消息
传统的Unix系统在引导时会产生许多诊断消息,告诉你引导过程的进展。这些消息首先来自内核,然后来自init启动的进程和初始化过程。然而,这些消息并不美观也不一致,在某些情况下甚至信息量不大。此外,硬件改进使得内核启动比以前快得多;消息一闪而过,很难看清发生了什么。因此,大多数当前的Linux发行版都尽量用启动画面和其他形式的填充内容来隐藏引导诊断信息,以便在你等待系统启动时分散你的注意力。
查看内核引导和运行时诊断消息的最佳方式是使用journalctl命令检索内核的日志。运行journalctl -k会显示当前启动的消息,但你可以使用-b选项查看之前的启动。我们将在第七章中更详细地介绍日志。
如果你没有使用systemd,可以检查类似/var/log/kern.log的日志文件,或者运行dmesg命令来查看内核环形缓冲区中的消息。
以下是你从journalctl -k命令中可能看到的一个示例:
microcode: microcode updated early to revision 0xd6, date = 2019-10-03
Linux version 4.15.0-112-generic (buildd@lcy01-amd64-027) (gcc version 7.5.0
(Ubuntu 7.5.0-3ubuntu1~18.04)) #113-Ubuntu SMP Thu Jul 9 23:41:39 UTC 2020 (Ubuntu
4.15.0-112.113-generic 4.15.18)
Command line: BOOT_IMAGE=/boot/vmlinuz-4.15.0-112-generic root=UUID=17f12d53-c3d7-4ab3-943e-
a0a72366c9fa ro quiet splash vt.handoff=1
KERNEL supported cpus:
--snip--
scsi 2:0:0:0: Direct-Access ATA KINGSTON SM2280S 01.R PQ: 0 ANSI: 5
sd 2:0:0:0: Attached scsi generic sg0 type 0
sd 2:0:0:0: [sda] 468862128 512-byte logical blocks: (240 GB/224 GiB)
sd 2:0:0:0: [sda] Write Protect is off
sd 2:0:0:0: [sda] Mode Sense: 00 3a 00 00
sd 2:0:0:0: [sda] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA
sda: sda1 sda2 < sda5 >
sd 2:0:0:0: [sda] Attached SCSI disk
--snip--
内核启动后,用户空间的启动过程通常会产生一些消息。这些消息可能更难查看和回顾,因为在大多数系统上,你不会在单个日志文件中找到它们。启动脚本被设计为将消息发送到控制台,但这些消息在引导过程完成后就会被清除。然而,在Linux系统上这不是问题,因为systemd会捕获本应发送到控制台的启动和运行时的诊断消息。
5.2 内核初始化与启动选项
启动时,Linux内核按以下一般顺序进行初始化:
- CPU检查
- 内存检查
- 设备总线发现
- 设备发现
- 辅助内核子系统设置(网络等)
- 根文件系统挂载
- 用户空间启动
前两步并不引人注目,但当内核处理设备时,就出现了依赖关系的问题。例如,磁盘设备驱动程序可能依赖于总线支持和SCSI子系统支持,正如你在第三章中所见。然后,在初始化过程的后期,内核必须在启动init之前挂载一个根文件系统。
一般来说,你不必担心这些依赖关系,除非某些必要的组件是可加载内核模块而非主内核的一部分。某些机器可能需要在真正的根文件系统挂载之前加载这些内核模块。我们将在6.7节中讨论这个问题及其初始RAM文件系统(initrd)的解决方案。
内核会发出某些种类的消息,表明它正准备启动其第一个用户进程:
Freeing unused kernel memory: 2408K
Write protecting the kernel read-only data: 20480k
Freeing unused kernel memory: 2008K
Freeing unused kernel memory: 1892K
在这里,内核不仅在清理一些未使用的内存,还在保护其自身的数据。然后,如果你运行的内核足够新,你会看到内核启动第一个用户空间进程为init:
Run /init as init process
with arguments:
--snip--
稍后,你应该能够看到根文件系统被挂载,并且systemd启动,它向内核日志发送一些自己的消息:
EXT4-fs (sda1): mounted filesystem with ordered data mode. Opts: (null)
systemd[1]: systemd 237 running in system mode. (+PAM +AUDIT +SELINUX +IMA
+APPARMOR +SMACK +SYSVINIT +UTMP +LIBCRYPTSETUP +GCRYPT +GNUTLS +ACL +XZ +LZ4
+SECCOMP +BLKID +ELFUTILS -IDN2 +IDN -PCRE2 default-hierarchy=hybrid)
systemd[1]: Detected architecture x86-64.
systemd[1]: Set hostname to <duplex>.
此时,你可以肯定用户空间已经启动。
5.3 内核参数
当Linux内核启动时,它会收到一组基于文本的内核参数,其中包含一些额外的系统细节。这些参数指定了许多不同类型的行为,例如内核应产生多少诊断输出以及设备驱动程序特定的选项。
你可以通过查看/proc/cmdline文件来查看传递给系统当前运行内核的参数:
$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-4.15.0-43-generic root=UUID=17f12d53-c3d7-4ab3-943e
-a0a72366c9fa ro quiet splash vt.handoff=1
这些参数要么是简单的单字标志,例如ro和quiet,要么是key=value对,例如vt.handoff=1。许多参数并不重要,例如用于显示启动画面的splash标志,但其中一个关键参数是root参数。这是根文件系统的位置;没有它,内核无法正常执行用户空间启动。
根文件系统可以指定为一个设备文件,如下例所示:
root=/dev/sda1
在大多数当代系统上,有两种更常见的替代方式。首先,你可能会看到一个逻辑卷,例如:
root=/dev/mapper/my-system-root
你也可能会看到一个UUID(参见4.2.4节):
root=UUID=17f12d53-c3d7-4ab3-943e-a0a72366c9fa
这两种方式都是更可取的,因为它们不依赖于特定的内核设备映射。
ro参数指示内核在用户空间启动时以只读模式挂载根文件系统。这是正常的;只读模式确保fsck可以在尝试进行任何严肃操作之前安全地检查根文件系统。检查完成后,引导过程会以读-写模式重新挂载根文件系统。
当遇到它不理解的参数时,Linux内核会保存该参数。内核稍后在执行用户空间启动时将该参数传递给init。例如,如果你在內核参数中添加-s,内核会将-s传递给init程序,以指示它应以单用户模式启动。
如果你对基本引导参数感兴趣,bootparam(7)手册页提供了概述。如果你在寻找非常具体的内容,可以查看kernel-params.txt,这是Linux内核附带的一个参考文件。
了解了这些基础知识后,你可以自由地跳到第六章,了解用户空间启动、初始RAM磁盘以及内核作为其第一个进程运行的init程序的更多细节。本章的其余部分详细介绍了内核如何加载到内存并启动,包括它如何获取参数。
5.4 引导加载程序
在引导过程开始时,在内核和init启动之前,有一个引导加载程序负责启动内核。引导加载程序的工作听起来很简单:它从磁盘上的某个位置将内核加载到内存中,然后用一组内核参数启动内核。然而,这项工作比看起来要复杂得多。为了理解原因,请考虑引导加载程序必须回答的问题:
- 内核在哪里?
- 内核启动时应向它传递哪些内核参数?
答案(通常是)内核及其参数通常位于根文件系统上的某个位置。听起来内核参数应该很容易找到,但请记住,内核本身尚未运行,而通常是由内核来遍历文件系统以找到必要的文件。更糟糕的是,通常用于访问磁盘的内核设备驱动程序也不可用。把这看作是一种“先有鸡还是先有蛋”的问题。情况甚至可能比这更复杂,但现在,让我们看看引导加载程序如何克服驱动程序和文件系统的障碍。
引导加载程序确实需要一个驱动程序来访问磁盘,但它不是内核使用的那个。在PC上,引导加载程序使用传统的基本输入输出系统 (BIOS)或更新的统一可扩展固件接口 (UEFI)来访问磁盘。(可扩展固件接口,即EFI,以及UEFI将在5.8.2节中更详细地讨论。)当代磁盘硬件包含固件,允许BIOS或UEFI通过逻辑块寻址 (LBA)访问连接的存储硬件。LBA是一种通用的、简单的方法,可以从任何磁盘访问数据,但其性能很差。不过这不是问题,因为引导加载程序通常是唯一必须使用这种模式进行磁盘访问的程序;启动后,内核可以访问自己的高性能驱动程序。
NOTE
要确定你的系统使用 BIOS 还是 UEFI,运行
efibootmgr。如果你看到一个启动目标列表,则系统使用 UEFI。如果你被告知不支持 EFI 变量,则系统使用 BIOS。或者,你可以检查/sys/firmware/efi是否存在;如果存在,则系统使用 UEFI。
在解决对磁盘原始数据的访问之后,引导加载程序必须完成在文件系统上定位所需数据的工作。大多数常见的引导加载程序可以读取分区表,并内置了对文件系统的只读访问支持。因此,它们可以找到并读取将内核加载到内存所需的文件。这种能力使得动态配置和增强引导加载程序变得容易得多。Linux 引导加载程序并非一直具备这种能力;没有它,配置引导加载程序会困难得多。
总的来说,有一种模式:内核添加新功能(尤其是在存储技术方面),然后引导加载程序添加这些功能的独立简化版本作为补偿。
5.4.1 引导加载程序的任务
Linux 引导加载程序的核心功能包括能够:
- 从多个内核中进行选择。
- 在内核参数集之间切换。
- 允许用户手动覆盖和编辑内核映像名称和参数(例如,进入单用户模式)。
- 提供对其他操作系统启动的支持。
自从 Linux 内核诞生以来,引导加载程序已经变得相当先进,具有命令行历史和菜单系统等功能,但内核映像和参数选择的灵活性一直是基本需求。(一个令人惊讶的现象是,某些需求实际上已经减少了。例如,因为你可以从 USB 存储设备执行紧急或恢复启动,所以很少需要手动输入内核参数或进入单用户模式。)当前的引导加载程序提供了前所未有的强大功能,这在构建自定义内核或只想调整参数时尤其有用。
Linux 内核如何启动 ··· 123
5.4.2 引导加载程序概述
以下是你可能遇到的主要引导加载程序:
- GRUB:Linux 系统上近乎通用的标准,有 BIOS/MBR 和 UEFI 版本。
- LILO:最早的 Linux 引导加载程序之一。ELILO 是其 UEFI 版本。
- SYSLINUX:可以配置为从多种不同类型的文件系统运行。
- LOADLIN:从 MS-DOS 启动内核。
- systemd-boot:一个简单的 UEFI 启动管理器。
- coreboot(原名 LinuxBIOS):一个高性能的 PC BIOS 替代品,可以包含一个内核。
- Linux Kernel EFISTUB:一个内核插件,用于直接从 EFI/UEFI 系统分区(ESP)加载内核。
- efilinux:一个 UEFI 引导加载程序,旨在作为其他 UEFI 引导加载程序的模型和参考。
本书几乎只涉及 GRUB。使用其他引导加载程序的原因是它们比 GRUB 配置更简单、速度更快,或者提供某些其他特殊用途的功能。
你可以通过进入一个引导提示符(可以输入内核名称和参数)来了解引导加载程序的很多信息。为此,你需要知道如何进入引导提示符或菜单。不幸的是,有时这很难弄清楚,因为 Linux 发行版大量定制了引导加载程序的行为和外观。通常仅通过观察启动过程无法判断发行版使用哪个引导加载程序。接下来的章节将告诉如何进入引导提示符以输入内核名称和参数。当你对此熟悉后,你将看到如何配置和安装引导加载程序。
5.5 GRUB 简介
GRUB 代表 Grand Unified Boot Loader。我们将介绍 GRUB 2,但也有一个称为 GRUB Legacy 的旧版本,现已不再活跃使用。GRUB 最重要的能力之一是文件系统导航,这允许轻松选择内核映像和配置。查看其菜单是观察这一能力以及了解 GRUB 整体情况的最佳方式之一。界面易于导航,但你很可能从未见过它。
要访问 GRUB 菜单,请在 BIOS 启动屏幕首次出现时按住 SHIFT,如果系统使用 UEFI 则按 ESC。否则,引导加载程序配置可能在加载内核前不暂停。图 5-1 显示了 GRUB 菜单。
124 第 5 章
图 5-1:GRUB 菜单
尝试以下操作来探索引导加载程序:
-
重新启动或打开 Linux 系统。
-
在 BIOS 自检期间按住 SHIFT,或在固件启动画面时按 ESC 以获取 GRUB 菜单。(有时这些屏幕不可见,所以你必须猜测何时按键。)
-
按 e 查看默认启动选项的引导加载程序配置命令。你应该会看到类似图 5-2 的内容(你可能需要向下滚动才能看到所有细节)。
图 5-2:GRUB 配置编辑器
这个屏幕告诉我们,对于此配置,根目录设置了 UUID,内核映像是 /boot/vmlinuz-4.15.0-45-generic,内核参数包括 ro、quiet 和 splash。初始 RAM 文件系统是 /boot/initrd.img-4.15.0-45-generic。但如果你以前从未见过这种配置,可能会觉得有些困惑。为什么有多个对 root 的引用,而且它们为什么不同?为什么这里会有 insmod?如果你以前见过,你可能记得它是 Linux 内核的一个功能,通常由 udevd 运行。
这种疑惑是合理的,因为 GRUB 并不使用 Linux 内核(记住,它的工作是启动内核)。你看到的配置完全由 GRUB 内部的命令和功能组成,它存在于自己的独立世界中。
这种混淆部分源于 GRUB 从许多来源借用了术语。GRUB 有自己的“内核”和它自己的 insmod 命令来动态加载 GRUB 模块,完全独立于 Linux 内核。许多 GRUB 命令类似于 Unix shell 命令;甚至有一个 ls 命令来列出文件。
NOTE
有一个用于 LVM 的 GRUB 模块,在内核驻留在逻辑卷上的系统中需要它。你可能会在你的系统上看到这个。
到目前为止,最令人困惑的是 GRUB 对单词 root 的使用。通常,你认为根目录是系统根文件系统。在 GRUB 配置中,这是一个内核参数,位于 linux 命令的映像名称之后的某个位置。配置中对 root 的其他所有引用都是指 GRUB 根目录,它仅存在于 GRUB 内部。GRUB“根目录”是 GRUB 搜索内核和 RAM 文件系统映像文件的文件系统。
在图 5-2 中,GRUB 根目录首先设置为一个 GRUB 特定设备 (hd0,msdos1),这是此配置的默认值 1。在下一个命令中,GRUB 然后在一个分区上搜索特定的 UUID 2。如果找到该 UUID,它将 GRUB 根目录设置为该分区。
总结一下,linux 命令的第一个参数(/boot/vmlinuz-...)是 Linux 内核映像文件的位置 3。GRUB 从 GRUB 根目录加载此文件。initrd 命令类似,指定了第 6 章将介绍的初始 RAM 文件系统的文件 4。
你可以在 GRUB 内部编辑此配置;这样做通常是临时修复错误启动的最简单方法。要永久修复启动问题,你需要更改配置(参见第 5.5.2 节),但现在,让我们更进一步,通过命令行界面检查一些 GRUB 内部机制。
5.5.1 使用 GRUB 命令行探索设备和分区
如图 5-2 所示,GRUB 有自己的设备寻址方案。例如,找到的第一个硬盘命名为 hd0,然后是 hd1,依此类推。设备名称分配可能会改变,但幸运的是,GRUB 可以搜索所有分区中的 UUID 以找到内核所在的分区,正如你在图 5-2 中看到的 search 命令。
列出设备
要了解 GRUB 如何引用你系统上的设备,请在引导菜单或配置编辑器中按 c 访问 GRUB 命令行。你应该会看到 GRUB 提示符:
grub>
你可以在此处输入在配置中看到的任何命令,但作为开始,请尝试一个诊断命令:ls。无参数时,输出是 GRUB 已知的设备列表:
grub> ls
(hd0) (hd0,msdos1)
在这种情况下,有一个主磁盘设备表示为 (hd0) 和一个分区 (hd0,msdos1)。如果磁盘上有交换分区,它也会显示出来,例如 (hd0,msdos5)。分区上的 msdos 前缀告诉你磁盘包含 MBR 分区表;对于 UEFI 系统上的 GPT,它将以 gpt 开头。(还有更深的组合,带有第三个标识符,其中 BSD 磁盘标签映射位于分区内部,但除非你在单台机器上运行多个操作系统,否则通常不必担心这个。)
要获取更详细的信息,请使用 ls -l。此命令特别有用,因为它会显示分区文件系统的任何 UUID。例如:
grub> ls -l
Device hd0: No known filesystem detected – Sector size 512B - Total size
32009856KiB
Partition hd0,msdos1: Filesystem type ext* – Last modification time
2019-02-14 19:11:28 Thursday, UUID 8b92610e-1db7-4ba3-ac2f-
30ee24b39ed0 - Partition start at 1024Kib - Total size 32008192KiB
此特定磁盘在第一个 MBR 分区上有一个 Linux ext2/3/4 文件系统。使用交换分区的系统会显示另一个分区,但你无法从输出中判断其类型。
文件导航
现在让我们看看 GRUB 的文件系统导航能力。使用 echo 命令确定 GRUB 根目录(回想一下,这是 GRUB 期望找到内核的地方):
grub> echo $root
hd0,msdos1
要使用 GRUB 的 ls 命令列出该根目录中的文件和目录,可以在分区末尾添加一个正斜杠:
grub> ls (hd0,msdos1)/
由于输入实际根分区很不方便,你可以用 $root 变量代替以节省时间:
grub> ls ($root)/
输出是该分区文件系统上的文件目录名称的简短列表,例如 etc/、bin/ 和 dev/。现在这是 GRUB ls 命令的完全不同的功能。之前,你在列出设备、分区表以及可能的一些文件系统头信息。现在你实际上是在查看文件系统的内容。
你可以以类似的方式深入查看分区上的文件和目录。例如,要检查 /boot 目录,请从以下命令开始:
grub> ls ($root)/boot
NOTE
使用上下箭头键翻阅 GRUB 命令历史,使用左右箭头编辑当前命令行。标准 readline 键(CTRL-N、CTRL-P 等)也有效。
你还可以使用 set 命令查看所有当前设置的 GRUB 变量:
grub> set
?=0
color_highlight=black/white
color_normal=white/black
--snip--
prefix=(hd0,msdos1)/boot/grub
root=hd0,msdos1
其中最重要的变量之一是 $prefix,即 GRUB 期望找到其配置文件和辅助支持的文件系统和目录。我们接下来将讨论 GRUB 配置。
完成 GRUB 命令行界面后,可以按 ESC 返回 GRUB 菜单。或者,如果你已设置了所有必要的启动配置(包括 linux 和可能的 initrd 变量),可以输入 boot 命令来启动该配置。无论如何,启动你的系统。我们将探索 GRUB 配置,最好在完整系统可用时进行。
5.5.2 GRUB 配置
GRUB 配置目录通常位于 /boot/grub 或 /boot/grub2。其中包含主配置文件 grub.cfg、一个特定于架构的目录(如 i386-pc,内含后缀为 .mod 的可加载模块),以及一些其他项目(如字体和本地化信息)。我们不会直接修改 grub.cfg;而是使用 grub-mkconfig 命令(在 Fedora 上为 grub2-mkconfig)。
查看 grub.cfg
首先,快速查看 grub.cfg,了解 GRUB 如何初始化其菜单和内核选项。你会看到该文件由 GRUB 命令组成,通常以若干初始化步骤开头,随后是一系列针对不同内核和启动配置的菜单条目。初始化过程并不复杂,但开头有许多条件语句,可能会让你误以为很复杂。第一部分只是一堆函数定义、默认值和视频设置命令,例如:
if loadfont $font ; then
set gfxmode=auto
load_video
insmod gfxterm
--snip--注意
许多变量(如
$font)源自grub.cfg开头附近的load_env调用。
在配置文件的后续部分,你会找到可用的启动配置,每个都以 menuentry 命令开头。根据你在前一节学到的知识,你应该能够阅读并理解以下示例:
menuentry 'Ubuntu' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-8b92610e-1db7-4ba3-ac2f-30ee24b39ed0' {
recordfail
load_video
gfxmode $linux_gfx_mode
insmod gzio
if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi
insmod part_msdos
insmod ext2
set root='hd0,msdos1'
search --no-floppy --fs-uuid --set=root 8b92610e-1db7-4ba3-ac2f-30ee24b39ed0
linux /boot/vmlinuz-4.15.0-45-generic root=UUID=8b92610e-1db7-4ba3-ac2f-30ee24b39ed0 ro quiet splash $vt_handoff
initrd /boot/initrd.img-4.15.0-45-generic
}检查你的 grub.cfg 文件中是否包含包含多个 menuentry 命令的 submenu 命令。许多发行版对旧版本的内核使用 submenu 命令,以避免 GRUB 菜单过于拥挤。
生成新配置文件
如果你想更改 GRUB 配置,不要直接编辑 grub.cfg 文件,因为它是自动生成的,系统有时会覆盖它。你应在其他地方设置好新配置,然后运行 grub-mkconfig 来生成新配置。
要了解配置生成的工作原理,请查看 grub.cfg 的开头部分。应该有这样的注释行:
### BEGIN /etc/grub.d/00_header ###进一步查看会发现,/etc/grub.d 中的几乎所有文件都是一个 shell 脚本,它们生成 grub.cfg 文件的某一部分。grub-mkconfig 命令本身也是一个 shell 脚本,它会运行 /etc/grub.d 中的所有脚本。请记住,GRUB 本身不会在启动时运行这些脚本;我们在用户空间中运行这些脚本来生成 GRUB 在启动时使用的 grub.cfg 文件。
你可以以 root 身份亲自尝试。不用担心覆盖当前配置。这个命令本身只是将配置打印到标准输出:
# grub-mkconfig如果你想向 GRUB 配置中添加菜单条目和其他命令,那么简要答案是:你应该将自定义内容放入 GRUB 配置目录(通常是 /boot/grub/custom.cfg)中新建的 custom.cfg 文件中。
详细解答稍微复杂一些。/etc/grub.d 配置目录提供了两个选项:40_custom 和 41_custom。第一个,40_custom,是一个你可以自己编辑的脚本,但它最不稳定;包升级很可能会破坏你做出的任何更改。41_custom 脚本更简单;它只是一系列命令,在 GRUB 启动时加载 custom.cfg。如果你选择第二个选项,你的更改不会在生成配置文件时出现,因为 GRUB 在启动时完成所有工作。
注意
文件名前的数字影响处理顺序;数字较小的文件在配置文件中排在前面。
自定义配置文件的这两个选项并不特别丰富,也没有阻止你添加自己的脚本来生成配置数据。你可能会在 /etc/grub.d 目录中看到一些特定于你发行版的添加项。例如,Ubuntu 添加了内存测试器启动选项 (memtest86+)。
要写入并安装新生成的 GRUB 配置文件,你可以使用 -o 选项将配置写入 GRUB 目录,如下所示:
# grub-mkconfig -o /boot/grub/grub.cfg像往常一样,备份旧配置,并确保安装到正确的目录。
现在我们将深入探讨 GRUB 和引导加载程序的一些更技术性的细节。如果你已经厌倦了引导加载程序和内核,请跳到第 6 章。
5.5.3 GRUB 安装
安装 GRUB 比配置它更复杂。幸运的是,你通常不必担心安装,因为你的发行版应该会为你处理。但是,如果你想复制或恢复可启动磁盘,或准备自己的启动顺序,则可能需要自行安装。
在继续之前,请阅读第 5.4 节以了解 PC 如何启动,并确定你使用的是 MBR 还是 UEFI 启动。接下来,构建 GRUB 软件集并确定 GRUB 目录的位置;默认为 /boot/grub。如果你的发行版已经为你构建了 GRUB,则可能不需要构建它,但如果你需要,请参阅第 16 章以了解如何从源代码构建软件。确保构建正确的目标:MBR 和 UEFI 启动的目标不同(甚至 32 位和 64 位 EFI 之间也有差异)。
在系统上安装 GRUB
安装引导加载程序需要你或安装程序确定以下内容:
- 当前正在运行的系统所看到的 目标 GRUB 目录。如上所述,通常是
/boot/grub,但如果你正在为另一个系统在另一块磁盘上安装 GRUB,则可能不同。 - GRUB 目标磁盘的 当前设备。
- 对于 UEFI 启动,EFI 系统分区的 当前挂载点(通常是
/boot/efi)。
请记住,GRUB 是一个模块化系统,但为了加载模块,它必须读取包含 GRUB 目录的文件系统。你的任务是构建一个能够读取该文件系统的 GRUB 版本,以便它能够加载其余配置 (grub.cfg) 和任何所需模块。在 Linux 上,这通常意味着构建一个预加载了 ext2.mod 模块(可能还有 lvm.mod)的 GRUB 版本。拥有这个版本后,你只需要将其放置在磁盘的可启动部分,并将其余所需文件放入 /boot/grub 中。
幸运的是,GRUB 附带了一个名为 grub-install 的工具(不要与某些旧系统上可能存在的 install-grub 混淆),它可以为你完成安装 GRUB 文件和配置的大部分工作。例如,如果当前磁盘位于 /dev/sda,并且你想将 GRUB 安装到该磁盘的 MBR,同时使用当前的 /boot/grub 目录,请使用以下命令:
# grub-install /dev/sda警告
错误地安装 GRUB 可能会破坏系统的启动序列,因此不要轻视此命令。如果你担心,请研究如何使用
dd备份 MBR,备份任何其他当前安装的 GRUB 目录,并确保你有一个应急启动计划。
使用 MBR 在外部存储设备上安装 GRUB
要在当前系统之外的存储设备上安装 GRUB,你必须手动指定该设备上当前系统所能看到的 GRUB 目录。例如,假设目标设备为 /dev/sdc,并且该设备的根文件系统(包含 /boot,例如 /dev/sdc1)挂载在当前系统的 /mnt 上。这意味着当你安装 GRUB 时,当前系统将看到 /mnt/boot/grub 中的 GRUB 文件。运行 grub-install 时,按如下方式指定这些文件应去往的位置:
# grub-install --boot-directory=/mnt/boot /dev/sdc在大多数 MBR 系统上,/boot 是根文件系统的一部分,但某些安装将 /boot 放在其自己的单独文件系统中。确保你知道目标 /boot 所在的位置。
使用 UEFI 安装 GRUB
UEFI 安装应该更简单,因为你只需将引导加载程序复制到位即可。但你还必须使用 efibootmgr 命令向固件“告知”引导加载程序——即将加载程序配置保存到 NVRAM。grub-install 命令会在可用时运行它,因此通常你可以像这样在 UEFI 系统上安装 GRUB:
# grub-install --efi-directory=efi_dir --bootloader-id=name这里,efi_dir 是 UEFI 目录在您当前系统上出现的位置(通常是 /boot/efi/EFI,因为 UEFI 分区通常挂载在 /boot/efi),name 是引导加载程序的标识符。
不幸的是,安装 UEFI 引导加载程序时可能会出现许多问题。例如,如果你要安装到最终将用于另一系统的磁盘,你必须弄清楚如何将该引导加载程序告知新系统的固件。而且可移动介质的安装过程也有所不同。
但最大的问题之一是 UEFI 安全启动。
5.6 UEFI 安全启动问题
影响 Linux 安装的一个较新问题是处理现代 PC 上的安全启动 (secure boot) 功能。当激活时,这种 UEFI 机制要求任何引导加载程序必须由受信任的权威机构进行数字签名才能运行。微软要求硬件供应商在搭载 Windows 8 及更高版本的系统上使用安全启动。结果是,如果你尝试在这些系统上安装未签名的引导加载程序,固件将拒绝加载器,操作系统将无法加载。
主流 Linux 发行版在使用安全启动时没有问题,因为它们包含已签名的引导加载程序,通常基于 UEFI 版本的 GRUB。通常,UEFI 和 GRUB 之间有一个小的已签名 shim;UEFI 运行 shim,shim 再执行 GRUB。如果你的机器不在可信环境中,或者需要满足某些安全要求,防止启动未经授权的软件是一项重要功能,因此一些发行版更进一步,要求整个启动序列(包括内核)都经过签名。
安全启动系统存在一些缺点,尤其是对于尝试构建自己引导加载程序的人。你可以通过在 UEFI 设置中禁用它来绕过安全启动要求。然而,这对于双启动系统来说并不完美,因为 Windows 在未启用安全启动的情况下无法运行。
5.7 链式加载其他操作系统
UEFI 使支持加载其他操作系统相对容易,因为你可以在 EFI 分区中安装多个引导加载程序。但是,较旧的 MBR 风格不支持此功能,即使你确实拥有 UEFI,你可能仍然有一个包含 MBR 风格引导加载程序的独立分区,并希望使用它。GRUB 可以配置为加载并运行磁盘上特定分区上的另一个引导加载程序,而不是配置和运行 Linux 内核;这称为链式加载 (chainloading)。
要执行链式加载,请在 GRUB 配置中创建一个新的菜单条目(使用“生成新配置文件”一节中描述的方法之一)。以下是针对磁盘上第三个分区上的 Windows 安装的示例:
menuentry "Windows" {
insmod ntfs
set root=(hd0,3)
chainloader +1
}(注意:实际条目可能包含更多细节,如 search 命令以通过 UUID 查找分区。以上是基本结构。)
5.8 引导加载器细节
现在我们来快速了解一些引导加载器的内部机制。要理解像 GRUB 这样的引导加载器是如何工作的,首先需要了解 PC 开机时的启动流程。由于必须应对传统 PC 启动机制的诸多不足,引导加载方案有多种变体,但主要有两种:MBR 和 UEFI。
5.8.1 MBR 引导
除了第 4.1 节描述的分区信息外,MBR 还包含一个 441 字节的小区域,PC BIOS 在其加电自检(POST)之后会加载并执行该区域中的代码。不幸的是,这个空间几乎无法容纳任何引导加载器,因此需要额外的空间,这就产生了所谓的多阶段引导加载器。在这种情况下,MBR 中的初始代码片段除了加载其余的引导加载器代码外,不做其他任何事情。引导加载器的剩余部分通常被塞进 MBR 与磁盘上第一个分区之间的空间。这种方案并不十分安全,因为任何内容都可能覆盖那里的代码,但大多数引导加载器(包括大多数 GRUB 安装)都采用这种方式。
这种将引导加载器代码塞在 MBR 之后的做法在使用 BIOS 引导的 GPT 分区磁盘上无法工作,因为 GPT 信息位于 MBR 之后的区域(GPT 为了向后兼容保留了传统的 MBR)。针对 GPT 的变通方法是创建一个称为 BIOS 引导分区的小型分区,并指定一个特殊的 UUID(21686148-6449-6E6F-744E-656564454649),为完整的引导加载器代码提供存放位置。然而,这不是常见的配置,因为 GPT 通常与 UEFI 一起使用,而不是传统的 BIOS。这种配置通常只出现在使用非常大磁盘(大于 2TB)的老旧系统上,这些磁盘对于 MBR 来说太大了。
5.8.2 UEFI 引导
PC 制造商和软件公司意识到传统的 PC BIOS 存在严重局限性,因此他们决定开发一种替代品,称为可扩展固件接口 (EFI),我们在本章几个地方已经讨论过。EFI 在大多数 PC 上花了一段时间才流行起来,但如今它已成为最常见的固件,特别是考虑到 Microsoft 要求 Windows 支持安全启动。当前标准是统一可扩展固件接口 (UEFI),它包含内置 shell 和读取分区表、浏览文件系统等功能。GPT 分区方案是 UEFI 标准的一部分。
UEFI 系统上的引导与 MBR 截然不同。在大多数情况下,它更容易理解。可执行引导代码并不存放在文件系统之外,而是始终存在一个称为 EFI 系统分区 (ESP) 的特殊 VFAT 文件系统,其中包含一个名为 EFI 的目录。ESP 通常挂载在 Linux 系统上的 /boot/efi 目录,因此你很可能在 /boot/efi/EFI 下找到大部分 EFI 目录结构。每个引导加载器都有自己的标识符和相应的子目录,例如 efi/microsoft、efi/apple、efi/ubuntu 或 efi/grub。引导加载器文件具有 .efi 扩展名,并与其他支持文件一起存放在这些子目录中。如果你去探索,可能会找到诸如 grubx64.efi(GRUB 的 EFI 版本)和 shimx64.efi 之类的文件。
注意
ESP 与第 5.8.1 节描述的 BIOS 引导分区不同,并且具有不同的 UUID。你不应该遇到同时包含两者的系统。
不过有一个问题:你不能简单地将旧引导加载器代码放入 ESP,因为旧代码是为 BIOS 接口编写的。相反,你必须提供一个为 UEFI 编写的引导加载器。例如,使用 GRUB 时,你必须安装 UEFI 版本的 GRUB,而不是 BIOS 版本。而且,正如前面“在 UEFI 下安装 GRUB”部分所述,你必须向固件注册新的引导加载器。
最后,如第 5.6 节所述,我们还必须应对“安全启动”问题。
5.8.3 GRUB 如何工作
让我们通过查看 GRUB 的工作流程来结束对 GRUB 的讨论:
- PC 的 BIOS 或固件初始化硬件,并按照启动顺序搜索存储设备上的引导代码。
- 找到引导代码后,BIOS/固件加载并执行它。这就是 GRUB 开始的地方。
- GRUB 核心加载。
- 核心初始化。此时,GRUB 可以访问磁盘和文件系统。
- GRUB 识别其启动分区并加载该分区上的配置。
- GRUB 给用户一个更改配置的机会。
- 在超时或用户操作后,GRUB 执行配置(
grub.cfg文件中的命令序列,如第 5.5.2 节所述)。 - 在执行配置的过程中,GRUB 可能会在启动分区中加载额外的代码(模块)。其中一些模块可能被预加载。
- GRUB 执行
boot命令,以加载并执行配置中由linux命令指定的内核。
该过程的第 3 步和第 4 步(GRUB 核心加载)可能很复杂,因为传统的 PC 启动机制存在不足。最大的问题是:“GRUB 核心在哪里?” 有三种基本可能:
- 部分塞在 MBR 和第一个分区起始位置之间
- 在常规分区中
- 在特殊的启动分区中:GPT 启动分区、ESP 或其他地方
在所有情况下,除非你使用的是 UEFI/ESP,否则 PC BIOS 会从 MBR 加载 512 字节,这就是 GRUB 的起点。这一小段代码(源自 GRUB 目录中的 boot.img)还不是核心,但它包含核心的起始位置,并从这个位置加载核心。
然而,如果你有 ESP,GRUB 核心会作为一个文件存放在那里。固件可以浏览 ESP 并直接执行那里的所有 GRUB 或任何其他操作系统加载器(你可能会在 ESP 中有一个 shim,它位于 GRUB 之前以处理安全启动,但思路是一样的)。
尽管如此,在大多数系统上,这还不是完整的图景。引导加载器可能还需要在加载和执行内核之前,将初始 RAM 文件系统镜像加载到内存中。这就是 initrd 配置参数指定的内容,我们将在第 6.7 节介绍。但在学习初始 RAM 文件系统之前,你应该了解用户空间的启动——这就是下一章开始的内容。