Linux-启动实用指南-全-

Linux 启动实用指南(全)

原文:Hands-on Booting

协议:CC BY-NC-SA 4.0

一、简介

不是每个人都知道 Fedora。有一天,有人问我一个问题:

  • 学生:什么是 Fedora?

  • : Fedora 是 Linux。

  • 学生:什么是 Linux?

  • Me : Linux 是一个操作系统。

  • 学生:什么是操作系统?

  • 我:它运行电脑。

  • 学生:什么是电脑?

  • 我:电脑帮助用户。

  • 学生:什么是用户?

  • :一个用户跟我一样。

  • 学生:你到底是谁?

  • 我:嗯,我的名字是约格什·巴巴尔。我在 Red Hat 工作了十年,我喜欢谈论操作系统是如何启动的。

为什么呢?

众所周知,操作系统启动大约需要 20 到 30 秒。那么,我为什么要写一本 486 页的关于 30 秒启动序列的书呢?答案很简单。

  • 没有合适的文档/文章/书籍解释完整的引导顺序。你会发现数百本关于操作系统的好书,但没有一本是关于系统如何启动的。

  • 只有知道系统如何引导,才能解决引导问题。

  • 如果你是一名系统管理员,参加一个面试,面试官会问 Linux 是如何启动的。

  • “无法启动”问题总是最严重的,因为整个生产系统都会因此而停机。如果系统运行缓慢,生产仍在继续运行;虽然受到影响,但至少它还在运行。一台拥有 10,000 名用户但无法启动的服务器意味着整个生产系统都瘫痪了。这就是引导的重要性,正如我所说,如果你不知道系统如何引导,你就无法解决引导问题。

  • 理解启动过程很有趣。

  • 在学习这一切的同时,你会获得巨大的快乐。

什么事?

那么,到底什么是引导呢?用专业术语来说,把内核从硬盘复制到内存,然后执行的过程叫做引导。但是这个定义并没有真正启发我们去学习引导。

我用我自己的话来说:母亲是一个超集,她的新生儿是她的子集。同样,操作系统是一个超集,而引导是它的一个子集。子集属于它的超集。

现在考虑这个说法:“一个孩子生了一个妈妈。”

从技术上来说这是错误的,但是想象一下,直到一个女人有了孩子,她还是一个女人;当一个女人有了孩子,她就成了母亲。所以,孩子生了妈妈。

同样的情况也发生在电脑上。技术上来说引导是操作系统的一部分,操作系统应该催生引导,但是反过来了。是引导催生了操作系统。因此,我们可以说引导是产生操作系统的过程。

这本书的重点

本书解释了基于 x86 体系结构的台式机或服务器系统的引导过程,并涵盖了各种操作系统的引导过程。主要重点是对 Linux 引导过程的深入分析,次要重点是其他流行的操作系统,如 Windows 和 UNIX。如你所知,有大量的 Linux 发行版。有些是为桌面用户设计的,有些是为企业用户设计的,有些只是为了游戏目的,有些是为喜欢自己动手的用户设计的。几乎不可能涵盖每个发行版的引导顺序。因此,我决定选择 Linux 发行版,这是企业用户的第一选择,那就是 Red Hat Enterprise Linux (RHEL)。

RHEL 是基于 Fedora Linux 的。Fedora 的发行速度很快(六个月的发行周期),而 RHEL 的发行速度很慢(两到三年的发行周期)。这意味着只要 QE(质量工程)团队一开绿灯,Fedora 就会采用最新的开发成果。因为 Fedora 是流行的企业 Linux 发行版的测试平台,所以 Fedora 中可用的任何东西最终都会成为 RHEL 的一部分。systemd 就是最好的例子。这就是为什么我选择 Fedora Linux 来解释 Linux 引导序列。

电源

当你按下电源按钮时,一切都开始了。按下电源按钮,电源就到了主板上。主板向你的电源(SMPS/PSU)发送信号,它返回一个良好的电源,结果主板试图启动 CPU。

中央处理器

当基于 x86 架构的 CPU 启动时,它会清除所有寄存器中的旧数据,并从以下内容开始:

IP              0xfff0
CS selector     0xf000
CS base         0xffff0000

0xffff0000 + 0xfff0 = 0xfffffff0。这是 CPU 期望找到要执行的第一条指令的内存位置。在这个位置,它包含一个指向 BIOS 入口点的跳转指令。换句话说,这就是 BIOS 启动或 CPU 进入 BIOS/固件的方式。

在这之后,固件和引导装载程序是引导过程的下一个阶段。启动操作系统的引导程序是固件的工作。在下一章,我将讨论固件中发生了什么,以及它是如何执行引导程序的。

二、多启动

理解引导装载程序和固件是复杂的。不一定难,题目可以很复杂。为了让本书的读者容易理解,我将使用三个测试系统。

|

系统编号

|

系统名称

|

目的

|
| --- | --- | --- |
| one | 基本输入输出系统 | 演示 BIOS |
| Two | 断续器 | 展示 UEFI |
| three | 贾维斯(m.) | 对于 100 多个操作系统的多重引导项目 |

由于引导加载程序和固件紧密配合,我将从在每个系统上安装特定的操作系统列表开始,同时解释引导加载程序和固件之间的关系。这种方法会让复杂的话题变得更容易理解,更有趣,也更有乐趣。简而言之,我将一起解释引导程序和固件(BIOS/UEFI ),尽管它们是不同的概念。

Note

本章中基于 BIOS 的多重引导部分受到了 Vijay Gokhale 先生关于这个主题的研讨会的启发。我感谢他的启发。

操作系统列表

我们将在我们的第一个 BIOS 系统上安装以下操作系统,这意味着在安装了 BIOS 固件的系统上:

  • Sun OpenSolaris 2009

  • Fedora Linux 15

  • PC BSD 9.0

  • Windows 7

  • 红帽企业版 Linux 6.0

  • Windows Server 2003 (2k3)

  • Windows XP

我知道这些操作系统很老了,但我选择它们是有原因的。

看,BIOS 本身是一个过时的固件,所以如果你想了解 BIOS,你只能使用旧的操作系统。记住,只有了解 BIOS,才能理解 UEFI(当前固件)。就好像你对 C 很了解,就会更懂 Java 一样。此外,使用这些旧的操作系统将让我有机会接触到 Windows 和 Unix 引导加载程序。此外,它将为我提供解释 Linux 的 GRUB 遗留引导装载程序的机会。

这个想法是用前面提到的所有操作系统多重引导我们的 BIOS 系统。为此,我们需要遵守每个操作系统的规则和规定。

|

旧历法

|

规则

|
| --- | --- |
| Unix 操作系统 | Unix 操作系统(OpenSolaris 和 BSD)只能安装在主分区上。 |
| Linux 操作系统 | Linux 没有任何安装规则。它可以安装在任何主分区或逻辑分区上。 |
| Windows 操作系统 | Windows 操作系统可以安装在任何分区(主分区或逻辑分区)上,但是 Windows 系列的前身必须位于第一个主分区上。这意味着您可以在逻辑分区上安装 Windows 7,但它的前身 XP 或 win2k3 必须存在于第一个主分区上。此外,您不能破坏 Windows 操作系统的安装顺序。例如,不能先安装 Windows 7,然后再安装较旧的 win2k3 或 XP。必须是这个顺序:98,然后 2000,然后 XP。 |

花一些时间,尝试准备您的操作系统安装顺序。现在验证您的引导顺序。

操作系统的最终顺序如下所示:

  1. Windows XP

  2. 2008 年 Sun OpenSolaris

  3. PC BSD 9.0

  4. Windows 服务器 2003

  5. Windows 7

  6. 红帽企业版 Linux 6

  7. Fedora 15

安装操作系统

现在我们将讨论安装操作系统。

主/逻辑分区

使用 BIOS,我们只能创建四个分区。当然,你可能见过比这更多的分区。所以,让我稍微改变一下我的陈述。在基于 BIOS 的系统上,您只能在磁盘上创建四个分区。如果您想要更多,那么您需要将第四个主分区变成一个次分区(也称为扩展)分区。扩展分区将作为一个容器工作,在这个容器中,您可以创建任意多的逻辑分区。为什么这些分区被称为逻辑分区,因为它们对 BIOS 是不可见的?还有,为什么 BIOS 只能做四个主分区?这些问题将在我们讨论主引导记录时得到解答。

分割

让我们先对 BIOS 系统的硬盘进行分区。为此,我们将使用 GParted live CD。GParted 是来自 GNU 社区的工具。这是一个免费的、开源的、基于 Debian Linux 的实时 ISO 映像。图 2-1 显示了我们的 BIOS 系统的分区布局。

img/493794_1_En_2_Fig1_HTML.jpg

图 2-1

GParted 中 BIOS 的分区布局

对硬盘进行分区的操作非常简单。我们将在 75 GB 的磁盘空间上创建如图 2-2 所示的分区布局。

img/493794_1_En_2_Fig2_HTML.jpg

图 2-2

GParted-made 分区布局

有关如何使用 GParted 对硬盘进行分区的更多信息,请参考位于 https://gparted.org/articles.php 的 GParted 文档。

在图 2-3 中,您可以看到磁盘名称、分区大小、使用的文件系统和相关标志(如果有的话)。

img/493794_1_En_2_Fig3_HTML.jpg

图 2-3

GParted-made 文件系统布局

让我们在第一个主分区上安装第一个操作系统。

首次操作系统安装:XP

在图 2-4 中,您可以看到 Windows XP 安装程序显示的分区布局。

img/493794_1_En_2_Fig4_HTML.jpg

图 2-4

XP 安装程序显示的分区布局

我们正在第一个主分区上安装 XP。Windows 方面,是 C: drive,如图 2-4 。完成安装并重启系统后,我们在屏幕上看到了 Windows XP(图 2-5 )。

img/493794_1_En_2_Fig5_HTML.jpg

图 2-5

成功安装后的 XP

是时候了解 Windows XP 是如何引导的了,但在此之前,我们需要了解引导扇区。引导扇区是每个硬盘的第一个扇区(512 字节)加上 31 KB 的空间;换句话说,它是引导介质上的前 63 个扇区(0 到 62)。或者,您可以考虑在引导扇区下,每个分区的一些空间(512 字节+ 31 KB)将被保留来存储与引导装载程序相关的信息。这个空间(还是 512 字节+ 31 KB)不会被操作系统显示给用户。在这个保留空间之后,分区中的实际数据存储开始。参见图 2-6 以更好地理解这一点。

img/493794_1_En_2_Fig6_HTML.jpg

图 2-6

基于 BIOS 的系统上的磁盘布局

引导扇区

梵语中有一句惊人的谚语是这样说的:。这意味着只有一个真理,但有各种方式达到它。如图 2-7 所示,引导扇区有不同的名称,但最终概念是相同的。人们用以下名称来指代这种结构:

img/493794_1_En_2_Fig7_HTML.jpg

图 2-7

引导扇区

  • 主引导记录

  • 引导记录

  • 引导扇区

  • 引导加载程序

在本书中,我们称之为引导扇区,因为硬盘驱动器(HDD)总是被分成扇区,每个扇区的大小要么是 512 字节,要么是 4 KB。大多数硬盘遵循 512 字节的扇区大小。

在基于 BIOS 的系统上,每个操作系统供应商(不管是 Windows、Unix 还是 Linux)都必须将引导装载程序分成三个部分。bootloader 的第一部分将保存在 bootstrap 中,它有 440 个字节。第二部分将保存在 bootloader 部分,大小为 31 KB,最后的第三部分将保存在安装了特定操作系统的实际分区中。因此,简单地说,每当安装一个操作系统(在我们的例子中是 Windows XP)时,它会将其新技术加载程序(NTLDR)引导程序分成三个部分。

|

位置

|

大小

|

部分

|

信息

|
| --- | --- | --- | --- |
| 引导程序 | 440 字节 | NTLDR 第一部分 | 最微小的部分 |
| 引导装载程序 | 31 KB | NTLDR 第二部分 | 与第一部分相比更大 |
| 在实际的操作系统分区中 | 没有大小限制 | NTLDR 第三部分 | 最大的部分 |

但是为什么 bootloader 要分成三个部分呢?

是因为历史原因。BIOS 有技术上的限制,因为它不能访问超过 512 字节,或者不能读取超过第一个扇区的内容。因此,很明显,当 BIOS 完成它的任务时,它跳到整个硬盘的前 512 个字节,无论是谁,只要运行那个程序。幸运的是,该程序将是我们的引导程序(440 字节)。由于 bootstrap 的大小很小,它只做一件事,那就是跳到一个更大的空间,这就是 part-2 bootloader。大小为 31 KB。这个 31 KB 也非常小,它必须找到一个更大的大小。这个引导装载程序将跳转到 part-3,它位于一个分区内。这个 part-3 文件将位于 C:驱动器,文件名为 NTLDR。XP 的 bootloader 的 part-3 文件在图 2-8 中可见。

img/493794_1_En_2_Fig8_HTML.jpg

图 2-8

XP 引导程序的第三部分文件

如您所见,该文件的大小要大得多(245 KB)。这个文件将完成引导加载程序的实际工作,即从内存中的C:\windows复制 Windows XP 的内核winload.exe(这个文件知道 XP 的内核在哪里)。一旦内核被复制到内存中,引导装载程序的工作就完成了,它就消失了。记住,OS==kernel==OS。一旦内核在内存中,它将负责引导序列的其余部分。你可以在图 2-9 中看到 XP 的启动顺序。

img/493794_1_En_2_Fig9_HTML.jpg

图 2-9

Windows XP 的引导顺序

我知道你心里可能有很多疑问。但是继续读下去,你所有的问题都会得到解答。让我们继续讨论我还没有解释的引导扇区的字段。这可以参考图 2-10 。

img/493794_1_En_2_Fig10_HTML.jpg

图 2-10

引导扇区

供应商签名字段适用于硬盘供应商。这里提到的数据告诉我们哪个供应商制造了这种硬盘,如希捷,西部数据,三星等。因此,基本上它保存了硬盘制造商的信息。

NULL 只有 2 个字节的空间。空就是空的意思。如果这不为空,则 BIOS 将认为该 HDD 在 POST 例程时有故障/损坏,并且引导将被停止。所以,它必须为空。每当操作系统突然重启,或者当操作系统或硬盘本身检测到坏扇区或某种严重损坏时,该字段将被标记为非空。

MBR 字段可能是所有这些字段中最受欢迎的部分。MBR 代表“主引导记录”,大小为 64 字节。MBR 进一步分为四个部分。每个部分的大小是 16 字节,每个部分保存一个分区的信息。

|

大小

|

部件

|

商店

|
| --- | --- | --- |
| 16 字节 | 第一部分 | 第一分区的信息 |
| 16 字节 | 第二部分 | 第二分区的信息 |
| 16 字节 | 第三部分 | 第三分区的信息 |
| 16 字节 | 第四部分 | 第四分区的信息 |

这意味着 64 字节的 MBR 只能容纳 4 个分区条目,这就是为什么在基于 BIOS 的系统上只能创建 4 个主分区的原因。

fdisk 签名也被称为引导标志;有些人简单地称它为*,或者在 Windows 风格中,它也被称为活动/非活动标志。fdisk 在多重引导不同操作系统的情况下很重要,我们现在不讨论这个问题。现在,我要你记住这两条规则:

  • 逻辑分区不能是活动的。

  • 操作系统无法从逻辑分区启动。

到目前为止,这两条规则对你来说没有任何意义,但是我们会在适当的时候讨论它们。图 2-11 显示了 Windows XP 的完整引导序列。

img/493794_1_En_2_Fig11_HTML.jpg

图 2-11

Windows XP 的引导顺序

我们现在将安装并引导一个新的操作系统,即 OpenSolaris 2008。

OpenSolaris 2008

图 2-12 显示了使用 OpenSolaris 2008 安装介质引导时的屏幕。

img/493794_1_En_2_Fig12_HTML.jpg

图 2-12

OpenSolaris 2008 安装介质的欢迎屏幕

我们需要在第二个分区上安装 OpenSolaris。您可以在图 2-13 中看到,我们已经为安装选择了第二个主分区。

img/493794_1_En_2_Fig13_HTML.jpg

图 2-13

OpenSolaris 2008 安装程序显示的磁盘布局

但是正如您在图 2-14 中看到的,安装失败并显示一些错误信息。

img/493794_1_En_2_Fig14_HTML.jpg

图 2-14

安装失败,并显示一些错误消息。

错误消息与文件系统相关。因此,我们将使用 fdisk 实用程序手动准备文件系统;但是,在此之前,您应该知道 OpenSolaris 已经分配了什么硬盘名称。pfexec format命令输出(如图 2-15 所示)将为我们提供硬盘名称。

img/493794_1_En_2_Fig15_HTML.jpg

图 2-15

OpenSolaris 分配的硬盘名称

所以,分配的硬盘的名字是 c4d1。我们需要将这个设备名传递给 fdisk 实用程序。参见图 2-16 中的完整命令。

img/493794_1_En_2_Fig16_HTML.jpg

图 2-16

fdisk 命令

磁盘名称表示控制器号 4、磁盘号 1 和分区号 0。通过 fdisk 实用程序,我们首先删除了第二个分区(它是 ext3/Linux 本地的)并创建了一个具有 Solaris2 文件系统的新分区。新分区成为分区号 4。此外,它会自动成为活动分区(参见图 2-17 )。我们还没有谈到“活动或 fdisk 签名”部分,但我们很快就会谈到它。

img/493794_1_En_2_Fig17_HTML.jpg

图 2-17

通过 fdisk 命令所做的更改

回到我们的安装,让我们重新开始安装,如图 2-18 所示,这次我们选择了 OpenSolaris 文件系统格式的分区来安装我们的 OpenSolaris 2008。

img/493794_1_En_2_Fig18_HTML.jpg

图 2-18

在 OpenSolaris 文件系统分区上安装 OpenSolaris

这一次,安装不会失败(参见图 2-19 ),OpenSolaris 2008 将被安装。

img/493794_1_En_2_Fig19_HTML.jpg

图 2-19

安装程序不会失败

安装后,我们将重新启动我们的 BIOS 系统。你认为什么操作系统会启动?

  • Windows XP?

  • OpenSolaris?

  • XP 和 OpenSolaris 一起?

  • 没有吗?

在继续之前,花点时间思考一下....

图 2-20 显示了重启后我们在屏幕上看到的内容。

img/493794_1_En_2_Fig20_HTML.jpg

图 2-20

重启后的欢迎屏幕

这里启动的操作系统是 OpenSolaris,它也为我们提供了启动 XP 的选项。让我们来解释一下背景中发生了什么。OpenSolaris 发现它被安装在自己的分区(第二个分区)中,但是在第一个分区中还有另一个可用的操作系统,即 Windows(或者至少是一个“非 Unix 操作系统”)。

但是 OpenSolaris 如何知道在第一个主分区上安装了另一个操作系统呢?

当 OpenSolaris 安装在它自己的分区中时,它看到 fdisk 签名设置在第一个主分区上。(同样,fdisk 签名也被称为活动标志或简称为*标志。)正如我们在前面的引导扇区规格图(图 2-21 )中看到的,每个分区都有 512 字节+ 31 KB 的空间保留用于引导,这些空间对用户是隐藏的。

img/493794_1_En_2_Fig21_HTML.jpg

图 2-21

引导扇区

换句话说,当我们通过 GParted 创建一个分区布局时,该工具为每个分区创建了以下区间:

  1. 引导程序

  2. 供应商签名

  3. 主引导记录

  4. Fdisk 签名

  5. 引导加载程序

但是它只在供应商签名和 MBR 字段中填充数据。供应商签名字段将包含 HDD 供应商的数据,而对于 MBR 字段,数据如下:

  • 第一个主分区的开始和结束

  • 第二主分区的开始和结束

  • 第三个主分区的开始和结束

  • 第四个主分区的开始和结束

基本上会有四个条目,每个条目会消耗 16 个字节。除了供应商签名和 MBR,其他字段将为空。另外,请注意 GParted 将准备所有的区间(512 字节+ 31 KB ),但是将只填充第一个主分区的供应商签名和 MBR 字段。

回到 fdisk 签名字段,当安装 Windows XP 时,它建立了以下内容:

  • 引导中的 NTLDR 的第一部分

  • 引导程序中 NTLDR 的第二部分

  • 第一个主分区中 NTLDR 的第三部分

然后,它在自己的分区(2 字节)中设置 fdisk 签名。

因此,磁盘布局将如图 2-22 所示。

img/493794_1_En_2_Fig22_HTML.jpg

图 2-22

XP 安装后的磁盘布局

OpenSolaris 找到了这个磁盘布局。当 OpenSolaris 安装完成并希望安装其引导加载程序(GRUB)时,它在第一个主分区上看到一个星号(*),这时它意识到已经安装了一个 Windows 操作系统。现在 GRUB(OpenSolaris 引导加载程序)有两个选项。

img/493794_1_En_2_Fig23_HTML.jpg

图 2-23

OpenSolaris 安装后 GParted 中的磁盘布局

  • 将 Grand Unified Bootloader (GRUB)的 part-1 (bootstrap)和 part-2 (bootloader)安装在第一个主分区中,将 GRUB 的 part-3 安装在自己的分区中(已安装 OpenSolaris 的第二个分区)。

  • 或者把 part-1 (bootloader)安装在自己分区的前 512 字节,part-2 安装在自己分区的 31 KB,part-3 也安装在自己分区;然后将*放到自己的第二个分区上(参见图 2-23 )。

请注意,引导标志回到 OpenSolaris 分区。另外,GParted 不理解 Solaris2 分区;因此,它将 ext3 显示为文件系统名称。

如果 OpenSolaris 选择选项 1,则 OpenSolaris 必须清除 Windows XP 的引导加载程序的第一部分和第二部分。这也意味着只有 OpenSolaris 会启动,而 XP 将永远无法启动。因此,OpenSolaris 选择了选项-2,给予了引导 Windows XP 的同等机会。OpenSolaris 还在它自己的一个文件中创建了一个 Windows XP 条目(我们将在本章后面讨论这个文件)。每当 OpenSolaris 启动时,GRUB 将引用该文件,并在其中找到 Windows 条目,这将显示在屏幕上。图 2-24 显示了 OpenSolaris 欢迎屏幕。

img/493794_1_En_2_Fig24_HTML.jpg

图 2-24

OpenSolaris 欢迎屏幕

因此,OpenSolaris 的完整引导序列如下:

img/493794_1_En_2_Fig26_HTML.jpg

图 2-26

OpenSolaris 欢迎屏幕

  1. 打开系统电源。

  2. CPU 跳转到 BIOS。

  3. BIOS 运行 POST 程序。

  4. 我们回到 BIOS。

  5. BIOS 有点笨;它将检查用户设置的启动优先级。

    • 当我说启动优先级时,我指的是系统将通过哪个设备启动。

    • 它可以是光驱、USB、硬盘、PXE 等。

  6. BIOS 将跳转到整个硬盘的前 512 个字节或引导设备的第一个扇区。

    • 引导设备可以是任何东西,但现在我们正在考虑硬盘。
  7. BIOS 会将控制权移交给引导程序中存在的任何二进制文件。

    • 你认为那里有谁?Windows bootloader (NTLDR)还是 OpenSolaris (GRUB)?想一会儿再继续。

    • 前 512 字节存储的引导扇区是 Windows XP 的 NTLDR。

    • 你一定注意到了 440 字节的引导空间非常小,没有任何代码可以从中引导操作系统。因此,NTLDR (bootstrap)的 part-1 只是跳转到更大的空间,也就是 part-2(boot loader/31kb/虚拟引导记录)。第二部分检查 MBR (64 字节)并在其中找到四个条目。这意味着磁盘有四个主要分区。但是这里有一个问题:在四个主分区中,哪个分区有操作系统?你可能会说,当然,它是第一和第二分区,但是引导装载程序怎么知道操作系统在哪里呢?又该引导哪个呢?这是一个真正的问题,为了解决这个问题,创建了 fdisk 签名字段。无论哪个分区填充或设置了这两个字节,该分区都有一个操作系统。因此,当安装 Windows XP 或 OpenSolaris 时,该操作系统有责任填充 fdisk 签名字段的 2 个字节,或者在其自己的分区上设置*,以便引导加载程序知道哪个分区有该操作系统。在我们的例子中,*在它的第二个分区上(OpenSolaris 在安装它的时候保存了它)。这就是 NTLDR 的 part-2 如何知道它必须跳转到第二个分区。

  8. NTLDR 的 Part-2 跳转到第二个分区,这意味着它只是跳转到第二个分区(bootstrap)中 GRUB bootloader 的 part-1。

  9. GRUB 的第一部分(bootstrap/440 bytes)也很小,所以它将再次跳转到一个更大的空间,这是 GRUB 的第二部分(bootloader)。

  10. Part-2 knows where part-3 is. The location of part-3 will be hard-coded in part-2, so it will simply jump to part-3. Part-3 will read the text file /rpool/boot/grub/menu.lst (see Figure 2-25); this is the same file that was created by OpenSolaris when it detected XP on the first primary.

![img/493794_1_En_2_Fig25_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-linux-zh/raw/master/docs/handson-boot/img/493794_1_En_2_Fig25_HTML.jpg)

图 2-25

OpenSolaris menu.lst 文件
  1. GRUB 的第三部分将读取这个文本文件,并打印出在'title变量后写入的任何内容,这就是我们如何到达图 2-26 所示的屏幕。

图 2-27 显示了 OpenSolaris 的完整引导序列。

img/493794_1_En_2_Fig27_HTML.jpg

图 2-27

OpenSolaris 引导序列

如果用户选择 OpenSolaris 选项来引导,那么 OpenSolaris GRUB 的第三部分知道 OpenSolaris 的内核在哪里,它在/boot目录中。GRUB 会将内核从/boot复制到内存,并将控制权交给内核。这是 GRUB 引导装载程序的任务结束的地方,它就消失了。现在 OpenSolaris 的内核将负责引导序列的其余部分。我们将在第四章中讨论内核。

如果用户选择 Windows XP 选项来引导,那么 OpenSolaris GRUB 的第三部分将跳回到 NTLDR(引导)的第一部分。NTLDR 的第一部分将跳转到 NTLDR 的第二部分。第二部分将跳转到第三部分。NTLDR 的第三部分将在内存中加载winload.exewinload.exe文件知道 XP 的内核在哪里。它最终会被 NTLDR 复制或加载到内存中。一旦内核在内存中,NTLDR 的工作就完成了(记住,kernel=OS=kernel)。由于 XP 的内核在内存中,它将负责引导序列的其余部分。

PC BSD 9.0

引导标志在 OpenSolaris 分区上,所以现在我们将安装 PC-BSD 9.0。在图 2-28 中,PC-BSD 的安装程序显示了可以安装 PC-BSD 9.0 的分区数量。

img/493794_1_En_2_Fig28_HTML.jpg

图 2-28

分区的数量

如您所见,BSD 中的硬盘命名约定与早期的操作系统不同。我们需要在第三个分区上安装 BSD,也就是 ada0s2。它代表“0 号适配器和 2 号片”切片可以被认为是一个分区。图 2-29 显示了磁盘布局和磁盘命名惯例。

img/493794_1_En_2_Fig29_HTML.jpg

图 2-29

磁盘布局和磁盘命名约定

the ada0s2空间分配给/(根文件系统)。图 2-30 显示了 PC-BSD 9.0 的分区布局。您还会注意到 BSD 的文件系统是UFS,这是 Unix 文件系统。

img/493794_1_En_2_Fig30_HTML.jpg

图 2-30

PC-BSD 9.0 的分区布局

安装完成后,系统将重新启动。现在花点时间想想哪个操作系统会启动。

会是以下哪一个?

  • OpenSolaris,这将给它一个引导 Windows 和 BSD 的机会

  • 会不会是 PC-BSD,这样就有机会启动另外两个操作系统?

  • 会是 PC-BSD 一个人吗?

  • 会不会只有 Windows XP?

  • 会是 OpenSolaris 一个人吗?

  • 还是没有操作系统可以启动?

请访问早期操作系统的引导流程图,并尝试提出自己的引导顺序。

正如您在图 2-31 中看到的,将要引导的操作系统是 OpenSolaris,这将创造一个仅引导 Windows 的机会。

img/493794_1_En_2_Fig31_HTML.jpg

图 2-31

PC-BSD 无法启动。

PC-BSD 无法启动。在进入下一页之前,再次花些时间想想发生了什么

你是对的——PC-BSD 可能没有将* /boot flag/fdisk 签名保存在自己的分区上。我们来看看是不是这样。我们将使用 GParted 启动(图 2-32 )并验证我们的理论。

img/493794_1_En_2_Fig32_HTML.jpg

图 2-32

GParted 欢迎屏幕

如图 2-33 所示,PC-BSD 并没有在自己的分区上设置*

img/493794_1_En_2_Fig33_HTML.jpg

图 2-33

GParted 上的磁盘布局

因此,引导顺序如图 2-34 所示。

img/493794_1_En_2_Fig34_HTML.jpg

图 2-34

启动顺序以及 PC-BSD 无法启动的原因

这意味着 OpenSolaris 不知道 BSD 安装在第三个分区上。因此,PC-BSD 条目不在 OpenSolaris 中。如果我们在 BSD 的分区上保留引导标志会怎么样?它能启动吗?但是我们如何在第三个分区上保留引导标志呢?很简单——GParted 给了我们这个选择。右键单击第三个分区,选择引导标志,如图 2-35 所示。

img/493794_1_En_2_Fig35_HTML.jpg

图 2-35

在 PC-BSD 上设置引导标志

图 2-36 显示了在 BSD 的第三个分区上设置引导标志后磁盘布局的样子。

img/493794_1_En_2_Fig36_HTML.jpg

图 2-36

磁盘布局

现在你认为哪个操作系统会启动?

  • 单独 PC-BSD?

  • PC-BSD,这将有机会引导所有其他操作系统?

  • 同样是 OpenSolaris,它会创建一个引导 Windows 的选项。

  • 单单 OpenSolaris?

  • 单单 Windows XP?

图 2-37 显示答案;重新启动后,只有 PC-BSD 会启动,它不会提供启动任何其他操作系统的选项。

img/493794_1_En_2_Fig37_HTML.jpg

图 2-37

PC-BSD 的欢迎屏幕

让我们试着理解一下 PC-BSD 是如何成功引导的。

img/493794_1_En_2_Fig38_HTML.jpg

图 2-38

PC-BSD 的引导序列

  1. 打开系统电源。

  2. BIOS 执行 POST 程序。开机自检检查硬件运行状况,如果一切正常,会发出一声正常的蜂鸣声,然后返回 BIOS。

  3. BIOS 是哑的,它只是跳到整个硬盘的第一个扇区,这是 Windows XP 的引导程序。

  4. XP 的 part-1 (NTLDR)跳转到更大的空间,这是 NTLDR(boot loader)的 part-2。引导装载程序检查 MBR,发现有四个主分区,但是哪个是活动的呢?为了检查这一点,引导装载程序检查第一个主分区的 fdisk 签名,该签名没有设置,因此它检查第二个分区的引导标志,该标志也没有设置。因此,它跳转到第三个分区,在那里找到启动标志集。NTLDR 的 bootloader(第二部分)跳转到 BSD 的分区并运行 BSD 的 bootloader 的引导程序。BSD 的引导程序是 BTX,代表引导扩展。BTX 跳到第二部分,最后跳到第三部分。BTX 的第三部分知道 BSD 的内核在哪里。BTX 的第三部分复制内存中 BSD 的内核映像,这是 BTX 停止的地方,PC-BSD 开始引导并向我们显示一个欢迎屏幕。图 2-38 显示了 PC-BSD 启动顺序的流程图。

BSD 引导的有趣之处在于,当安装 PC-BSD 时,它在第二个分区(OpenSolaris 分区)上找到了引导标志。现在 BSD 有三个选择。

  1. 将引导标志保留在它自己的第三个分区上。

  2. 将引导标志保留在它自己的第三个分区上,并在它的一些文件中创建一个 OpenSolaris 条目。

  3. 保留第二个分区上的引导标志。

如果 BSD 选择第一个选项(a),那么只有 BSD 能够引导,这对其他安装的操作系统是不公平的。我们希望 BSD 选择第二个选项(b ),因为它公平地引导其他操作系统,但是 BTX 是一个旧的引导程序,它没有能力多重引导其他操作系统。因此,BSD 选择第三个选项(c)。因此,只有 OpenSolaris 在引导,它提供了引导 XP 的选项。记住,XP 没有启动。只有 OpenSolaris 在引导,通过读取menu.lst文件,它给出了引导 XP 的选项。这也意味着 BSD 自己选择不引导。

如果我们回到 Windows XP 的第一个分区上保留引导标志会怎么样?那会引导哪个 OS 呢?在图 2-39 中,我们做到了这一点。

img/493794_1_En_2_Fig39_HTML.jpg

图 2-39

PC-BSD 的引导序列

只有 Windows XP 会启动,启动顺序很简单。图 2-40 解释了 Windows XP 是如何启动的。

img/493794_1_En_2_Fig40_HTML.jpg

图 2-40

Windows XP 的引导顺序

在安装新操作系统之前,我们需要将引导标志从 BSD 的第三个分区移动到 OpenSolaris 的第二个分区。图 2-41 显示了从 XP 分区到 OpenSolaris 分区的更改后的引导标志。

img/493794_1_En_2_Fig41_HTML.jpg

图 2-41

GParted 中的磁盘布局

通过这一更改,OpenSolaris 将开始引导,同时,Windows XP 也将引导,但 BSD 将无法引导。那么,这是否意味着每次我们引导 BSD 时,我们都必须将引导标志放回 BSD 的分区?到目前为止,是的,但是我们将在引导加载器的帮助下自动完成所有这些。

Windows 服务器 2003

如图 2-42 所示,我们将在第一个逻辑分区上安装 Windows Server 2003 (win2k3)。对于 win2k3,它是一个 D:驱动器。

img/493794_1_En_2_Fig42_HTML.jpg

图 2-42

win2k3 安装程序显示的磁盘布局

安装完成后,你认为哪个操作系统会启动?

  • 单独 win2k3?

  • win2k3 会提供每隔一个操作系统启动的选项吗?

  • win2k3 和 OpenSolaris 一起?

  • PC BSD?

  • 单独 XP?

  • win2k3 和 XP?

在继续之前,想一想,想出自己的答案。

如图 2-43 所示,将要引导的 OS 是 win2k3。

img/493794_1_En_2_Fig43_HTML.jpg

图 2-43

重启后 win2k3 的欢迎屏幕

并且 win2k3 给出了启动 Windows XP 的选项。这意味着只有 Windows 系列操作系统正在启动。还有,这里有一些我们应该考虑的问题:

  • 现在开机旗在哪里?

  • 如果我们在第二个分区上保留引导标志,将引导哪个操作系统?

  • 如果我们在第三个分区上保留引导标志,将引导哪个操作系统?

  • 如果我们在逻辑分区(win2k3 的分区)上保留引导标志,会引导哪个 OS?

  • 有没有办法只开机 Windows XP?

在接下来的讨论中,你会得到这些问题的所有答案。

这里有一点很清楚:win2k3 是唯一正在引导的操作系统。在讨论它如何引导之前,我们需要检查它在磁盘上创建了什么场景才能成功引导。

当 win2k3 被安装时,它看到它被安装在一个逻辑分区上,并且引导标志在 OpenSolaris 分区上(参见图 2-44 )。

img/493794_1_En_2_Fig44_HTML.jpg

图 2-44

安装 win2k3 时的磁盘布局

为了引导,win2k3 必须将引导标志放在它自己的分区上,方法是在它自己的 512 字节+ 31 KB 中安装它的引导加载程序(同样是 NTLDR 的)part-1 和 part-2。但是这里有一个问题。你还记得我们在 Windows XP 安装时看到的规则吗?

  • 逻辑分区不能是活动的。

  • 操作系统无法从逻辑分区启动。

由于这两条规则,win2k3 无法在自己的分区上保留引导标志,最终无法从逻辑分区引导。图 2-45 显示了 win2k3 无法从逻辑分区引导的引导顺序。但是这样的规则是出于什么原因呢?

img/493794_1_En_2_Fig45_HTML.jpg

图 2-45

win2k3 的引导序列,如果它试图从逻辑分区引导

很简单:MBR 只有四个条目,如下:

  • 第一主= sda1

  • 第二主= sda2

  • 第三原色= sda3

  • 第四个主分区=扩展分区(非逻辑分区)= sda4

win2k3 分区是 sda5。换句话说,它是 SATA 磁盘 a(第一个)和分区号 5。由于 MBR 没有逻辑分区的条目,XP 的 NTLDR 的 part-2 不知道还有第五个分区可用。所以,即使 win2k3 把引导标志保存在自己的分区上,XP 的 NTLDR 也看不到。因此,win2k3 永远不会启动。现在,为什么 MBR 不能有超过五个条目?这是因为 64 字节只能存储四个条目。为什么不增加 MBR 的大小?实际上,即使开发者想增加 MBR 的大小,他们也不能。当我们在本章后面讨论 UEFI 固件时,你就会明白原因了。

现在这已经成为 win2k3 的鸡和蛋的问题。它想要引导,但是为此它必须在它自己的分区上保留引导标志,但是如果它这样做了,那么 BIOS 就看不到那个分区。我们如何解决这个问题?

一些了不起的开发人员已经解决了这个问题,无论是谁想出这个主意,都只是一个传说。win2k3 在第一个主服务器上传输其 NTLDR 引导加载程序,这意味着 part-1、part-2 和 part-3。这也意味着 win2k3 将删除所有的 XP NTLDR 的部分,因为空间(512 字节+ 31 KB)很小,两个引导加载程序都放不下。(这里有一个甜蜜点,叫做 VBR,不在本书讨论范围之内。)但是,在删除的同时,XP 的 bootloader win2k3 将 XP 的条目制作在它的一个文本文件中,并保存在第一个主分区。文件名为boot.ini,如图 2-46 所示。

img/493794_1_En_2_Fig46_HTML.jpg

图 2-46

boot.ini 文件

这样做时,win2k3 只在第一个主分区上保留引导标志。这就是 win2k3 的启动方式:

img/493794_1_En_2_Fig48_HTML.jpg

图 2-48

win2k3 的 NTLDR 第三部分文件的大小

  1. 打开系统电源。

  2. CPU 进入 BIOS。BIOS 运行开机自检。

  3. 开机自检,硬件发出正常的蜂鸣声并返回 BIOS。

  4. BIOS 跳转到第一个主分区的第一个 512 字节。

  5. 将启动引导程序,这是 win2k3 的 NTLDR 的第一部分。

  6. 第一部分将寻找 NTLDR 的第二部分。

  7. 第二部分将检查 MBR 并检查 fdisk 签名。

  8. fdisk 签名设置在第一个主分区上,这意味着 part-2 将跳转到 XP 的第一个主分区内,并将运行 win2k3 的 NTLDR 的 part-3。只是给你一个想法,第三部分是新的,而不是 XP 的旧 NTLDR。这里我提供两张图片。

    • Note the size of NTLDR (part-3) in Figure 2-47. This is when we installed Windows XP.

      img/493794_1_En_2_Fig47_HTML.jpg

      图 2-47

      Windows XP 的 NTLDR 第三部分文件的大小

    • 在图 2-48 中,注意安装 win2k3 后 NTLDR (part-3)的大小。

正如你所看到的,Windows XP 的 NTLDR 的第三部分是 245 KB,但现在有了 win2k3 它是 291 KB。

img/493794_1_En_2_Fig50_HTML.jpg

图 2-50

Windows XP 的引导顺序

  1. Part-3 of NTLDR (win2k3) will read the boot.ini file from the same partition (the first primary) and will print whatever is written in quotes. Figure 2-49 shows what will be printed on the screen.

    img/493794_1_En_2_Fig49_HTML.jpg

    图 2-49

    win2k3 显示的欢迎屏幕

  2. 如果用户选择 Windows Server 2003 企业选项,那么 win2k3 的 NTLDR 的 part-3 就知道 win2k3 的内核在哪里。这是在安装了 win2k3 的第五个分区。它在内存中复制内核,win2k3 的 NTLDR 不了了之。

  3. 如果用户选择了 Microsoft Windows XP Professional 选项,那么 NTLDR 的 part-3 也知道 Windows XP 的内核在哪里。这是在第一个主分区中。首先它开始winload.exe;最终winload.exe在内存中复制 XP 的内核,NTLDR 不了了之。图 2-50 显示了 Windows XP 的完整启动顺序。

所以,这就是 Windows XP 和 win2k3 能够启动的方式。让我们回到 fdisk 签名的讨论;由于只有 win2k3 在启动,其他操作系统都无法启动,我有一些问题要问:

  • 只能启动 Windows XP 吗?

  • 如果我们在 OpenSolaris 上保留引导标志会怎么样?

  • 如果我们在 PC-BSD 上保留引导标志呢?

  • 如果我们不把引导标志放在任何地方呢?

慢慢来,思考,重温流程图,然后得出你的答案。

准备好了吗?我们不能只启动 Windows XP。这是不可能的,因为在 Windows XP 引导加载程序中,所有的部分都被 win2k 的 NTLDR 所取代。还有,现在只有 win2k3 知道 XP 在哪里,只有 win2k3 可以引导 Windows XP。这也意味着如果 win2k3 的 bootloader 的 part-1 被破坏或删除,我们将永远失去 XP。但是如果我们在 PC-BSD 上保留引导标志,那么它将照常引导。图 2-51 显示了 PC-BSD 的启动顺序。

img/493794_1_En_2_Fig51_HTML.jpg

图 2-51

PC-BSD 的引导序列

如果我们不在任何分区上保留引导标志,那么它就不能引导。这类似于我们在讨论如果在逻辑分区上设置了引导标志会发生什么时所讨论的情况。图 2-52 显示了引导顺序,以解释为什么没有操作系统能够引导。

img/493794_1_En_2_Fig52_HTML.jpg

图 2-52

显示操作系统无法启动的原因的启动顺序

在逻辑分区上设置引导标志与在任何地方都不设置引导标志一样好。

现在,主要问题是,如果我们在 OpenSolaris 分区上保留引导标志会怎么样?OpenSolaris 将无法引导。OpenSolaris 引导加载程序(GRUB)将抛出如图 2-53 所示的错误消息。

img/493794_1_En_2_Fig53_HTML.jpg

图 2-53

GRUB 根据提示丢弃

但是为什么呢?它应该能启动,对吗?OpenSolaris 中没有任何变化(512 字节+ 31 KB)。只是 win2k3 把引导标志从 OpenSolaris 分区移到了第一主。所以,理想情况下应该能开机,但不会,原因是 win2k3 的行为。当 win2k3 被安装时,它面临着 OpenSolaris 和 PC-BSD 面临的类似情况。换句话说,引导标志在不同的分区上,并且该分区有另一个操作系统。OpenSolaris 在这种情况下所做的是将引导标志从 XP 的分区移动到它自己的第二个分区,但由于这将使 XP 无法引导,它慷慨地在自己的文件(menu.lst)中为 XP 创建了一个条目。OpenSolaris 每次都会读取该文件,并给 XP 同等的引导机会。

在 PC-BSD 的情况下,它检测到 OpenSolaris 上的引导标志,如果它被移动到自己的分区,它将使 OpenSolaris 无法引导。因此,BSD 慷慨地选择不把引导标志放在它自己的分区上,这样另一个 OS 就不会变得不可引导。但是 win2k3 没有那种大度。当安装 win2k3 时,它发现引导标志在非基于 Windows 的操作系统上。因此,它移动了 OpenSolaris 的引导标志,但由于这是非基于 Windows 的操作系统,它没有在boot.ini中创建条目。更进一步,win2k3 甚至破坏/删除了 OpenSolaris GRUB 的 part-1。因此,OpenSolaris 现在无法引导。

后来,win2k3 清除了 XP 的引导装载程序,但它在boot.ini中为 XP 做了条目,因为它是 Windows 操作系统。这就是为什么我说 win2k3 不像 OpenSolaris 和 PC-BSD 那样慷慨。但是我们将在本章的“调整 GRUB”一节中修复 OpenSolaris。

Windows 7

如图 2-54 所示,我们正在第五个分区安装 Windows 7。

img/493794_1_En_2_Fig54_HTML.jpg

图 2-54

Windows 7 安装程序显示的磁盘布局

Windows 没有显示扩展分区,以免简单的桌面用户混淆。

1st  = XP     2nd = Solaris    3rd  = PC-BSD      4th  = win2k3      5th  = 7

安装完成后,你认为哪个操作系统会启动?像往常一样,慢慢来,想出你的答案再继续算 2-55 。

img/493794_1_En_2_Fig55_HTML.jpg

图 2-55

Windows 7 显示的欢迎屏幕

你猜对了:Windows 7 会启动。以下是 Windows 7 的完整引导顺序:

img/493794_1_En_2_Fig56_HTML.jpg

图 2-56

bcdedit.exe 的产量

  1. 打开系统电源。

  2. CPU 会跳转到 BIOS。

  3. 在 POST 程序之后,BIOS 将跳转到整个硬盘的第一个扇区。

  4. 安装 Windows 7 时,*位于第一个主服务器上,Windows 7 安装在一个逻辑分区中。所以,Windows 7 面临着和 win2k3 同样的问题。

  5. 为了使自己可启动,Windows 7 将遵循相同的路径,其次是 win2k3。Windows 7 将在第一个主分区上安装其 part-1、part-2 和 part-3。第三部分不需要安装在第一个主服务器上,因为第二部分有第三部分的硬编码位置,但这就是 Windows 系列的工作方式。

  6. 当 Windows 7 的 part-1 和 part-2 安装在第一个主服务器上时,显然 Windows 7 必须删除 win2k3 NTLDR (part-1 和 part-2),但在删除文件时,Windows 7 会识别出 win2k3 是 Windows 系列操作系统;因此,Windows 7 的 bootloader 称为 Boot Configuration Data (BCD ),在自己的文件中为 win2k3 创建了一个条目,这可以在bcdedit.exe.检查图 2-56 中看到bcdedit.exe的输出。

图 2-56 中的“Windows Legacy OS Loader”是指 win2k3。

img/493794_1_En_2_Fig58_HTML.jpg

图 2-58

Windows 7 在引导过程中显示的动画

  1. 所以,回到引导序列,它看起来像这样:BIOS ➤邮政➤ BIOS ➤硬盘的第一个扇区。

  2. 引导程序的第一个 440 字节是 Window 7 的 BCD 引导程序的第一部分。它会寻找一个更大的空间,也就是 BCD 的第二部分。

  3. BCD 的第二部分将读取 MBR,并且将知道在这个 HDD 上有四个主分区,但是为了检查哪个是活动的,它将开始检查每个分区的 fdisk 签名,但是它将发现第一个主分区本身是活动的。

  4. Part-2 will jump inside the first primary where part-3 of Window 7’s BCD bootloader is stored. Part-3 will read its bootloader configuration file through bcdedit.exe and will list the entries that are mentioned in front of the description variable. Figure 2-57 shows what will appear on-screen.

    img/493794_1_En_2_Fig57_HTML.jpg

    图 2-57

    Windows 7 显示的欢迎屏幕

  5. 如果用户选择 Windows 7,那么正如你在bcdedit.exe中看到的,BCD 的第三部分将从C:\windows\systemd32调用winload.exe。记住,这里 C:是指 Windows 7 的分区,也就是第六个逻辑分区。

  6. winload.exe文件知道 Windows 7 内核的位置。它将开始在内存中加载内核,一旦完成,Windows 7 的内核将负责引导序列的其余部分。在图 2-58 中,您可以看到 Windows 7 启动后显示的动画。

图 2-59 显示了 Windows 7 启动序列的完整流程图。

img/493794_1_En_2_Fig59_HTML.jpg

图 2-59

Windows 7 的启动顺序

img/493794_1_En_2_Fig60_HTML.jpg

图 2-60

win2k3 和 XP 的启动顺序

  1. 如果用户选择早期版本的 Windows,那么 BCD 的 part-3 将调用 NTLDR 的 part-3,它只在第一个主分区上,引导序列将继续,这是我们在 win2k3 中看到的。图 2-60 说明 win2k3 和 XP 的启动顺序。

红帽企业版 Linux 6 (RHEL 6)

RHEL 安装工的名字叫阿纳康达。所有基于 Fedora 的发行版都使用 Anaconda 安装程序。在图 2-61 中,我们已经开始安装 RHEL 6。

img/493794_1_En_2_Fig61_HTML.jpg

图 2-61

RHEL 6 的引导介质的欢迎屏幕

图 2-62 显示了我们当前的分区布局。

img/493794_1_En_2_Fig62_HTML.jpg

图 2-62

Anaconda 安装程序显示的分区布局

如图 2-63 所示,我们需要将 root ( /)分配给 sda7 分区,并用 ext4 重新格式化,ext 4 是 RHEL 6 的默认文件系统选择。

img/493794_1_En_2_Fig63_HTML.jpg

图 2-63

Anaconda 将实现的分区方案

如图 2-64 所示,RHEL 6(或 Anaconda)已经检测到一些操作系统,它正试图给其他操作系统同等的引导机会(指定为其他)。有两个操作系统条目,RHEL 6 的引导程序(GRUB)会在引导时显示。

img/493794_1_En_2_Fig64_HTML.jpg

图 2-64

Anaconda 正在检测另一个操作系统

根据 RHEL 6,另一个操作系统将从 sda5 启动。这意味着:

sda1 = XP
sda2 = Solaris
sda3 = PC BSD
sda4 = Extended partition
sda5 = Win win2k3    <<<-----------

在启动时,如果用户选择了其他选项,win2k3 应该会启动。选择另一个选项后,将启动哪个操作系统?慢慢来,想出自己的引导顺序。

让我们重新启动系统,看看哪个操作系统正在启动。正如你在图 2-65 中看到的,是 RHEL 6 在引导,给你一个引导其他操作系统的机会。

img/493794_1_En_2_Fig65_HTML.jpg

图 2-65

RHEL 6 欢迎屏幕

RHEL 6 号的靴子是这样的:

img/493794_1_En_2_Fig66_HTML.jpg

图 2-66

grub.conf 文件

  1. 当系统开机时,它进入 BIOS,然后从 BIOS 进入 POST,再从 POST 返回 BIOS。

  2. BIOS 最终会进入整个硬盘的第一个扇区并运行引导程序。

  3. 当 RHEL 6 被安装时,*在第一个主分区上。

  4. win2k3 和 Windows 7 面临的问题,RHEL 6 也面临着。RHEL 6 被安装在 BIOS 无法到达或看到的逻辑分区中。因此,为了解决这个问题,RHEL 6 必须将其引导程序(GRUB)的第一部分和第二部分转移到第一个主分区。请记住,Windows 也将 part-3 转移到了第一主分区,但是 RHEL(以及一般的任何 Linux 操作系统)只会将前两部分转移到第一主分区,而 GRUB 的 part-3 将保存在自己的分区中;在我们的例子中,这是 sda-7。

  5. 在替换第一个主分区的 part-1 和 part-2 时,RHEL 注意到已经安装了一些其他操作系统,为了给它一个平等的引导机会,它在自己分区的/boot/grub/grub.conf命名配置文件中为它创建了一个条目。图 2-66 为grub.conf档。

正如你所看到的,在title变量之后写的任何东西都将被打印在屏幕上。

img/493794_1_En_2_Fig69_HTML.jpg

图 2-69

隐藏复杂日志消息的动画

  1. 回到引导序列,第一个主分区中的引导来自 RHEL。

  2. RHEL 食物的第一部分将跳到第二部分。

  3. GRUB 的第二部分有一个 GRUB 的第三部分的硬编码位置。GRUB 的第三部分位于 RHEL 的分区 sda7 上。

  4. Part-3 of GRUB will read the grub.conf file from the /boot/grub directory, and whatever is written after title will be printed on the screen. Figure 2-67 shows this.

    img/493794_1_En_2_Fig67_HTML.jpg

    图 2-67

    RHEL 6 的 GRUB 显示的欢迎屏幕

  5. If a user chooses the first entry, which is Red Hat Enterprise Linux 6, then part-3 of GRUB knows where the kernel of RHEL is. Figure 2-68 shows the grub.conf file.

    img/493794_1_En_2_Fig68_HTML.jpg

    图 2-68

    RHEL 6 的 grub.conf 文件

  6. 内核二进制文件将位于/boot/vmlinuz。(注意图 2-68 中的kernel变量。)基本上,同一个grub.conf文件会告诉 GRUB 的第三部分内核的位置。它将在内存中复制内核(vmlinuz),GRUB 引导程序的工作就完成了。RHEL 的内核将负责引导序列的其余部分。同时,当系统启动时,屏幕上会出现如图 2-69 所示的精美动画。

图 2-70 显示了 RHEL 6 的完整启动序列的流程图。

img/493794_1_En_2_Fig70_HTML.jpg

图 2-70

RHEL 6 的启动顺序

img/493794_1_En_2_Fig72_HTML.jpg

图 2-72

错误消息

  1. If a user chooses Other instead, then it will call whatever is present on the sda5 partition. As you can see in Figure 2-71, sda5 is on win2k3’s partition.

    img/493794_1_En_2_Fig71_HTML.jpg

    图 2-71

    另一个操作系统在分区 5 上

  2. 当安装 win2k3 时,它将其所有的引导程序部分转移到第一个主服务器。这意味着 win2k3 的分区没有引导加载程序,所以当然不会有操作系统启动。图 2-72 显示了当您尝试启动另一个操作系统时,屏幕上显示的错误信息。

现在,我有几个问题要问:

  • *现在在哪里?

  • 如果我把*放在第二个分区,哪个操作系统会启动?

  • 如果我把*放在第三个分区,哪个操作系统会启动?

  • 如果我把*放在第五个(逻辑)分区,哪个操作系统会启动?

  • 如果我没有在任何分区上保存*,哪个操作系统将会启动?

在所有这些场景中,只有一个操作系统会启动,那就是 RHEL 6(图 2-73 )。

img/493794_1_En_2_Fig73_HTML.jpg

图 2-73

RHEL 6 桌面屏幕

不管你把*放在哪里,或者即使你不把*放在任何分区上,只有 RHEL 会一直引导。原因很简单,但是它完全改变了引导顺序。Red Hat Enterprise Linux bootloader,也就是 GRUB,不遵循*,在调用其 boot loader 的 part-3 之前不检查哪个分区是活动的。事实上,没有一个 Linux 操作系统会费心去检查活动分区。他们只是跳过这一步。因此,引导顺序如下:

  1. 首先,系统进入 BIOS,然后是 POST,然后返回 BIOS,最后是第一个主分区的引导。

  2. RHEL 的 GRUB 的第一部分跳转到 GRUB 的第二部分,第二部分(跳过 fdisk 签名部分后)跳转到 GRUB 的第三部分。

  3. GRUB 的第三部分转到/boot/grub.conf,它打印 OS 条目。

  4. 如果用户选择 RHEL,那么内核从内存中的/boot/vmlinuz开始加载。

  5. 内核将负责操作系统引导的其余部分,这将在本书的其余部分详细解释。

这也意味着目前只有一个操作系统正在启动,那就是 RHEL 6。那就糟了!因此,我们需要调整 GRUB 来引导其余的操作系统。

调整食物

GRUB 最好的特性是它可以引导任何其他操作系统,不管它是否基于 Linux。引导 GRUB 使用的另一个操作系统的技巧很简单,但是很神奇。对于任何引导操作系统的引导加载程序,您只需要在内存中加载相应操作系统的内核。GRUB 知道 Linux 操作系统的内核在哪里(/boot/vmlinuz)。但是 GRUB 不知道 Windows 或者 PC-BSD 的内核在哪里。诀窍在于这些操作系统各自的引导加载程序知道它们各自内核的位置。所以,GRUB 只是调用它们各自的 bootloaders 例如,如果 GRUB 想要引导 BSD,它位于第三个主分区。参见图 2-74 ,其显示了分区布局,以更好地理解这一点。

img/493794_1_En_2_Fig74_HTML.jpg

图 2-74

BIOS 的分区布局

BSD 在它自己的分区的保留的 512 字节+ 31 KB 上安装了它的引导装载程序。因此,GRUB 将调用 BTX 的第一部分。这叫做链装。GRUB 引导程序的第三部分将链接加载 BTX 的第一部分。BTX 的第一部分知道下一步该做什么,那就是寻找第二部分。第二部分将跳转到第三部分,它将在内存中加载 BSD 的内核,这样 BSD 将开始启动。为了实现这种链加载,我们需要通过grub.conf文件告诉 GRUB BTX 第一部分的位置。位置将是硬盘号 1 和分区号 3,但是 GRUB 从 0 开始计数,所以位置将是硬盘号 0 和分区号 2。/boot/grub.conf中的条目如下:

              title pc-bsd               <<<---- the os entry title
              rootnoverify (hd0,2)       <<<---- location of BTX
              chainloader +1             <<<---- grub will chainload the BTX

如图 2-75 所示,其他操作系统条目与 BSD 类似;只有分区号会改变。

img/493794_1_En_2_Fig75_HTML.jpg

图 2-75

调整了 RHEL 6 的 grub.conf 文件

重启后,GRUB 会显示上面提到的title条目。见图 2-76 。

img/493794_1_En_2_Fig76_HTML.jpg

图 2-76

RHEL 6 显示的 GRUB 欢迎屏幕

如果用户选择 Windows,它将调用 BCD 的 part-2,它位于第一个主节点的 31 KB 空间中。这个 31 KB 的空间也被称为卷引导记录 (VBR)。我故意跳过了 VBR 的解释,因为它会造成不必要的混乱。因此,在 Windows 链加载的情况下,请记住,将调用 part-2 而不是 part-1。对于那些想了解更多关于 VBR 的信息的人来说,MBR 是硬盘的主引导记录,位于硬盘的第一个扇区。每个卷(认为是分区)都有自己的引导记录,称为 VBR,作为分区的第一个扇区。两个相似事物的两个名称。

因此,BCD 的 part-2 将调用 BCD 的 part-3,它在第一个主分区中。它将读取 BCD OS 条目(bcdedit.exe),如图 2-77 所示,并将它们打印在屏幕上。

img/493794_1_En_2_Fig77_HTML.jpg

图 2-77

BCD 引导程序显示的操作系统条目

如果用户选择早期版本的 Windows,正如我们前面看到的(在 Windows 7 的引导序列期间),它将运行 NTLDR 的第三部分,这也是在第一个主分区上。如图 2-78 所示,NTLDR 将从 c 盘读取boot.ini文件并打印 OS 条目。

img/493794_1_En_2_Fig78_HTML.jpg

图 2-78

win2k3 的 NTLDR 显示的操作系统条目

如果用户选择 XP,NTLDR 的 part-3 知道 XP 的内核在哪里。而是用户选择 win2k3,然后同样的 NTLDR 会在内存中加载 win2k3 的内核。

如果用户选择 OpenSolaris,请参见图 2-79 ,这是 RHEL 提供的主引导屏幕。

img/493794_1_En_2_Fig79_HTML.jpg

图 2-79

RHEL 显示的操作系统条目

以下是 GRUB 将遵循的指令:

title Solaris
      rootnoverify (hd0,1)
      chainloader  +1

所以,RHEL GRUB 的 part-3 会将控制权移交给第二个主分区的引导程序,但是请记住,win2k3 已经清除了 OpenSolaris GRUB 的 part-1。因此,如图 2-80 所示,它将无法启动。

img/493794_1_En_2_Fig80_HTML.jpg

图 2-80

OpenSolaris 无法引导

这意味着我们需要首先修复 OpenSolaris 引导程序。要解决这个问题,我们需要从 OpenSolaris live CD 映像启动,我们用它来安装 OpenSolaris,启动后,将 GRUB 的 part-1 和 part-2 (part-2 不是必需的,但有利于重新安装)从 live CD 安装到 OpenSolaris 分区的保留的 512 字节+ 31 KB。我们将使用的命令是installgrub。顾名思义,该命令将从实时映像中复制 GRUB 的 part-1 ( stage1)和 part-2 ( stage2)并将它们放在 OpenSolaris 分区的 512 字节+ 31 KB 空间中。图 2-81 显示了操作中的命令。

img/493794_1_En_2_Fig81_HTML.jpg

图 2-81

installgrub 命令

#installgrub  /boot/grub/stage1  /boot/grub/stage2  /dev/rdsk/c4d1s0

重启后,RHEL 将再次显示相同的操作系统条目(图 2-82 ),因为对 RHEL 来说没有任何变化。

img/493794_1_En_2_Fig82_HTML.jpg

图 2-82

RHEL 显示的操作系统条目

如果这次我们选择 OpenSolaris,那么 RHEL GRUB 的 part-3 将从第二个分区链接加载 OpenSolaris GRUB 的 part-1。Part-1 将调用 part-2,最终它将从实际的 OpenSolaris 分区调用 part-3。OpenSolaris GRUB 的第三部分将读取/rpool/boot/grub/menu.lst,如图 2-83 所示,它将在屏幕上打印标题。

img/493794_1_En_2_Fig83_HTML.jpg

图 2-83

OpenSolaris 显示的操作系统条目

如果用户选择 OpenSolaris,那么 OpenSolaris GRUB 的第三部分将从/boot加载内核。如果用户选择 Windows,那么 OpenSolaris GRUB 的第三部分将遵循来自/rpool/boot/grub/menu.lst的这些指令:

title Solaris
      rootnoverify (hd0,1)
      chainloader  +1

我们现在知道屏幕上会出现什么(参见图 2-84 )。

img/493794_1_En_2_Fig84_HTML.jpg

图 2-84

BCD 显示的 OS 项目

如果用户选择我们已经讨论过的早期版本的 Windows,故事将会继续。回到最初的操作系统列表,图 2-85 显示了 RHEL 的 GRUB 所呈现的内容。

img/493794_1_En_2_Fig85_HTML.jpg

图 2-85

RHEL 显示的操作系统条目

如果用户选择启动 BSD,您就知道会发生什么。RHEL GRUB 的第三部分将从第三个主要分区链接 BTX 的第一部分。BTX 的第一部分将呼叫第二部分,第二部分将呼叫 BTX 的第三部分。BTX 的第三部分将显示欢迎界面,如图 2-86 所示。

img/493794_1_En_2_Fig86_HTML.jpg

图 2-86

PC-BSD 的欢迎屏幕

一旦选择启动,BTX 的第三部分将在内存中加载 BSD Unix 的内核。因此,所有的操作系统,无论我们目前安装的是哪一个,现在都能够引导,并且哪个分区是活动的并不重要。但是我们能黑掉 Windows bootloaders 并强迫他们从我们的列表中引导 Linux 和 Unix 操作系统吗?我们可以,这就是我们现在要做的。

入侵 Windows 引导程序

欺骗 Windows 引导程序其实很容易。正如我们前面看到的,靴带装载者做链式装载;例如,part-1 调用它的 bootloader 的 part-2 等等。为了理解这个窍门,我们以 BSD 为例。BCD 的 Part-1 正在调用它的 part-2,但是如果我们告诉 BCD 的 part-1 链接加载 RHEL 的 part-1,那么 RHEL 的 part-1 将运行,并且它最终将遵循它自己的引导序列。GRUB (RHEL)的 Part-1 将调用 GRUB 的 part-2,并且它最终将链接加载 GRUB 的 part-3,因为 part-3 的块地址是硬编码在 part-2 中的。这意味着一旦任何引导装载程序的第一部分运行,它将开始遵循自己的引导序列,我们将利用这一行为。

为了实现这一点,我们需要获得每个非基于 Windows 的引导装载程序的第一部分,并将其放入 Windows 文件系统。因此,文件系统可以是 FAT32 或 NTFS。显然,将每个非基于 Windows 的引导加载程序的第一部分放在第一主分区上具有最大的优势,因为每个 Windows 操作系统都在第一主分区上安装了它们各自的引导加载程序。于是,通过dd命令,我们将复制每一个非基于 Windows 的 OS 的前 512 个字节(甚至前 440 个字节就足够了),并把它们放在 XP 的分区中。让我们挂载第一个主分区,如图 2-87 所示。

img/493794_1_En_2_Fig87_HTML.jpg

图 2-87

mount 命令

让我们复制前 512 个字节,并将它们放在 sda1 分区上。参见图 2-88 。

img/493794_1_En_2_Fig88_HTML.jpg

图 2-88

将前 512 个字节传输到第一主节点

现在我们将在 XP 中启动,如图 2-89 所示,我们将在boot.ini文件中添加 part-1 文件条目。boot.ini文件由两个 Windows bootloaders 读取,分别是 BCD 和 win2k3 的 NTLDR。

img/493794_1_En_2_Fig89_HTML.jpg

图 2-89

在 boot.ini 文件中添加条目

以下是我们添加的条目:

c:\RHEL.out="RHEL"
c:\SOLARIS.out = "SOLARIS"
c:\BSD.out="BSD"

就像grub.conf文件一样,boot.ini中写在双引号中的任何内容都将被视为 OS 条目的标题。现在让我们重新启动系统,从 RHEL 操作系统列表中选择 Windows 操作系统条目(参见图 2-90 )。

img/493794_1_En_2_Fig90_HTML.jpg

图 2-90

RHEL 展示的操作系统列表

我们如何到达这个屏幕很容易理解。

  1. 系统首先进入 BIOS,然后是 POST,然后是 BIOS,然后是前 512 个字节,然后是 RHEL (GRUB)的引导程序(第一部分)。

  2. 然后是 GRUB 的 part-1,它跳转到 GRUB 的 part-2,后者跳转到 GRUB 的 part-3,后者转到/boot/grub.conf,后者打印操作系统标题。

  3. 用户选择了 Windows,因此接下来是来自第一个主分区的 BCD 的第一部分,然后是 BCD 的第二部分。

  4. 最后,它转到第三部分,然后是bcd.exe,它将读取boot.ini文件,写在双引号中的内容将被打印在屏幕上。

操作系统列表如图 2-91 所示。

img/493794_1_En_2_Fig91_HTML.jpg

图 2-91

Windows 7 显示的操作系统条目(BCD)

如果用户选择早期版本的 Windows,那么 BCD 的 part-3 将调用 win2k3 的 NTLDR 的 part-3。NTLDR 将再次读取boot.ini文件并打印 OS 列表,如图 2-92 所示。

img/493794_1_En_2_Fig92_HTML.jpg

图 2-92

win2k3 的 NTLDR 显示的操作系统条目

如果用户选择 OpenSolaris,那么 NTLDR 的第三部分将从 C:(第一个主分区)运行Solaris.out文件。Solaris.out文件只不过是来自第二个分区的 OpenSolaris 引导程序的第一部分。OpenSolaris 引导加载程序的第一部分将调用 GRUB 的第二部分,并最终调用第三部分。它将读取menu.lst文件并打印操作系统列表(图 2-93 )。

img/493794_1_En_2_Fig93_HTML.jpg

图 2-93

OpenSolaris GRUB 显示的操作系统条目

如果用户再次选择 Windows,那么 OpenSolaris 的 part-3 将从第一个主分区(rootnoverify (hd0,0))调用 BCD 的 part-2。(BCD 的第二部分将位于第一个主分区的 VBR 部分。我们不会在本书中涉及 VBR。)BCD 的 part-2 会调用 BCD 的 part-3。它将通过bcdedit.exe和从boot.ini读取操作系统条目,并打印操作系统条目。在图 2-94 中可以看到屏幕上打印的操作系统条目。

img/493794_1_En_2_Fig94_HTML.jpg

图 2-94

Windows 7 显示的操作系统条目(BCD)

这就是我们如何创建一个引导程序的循环(参见图 2-95 和图 2-96 )。

img/493794_1_En_2_Fig96_HTML.jpg

图 2-96

RHEL 的 GRUB 显示的操作系统条目

img/493794_1_En_2_Fig95_HTML.jpg

图 2-95

RHEL 条目已被选中启动

大家可以看到,Linux 在引导 Windows,Linux 在引导 Unix,Unix 在引导 Windows,Windows 在引导 Windows,Windows 在引导 Linux,但是还缺少一点,那就是 Linux 在引导 Linux。为此,我们将安装列表中的最后一个操作系统,那就是 Fedora 15。

Fedora 15

如图 2-97 所示,我们正在 sda8 上安装 Fedora 15。

img/493794_1_En_2_Fig97_HTML.jpg

图 2-97

Fedora 安装程序

默认情况下,Fedora 会尝试在第一个主服务器上安装它的引导程序,但是如果我们允许的话,那么我们需要在它的grub.conf中添加所有其他操作系统的条目。相反,我们将遵循不同的方法。我们将把 Fedora (GRUB)的 bootloader 安装在它自己的分区(sda8)上,而不是 sda1。见图 2-98 。

img/493794_1_En_2_Fig98_HTML.jpg

图 2-98

引导加载程序设备选择

这意味着重启后 Fedora 将永远无法启动,因为 RHEL 的 GRUB 不知道这个新操作系统,所以我们需要将 Fedora 的条目添加到 RHEL 的grub.conf中。为此,让我们安装 sda8,如图 2-99 所示。

img/493794_1_En_2_Fig99_HTML.jpg

图 2-99

软呢帽隔断的安装

从 Fedora GRUB 的grub.conf文件:/mnt/boot/grub.conf中复制 Fedora 的条目(见图 2-100 )。

img/493794_1_En_2_Fig100_HTML.jpg

图 2-100

Fedora 15 的 grub.conf 文件

条目很简单。每当调用 Fedora 的 part-3 时,它都会将 Fedora 的内核从/boot/vmlinuz-2.6.38.6-26.rc1.fc15.x86_64加载到内存中。之后,它会将 initramfs 从/boot/initramfs-2.6.38.6-26.rc1.fc15.x86_64.img加载到内存中。

图 2-101 显示了从/mnt/etc/grub.conf复制 Fedora 的条目后 RHEL 的/etc/grub.conf文件。

img/493794_1_En_2_Fig101_HTML.jpg

图 2-101

RHEL 的 grub.conf 文件

重启后,我们会得到 Fedora 条目(图 2-102 )。

img/493794_1_En_2_Fig102_HTML.jpg

图 2-102

RHEL 显示的操作系统条目

当用户选择 Fedora 进行引导时,按照 RHELgrub.conf文件中的条目,RHEL GRUB 的第三部分将从第八个分区(Fedora 的 sda8)加载内核,还将从同一位置加载 initramfs(我们将在第五章中讨论 initramfs),引导加载程序将离开。

完整的流程图

图 2-103 显示了到目前为止我们已经安装的每个操作系统的完整流程图。

img/493794_1_En_2_Fig103_HTML.jpg

图 2-103

所有操作系统的完整流程图

我希望你现在理解了引导程序在基于 BIOS 的系统上引导操作系统的方式。现在是时候了解一下新的固件了,它就是统一可扩展固件接口(UEFI)。

统一可扩展固件接口

以下是迄今为止您观察到的 BIOS 限制:

  • 您只能有四个主分区。

  • BIOS 无法读取逻辑分区。

  • BIOS 有点笨;它会跳到你硬盘的第一个扇区。

  • 基于 BIOS 的系统的最大分区大小为 2.2 TB。

为什么会有这样的局限性?BIOS 固件是在 1982 年为 IBM PC-5150(图 2-104 )设计的,它曾经有这样的配置:

img/493794_1_En_2_Fig104_HTML.jpg

图 2-104

IBM PC-5150

CPU       = 8088 - 16bit x86 processor
Memory    = upto 256KB max
OS        = MS-DOS

如你所见,BIOS 是 38 年前为这台 PC 设计的。在这三十年里,操作系统从软盘发展到 NVME 磁盘,从文本模式发展到闪亮的图形用户界面。硬件设备从驱动程序到即插即用,但 BIOS 保持不变,最初是 16 位指令集,后来开始使用 32 位指令集。现在我们有 64 位 CPU,但 BIOS 仍然是由 32 位指令组成的。我们没有将 BIOS 升级到 64 位的原因是因为历史原因。当一切都正常工作时,为什么要重写一些东西呢?无论如何,这是计算机行业采用的哲学。当 CPU 从 16 位(8088)变为 64 位(i9)时,BIOS 保持在 16 位或 32 位,因为在引导的早期阶段,没有必要拥有 64 位 CPU,这就是我们拥有 CPU 模式(实模式、受保护模式和长模式)的原因。

在实模式下,CPU 将被限制为 16 位。在这种模式下,具有 16 位指令的旧 BIOS 等程序将运行。这些程序不能在任何其他模式下运行。稍后,CPU 将从实模式切换到保护模式。保护模式是 32 位,现在的程序,如 BIOS,具有 32 位指令集,将在这种模式下运行,后来 CPU 将被置于长模式,即 64 位。记住,这些模式不是由 CPU 实现的;相反,它们是由 BIOS 之类的固件实现的。这意味着,如果我们将同一个 CPU 从启用实模式的系统中移除,并将其放在没有实模式的系统上,则同一个 CPU 将直接以保护模式启动。我们将在第四章再次谈到这些模式。

由于 BIOS 在保护模式下运行,BIOS 可用的地址空间只有 4 GB。如果系统有 20 GB 内存,BIOS 最多只能寻址 4 GB。尽管系统有一个 64 位的 I9 处理器,BIOS 仍然只能使用其中的 32 位。由于这些硬件挑战,BIOS 受到了限制。

BIOS 限制

以下是 BIOS 的一些限制:

  • BIOS 将只能跳到第一个扇区,即 512 字节。

    • 大小为 64 字节的 MBR 是第一个引导扇区的一部分。如果我们增加 MBR 的大小,它将超过 512 字节;因此,我们不能增加 MBR 的大小,这就是 BIOS 只能提供四个主分区的原因。
  • BIOS 不能生成好的图形/图形用户界面。

    • 现在,这是一个通用的说法,用于与 UEFI 进行比较。有一些 BIOS 供应商已经在操作系统之外实现了 web 浏览器,但是这种实现在普通的桌面硬件上很少见。

    • 此外,在 Phoenix,一些 BIOS 实现中有一个 FAT32 驱动程序,通过它可以在设置中显示图标。

  • 在 BIOS 中不能使用鼠标。

    • 有许多 BIOS 供应商都有鼠标支持,但是在普通的桌面系统中很少见到。
  • 最大分区大小为 2.2 TB。

    • BIOS 使用并支持 MS-DOS 分区表,这是一个非常老的表,它有自己的缺点,比如最大分区大小为 2.2 TB。
  • BIOS 是哑的,因为它不理解引导装载程序或操作系统。

  • 由于硬件的限制,速度很慢。

    • 就启动速度而言,BIOS 很慢,因为它需要时间来初始化硬件。

    • BIOS 需要将近 30 秒的时间来启动实际的操作系统引导。

  • 它努力初始化新一代硬件设备。

  • BIOS 的预启动工具有限。

    • 与 UEFI 固件相比,BIOS 具有非常少的预启动工具,如远程硬件诊断等。

因此,为了克服所有这些 BIOS 限制,英特尔于 1998 年启动了一项名为英特尔启动计划(IBI)的计划;后来,它成为可扩展固件接口(EFI)。其他所有可能的操作系统和硬件供应商(惠普/苹果/戴尔/微软/IBM/华硕/AMD/美国大趋势/凤凰科技)都加入了英特尔。他们为这个项目做了一个开源论坛,最后变成了统一可扩展接口(UEFI)。

开源代码是在 BSD 许可下签署的,但是英特尔的基础代码仍然是专有的。UEFI 基本上是一个开源框架,厂商基于 UEFI 提供的规范在其上构建他们的应用。org 。比如美国大趋势打造 APTIO,凤凰科技打造 SecureCore UEFI 固件。苹果是第一个敢于推出内置 UEFI 固件的系统的公司。BIOS 的所有缺点都是因为它的 16 位指令集。由于这个 16 位指令集将 BIOS 硬件的使用限制在 1 MB 的地址空间,UEFI 瞄准并解决了这一限制。

UEFI 优势

UEFI 支持 64 位处理器;因此,它不会面临 BIOS 所面临的任何硬件限制。

  • UEFI 是个小 OS。

    1. 您将可以完全访问音频和视频设备。

    2. 您将能够连接到 WiFi。

    3. 您将能够使用鼠标。

    4. 在 GUI 方面,UEFI 将提供丰富的图形界面。

    5. UEFI 将拥有自己的应用商店,就像我们为 Android 和苹果手机所做的那样。

    6. 你可以从 UEFI 应用商店下载并使用这些应用,就像在安卓和苹果手机上一样。数百个应用程序可用,如日历、电子邮件客户端、浏览器、游戏、Shell 等。

    7. UEFI 能够运行任何具有 EFI 可执行格式的二进制文件。

    8. 它借助安全引导功能安全地引导操作系统。我们将在本书后面深入讨论安全引导特性。

    9. UEFI 是向后兼容的,这意味着它将支持“BIOS 方式”的引导。换句话说,不支持 UEFI 的操作系统也可以使用 UEFI 启动。

  • UEFI 可以使用全部 CPU。不像 BIOS(被 16 位处理器卡住),UEFI 最多可以访问 64 位。

  • UEFI 可以使用全 RAM 模块。与 BIOS 的 1 MB 地址空间不同,UEFI 可以支持和使用太字节的 RAM。

  • UEFI 使用 GPT (GUID)分区表,而不是 64 字节的微型 MBR,这将提供无限数量的分区,并且所有分区都将是主分区。事实上,没有主分区和逻辑分区的概念。

  • 最大分区大小为 8z 字节。

  • UEFI 有企业管理工具。

    1. 要更改 BIOS 的设置,我们必须重新启动系统,因为操作系统在长模式下运行,而 BIOS 在实模式下运行,而实模式只能在启动时运行。

    2. 你将能够远程修理计算机。

    3. 您将能够在 UEFI 固件中浏览互联网。

    4. 您将能够从操作系统中更改 UEFI 固件行为/设置。

UEFI 的图形用户界面

图 2-105 显示了华硕的 GUI 实现。

img/493794_1_En_2_Fig105_HTML.jpg

图 2-105

华硕 UEFI 实施

以下是一些需要注意的事项:

  • 丰富的 GUI

  • 鼠标指针

  • 图标、按钮、滚动选项、动画、图形、下拉选项等。

当然,你需要一个昂贵的主板来获得如此丰富的 UEFI 实现,但即使是基本的 UEFI 实现也比 BIOS 实现好得多。

UEFI 实施

UEFI 论坛发布了 UEFI 规范。写这本书时,当前的 UEFI 规范是 2.8,可以从 https://uefi.org/specifications 下载。目前的规范长达 2551 页,每个厂商(主板、OS、UEFI 开发者等。)也只好同意了。该规范强制每个供应商都必须遵守的法规。以下是 UEFI 的一些主要规定。

EFI 系统分区(ESP)

每个操作系统供应商都必须创建一个 EPS 分区,并且引导加载程序只能安装在这个分区中。没有必要将 ESP 创建为第一个分区;它可以在任何地方创建,但是 ESP 应该有 FAT16/32(最好是 FAT32)文件系统。建议的 ESP 大小至少为 256 MB。操作系统供应商必须在 ESP 中创建以下目录结构:

EFI System Partition
      ├── EFI
      │     ├── <OS_vendor_name>
      │     │         ├── <boot_loader_files>

一旦创建了这个结构,操作系统只需要在/EFI/<os_vendor_name>/位置安装引导装载程序。图 2-106 显示了 UEFI 的结构。

img/493794_1_En_2_Fig106_HTML.jpg

图 2-106

UEFI 结构

这意味着,就像为引导加载程序保留的 512 字节+ 31 KB 空间一样,我们在 UEFI 中为引导加载程序保留了 256 MB 的最小专用空间。ESP 分区将被挂载到 Linux 中的挂载点/boot/efi下。

=electronicfuelinjection 电子燃料的注入

每个操作系统供应商都必须以 EFI 可执行格式编写引导程序文件。此外,文件应该有.efi扩展名。

安全启动

UEFI 提供的最佳特性之一是安全引导。该特性由微软提出,后来被添加到 UEFI 规范中。微软首次在 Windows 8 中使用安全引导功能。一旦我们熟悉了 UEFI 的工作原理,我们将详细讨论安全引导。

分区表

推荐的分区表是 GPT,它是一个 GUID 分区表,而 BIOS 使用的是 MS-DOS 分区表。

为了更好地理解 UEFI,我们将使用与 BIOS 相同的方法。我们将使用一个名为 UEFI 的新系统,它上面有 UEFI 固件,我们将在其中安装一些操作系统。

操作系统列表

如你所知,UEFI 使用 GPT 分区表;因此,没有主要或次要/逻辑分区的概念。这也意味着操作系统的安装没有特别的优先权。你可以用任何你想要的方式安装操作系统。我们将按以下顺序安装操作系统:

  1. Ubuntu 18

  2. Windows 10

  3. Fedora 31

Ubuntu 18.04 lt

我们有将近 64.4 GB 的硬盘。没有必要像我们使用 BIOS 那样使用类似 GParted 的工具来创建分区布局。我们将使用 Ubuntu 提供的默认磁盘工具。参见图 2-107 。

img/493794_1_En_2_Fig107_HTML.jpg

图 2-107

Ubuntu 提供的磁盘布局

如图 2-108 所示,我们先创建一个 3 GB 的 ESP 分区。

img/493794_1_En_2_Fig108_HTML.jpg

图 2-108

创建 ESP 分区

一旦创建了 ESP,我们将为 Ubuntu 的根文件系统再创建一个分区(10 GB)。图 2-109 显示了 Ubuntu 最终的分区布局。

img/493794_1_En_2_Fig109_HTML.jpg

图 2-109

Ubuntu 的分区布局

安装完成后,在图 2-110 中可以看到,ESP 挂载在/boot/efi上,根文件系统挂载在 sda2 上。

img/493794_1_En_2_Fig110_HTML.jpg

图 2-110

挂载点

此外,根据 UEFI 规范,Ubuntu 在/boot/efi (sda1)挂载点创建了一个目录结构/EFI/ubuntu,并在其中安装了 GRUB bootloader。参见图 2-111 。

img/493794_1_En_2_Fig111_HTML.jpg

图 2-111

Ubuntu 的 EFI 目录

还要注意引导程序文件的.efi扩展名。以下是 UEFI 系统上的 Ubuntu 引导序列:

  1. 打开系统电源。

  2. 它进入 UEFI 固件。UEFI 发布帖子。

  3. 开机自检检查硬件,如果一切正常,会发出正常的蜂鸣声。

  4. 帖子回到 UEFI。

  5. UEFI 很聪明;UEFI 不是跳到前 512 个字节,而是找到 ESP 分区。

  6. It jumps inside ESP. Again, UEFI is smart, and it understands the bootloader. It lists the bootloader’s name on the screen. In Ubuntu’s case, it sees the grubx64.efi file; hence, it lists the Ubuntu name in the boot priority of UEFI. Please refer to Figure 2-112, where you can see the ubuntu entry inside UEFI’s boot priority menu.

    img/493794_1_En_2_Fig112_HTML.jpg

    图 2-112

    UEFI 的引导优先级窗口

  7. 记住,引导装载程序还没有被 UEFI 调用或启动。BIOS 过去只向您显示可用的引导设备名称,如 CD-ROM、HDD 和 PXE,但 UEFI 会进入设备内部检查 ESP 分区,并直接显示操作系统名称。

  8. The moment the user chooses the Ubuntu option, UEFI will run grubx64.efi from the ESP partition. The absolute path will be /boot/efi/EFI/ubuntu/grubx64.efi Next, grubx64.efi will read grub.cfg, which is present in the same directory, and as shown in Figure 2-113, it will print the title entries.

    img/493794_1_En_2_Fig113_HTML.jpg

    图 2-113

    Ubuntu 的欢迎界面

    With the BIOS, there used to be jumps like this:

    1. 转到 fdisk 签名,转到引导程序的第一部分,然后转到引导程序的第二部分。

    2. 转到引导程序的第三部分,然后转到引导程序配置文件,如menu.lstgrub.cfg

    3. 打印标题。

使用 UEFI,跳过(a)跳。UEFI 直接跳到(b)。BIOS 曾经因为空间限制把 bootloader 分成三个部分,但是 UEFI 没有任何空间限制。因此,整个引导装载程序只包含在一个二进制文件中。例如,在 Ubuntu 的情况下,grubx64.efi有一个、两个和三个部分都添加在一个二进制文件中,这个二进制文件就是grubx64.efi

grubx64.efi文件最终会将内核(vmlinuz)和 initramfs 从/boot加载到内存中,然后 Ubuntu 的 GRUB bootloaders 工作就完成了。图 2-114 显示了 Ubuntu 的引导顺序流程图。

img/493794_1_En_2_Fig114_HTML.jpg

图 2-114

Ubuntu 的引导顺序

Windows 10

如图 2-115 所示,分区 1 是 ESP,分区 2 是 Ubuntu 的根(/)。

img/493794_1_En_2_Fig115_HTML.jpg

图 2-115

Windows 10 显示的分区布局

现在我们将为 Windows 创建一个新的分区。创建新分区时,Windows 将为名为 MSR (Microsoft Recovery,partition 3)的 Windows 恢复工具保留一些空间。参见图 2-116 。

img/493794_1_En_2_Fig116_HTML.jpg

图 2-116

MSR 空间保留区

如图 2-117 所示,在新创建的分区 4 上,我们将安装 Windows 10。

img/493794_1_En_2_Fig117_HTML.jpg

图 2-117

在分区 4 上安装 Windows 10

默认情况下,Windows 会检测 ESP 分区,并遵循 UEFI 规范,在其中创建一个名为Microsoft的目录,并在其中安装引导程序(BCD)。如果 Windows 找不到 ESP,它会为我们创建一个。由于 Windows 主要面向桌面用户,所以它不会像 Ubuntu 那样向我们展示 ESP 分区(参见图 2-118 )。

img/493794_1_En_2_Fig118_HTML.jpg

图 2-118

ESP 是隐藏的

这是 Windows 10 在基于 UEFI 的系统上的启动方式:

img/493794_1_En_2_Fig121_HTML.jpg

图 2-121

Windows 10 的启动顺序

  1. 打开系统电源:首先是 UEFI,然后是 POST,然后是 UEFI,然后是 ESP。

  2. As visible in Figure 2-119, print the OS entries as per the directories found in ESP (/boot/efi/EFI).

    img/493794_1_En_2_Fig119_HTML.jpg

    图 2-119

    UEFI 中的操作系统条目

  3. 当用户选择 Windows 启动管理器时,UEFI 将从EFI/Microsoft目录启动bootmgfw.efi文件。在基于 Linux 的系统上,相同文件的绝对路径将是/boot/efi/EFI/Microsoft/bootmgfw.efi.

  4. bootmgfw.efi最终会从C:\windows\system32\开始加载 Windows 的内核。

  5. The Windows kernel will take care of the rest of the booting, and while doing that, a famous animation, shown in Figure 2-120, will be shown to users.

    img/493794_1_En_2_Fig120_HTML.jpg

    图 2-120

    著名的 Windows 加载屏幕

  6. 从图 2-121 可以看出,截至目前,只有一个 OS 在启动,那就是 Windows 10。不过不用担心,因为 Windows 10 是绑定遵循 UEFI 规范的,所以并没有触及 Ubuntu 的目录,当然也没有在自己的 bootloader 文件中添加 Ubuntu 的条目。

Fedora 31

我们将安装的最后一个操作系统是 Fedora 31。如图 2-122 所示,我们将再次创建一个标准分区,即 sda5,我们将在/boot/efi上挂载/dev/sda1 (ESP)。

img/493794_1_En_2_Fig122_HTML.jpg

图 2-122

Fedora 装置

记住,不要格式化 sda1,它是 ESP。失去 ESP 就意味着失去 Windows 和 Ubuntu 的 bootloaders。安装完成后,Fedora 的 GRUB 会给我们呈现 OS 列表(图 2-123 )。

img/493794_1_En_2_Fig123_HTML.jpg

图 2-123

Fedora 显示的操作系统条目

在安装 GRUB 时,Fedora 安装程序 Anaconda 从 ESP 中检测到其他操作系统。为了给他们一个平等的启动机会,Fedora 在grub.cfg中添加了 Ubuntu 和 Windows 条目。以下是 Fedora 的引导顺序:

img/493794_1_En_2_Fig124_HTML.jpg

图 2-124

UEFI 中的 Fedora 条目

  1. 打开系统电源:首先是 UEFI,然后是 POST,然后是 UEFI。

  2. UEFI 会跳到 ESP 里面。

  3. 它将进入 ESP 目录,并通过检查引导优先级来选择要引导的操作系统。到目前为止,引导优先级设置为 Fedora。检查图 2-124 。

img/493794_1_En_2_Fig127_HTML.jpg

图 2-127

Fedora 的引导序列

  1. Since the boot priority is set to Fedora, UEFI will go inside the /boot/efi/EFI/fedora directory (refer Figure 2-125) and will launch the file grubx64.efi.

    img/493794_1_En_2_Fig125_HTML.jpg

    图 2-125

    Fedora EFI 目录

  2. grubx64.efi will read the file grub.cfg and print the OS entries on-screen. Figure 2-126 shows this.

    img/493794_1_En_2_Fig126_HTML.jpg

    图 2-126

    Fedora 显示的操作系统条目

  3. 当用户选择 Fedora 时,同样的grubx64.efi会从/boot (sda4)加载vmlinuz和 Fedora 的 initramfs 到内存中。Fedora 内核将负责引导序列的其余部分。流程图见图 2-127 。内核采取的步骤将在第四章详细讨论。

UEFI Shell

UEFI 是一个小的操作系统。与普通操作系统一样,UEFI 提供了运行应用程序所需的环境。当然,UEFI 不能运行每一个二进制文件,但是以 EFI 可执行格式构建的二进制文件将很容易运行。UEFI 提供的最好的 app(应用/二进制)之一就是 shell。如图 2-128 所示,大多可以在 UEFI 的开机优先级设置中找到。

img/493794_1_En_2_Fig128_HTML.jpg

图 2-128

内置的 UEFI Shell

如果您的系统的 UEFI 实现不提供 shell,那么您可以从 TianoCore 项目网站或其 EDK-II GitHub 页面下载 shell 应用程序。

https://www.tianocore.org/

https://github.com/tianocore/edk2/blob/UDK2018/ShellBinPkg/UefiShell/X64/Shell.efi

用 FAT32 文件系统格式化 USB 设备,并将下载的Shell.efi文件放入其中。使用相同的 USB 设备启动,UEFI 将通过其启动优先级窗口向您呈现一个 UEFI shell。参见图 2-129 。

img/493794_1_En_2_Fig129_HTML.jpg

图 2-129

从 USB 加载的 UEFI Shell

令人惊讶的是,UEFI 没有显示系统连接了 USB 设备。相反,UEFI 进入了 USB 设备,看到了 FAT32 文件系统。它看到了shell.efi文件,并意识到这不是一个普通的 EFI 应用程序;相反,它将向用户提供 Shell。如果它是一个 BIOS,它只会显示该系统连接了 USB 磁盘,但这里 UEFI 显示您在 USB 连接的磁盘中有一个 Shell。

当您选择从 USB 驱动器启动 EFI Shell 选项时,它将执行shell.efi文件,并在操作系统不存在时向您显示一个 Shell(图 2-130 )。这是了不起的。

img/493794_1_En_2_Fig130_HTML.jpg

图 2-130

UEFI Shell

blk*条目是设备名称,而fs*是文件系统命名约定。由于 UEFI shell 能够读取 FAT32 文件系统(ESP 分区),我们可以浏览 ESP 目录,如图 2-131 所示。

img/493794_1_En_2_Fig131_HTML.jpg

图 2-131

浏览 EFI 目录

fs0代表文件系统号 0。我们可以使用 shell 的内部命令来更改分区。从图 2-132 和图 2-133 中可以看出,fs2就是我们的 ESP。

img/493794_1_En_2_Fig133_HTML.jpg

图 2-133

Ubuntu 的引导程序目录

img/493794_1_En_2_Fig132_HTML.jpg

图 2-132

EFI 目录

我们可以简单地通过 shell 运行grubx64.efi文件,GRUB 就会出现在屏幕上。参见图 2-134 。

img/493794_1_En_2_Fig134_HTML.jpg

图 2-134

乌班图的食物

对于一个 UEFI shell 来说,grubx64.efi就是一个简单的 app。以类似的方式,如图 2-135 所示,我们也可以启动 Windows 引导程序。参见图 2-136 。

img/493794_1_En_2_Fig136_HTML.jpg

图 2-136

著名的 Windows 动画

img/493794_1_En_2_Fig135_HTML.jpg

图 2-135

从 UEFI shell 启动 Windows 引导加载程序

shell 在解决“无法启动”的情况时非常有用。考虑图 2-137 所示的场景,其中系统在 GRUB 提示符下抛出一个错误。

img/493794_1_En_2_Fig137_HTML.jpg

图 2-137

系统无法启动

通过使用 UEFI shell,我们能够检查 GRUB 相关的文件是否存在。

对 UEFI 的误解

以下是对 UEFI 的一些误解。

误解 1: UEFI 是新 BIOS 或者 UEFI 是 BIOS

人们一直说 UEFI 是一个新的 BIOS。事实上,当你进入 UEFI 固件时,固件本身会说它是 UEFI BIOS。检查图 2-138 。

不,UEFI 不是 BIOS,也不是新的 BIOS。UEFI 是来代替 BIOS 的。UEFI 是一个全新的固件,你不能在同一个系统上有 BIOS 和 UEFI。你有 UEFI 或者 BIOS。

img/493794_1_En_2_Fig138_HTML.jpg

图 2-138

UEFI 不是 BIOS

识别你是否有 BIOS 或者 UEFI 是非常简单的。如果你可以在固件中使用鼠标,那么你就有 UEFI,如果你看到一个丰富的 GUI,那么你就有 UEFI。正确的检查方法是使用类似于efibootmgr的命令。

# efibootmgr -v
Fatal: Couldn't open either sysfs or procfs directories for accessing EFI variables.
Try 'modprobe efivars' as root.

如果您在 Linux 系统上从efibootmgr命令得到这样的输出,那么您就有了一个 BIOS。如果你得到这样的东西,那么你就有 UEFI:

# efibootmgr -v

BootCurrent: 0005
Timeout: 2 seconds
BootOrder: 0005,0004,0003,0000,0001,0002,0006,0007,000A
Boot0000* EFI VMware Virtual SCSI Hard Drive (0.0)
      PciRoot(0x0)/Pci(0x15,0x0)/Pci(0x0,0x0)/SCSI(0,0)
Boot0001* EFI VMware Virtual SATA CDROM Drive (1.0)
      PciRoot(0x0)/Pci(0x11,0x0)/Pci(0x4,0x0)/Sata(1,0,0)

这是识别系统固件的正确方法。回到我们的 UEFI BIOS 讨论,供应商们一起使用 UEFI 和 BIOS 术语,因为大多数用户不理解术语 UEFI。例如,一篇文章说“更改您的 UEFI 中的参数”可能会让大多数用户感到困惑,但说“更改您的 BIOS 中的参数”会被每个人很好地理解。因此,供应商使用术语 UEFI/BIOS 只是为了便于理解,但是记住你一次只能有一个固件,不能两个都有。

误解二:微软是邪恶的

正如我们所见,UEFI 是一个论坛,操作系统厂商是其中的一部分,包括微软。为了使引导更加安全,微软在 UEFI 中提出了安全引导功能。安全引导将在引导时停止未授权或受损二进制文件的执行。这解决了这三个问题:

  • 它保证即将运行的grubx64.efi来自一个真实的来源。

  • 它保证 BCD 没有任何后门。

  • 如果某个东西未经授权,它会阻止它执行。

安全启动是这样工作的:

  1. Microsoft 将生成一个密钥对(公钥和私钥)。

  2. 微软将用私钥对其引导程序或文件进行数字签名。

  3. 微软的公钥将保存在 UEFI 固件中。

  4. 步骤 2 中生成的数字签名将由 UEFI 中的微软公钥重新生成。

  5. 如果数字签名匹配,那么只有 UEFI 将允许执行*.efi文件。

  6. 如果数字签名不匹配,那么 UEFI 将认为这是一个有害的程序,或者至少它不是微软提供的,UEFI 将停止执行。

微软的实现相当不错,对吧?是的,它是。但是当安全引导特性被启用并且您选择 Linux 来引导时,问题就出现了。UEFI 将取出微软的公钥,并将生成grubx64.efi的数字签名。生成的数字签名当然不会和微软的 bootloader 文件匹配,所以会被认为是未授权程序,UEFI 会停止执行。换句话说,Linux 或任何非 Windows 操作系统将永远无法启动。那么,这个问题的解决方案是什么?简单:UEFI 应该提供一个选项来禁用安全引导特性,它确实做到了。参见图 2-139 。事实上,禁用安全引导功能的选项必须存在于 UEFI 固件中。这是 UEFI 规范中规定的。

img/493794_1_En_2_Fig139_HTML.jpg

图 2-139

禁用安全引导功能

但是微软已经明确表示,只有启用了安全引导的系统才能通过认证。这意味着,如果您是硬件供应商,并希望您的系统获得 Windows 认证,那么它必须启用安全引导。这一举动被一些行业领导者认为是“邪恶的”,因为非基于 Windows 的操作系统将不能在相同的硬件上启动。我们后面还会回到微软到底是不是邪恶的讨论,但是先来看看非 Windows OSs 有哪些选项。

Linux 供应商应该制作他们自己的密钥对

是的,每个 Linux 操作系统供应商都应该制作他们自己的密钥对,然后用他们的私钥签署他们的引导加载程序,并将公钥保存在 UEFI 固件中。每当用户选择 Windows 引导时,UEFI 将使用 Windows 公钥,每当用户选择 Linux 引导时,UEFI 将使用 Linux 公钥重新生成 Linux 引导加载程序文件的数字签名。这似乎是一个简单的解决办法,但这是行不通的。市场上几乎有 200 多个活跃的 Linux 发行版,它们通常每六个月发布一个新版本。这意味着几乎每六个月你就会有一个新版本的 Linux 发行版上市。这大致意味着 Linux 供应商每年将有近 400 个密钥,所以很明显你不能在 UEFI 中容纳这么多的密钥。即使你可以,这也会妨碍 UEFI 设计的主要格言之一,即快速启动。因此,简而言之,这不可能是一项决议。

所有 Linux 供应商应该只制作一个密钥对

这也不能成为一项决议。有 200 多个活跃的 Linux 发行版,它们的办公室遍布世界各地。如果所有的 Linux 供应商走到一起,只制作一个密钥对,那么这个密钥对将不得不通过互联网发送给全世界的开发者。这将是一场安全噩梦。所以简而言之,这将很难维持;因此,这不是一项决议。

禁用 UEFI 的安全引导功能

这似乎是唯一可行的方法。UEFI 确实提供了禁用安全引导功能的工具,微软不反对提供这样的工具。例如,假设你有一个双启动系统,安装了 Windows 10 和 Fedora 31。如果您想引导 Windows,则必须在 UEFI 中启用安全引导,如果下次您想引导 Linux,则必须进入 UEFI,将启用的安全引导更改为禁用状态。您可以认为这是一种变通方法,但这并不实际;因此,它不能被视为一项决议。

那么,Linux 如何利用安全引导呢?解决方法只有一个,那就是使用微软的私钥对 Linux 引导程序文件进行数字签名,你猜怎么着,微软已经同意了这一点。因此,在这个阶段,Linux 能够通过使用微软的密钥对来安全引导,因此微软肯定不是邪恶的。它只是想让它的启动序列安全。

但是这种安排有一个问题;GRUB 开发将依赖于微软的密钥对。如果 GRUB 有任何新的变更,我们需要使用微软的密钥重新签名。Ubuntu 首先通过引入一个叫做 shim 的更小的引导程序解决了这个问题。这个引导装载程序应该用微软的密钥签名,然后这个引导装载程序的工作是调用实际的引导装载程序,也就是 GRUB。通过这种方法,Linux 世界打破了微软的签名依赖。因为 shim 永远不会改变(至少很少改变),所以 GRUB 开发将会继续它的方式。

因此,如果启用了安全引导,那么 Linux 的引导顺序如下:

  1. 打开系统电源:首先是 UEFI,然后是 POST,然后是 UEFI。

  2. ESP 列出了操作系统和可用的可引导设备。

  3. 如果用户选择 Linux,引导过程将使用微软的公钥重新生成shim.efi文件的数字签名。

  4. 如果数字签名匹配,则允许执行shim.efi

  5. shim.efi会调用原来的 bootloader,也就是grubx64.efi

  6. grubx64.efi将从 ESP 中读取grub.cfg文件,并显示可用操作系统列表。

  7. 如果用户再次选择 Linux,那么同一个grubx64.efi文件将开始在内存中加载内核和 initramfs。

参见图 2-140 查看该启动序列中涉及的文件列表。

img/493794_1_En_2_Fig140_HTML.jpg

图 2-140

所述引导序列中涉及的文件

误解 3:禁用 UEFI

最大的误解之一是你可以禁用 UEFI 并启动 BIOS。不,您不能禁用您系统的固件;此外,一个系统上不能有两个固件。你有 UEFI 或者 BIOS。当人们说“禁用 UEFI”时,这意味着他们想说,让 UEFI 用 BIOS 或传统方式启动。UEFI 的最大特点之一是它是向后兼容的,这意味着它理解 BIOS 的引导方式,即 512 字节+ 31KB 的方式。因此,当您将 UEFI 设置从 UEFI 方式更改为传统方式时,这仅意味着 UEFI 不会遵循 ESP 引导方式。相反,固件将遵循 BIOS 引导方式,但这并不意味着您禁用了 UEFI 固件。当你以 BIOS 方式启动 UEFI 系统时,你会失去 UEFI 提供的所有功能。

既然您现在对固件和引导加载程序的工作方式有了更好的理解,那么现在是深入研究 GRUB 引导加载程序的时候了。

三、GRUB 开机管理程序

Linux 系统现在使用的引导装载程序是 GRUB 版本 2。GRUB 2 的第一个稳定版本是在 2012 年,但它是在 2014 年随着 Centos 7 和 RHEL 7 开始出现在企业级 Linux 中的。2015 年后,它在几乎所有流行的 Linux 发行版中都被广泛采用。通常当用户提出错误或要求新功能时,开发人员会听取反馈,区分工作的优先级,并最终推出新版本的代码。然而,在 GRUB 的情况下,它以另一种方式工作。当用户对 GRUB Legacy(版本 1)感到满意时,开发人员决定改变 GRUB 2 的整个结构。

  • “由于混乱的代码和设计失败,GRUB Legacy 已经变得不可维护。我们收到了许多功能请求,并且在没有重新设计框架的情况下,将 GRUB 扩展到了原来的范围之外。这导致了一种状态,即如果不从根本上重新思考一切,就不可能进一步扩展 GRUB。”

  • —gnu grub 常见问题解答( https://www.gnu.org/software/grub/grub-faq.html

以下是 GRUB 2 提供或正在开发的一些特性:

  • 完全支持 USB。

  • Linux 统一设置密钥(LUKS)支持。LUKS 是 Linux 硬盘加密的标准。

  • 一个别致的菜单实现,将有动画,彩色效果,样式表等。

  • 一个“分开的”工具将被添加到引导装载程序中。添加后,用户将能够在引导时编辑磁盘配置。

本章将涵盖以下内容:

  • 如何为 BIOS 和 UEFI 固件实现 GRUB 2

  • GRUB 2 中固件特定的结构变化

  • GRUB 2 的引导程序规范特性

  • UEFI 的安全引导特性及其在 GRUB 2 中的实现

  • 几个与引导程序相关的问题以及我们如何修复它们

GRUB 2 实现

正如我们到目前为止所看到的,GRUB 控制了固件。这意味着它必须处理 UEFI 以及 BIOS。我们先来看看 GRUB 2 是如何在基于 BIOS 的系统上实现的。

基于 BIOS 的系统上的 GRUB 2

基于 BIOS 的系统上的 GRUB 2 将其所有文件保存在三个不同的位置。

  • /boot/grub2/

  • /etc/default/grub

  • /etc/grub.d/

以 Ubuntu 为例,GRUB 的名字中没有使用版本 2,所以会用/boot/grub/代替/boot/grub2/,用grub-install代替grub2-install,或者用grub-mkconfig代替grub2-mkconfig

让我们讨论一下位置和它们的内容。

/boot/grub2

这是 GRUB 2 的安装位置。正如你在图 3-1 中看到的,这个目录保存了引导装载程序的核心文件。

img/493794_1_En_3_Fig1_HTML.jpg

图 3-1

/boot/grub2 中的文件

设备.地图

GRUB 不理解 sda 或 vda 等磁盘名称,因为这些磁盘命名约定是由操作系统的 SCSI 驱动程序创建的。很明显,GRUB 在操作系统不存在时运行,因此它有自己的磁盘命名约定。以下是 GRUB 的磁盘命名约定:

|

GRUB 版本

|

磁盘命名约定

|

意义

|
| --- | --- | --- |
| Two | hd0msdos1 | 硬盘号 0 和分区号 1,其中有一个 MS-DOS 分区表 |
| Two | hd1msdos3 | 2 号硬盘和 3 号分区,其中有一个 MS-DOS 分区表 |
| Two | hd2gpt1 | 3 号硬盘和 1 号分区,后者有一个 GPT 分区表 |
| one | hd00 | 硬盘号 0 和分区号 1 |

在 GRUB 中,硬盘从 0 开始,分区号从 1 开始,而磁盘和分区的操作系统命名约定从 1 开始。由于 OS 和 GRUB 磁盘命名约定不同,所以必须有一个用户映射,这就是为什么创建了device.map文件。

# cat /boot/grub2/device.map
      # this device map was generated by anaconda
      (hd0)      /dev/sda

grub2-install like 命令将使用device.map文件来了解 GRUB 的核心文件安装在哪个磁盘上。以下是该文件的一个示例:

# strace -o delete_it.txt  grub2-install  /dev/sda
      Installing for i386-pc platform.
      Installation finished. No error reported.

# cat delete_it.txt | grep -i 'device.map'
      openat(AT_FDCWD, "/boot/grub2/device.map", O_RDONLY) = 3
      read(3, "# this device map was generated "..., 4096) = 64
      openat(AT_FDCWD, "/boot/grub2/device.map", O_RDONLY) = 3
      read(3, "# this device map was generated "..., 4096) = 64

因为用户不知道 GRUB 磁盘命名约定,所以grub2-install命令将以 OS 磁盘命名约定的形式接受输入。在执行过程中,grub2-install会通过读取device.map文件将 SCSI 磁盘命名约定转换为 GRUB 磁盘命名约定。

grub.cfg

这是 GRUB 的主要配置文件。正如你在图 3-2 中看到的,这是一个巨大的脚本文件,它是通过引用其他一些脚本文件生成的,我们将很快讨论这些文件。强烈建议不要更改grub.cfg的内容,因为这样做可能会使您的 Linux 版本无法启动。GRUB part-3 从这个文件中获取如下指令:

img/493794_1_En_3_Fig2_HTML.jpg

图 3-2

grub.cfg 文件

  • 内核和 initramfs 的位置

    • /boot/vmlinuz-<version>

    • /boot/initramfs-<version>

  • 内核命令行参数

    • 根文件系统名称及其位置等。

GRUB 有自己的命令集,正如您在这里看到的:

|

GRUB 命令

|

目的

|
| --- | --- |
| menuentry | 这将在屏幕上打印标题。 |
| set root | 这将提供存储内核和 initramfs 的磁盘和分区名称。 |
| linux | Linux 内核文件的绝对路径 |
| initrd | Linux 的 initramfs 文件的绝对路径 |

因此,GRUB 2 在基于 BIOS 的 Fedora 系统上的引导顺序如下:

  1. 开机:首先是 BIOS,然后是 POST,然后是 BIOS,最后是第一个扇区。

  2. 首先是引导程序(GRUB 的第一部分),然后是 GRUB 的第二部分,最后是 GRUB 的第三部分。

  3. Part-3 of GRUB will read the previously shown grub.cfg from /boot/grub2/ (in the case of Ubuntu, it will be /boot/grub/) and will print the welcome screen, as shown in Figure 3-3.

    img/493794_1_En_3_Fig3_HTML.jpg

    图 3-3

    欢迎屏幕

  4. 当用户选择 Ubuntu menuentry时,它将运行set rootlinuxinitrd命令,并开始在内存中加载内核和 initramfs。

  5. 在类似 Fedora 的 Linux 发行版中,您会发现一种不同的方法。会有一个grub.cfg文件,但是menuentryset rootlinuxinitrd命令在grub.cfg中不可用。一个叫做 BLS 的 GRUB 上游项目有了新的进展。我们将在本章的后面讨论这个问题。

i386 电脑

该目录包含所有 GRUB 支持的文件系统模块(驱动程序)(请参见图 3-4 )。所有的*.mod文件都是模块。通过使用这些模块,GRUB 可以在内存中加载内核和 initramfs 文件。例如,这个系统的/boot有一个 ext4 文件系统,所以显然当从/boot中探索和加载vmlinuz和 initramfs 文件时,GRUB 需要 ext4 模块,它从ext4.mod文件中获得这个模块。这类似于 XFS 或 UFS 文件系统上的/boot;因此,xfs.modufs.mod文件存在于/boot/grub2/i386-pc中。同时你会发现像http.modpxe.mod这样的模块。这意味着 GRUB 2 的 part-3 可以从httppxe设备加载内核和 initramfs 文件。一般来说,*.mod文件增加了功能,而不仅仅是设备。这些功能可能包括设备支持、文件系统支持或协议支持。

早先,/boot在 LVM 治下是不可能的,原因很简单。GRUB 必须了解 LVM 的设备。为了理解和组装 LVM 设备,GRUB 需要 LVM 模块和 LVM 二进制文件,如vgscan, vgchange, pvs, lvscan,等。它会增加 GRUB 作为一个包的大小;因此,企业 Linux 系统厂商一直避免在 LVM 设备下使用/boot。但是自从 UEFI 引入以来,GRUB 已经开始在 LVM 设备上支持/boot

img/493794_1_En_3_Fig4a_HTML.jpgimg/493794_1_En_3_Fig4b_HTML.jpg

图 3-4

那个。/boot/grub2/i386-pc 中的 mod*文件

如图 3-5 所示,除了这些*.mod文件,你还会在/boot/grub2/i386-pc/位置找到几个其他文件。

img/493794_1_En_3_Fig5_HTML.jpg

图 3-5

这些文件除了*。现代的

core.img文件是 GRUB 2 的第三部分。因此,Linux 引导顺序如下:

-> Power on -> BIOS -> POST -> BIOS ->
-> part-1 of GRUB2 -> Part-2 of GRUB2 -> core3.img -> grub.cfg ->
-> if /boot is on an xfs filesystem -> /boot/grub2/i386-pc/xfs.mod ->
-> load vmlinuz & initramfs in main memory.

一旦内核在内存中,GRUB 2 的工作就完成了。引导序列的其余部分将由内核执行,我们将在第四章中讨论。

/etc/默认值/grub

另一个重要的文件当然是/etc/default/grub。请参见图 3-6 。

img/493794_1_En_3_Fig6_HTML.jpg

图 3-6

/etc/default 目录的内容

GRUB 使用该文件接受用户对外观和内核命令行的更改。

$ cat /etc/default/grub
GRUB_TIMEOUT=10
GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"
GRUB_DEFAULT=saved
GRUB_DISABLE_SUBMENU=true
GRUB_TERMINAL_OUTPUT="console"
GRUB_CMDLINE_LINUX="resume=/dev/mapper/root_vg-swap rd.lvm.lv=root_vg/root rd.lvm.lv=root_vg/swap console=ttyS0,115200 console=tty0"
GRUB_DISABLE_RECOVERY="true"
GRUB_ENABLE_BLSCFG=true

如您所见,在这个文件中,我们可以更改 GRUB 欢迎屏幕的默认超时、字体、子菜单和默认内核命令行参数,如根设备名、交换设备名等。

/etc/grub.d/

这就是 GRUB 2 真正有趣的地方。

GRUB 2 有一个名为grub2-mkconfig .的命令,该命令的名字表明它将创建 GRUB 配置文件grub.cfg,,GRUB 的第三部分将引用该文件来显示欢迎屏幕。grub2-mkconfig文件将首先从/etc/default/grub获取外观和内核命令行参数输入,并从/etc/grub.d/目录运行图 3-7 中列出的脚本文件。

img/493794_1_En_3_Fig7_HTML.jpg

图 3-7

/etc/grub.d/目录的内容

如您所见,这些文件都有编号。这意味着它们将按顺序运行。

00_header01_users08_fallback_counting10_reset_boot_success12_menu_auto_hide脚本文件做内务工作。例如,00_header脚本文件负责向grub.cfg文件添加一个头。例如,在 Fedora Linux 上,运行grub2-mkconfig文件后,会在grub.cfg中添加以下头:

### BEGIN /etc/grub.d/00_header ###
set pager=1

if [ -f ${config_directory}/grubenv ]; then
  load_env -f ${config_directory}/grubenv
elif [ -s $prefix/grubenv ]; then
  load_env
fi
if [ "${next_entry}" ] ; then
   set default="${next_entry}"
   set next_entry=
   save_env next_entry
   set boot_once=true
else
   set default="${saved_entry}"
fi

if [ x"${feature_menuentry_id}" = xy ]; then
  menuentry_id_option="--id"
else
  menuentry_id_option=""
fi

export menuentry_id_option

if [ "${prev_saved_entry}" ]; then
  set saved_entry="${prev_saved_entry}"
  save_env saved_entry
  set prev_saved_entry=
  save_env prev_saved_entry
  set boot_once=true
fi
function savedefault {
  if [ -z "${boot_once}" ]; then
    saved_entry="${chosen}"
    save_env saved_entry
  fi
}

function load_video {
  if [ x$feature_all_video_module = xy ]; then

    insmod all_video
  else
    insmod efi_gop
    insmod efi_uga
    insmod ieee1275_fb
    insmod vbe
    insmod vga
    insmod video_bochs
    insmod video_cirrus
  fi
}

terminal_output console
if [ x$feature_timeout_style = xy ] ; then
  set timeout_style=menu
  set timeout=5
# Fallback normal timeout code in case the timeout_style feature is
# unavailable.
else
  set timeout=5
fi
### END /etc/grub.d/00_header ###

08_fallback_counting脚本文件将在grub.cfg中添加以下内容:

### BEGIN /etc/grub.d/08_fallback_counting ###
insmod increment
# Check if boot_counter exists and boot_success=0 to activate this behaviour.
if [ -n "${boot_counter}" -a "${boot_success}" = "0" ]; then
  # if countdown has ended, choose to boot rollback deployment,
  # i.e. default=1 on OSTree-based systems.
  if  [ "${boot_counter}" = "0" -o "${boot_counter}" = "-1" ]; then
    set default=1
    set boot_counter=-1
  # otherwise decrement boot_counter
  else
    decrement boot_counter
  fi
  save_env boot_counter
fi
### END /etc/grub.d/08_fallback_counting ###

如您所见,该文件添加了监控 GRUB 欢迎屏幕的默认超时值的代码,与其余文件(10_reset_boot_successmenu_auto_hide)为 GRUB 做家务的方式相同。让我们看看让 GRUB 2 成为多引导最佳引导加载程序之一的脚本文件。

10_linux

这个文件包含将近 500 行 bash 脚本文件。每当用户执行grub2-mkconfig命令时,它就会运行这个脚本。10_linux文件将找出您的系统上还安装了哪些其他的 Linux 发行版。它会一个分区一个分区地查找已经安装在您系统上的所有其他 Linux 版本。如果还有其他的,那么它会在grub.cfg里做一个menuentry。与menuentry一起,它将添加各自的内核和 initramfs 条目。是不是很神奇?

考虑你先装了 Ubuntu 再装了 Fedora 现在你不用手动把 Ubuntu 的条目加入 Fedora 的grub.cfg。你只要跑grub2-mkconfig就行了。该命令将为我们运行10_linux,它最终会发现 Ubuntu 已经安装,并将为它添加适当的条目。

S7-1200 可编程控制器

grub2-mkconfig之后,这个脚本文件会发现你的系统是否安装了 XEN 内核。如果是,那么它将在grub.cfg中为其添加适当的条目。大多数 Linux 发行商将 XEN 作为一个独立的内核包发布。XEN 主要由管理程序使用。

20_ppc_terminfo

如果您的系统有 IBM 的 PPC 或 PowerPC 架构,那么这个脚本文件将为它找到相应的内核,并将适当的条目添加到grub.cfg中。

30 秒后醒来

如果您的硬盘上安装了任何非基于 Linux 的操作系统,那么这个脚本文件将找到该操作系统,并为其创建适当的条目。换句话说,如果您的系统上安装了 Windows,它会自动找出并在grub.cfg中为其创建一个适当的条目。这就是为什么在 UEFI 系统上安装了我们的第三个操作系统(Fedora 31)后,我们什么也没做就获得了操作系统列表。在图 3-8 中可以看到 Fedora 31 呈现的欢迎画面。

img/493794_1_En_3_Fig8_HTML.jpg

图 3-8

欢迎屏幕

Fedora 安装完成后,Anaconda 在后台运行grub2-mkconfig,最终运行30_os_prober,它找到 Windows 安装并在grub.cfg中为其创建适当的条目。

30_uefi 韧体

只有当您拥有 UEFI 系统时,此脚本才能成功运行。该脚本文件的任务是在grub.cfg中添加 UEFI 固件的适当条目。如图 3-8 所示,System setup条目已经被30_uefi-firmware脚本文件添加。

### BEGIN /etc/grub.d/30_uefi-firmware ###
menuentry 'System setup' $menuentry_id_option 'uefi-firmware' {
        fwsetup
}
### END /etc/grub.d/30_uefi-firmware ###

如果用户选择“系统设置”选项,那么它将引导回 UEFI 固件。在图 3-9 中可以看到 UEFI 固件界面。

img/493794_1_En_3_Fig9_HTML.jpg

图 3-9

UEFI 固件

40 _ 自定义和 41 _ 自定义

这些是给用户的,以防用户想要添加一些自定义条目到grub.cfg。例如,如果grub2-mkconfig未能添加任何已安装的 OS 作为条目,那么用户可以向这两个自定义文件添加一个自定义条目。您可以创建自己的自定义文件,但需要确保每个文件都有一个编号,并且有可执行的权限。

基于 UEFI 的系统上的 GRUB 2

同样,GRUB 2 有三个存储文件的位置。图 3-10 显示了目录及其文件。

img/493794_1_En_3_Fig10_HTML.jpg

图 3-10

基于 UEFI 的系统上的 GRUB 2 位置

先前在/boot/grub2/中显示的grub.cfg文件已经被转移到 ESP ( /boot/efi/EFI/fedora/)中。另外,正如您所见,没有i386-pc目录。这是因为 EFI 提供了丰富的设备和文件系统支持。在 ESP 中,你会发现几个*.efi文件,包括我们的shim.efigrubx64.efi二进制文件。负责 GRUB 外观变化和内核命令行参数的etc/default/grub文件仍然在同一个位置。由于grub2-install命令在 UEFI 系统上没有意义,因此device.map文件不可用。我们将在本章的后面讨论这个命令。

引导加载程序规范(BLS)

BLS 是 GRUB 上游项目的新发展,还没有被许多主流发行版采用。具体来说,这种方案已经被 RHEL、Fedora、Centos、Oracle Linux 等基于 Fedora 的操作系统所采用。,但不是基于 Debian 的发行版,如 Ubuntu、Mint 等。

在基于 BIOS 的系统上,无论哪个操作系统控制了前 512 个字节,它就控制了所有操作系统的引导序列,这就是为什么每个操作系统都试图获得前 512 个字节。出现这种情况是因为 BIOS 总是进入硬盘的前 512 个字节,并调用引导程序的第一部分(引导程序)。part-1 到 part-2 和 part-2 到 part-3 的转换稍后发生,然后在最后,part-3 读取特定于引导加载程序的配置文件(对于 Windows,bcdedit对于 Linux,grub.cfg)。如果该配置文件包含其他已安装操作系统的条目,那么它们将有机会启动。所以,长话短说:谁控制了前 512 个字节,谁就控制了整个引导序列。但是有了 ESP,每个操作系统都有平等的启动机会,因为 UEFI 会检查 ESP 目录并列出所有可用的操作系统条目。开发人员开始考虑是否能在基于 BIOS 的系统中得到这样的东西,于是他们想到了 BLS。

在 BLS,引入了一个新的位置(第五个)来存储与引导程序相关的文件,那就是/boot/loader/.所以,我们现在有了五个 GRUB 存储文件的位置。

  • /boot/grub2/

  • /etc/default/grub

  • /etc/grub.d

  • /boot/efi/EFI/<OS_vendor>/(仅适用于 UEFI)

  • /boot/loader/ (BLS 的文件将存储在这里)

其思想是,在新内核安装之后,内核本身及其后脚本(类似于 Fedora 中的kernel-core包)将在/boot/loader/目录中为新内核创建一个条目。例如,我们安装了这个内核包:

# rpm -q kernel
Kernel-5.3.7-301.fc31.x86_64

这是将提供/boot/vmlinuz/boot/initramfs文件的同一个包。一旦安装了这个内核,它将准备以下文件:

# cat /boot/loader/entries/36543031048348f9965e3e12e48bd2b1-5.3.7-301.fc31.x86_64.conf

title Fedora (5.3.7-301.fc31.x86_64) 31 (Thirty One)
version 5.3.7-301.fc31.x86_64
linux /vmlinuz-5.3.7-301.fc31.x86_64
initrd /initramfs-5.3.7-301.fc31.x86_64.img
options $kernelopts
grub_users $grub_users
grub_arg --unrestricted
grub_class kernel

如您所见,该文件有四个条目。

  • GRUB 的第三部分将打印的标题

  • 内核文件的位置和名称

  • initramfs 文件的位置和名称

  • 已经在/boot/grub2/grubenv文件中声明的$kernelopts变量

# cat /boot/grub2/grubenv

# GRUB Environment Block
saved_entry=2058a9f13f9e489dba29c477a8ae2493-5.3.7-301.fc31.x86_64
menu_auto_hide=1
boot_success=0
kernelopts=root=/dev/mapper/fedora_localhost--live-root ro resume=/dev/mapper/fedora_localhost--live-swap rd.lvm.lv=fedora_localhost-live/root rd.lvm.lv=fedora_localhost-live/swap rhgb quiet
boot_indeterminate=0

基本上,kernelopts提供了内核命令行参数,比如根文件系统的名称(/dev/mapper/fedora_localhost--live-root)以及它必须以何种模式挂载(ro - read only)。

因此,引导顺序变成这样:

  1. BIOS > post > BIOS

  2. GRUB 的第一部分->第二部分->第三部分

  3. GRUB 的第三部分->阅读grub.cfg

  4. GRUB -> reads 的第三部分/boot/loader/entries/*

  5. 打印出现在/boot/loader/entries中的所有文件标题

例如,假设安装了新的操作系统或新的内核。它必须生成自己的条目文件,并将其放在第一个主分区的/boot/loader/entries/目录中。这样,每次第一个主操作系统的 GRUB part-3 读取条目时,其他操作系统都有机会引导。可以使用 Fedora 的kernel-install命令创建条目文件。

#kernel-install add 5.3.7-301.fc31.x86_64 /lib/modules/5.3.7-301.fc31.x86_64/vmlinuz

该命令将在/boot/loader/entries/中为kernel-5.3.7-301.fc31.x86_64创建适当的条目,如下所示:

# ls /boot/loader/entries/ -l
total 8
-rw-r--r--. 1 root root 329 Dec  9 10:18 2058a9f13f9e489dba29c477a8ae2493-0-rescue.conf
-rw-r--r--. 1 root root 249 Oct 22 01:04 2058a9f13f9e489dba29c477a8ae2493-5.3.7-301.fc31.x86_64.conf

*.conf文件相关的编号是唯一的。BLS 有自己的优势和劣势。

以下是优点:

  • 每个操作系统都有平等的启动机会。

  • 它的工作与 BIOS 和 UEFI 固件无关。

  • 在 BIOS 的情况下,最新的 Linux 安装删除了早期安装的操作系统的第一部分和第二部分,这已经变得过时,因为最新的 Linux 安装将通过早期操作系统上的kernel-install命令创建自己的条目。

以下是缺点:

  • BLS 尚未完全实现。如果第二个操作系统想要在第一个操作系统中创建它的条目,那么第一个操作系统的/boot必须被共享。目前情况并非如此。所以,我认为这是一个半实现。

  • BLS 不必要地使引导序列变得复杂,因为我们有两个配置文件需要参考:来自/boot/loader/entries/grub.conf<uniq_no><kernel_version>.conf。在解决“无法启动”问题的情况下,BLS 尤其让生活变得困难。

  • 除了基于 Fedora 的发行版,还没有人采用 BLS,这似乎是一个明智的决定。看起来 Fedora 是最致力于上游项目的;因此,BLS 已经在 Fedora 中实现。

常见的引导程序问题

基于这些知识,让我们尝试解决一些最常见的与引导程序相关的“无法引导”问题。

“无法引导”问题 1(引导加载程序)

问题:系统加电后,在 GRUB 提示符下退出,如图 3-11 所示。

img/493794_1_En_3_Fig11_HTML.jpg

图 3-11

GRUB 2 提示符

这是你在屏幕上看到的。你一生中肯定至少遇到过一次这种错误。让我们试着解决它。

  1. 只有当你知道问题是什么的时候,你才能解决问题。但是现在,我们不知道问题出在哪里,因为我们刚刚启动这个系统,这就是我们得到的结果。

  2. 该屏幕称为 GRUB 提示符。当这被称为提示符时,意味着您可以在它上面执行命令。请记住,这是一个 GRUB 命令提示符,这意味着它只能接受 GRUB 命令。

  3. 通过查看图 3-11 ,在 GRUB 的三个部分中,GRUB 的哪个部分为我们提供了 GRUB 提示符?

  4. 当然,它必须是 part-3,因为 part-1 和 part-2 的空间很小,所以它们无法容纳这样的功能。所以,我们已经成功地完成了 GRUB 的第三部分,最重要的是,这个系统是有 UEFI 还是 BIOS 并不重要。既然我们已经到了第三部分,这意味着我们已经离开了固件环境。这是至关重要的输入。现在我们不能只关注第三部分。

  5. GRUB 第三部分的目的是什么?很简单。它读取grub.cfg,并从那里获得内核和 initramfs 的位置。如果它是一个支持 BLS 的系统,那么它从/boot/loader/entries/目录中获取内核和 initramfs 名称。对于这个例子,我们将假设这个系统不知道 BLS。然后,第三部分在内存中加载vmlinuz和 initramfs。

  6. 由于第三部分已经向我们提供了 GRUB 提示符,但未能加载 OS,这意味着要么内核和 initramfs 文件不存在,要么grub.cfg文件没有指出这些文件的正确位置。

  7. 因此,在这种情况下,我们可以尝试手动引导 Fedora。手动意味着我们将使用 GRUB 提示符为内核和 initramfs 文件提供绝对路径。这是可以做到的。

  8. linux is a GRUB command through which we need to give the absolute path of the kernel (vmlinuz) file. As we know, the vmlinuz file is at /boot, and GRUB follows its own disk naming convention. So, the path of /boot will be hard disk number 0 and partition number 1. Of course, you might not be aware on which HDD or partition /boot has been stored. In that case, you can get the help of the autocomplete feature of GRUB. You can press Tab twice, and GRUB will prompt you for the available options. Let’s find out the HDD and partition number of /boot. Please refer to Figure 3-12.

    img/493794_1_En_3_Fig12_HTML.jpg

    图 3-12

    0 号硬盘上的可用分区

    The first tab after hd0 showed us that there are two partitions available under the hard disk number 0. The second partition is not readable to GRUB, so of course the second partition cannot be /boot. Hence, we will choose the msdos1 partition. Then, as shown in Figure 3-13, we will start looking for the vmlinuz file in it with the help of autocomplete.

    img/493794_1_En_3_Fig13_HTML.jpg

    图 3-13

    vmlinuz 文件

    As you can see inside HDD number 0 and partition number 1, we found two vmlinuz files; one is of a rescue kernel, and another one is the normal kernel file of Fedora 31. As shown in Figure 3-14, we will choose the normal kernel and will provide the root filesystem name to it. If you are unaware of the root filesystem name of your system, then you can boot the system with the rescue or live image and check the /etc/fstab entries. We will talk about the rescue mode in Chapter 10.

    img/493794_1_En_3_Fig14_HTML.jpg

    图 3-14

    根文件系统名称和 ro 标志

    The absolute path of the vmlinuz file is (hd0,msdos1)/vmlinuz-5.3.7-301.fc31.x86_64. Next to it is the ro kernel command-line parameter, which stands for “read-only.” After ro, we have a root kernel command-line parameter to which we have passed our system’s root filesystem name, which is - /dev/mapper/fedora_localhost--live-root. It’s an lvm device.

    grub> linux (hd0,msdos1)/vmlinuz-5.3.7-301.fc31.x86_64 ro
         root=/dev/mapper/fedora_localhost--live-root
    
    

    After successfully executing the linux command, we need to pass on the initramfs name. We have two commands available that we can use: initrd and initrd16. Please refer to Figure 3-15.

    grub> initrd (hd0,msdos1)/initramfs-5.3.7-301.fc31.x86_64.img
    
    

    img/493794_1_En_3_Fig15_HTML.jpg

    图 3-15

    运行中的 linux、initrd 和 boot 命令

  9. The moment you execute the boot command, as shown in Figure 3-16 and in Figure 3-17, GRUB’s part-3 will take these inputs and load /boot/vmlinuz-5.3.7-301.fc31.x86_64 from sda1 (hd0,msdos1). Then it will load /boot/initramfs-5.3.7-301.fc31.x86_64.img and give control to the kernel. The kernel will eventually mount the root (/) filesystem from /dev/mapper/fedora_locahost--live-root on the / directory and will show the login screen.

    img/493794_1_En_3_Fig17_HTML.jpg

    图 3-17

    登录屏幕

    img/493794_1_En_3_Fig16_HTML.jpg

    图 3-16

    引导时的控制台消息

  10. 在 Ubuntu 18 的情况下,命令略有不同。在 Fedora 31 上,我们将/boot分区的地址直接给了linux命令,而在 Ubuntu 中,我们有一个单独的 GRUB 命令,名为set root

如图 3-18 所示,Ubuntu 18 系统的根文件系统名称是/dev/sda1。这是一个标准分区,不像 Fedora 31 的lvm设备。

img/493794_1_En_3_Fig18_HTML.jpg

图 3-18

Ubuntu 有一个稍微不同的方法

一旦我们向 GRUB 2 提供了正确的输入,它就会把我们带到登录屏幕。图 3-19 可以看到 Ubuntu 的登录界面。

img/493794_1_En_3_Fig19_HTML.jpg

图 3-19

Ubuntu 显示的登录屏幕

img/493794_1_En_3_Fig20_HTML.jpg

图 3-20

grub2-mkconfig 命令

  1. 回到我们的 Fedora 系统,由于现在已经启动,我们可以使用grub2-mkconfig命令重新生成grub.cfg文件,如图 3-20 所示。

我们可以在 Ubuntu 的情况下执行grub-mkconfig。请参见图 3-21 。

img/493794_1_En_3_Fig21_HTML.jpg

图 3-21

Ubuntu 的 grub-mkconfig 命令

但是如果是 UEFI 系统,你想再生grub.cfg,那么如图 3-22 所示,grub.cfg的位置就是 ESP。

img/493794_1_En_3_Fig22_HTML.jpg

图 3-22

基于 UEFI 的系统上的 grub2-mkconfig

  1. 一旦生成了grub.cfg,我们需要为 Fedora 重新生成 BLS 条目。
#kernel-install add 5.3.7-301.fc31.x86_64 /lib/modules/5.3.7-301.fc31.x86_64/vmlinuz

该命令将在/boot/loader/entries/中为kernel-5.3.7-301.fc31.x86_64创建适当的条目。

  1. 如果 Fedora 在 UEFI 系统上,那么 BLS 步骤保持不变。

  2. 重启后,Fedora 能够顺利启动,“无法启动”的问题已经修复。

# ls /boot/loader/entries/ -l
total 8
-rw-r--r--. 1 root root 329 Dec  9 10:18 2058a9f13f9e489dba29c477a8ae2493-0-rescue.conf
-rw-r--r--. 1 root root 249 Oct 22 01:04 2058a9f13f9e489dba29c477a8ae2493-5.3.7-301.fc31.x86_64.conf

“无法引导”问题 2(引导加载程序)

问题:系统上电后,通过固件阶段,但之后如图 3-23 所示,屏幕上什么都没有。

img/493794_1_En_3_Fig23_HTML.jpg

图 3-23

空白屏幕

基于 BIOS 的系统的分辨率

以下是解决这个问题的步骤:

  1. 由于 BIOS 固件阶段已经过去,这意味着在引导装载程序级别有问题。

  2. 因为我们在屏幕上看不到任何东西,这意味着 GRUB 的第一部分或第二部分丢失了,或者至少它们被破坏了(512 字节+ 31 KB)。如果它到达了第三部分,那么我们至少会得到 GRUB 提示符。因此,这个问题已经被隔离,行动计划是替换 GRUB 的第一部分和第二部分。

  3. 这可以通过grub2-install命令来完成。首先用相同 Linux 发行版的 live 介质引导,或者,如果可以的话,用救援模式引导。现场图像和救援模式将在第十章中解释。

如图 3-24 所示,grub2-install将设备名称作为输入。请注意,设备名称不应是分区号;相反,它应该是一个磁盘名称。这是因为 GRUB 的第一部分和第二部分必须安装在磁盘的前 512 字节+ 31 KB 上,而不是安装在分区内。您需要用您的磁盘名称替换 sda。

img/493794_1_En_3_Fig24_HTML.jpg

图 3-24

grub2-install 命令

随着引导程序文件的第一部分和第二部分,grub2-install修复或重新安装i386-pc目录,其中包含 GRUB 2 引导程序的所有模块。我们可以通过在自定义目录中安装模块来交叉验证这一点。请参见图 3-25 。

img/493794_1_En_3_Fig25_HTML.jpg

图 3-25

在临时目录中安装 grub2

您可以看到所有的 GRUB 2 文件和 GRUB 的模块文件都已经恢复。

# ls temp/grub2/
      fonts  grubenv  i386-pc
# ls -l temp/grub2/i386-pc/ | wc -l
      279

重启后,Fedora 应该可以正常启动,“无法启动”的问题应该已经修复。如果 GRUB 让您进入命令提示符,那么您需要遵循问题 1 中提到的步骤,因为grub2-install会修复二进制文件,但它不会重新生成grub.cfg文件。

但是,如果您在基于 UEFI 的系统上面临类似的问题,该怎么办呢?

基于 UEFI 的系统的解决方案

以下是步骤:

img/493794_1_En_3_Fig26_HTML.jpg

图 3-26

基于 UEFI 的系统上的 grub-install 命令

  1. 正如你可能已经猜到的,我们只需改变grub2-install命令传递的设备名称,如图 3-26 所示。设备名称应该是 ESP。

“无法引导”问题 3(引导程序+内核)

问题:完整的/boot不见了。

基于 BIOS 的系统的分辨率

以下是步骤:

  1. 找回丢失的/boot是不可能的(或者至少超出了本书的范围)。

  2. 在救援模式下启动,或者用一个动态镜像启动,并挂载我们“不能启动”的系统的根文件系统。救援模式及其工作原理将在第十章中讨论。

  3. 首先创建一个新的/boot目录,并在其上设置适当的权限。

    • #mkdir /boot

    • #chmod 555 /boot

    • #chown root:root /boot

    • 如果/boot应该是一个单独的分区,那么用正确的分区挂载它。

  4. 正如我们所知,/boot是我们存储引导程序、内核和 initramfs 文件的地方。因为/boot丢失了,我们需要为它创建每个文件。

    • #dnf reinstall kernel
      • 这是一个基于 Fedora 的系统。如果是基于 Debian 的系统,那么可以使用apt-get命令,并且可以重装内核。

      • 这将安装vmlinuz文件并为其重新生成 initramfs 文件。

  5. 现在我们需要安装 GRUB。

    • # grub 2-安装/开发/ <磁盘名称>

      • 在我们的例子中,命令是#grub2-install /dev/sda.
    • 这将从/boot/grub2修复 GRUB 的 part-1、part-2 和i386-pc目录。

    • 为了修复 GRUB 的第三部分并使用 GRUB 提供的工具,我们需要在基于 Fedora 的系统上安装两个包。

      • #dnf reinstall grub2 grub2-tools

      • 顾名思义,grub2包将提供 GRUB 的第三部分,grub2-tools将提供一些类似grub2-install的工具。

    • 现在是重新生成 GRUB 配置文件的时候了。

      • #grub2-mkconfig -o /boot/grub2/grub.cfg
    • 最后,修复 BLS。

      • #kernel-install add 5.3.7-301.fc31.x86_64 /lib/modules/5.3.7-301.fc31.x86_64/vmlinuz

基于 UEFI 的系统的解决方案

以下是步骤:

  • /boot and /boot/efi/是单独的挂载点。

    • # mkdir /boot

    • # chmod 555 /boot

    • # chown root:root /boot

    • # yum reinstall kernel

  • 现在我们需要创建一个 ESP 分区,正如我们所知,它必须是一个 VFAT 分区。然后给它分配一个 ESP 分区类型。

    • #mkdir /boot/efi

    • #mount /dev/sda2 /boot/efi

      • 在我们的例子中,我为 ESP 创建的分区是 sda2。
    • #grub2-install --efi-directory=/boot/efi

      • 这将在 ESP 中安装grubx64.efi文件。
    • 其余的必需文件由grub2-efishimgrub2-tools包提供。

      • #yum reinstall grub2-efi shim grub2-tools
    • 重新生成配置文件。

      • #grub2-mkconfig -o /boot/efi/EFI/redhat/grub.cfg

      • #kernel-install add 5.3.7-301.fc31.x86_64 /lib/modules/5.3.7-301.fc31.x86_64/vmlinuz

重新启动系统后,它能够顺利启动。

现在是时候对 UEFI 的安全引导环境进行更多的阐述了。

UEFI 的安全引导功能

安全引导是 UEFI 的一个令人惊叹的功能。它确保启动时不会运行不可信的二进制文件。到目前为止,我们已经看到了以下内容:

  • 数字签名是唯一的字符串。

    • 任何文件的数字签名都将由私钥生成。

    • 可以从公钥中重新生成相同的数字签名。

    • 如果文件没有被修改,那么数字签名应该匹配。

  • 微软制作了它的密钥对(公钥和私钥)。

  • Microsoft 用其私钥对其引导加载程序相关文件(BCD)进行了数字签名。

  • 微软的公钥存在于 UEFI 内部。

  • 引导时,UEFI 将使用可用的公钥重新生成引导程序的数字签名。如果数字签名不匹配,那么 UEFI 将放弃执行.efi文件。

  • 为了在 Linux 环境中使用这个特性,已经创建了一个名为shim的新引导加载程序,它已经用微软的私钥进行了签名,因此 UEFI 将允许执行shim.efi

  • Shim.efi的工作是调用实际的 GRUB 文件,也就是grubx64.efi

但是安全引导并没有就此停止。因为有可能grubx64.efi本身已经被破坏,或者事实上在引导装载程序之后运行的任何代码都可能已经被破坏,所以仅仅保证引导环境达到引导装载程序级别是不够的;因此,现在安全引导特性保护了 Linux 的整个引导过程。它是这样工作的:

  1. Fedora 将准备自己的密钥对,并用 Fedora 的私钥签署 GRUB 文件。

  2. Fedora 的公钥将保存在shim.efi文件中。

  3. 随着引导序列的继续,GRUB 的数字签名将通过使用shim.efi中的公钥重新生成。

  4. 如果签名匹配,那么grubx64.efi和其他引导程序文件将被 UEFI 允许运行。

  5. GRUB 的最终工作是加载内核(/boot/vmlinuz)。

  6. 这个vmlinuz文件也可能被破坏,因此为了避免这种情况,内核将由用于签名 GRUB 的同一个私钥进行签名。

  7. 将使用shim.efi中的公钥重新生成Vmlinuz'的数字签名。

  8. 一旦数字签名匹配,内核就会控制引导序列。

  9. 但是内核使用了大量的模块/驱动程序,这些模块/驱动程序最终被插入内核内部。所以,这些二进制的模块可能会受到损害,而且由于它们将成为 kernel/ vmlinuz的一部分,最终内核本身也会受到损害。

  10. 因此,内核作为一个包将准备自己的密钥对。所有模块都将由这个内核的私钥签名,公钥将随内核包一起提供。内核包的私有密钥将在以后被销毁。

  11. 在引导时,在内核中插入模块时,模块的数字签名将通过使用内核中的公钥重新生成。

  12. 按照上面提到的步骤,安全引导特性确保只执行来自可信方的二进制文件。

图 3-27 所示的框图将进一步简化启动程序。

img/493794_1_En_3_Fig27_HTML.jpg

图 3-27

安全引导过程

100 操作系统多重引导项目

我的一个学生问了我一个问题:我们可以在一个系统上安装多少个操作系统,并用一个引导程序多重引导它们?我不知道答案,但我决定试着找出答案。我决定使用 GRUB 2 引导程序来引导我安装的每个操作系统。我已经安装和引导操作系统将近两年了。到目前为止,我已经安装了 106 个操作系统。这是我们的第三个系统,我命名为贾维斯。以下是 Jarvis 的硬件和软件细节:

  • UEFI 韧体。

  • 连接了两个磁盘(sda 和 sdb)。

  • 引导方法是 UEFI。

  • sda 是用 MS-DOS 分区表格式化的。

  • sdb 用 GPT 分区表格式化。

  • 所有的操作系统都由 GRUB 2 引导程序识别和引导。

sda 磁盘上安装的操作系统是通过将引导方法设置为 UEFI 来安装的,它拥有所有新的操作系统。sdb 上的操作系统是通过将固件的引导方法设置为 legacy 来安装的。sdb 托管大多数老一代操作系统,或者至少是那些不支持 UEFI 的操作系统。以下是详细情况:

|

划分

|

操作系统

|

文件系统

|

大小

|
| --- | --- | --- | --- |
| sda-1 | 电子燃油喷射系统分区 | FAT32 | 20 GB |
| sda-2 | MSR(微软恢复) | 发动机阻力矩控制系统 | 16 兆字节 |
| sda-3 | Windows 10 | Windows NT 文件系统(NT File System) | 9.7 GB |
| sda-4 | 交换 | 交换 | 2.01 GB |
| sda-5 | openSUSE Linux 13.2 | EXT4 | 10 GB |
| sda-6 | 就像 Linux 17.2 | EXT4 | 10 GB |
| sda-7 | Oracle OpenSolaris 11.2 | ZFS | 10 GB |
| sda-8 | Sabayon Linux 15.06 版 | EXT4 | 10 GB |
| sda-9 | 一些随机的自由空间 | 不适用的 | 8.4 兆字节 |
| sda-10 | Kali Linux 2.0 版 | EXT4 | 10 GB |
| sda-11 | Arch Linux 2015-8.1 | EXT4 | 10 GB |
| sda-12 | Debian Linux 8.1 | EXT4 | 10 GB |
| sda-13 | 简单 Linux 7.0.1 | EXT4 | 10 GB |
| sda-14 | Slackware 14.1 Linux | EXT4 | 10 GB |
| sda-15 | Openmandriva 2014.2 | EXT4 | 10 GB |
| sda-16 | 杀死 Ubuntu Linux15.04 | EXT4 | 10 GB |
| sda-17 | 蒸汽 OS beta 版 | EXT4 | 10 GB |
| sda-18 | Manjaro Linux 0.8.13.1 版 | EXT4 | 10 GB |
| sda-19 | Netrunner Linux 16 | EXT4 | 10 GB |
| sda-20 | Windows 8 | Windows NT 文件系统(NT File System) | 10 GB |
| sda-21 | Korora Linux 22 | EXT4 | 10 GB |
| sda-22 | 卡奥斯 Linux 2015.08 | EXT4 | 10 GB |
| sda-23 | ubuntu linux 15.04 | EXT4 | 10 GB |
| sda-24 | 声纳 Linux 2015.2 | EXT4 | 10 GB |
| sda-25 | Linux 2015 年 8 月 18 日 | EXT4 | 10 GB |
| sda-26 | linux 月 14 日 | EXT4 | 10 GB |
| sda-27 | 罗莎 Linux 新鲜版 R5 | EXT4 | 10 GB |
| sda-28 | SparkyLinux 4.0 | EXT4 | 10 GB |
| sda-29 | Linux 4.0 葡萄酒 | EXT4 | 10 GB |
| sda-30 | xubuntu linux 14 . 04 . 3 版 | EXT4 | 10 GB |
| sda-31 | Ubuntu Studio 14.04.3 版 | EXT4 | 10 GB |
| sda-32 | Suse 企业 12 | EXT4 | 10 GB |
| sda-33 | Ubuntu Linux 14.04 | EXT4 | 10 GB |
| sda-34 | Ubuntu Linux 15.04 | EXT4 | 10 GB |
| sda-35 | 科学 Linux 7 | EXT4 | 10 GB |
| sda-36 | 数百个 Linux 7 | EXT4 | 10 GB |
| sda-37 | Solus Linux 日报 | EXT4 | 10 GB |
| sda-38 | Ubuntu Server 14 Linux | EXT4 | 10 GB |
| sda-39 | Fedora 21 Linux | EXT4 | 10 GB |
| sda-40 | Fedora 22 Linux | EXT4 | 10 GB |
| sda-41 | 黑拱门 | EXT4 | 10 GB |
| sda-42 | gentoo linux multilib 20140826 | EXT4 | 10 GB |
| sda-43 | 计算 Linux 14.16.2 | EXT4 | 10 GB |
| sda-44 | Fedora 20 Linux | EXT4 | 10 GB |
| sda-45 | Fedora 23 Linux | EXT4 | 10 GB |
| sda-46 | Manjaro Linux 15-0.9 版 | EXT4 | 10 GB |
| sda-47 | Ubuntu Linux 16.04 | EXT4 | 10 GB |
| sda-48 | Linux 23 帽子 | EXT4 | 10 GB |
| sda-49 | Linux 22 归档类型 | EXT4 | 10 GB |
| sda-50 | Fx64 Linux 22 | EXT4 | 10 GB |
| sda-51 | 毒蛇 Linux 7 | EXT4 | 10 GB |
| sda-52 | Hanthana Linux 21 | EXT4 | 10 GB |
| sda-53 | Qubes R3.1 Linux | EXT4 | 10 GB |
| sda-54 | Fedora 24 | EXT4 | 10 GB |
| sda-55 | 科罗拉-23 | EXT4 | 10 GB |
| sda-56 | 萨巴永-16 | EXT4 | 10 GB |
| sda-57 | 科罗拉-24 | EXT4 | 10 GB |
| sda-58 | 声纳 16 Linux | EXT4 | 10 GB |
| sda-59 | 蝰蛇 9 Linux | EXT4 | 10 GB |
| sda-60 | Linux 23 归档类型 | EXT4 | 10 GB |
| sda-61 | Manjaro Linux 16 | EXT4 | 10 GB |
| sda-62 | Manjaro Linux 游戏 16 | EXT4 | 10 GB |
| sda-63 | 计算 Linux 15 | EXT4 | 10 GB |

因此,sda 磁盘上 UEFI OS 安装的总数是 59,因为 4 个分区是为类似 ESP 和 MSR 的东西保留的。以下是 sdb 磁盘安装的详细信息:

|

划分

|

操作系统

|

文件系统

|

大小

|
| --- | --- | --- | --- |
| sdb-1 | PCBSD 10.1.2 | ZFS | 10 GB |
| sdb-2 | Linux 魔法 2 | EXT4 | 10 GB |
| sdb-3 | Linux 魔法 3 | EXt4 | 10 GB |
| sdb-4 | 扩展/二级 | 不适用的 | 大约 970 GB |
| sdb-5 | Q4 Linux 1 . 2 . 8 作业系统 | EXT4 | 10 GB |
| sdb-6 | R2 Linux | EXT4 | 10 GB |
| sdb-7 | Pardus Linux 2013 版 | EXT4 | 10 GB |
| sdb-8 | 奥林帕斯 015 | EXT4 | 10 GB |
| sdb-9 | Crux Linux 3.1 | EXT4 | 10 GB |
| sdb-10 | 点 Linux 3.0 | EXT4 | 10 GB |
| sdb-11 | Extix Linux 15.3 | EXT4 | 10 GB |
| sdb-12 | Bodhi Linux 3.0 版 | EXT4 | 10 GB |
| sdb-13 | Debian Linux 7.0 | EXT4 | 10 GB |
| sdb-14 | Debian Linux 6.0 | EXT4 | 10 GB |
| sdb-15 | BOSS Linux 6.1 | EXT4 | 10 GB |
| sdb-16 | CrunchBang rc1 Linux 游戏 | EXT4 | 10 GB |
| sdb-17 | Linux 2.1 手机 | EXT4 | 10 GB |
| sdb-18 | Linux 2.4 版 | EXT4 | 10 GB |
| sdb-19 | Linux R9 瓦数 | EXT4 | 10 GB |
| sdb-20 | 平谷 14.04.3 Linux | EXT4 | 10 GB |
| sdb-21 | SuperX 3.0 Linux | EXT4 | 10 GB |
| sdb-22 | JuLinux 10X 版本 3.1 Linux | EXT4 | 10 GB |
| sdb-23 | 黑实验室 Linux 2015.7 | EXT4 | 10 GB |
| sdb-24 | Hamara Linux 1.0.3 | EXT4 | 10 GB |
| sdb-25 | 薄荷 LInux 20150518 | EXT4 | 10 GB |
| sdb-26 | Ubuntu 13.10 Linux | EXT4 | 10 GB |
| sdb-27 | linuxmint 13 杀手 | EXT4 | 10 GB |
| sdb-28 | Linux 薄荷 14.1 肉桂 | EXT4 | 10 GB |
| sdb-29 | linuxmint 15 xfce | EXT4 | 10 GB |
| sdb-30 | linuxmint 系统 16 KDE | EXT4 | 10 GB |
| sdb-31 | 薄荷 4 20131113 | EXT4 | 10 GB |
| sdb-32 | 薄荷 5 20140623 | EXT4 | 10 GB |
| sdb-33 | Fedora 12 | EXT4 | 10 GB |
| sdb-34 | 哪 7 个 Linux | EXT4 | 10 GB |
| sdb-35 | Oracle Linux 7.1 | EXT4 | 10 GB |
| sdb-36 | Fedora 14 Linux | EXT4 | 10 GB |
| sdb-37 | Fedora 15 Linux | EXT4 | 10 GB |
| sdb-38 | Fedora 17 Linux | EXT4 | 10 GB |
| sdb-39 | Fedora 19 Linux | EXT4 | 10 GB |
| sdb-40 | RHEL 6.5 Linux | EXT4 | 10 GB |
| sdb-41 | SolydX 201506 | EXT4 | 10 GB |
| sdb-42 | Oracle Linux 6.7 | EXT4 | 10 GB |
| sdb-43 | OpenSuse 11.3 | EXT4 | 10 GB |
| sdb-44 | LMDE (Linux Mint 2 Debian 版) | EXT4 | 10 GB |
| sdb-45 | Linux 12.04 中心 | EXT4 | 10 GB |
| sdb-46 | 基础操作系统 2013 | EXT4 | 10 GB |
| sdb-47 | 基础操作系统 2015 | EXT4 | 10 GB |
| sdb-48 | Sabayon 13.08 Linux | EXT4 | 10 GB |
| sdb-49 | Deepin 2013 Linux | EXT4 | 10 GB |
| sdb-50 | Deepin 15.1 Linux | EXT4 | 10 GB |

在 sdb 磁盘上以 BIOS 方式引导的操作系统总数为 50–2 = 48。

两个分区保留给交换分区和扩展分区。

因此,Jarvis 系统上的安装总数是 106,正如您在图 3-28 中看到的,所有这些操作系统都是通过使用 GRUB 2 引导程序进行多重引导的。有了这个项目,我意识到这是没有止境的。GRUB 2 和 UEFI 的组合可以处理 n 个操作系统。

img/493794_1_En_3_Fig28_HTML.jpg

图 3-28

GRUB 2 列出的 106 种操作系统

我是如何安装这么多操作系统的?很简单。每次安装新的操作系统后,我都会启动grub-mkconfig命令,从所有连接的磁盘中找到所有的操作系统。

# time grub-mkconfig -o multiboot_grub.cfg

前面的命令是在安装了列表中第 106 个操作系统 Ubuntu 18 之后使用的。

如图 3-29 所示,当我安装第 106 个操作系统时,grub-mkconfig花了将近一个小时才完成,结果 GRUB 配置文件有 5500 行。

img/493794_1_En_3_Fig29_HTML.jpg

图 3-29

grb-mkconfig 命令花费的时间

一个虚拟的小型引导程序

我们知道 BIOS 跳转到第一个 512 字节并调用 GRUB 2 引导程序。为了理解 BIOS 是如何调用引导装载程序的,我们将制作自己的引导装载程序。与 GRUB 2 相比,我们的引导程序非常小。我们的引导程序会在屏幕上显示!。但是通过这个例子,您将能够理解 BIOS 如何像 GRUB 2 一样跳转到引导加载程序,如下所示:

#cat boot.nasm
    ;
    ; Note: this example is written in Intel Assembly syntax
    ;
     [BITS 16]
     [ORG 0x7c00]

    boot:
        mov al, '!'       <<-- Character for interrupt
        mov ah, 0x0e      <<-- Display character
        mov bh, 0x00      <<-- Set video mode
        mov bl, 0x07      <<-- Clear/Scroll screen down
        int 0x10          <<--- BIOS interrupt 10 which is taking inputs from al, ah, bh, bl
        jmp $
        times 510-($-$$) db 0      <<--- Out of 512 bytes first 510 bytes are filled  with 0's.
                                   In the real world it will be filled with grub's boot strap.
        db 0x55           <<-- &
        db 0xaa           <<-- | tells BIOS that this is the device which is active/fdisk sign/boot flag.

     #nasm -f bin boot.nasm && qemu-system-x86_64 boot

这将从boot.nasm文件中创建一个boot磁盘(磁盘映像),它将成为qemu的输入,后者将执行它。如图 3-30 所示,你会看到!印在屏幕上。

img/493794_1_En_3_Fig30_HTML.jpg

图 3-30

我们的小型引导程序

基本上,qemu机器把boot当作一个磁盘,每当qemu机器完成它的 BIOS 阶段,BIOS 就在引导磁盘的前 512 个字节处落下。在这里你会发现前 510 个字节被写成 0,最后 2 个字节我们有!(引导程序),它会被打印在我们的屏幕上。

到目前为止,我们已经很好地了解了 GRUB 2;在下一节中,我们将进一步讨论 GRUB 2 内部到底发生了什么。

低级别的 GRUB 2

在写这本书的时候,GRUB 最新可用的源代码是 GRUB 2.04,我一直在这里使用。从 512 字节的第一个 440 字节开始的引导二进制文件(如果系统是基于 BIOS 的)被称为boot.img,它在/usr/lib/grub/i386-pc/boot.img可用。

# ls -lh /usr/lib/grub/i386-pc/boot.img
-rw-r--r--. 1 root root 512 Mar 28  2019 /usr/lib/grub/i386-pc/boot.img

# file  /usr/lib/grub/i386-pc/boot.img
/usr/lib/grub/i386-pc/boot.img: DOS/MBR boot sector

boot.img文件是从写在文件/GRUB 2.04/grub-core/boot/i386/pc/boot.S中的源代码创建的。

以下是其中的一个片段:

<snip>
1 /* -*-Asm-*- */
  2 /*
  3  *  GRUB  --  GRand Unified Bootloader
  4  *  Copyright (C) 1999,2000,2001,2002,2005,2006,2007,2008,2009  Free Software Foundation, Inc.
  5  *
  6  *  GRUB is free software: you can redistribute it and/or modify
  7  *  it under the terms of the GNU General Public License as published by
  8  *  the Free Software Foundation, either version 3 of the License, or
  9  *  (at your option) any later version.
 10  *
 11  *  GRUB is distributed in the hope that it will be useful,
 12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  *  GNU General Public License for more details.
 15  *
 16  *  You should have received a copy of the GNU General Public License
 17  *  along with GRUB.  If not, see <http://www.gnu.org/licenses/>.
 18  */
 19
 20 #include <grub/symbol.h>
 21 #include <grub/machine/boot.h>
 22
 23 /*
 24  *  defines for the code go here
 25  */
 26
 27         /* Print message string */
 28 #define MSG(x)  movw $x, %si; call LOCAL(message)
 29 #define ERR(x)  movw $x, %si; jmp LOCAL(error_message)
 30
 31         .macro floppy
 32 part_start:
 33
 34 LOCAL(probe_values):
 35         .byte   36, 18, 15, 9, 0
 36
 37 LOCAL(floppy_probe):
 38         pushw   %dx
 39 /*
 40  *  Perform floppy probe.
 41  */
 42 #ifdef __APPLE__
 43         LOCAL(probe_values_minus_one) = LOCAL(probe_values) - 1
 44         movw    MACRO_DOLLAR(LOCAL(probe_values_minus_one)), %si
 45 #else
 46         movw    MACRO_DOLLAR(LOCAL(probe_values)) - 1, %si
 47 #endif
 48
 49 LOCAL(probe_loop):
 50         /* reset floppy controller INT 13h AH=0 */
 51         xorw    %ax, %ax
 52         int     MACRO_DOLLAR(0x13)
 </snip>

您可以将boot.img视为引导加载程序的第一阶段或 GRUB 的第一部分。这个boot.img文件将控制权转移给diskboot.img,这是 GRUB 的第二部分。

# ls -lh /usr/lib/grub/i386-pc/diskboot.img
-rw-r--r--. 1 root root 512 Mar 28  2019 /usr/lib/grub/i386-pc/diskboot.img

# file /usr/lib/grub/i386-pc/diskboot.img
/usr/lib/grub/i386-pc/diskboot.img: data

diskboot.img文件由grub-2.04/grub-core/boot/i386/pc/diskboot.S.的源代码组成,以下是它的一个片段:

<snip>
1 /*
  2  *  GRUB  --  GRand Unified Bootloader
  3  * Copyright (C) 1999,2000,2001,2002,2006,2007,2009,2010 Free Software Foundation, Inc.
  4  *
  5  *  GRUB is free software: you can redistribute it and/or modify
  6  *  it under the terms of the GNU General Public License as published by
  7  *  the Free Software Foundation, either version 3 of the License, or
  8  *  (at your option) any later version.
  9  *
 10  *  GRUB is distributed in the hope that it will be useful,
 11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13  *  GNU General Public License for more details.
 14  *
 15  *  You should have received a copy of the GNU General Public License
 16  *  along with GRUB.  If not, see <http://www.gnu.org/licenses/>.
 17  */
 18
 19 #include <grub/symbol.h>
 20 #include <grub/machine/boot.h>
 21
 22 /*
 23  *  defines for the code go here
 24  */
 25
 26 #define MSG(x)  movw $x, %si; call LOCAL(message)
 27
 28         .file   "diskboot.S"
 29
 30         .text
 31
 32         /* Tell GAS to generate 16-bit instructions so that this code works
 33            in real mode. */
 34         .code16
 35
 36         .globl  start, _start
 37 start:
 38 _start:
 39         /*
 40          * _start is loaded at 0x8000 and is jumped to with
 41          * CS:IP 0:0x8000 in kernel.
 42          */
 </snip>

然后,diskboot.img文件加载 GRUB 2 的实际核心部分,这是 GRUB 的第三部分。你也可以认为 GRUB 的第三部分是引导装载程序的一个内核。在这个阶段,GRUB 2 将能够读取文件系统。

# ls /boot/grub2/i386-pc/core.img -lh
-rw-r--r--. 1 root root 30K Dec  9 10:18 /boot/grub2/i386-pc/core.img

/GRUB 2.00/grub-core/kern/main.c开始,GRUB 2 设置根设备名,读取grub.cfg,最后显示操作系统列表供选择。

希望你现在明白 GRUB 2 是怎么工作的了。以下是我们到目前为止所讨论内容的简要总结:

  1. 引导装载程序是固件之后运行的第一个代码。

  2. bootloader/GRUB 将内核复制到内存中。

  3. 引导加载程序将 initramfs 映像加载到内存中,并给内核一个指向它的指针。

  4. 引导装载程序将控制权移交给内核。

四、内核

本章将介绍内核。

在内存中加载内核

这是一个有趣的章节。到目前为止,我们已经看到 GRUB 2 已经完全控制了引导过程。现在它必须将控制权交给内核。在这一章中,我们将会看到引导装载程序是如何以及在哪里装载内核的。换句话说,内核是怎么提取的?然后我们将看到 Linux 内核完成的与引导相关的任务,最后我们将看到内核如何启动 systemd。

Note

本章使用的内核源代码是版本kernel-5.4.4。当我写这本书的时候,那是最新的稳定代码;参见 https://www.kernel.org/ .关于这个主题的一个很好的参考资料是由 0xAX 写的《Linux 内部的】一书。我从中学到了很多,我相信你也会的。你可以在 https://0xax.gitbooks.io/linux-insides/ 找到这本书。

为了将控制权交给内核,引导装载程序必须完成两件主要的事情。

  • 将内核装入内存

  • 按照引导协议设置内核的一些字段

完整的启动协议可从 https://www.kernel.org/doc/Documentation/x86/boot.txt 获得。最初的引导协议是由 Linus Torvalds 定义的。

         ~                               ~
         |  Protected-mode kernel        |
 100000  +-------------------------------+
         |  I/O memory hole              |
 0A0000  +-------------------------------+
         |  Reserved for BIOS            | Leave as much as possible unused
         ~                               ~
         |  Command line                 | (Can also be below the X+10000 mark)
X+10000  +-------------------------------+
         |  Stack/heap                   | For use by the kernel real-mode code.
X+08000  +-------------------------------+
         |  Kernel setup                 | The kernel real-mode code.
         |  Kernel boot sector           | The kernel legacy boot sector.
      X  +-------------------------------+
         |  Boot loader                  | <- Boot sector entry point 0000:7C00\. You will see the same
         |                               | address location at our boot.asm file which we created above.
  001000 +-------------------------------+
         |  Reserved for MBR/BIOS        |
 000800  +-------------------------------+
         |  Typically used by MBR        |
 000600  +-------------------------------+
         |  BIOS use only                |
 000000  +-------------------------------+

按照引导协议,引导装载程序有责任传递或设置内核头的一些字段。这些字段是根设备名称、挂载选项(如rorw)、initramfs 名称、initramfs 大小等。这些相同的字段被称为内核命令行参数,我们已经知道内核命令行参数是由 GRUB/boot loader 传递给内核的。

GRUB 不会在任何随机位置加载内核(/boot/vmlinuz);它将总是被装载在一个特殊的位置。这个特殊的位置会根据您使用的 Linux 发行版和版本以及系统的 CPU 架构而有所不同。vmlinuz是一个归档文件,归档文件由三部分组成。

Vmlinuz (bZimage) =  Header   + kernel setup code + vmlinux (actual compressed kernel)
                     (part-1)   (part-2)            (part-3)

将内核加载到内存中后

这里我们需要想象 GRUB 2 已经在内存中的特殊位置加载了内核。下面是内核归档文件vmlinuz加载到内存后执行的初始步骤:

  1. 一旦引导加载程序将内核加载到内存中的特定位置,由文件arch/x86/boot/header.S生成的二进制文件就会运行。

  2. 如果vmlinuz是一个归档文件,而引导装载程序还没有提取它,就会出现混乱。引导加载程序刚刚在一个特定的位置加载了内核。那么为什么vmlinuz归档文件中的代码能够运行呢?

  3. 我们先看简答,长答在“什么提取了 vmlinuz?”本章第节。所以,简单的答案是由arch/x86/boot/header.S文件生成的二进制文件不在存档中;相反,它是执行kernel_setup任务的报头的一部分。标头在归档文件之外。

    Vmlinuz (bZimage) = Header + kernel setup code + vmlinux (actual compressed kernel)
             --->Outside of archive<--- + -------->Inside archive<------->header.s file is here<---
    
    
  4. 现在让我们考虑vmlinuz已经被提取,让我们继续我们的引导序列。到目前为止,我们已经看到 GRUB 将内核加载到内存中的一个特殊位置,并运行由arch/x86/boot/header.S.生成的二进制文件。该二进制文件负责Kernel_setup部分.kernel_setup文件执行以下任务:

    1. 对齐段寄存器

    2. 设置堆栈和 BSS

在每一章中,一个流程图会给我们一个清晰的概念,让我们知道我们已经学了什么,以及在引导方面,我们已经达到了什么程度。图 4-1 显示了我们将在本章中构建的流程图的开始。显示由header.skernel_setup代码执行的动作。

img/493794_1_En_4_Fig1_HTML.jpg

图 4-1

kernel_setup 采取的步骤

  1. 然后在arch/x86/boot/main.c跳转到main()功能。main.c文件也是内核头文件的一部分,这个头文件在实际的归档文件之外。
Vmlinuz (bZimage) = Header + kernel setup code + vmlinux (actual compressed kernel)
         --->Outside of archive<--- + -------->Inside archive<---------
         --->main.c file is here<---

#vim arch/x86/boot/main.c
<snip>
134 void main(void)
135 {
136         /* First, copy the boot header into the "zeropage" */
137         copy_boot_params();
138
139         /* Initialize the early-boot console */
140         console_init();
141         if (cmdline_find_option_bool("debug"))
142                 puts("early console in setup code\n");
143
144         /* End of heap check */
145         init_heap();
146
147         /* Make sure we have all the proper CPU support */
148         if (validate_cpu()) {
149                 puts("Unable to boot - please use a kernel appropriate "
150                      "for your CPU.\n");
151                 die();
152         }
153
154         /* Tell the BIOS what CPU mode we intend to run in. */
155         set_bios_mode();
156
157         /* Detect memory layout */

158         detect_memory();
159
160         /* Set keyboard repeat rate (why?) and query the lock flags */
161         keyboard_init();
162
163         /* Query Intel SpeedStep (IST) information */
164         query_ist();
165
166         /* Query APM information */
167 #if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
168         query_apm_bios();
169 #endif
170
171         /* Query EDD information */
172 #if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)
173         query_edd();
174 #endif
175
176         /* Set the video mode */
177         set_video();
178
179         /* Do the last things and invoke protected mode */
180         go_to_protected_mode();
181 }
</snip>

如您所见,main.c源代码负责以下内容:

  1. 它从引导装载程序复制引导参数(内核命令行参数)。copy_boot_params函数将用于复制引导加载程序传递的以下引导参数:

  2. 它初始化控制台,并检查用户是否传递了类似于debug的内核命令行参数。如果有,内核将在屏幕上显示详细级别的消息。

  3. 它初始化堆。

  4. 如果 CPU 不能被验证,那么它通过validate_cpu()函数抛出一个错误消息。像 Fedora 和 Ubuntu 这样的发行版定制了错误消息,从'unable to boot - please use the kernel appropriate for your cpu'到类似'The CPU is not supported'的东西。定制还会使内核恐慌,引导将被中止。

  5. 然后,它会检测内存布局,并在引导的早期阶段将其打印在屏幕上。使用'dmesg'命令可以在引导后看到相同的内存布局消息,如下所示:

debug, earlyprintk, ro, root, ramdisk_image, ramdisk_size etc.

img/493794_1_En_4_Fig2_HTML.jpg

图 4-2

流程图

  1. 初始化键盘及其布局。

  2. 设置基本视频模式。

  3. 通过go_to_protected_mode()功能跳转到保护模式。请参考图 4-2 以便更好地理解。

[    0.000000] BIOS-provided physical RAM map:
[    0.000000] BIOS-e820: [mem 0x0000000000000000-0x0000000000057fff] usable
[    0.000000] BIOS-e820: [mem 0x0000000000058000-0x0000000000058fff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000000059000-0x000000000009cfff] usable
[    0.000000] BIOS-e820: [mem 0x000000000009d000-0x00000000000fffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000000100000-0x000000007e5f7fff] usable
[    0.000000] BIOS-e820: [mem 0x000000007e5f8000-0x000000007e5f8fff] ACPI NVS
[    0.000000] BIOS-e820: [mem 0x000000007e5f9000-0x000000007e5f9fff] reserved
[    0.000000] BIOS-e820: [mem 0x000000007e5fa000-0x0000000087f62fff] usable
[    0.000000] BIOS-e820: [mem 0x0000000087f63000-0x000000008952bfff] reserved
[    0.000000] BIOS-e820: [mem 0x000000008952c000-0x0000000089599fff] ACPI NVS
[    0.000000] BIOS-e820: [mem 0x000000008959a000-0x00000000895fefff] ACPI data
[    0.000000] BIOS-e820: [mem 0x00000000895ff000-0x00000000895fffff] usable
[    0.000000] BIOS-e820: [mem 0x0000000089600000-0x000000008f7fffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000f0000000-0x00000000f7ffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fe010000-0x00000000fe010fff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000100000000-0x000000086e7fffff] usable

保护模式

到目前为止,我们一直在实模式下工作,该模式有 20 位地址限制,因为我们可以访问高达 1 MB 的内存。通过go_to_protected_mode()函数,内核将 CPU 从实模式切换到保护模式。保护模式有 32 位地址限制,因此 CPU 可以访问高达 4 GB 的内存。简单地说,在实模式下,只有那些具有 16 位指令集的程序才会运行,例如 BIOS。在保护模式下,只有 32 位程序会运行。内核在保护模式下执行一些硬件相关的任务,然后在长模式下启动 CPU。

请注意,本书遵循 Intel 的 X86 架构,实模式、保护模式、长模式的讨论都是基于 Intel 的 64 位架构。

长模式

长模式对 CPU 没有任何内存限制。它可以使用所有已安装的内存。将 CPU 置于长模式将通过来自arch/x86/boot/compressed/head_64.Shead_64.S文件实现。它负责以下工作:

  1. 准备长模式意味着它将检查它是否支持长模式。

  2. 进入长模式。

  3. 解压内核。

以下是从head_64.S汇编文件中调用的函数:

|

功能

|

工作

|
| --- | --- |
| verify_cpu | 这将确保 CPU 具有长模式。 |
| make_boot_params | 这将负责引导加载程序传递的引导时参数。 |
| efi_main | UEFI 固件相关的东西。 |
| extract_kernel | 该功能在arch/x86/boot/compressed_misc.c中定义。这是将从vmlinuz解压vmlinux的函数。 |

$ cat arch/x86/boot/compressed/head_64.S | grep -i call
    call    1f
    call    verify_cpu
    call    get_sev_encryption_bit
    call    1f
    call    1f
    call    .Ladjust_got
     * this function call.
    call    paging_prepare
     * this function call.
    call    cleanup_trampoline
    call    1f
    call    .Ladjust_got
    call    1f
     * Relocate efi_config->call().
    call    make_boot_params
    call    1f
     * Relocate efi_config->call().
    call    efi_main
    call    extract_kernel    /* returns kernel location in %rax */
    .quad    efi_call

为了更好地理解,请参考图 4-3 所示的流程图。

img/493794_1_En_4_Fig3_HTML.jpg

图 4-3

更新后的流程图

等一下:如果内核还没有被解压缩,那么我们现在怎么继续呢?下面是长答案。

什么提取 vmlinuz?

到目前为止,我们知道是 GRUB 在内存中加载内核,但同时,我们注意到vmlinuz映像是一个归档文件。那么,是什么提取了这个图像呢?是幼虫吗?

不,这不是食物。更确切地说,是内核自己提取。对,我说的是提取内核的内核。vmlinuz可能是操作系统世界中唯一能自我解压的文件。但是怎么可能提取自己呢?为了理解这一点,让我们先对vmlinuz有更多的了解。

vmlinuz的“vm”代表“虚拟内存”。在 Linux 开发的早期阶段,虚拟内存的概念还没有发展起来,所以在添加虚拟内存时,会将“vm”字符添加到 Linux 内核的名称中。“z”代表压缩文件。

$ file vmlinuz-5.0.9-301.fc30.x86_64

vmlinuz-5.0.9-301.fc30.x86_64: Linux kernel x86 boot executable bzImage, version 5.0.9-301.fc30.x86_64 (mockbuild@bkernel04.phx2.fedoraproject.org) #1 SMP Tue Apr 23 23:57:35 U, RO-rootFS, swap_dev 0x8, Normal VGA

如你所见,vmlinuz就是bzImage ( bzImage代表“大紫马哥”)。vmlinuz是实际内核的二进制文件vmlinux的压缩文件。你不能用gunzip/bunzip甚至tar解压这个文件。提取vmlinuz并获得vmlinux文件的最简单方法是使用kernel-devel包提供的extract-vmlinux脚本文件(在 Fedora 的情况下)。文件将出现在/usr/src/kernels/<kernel_version>/scripts/extract-vmlinux

# ./extract-vmlinux /boot/vmlinuz-5.3.7-301.fc31.x86_64 >> /boot/temp/vmlinux
# file /boot/temp/*
/boot/temp/vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
statically linked, BuildID[sha1]=ec96b29d8e4079950644230c0b7868942bb70366, stripped

打开vmlinuxvmlinuz内核文件有多种方式。

     $ xxd vmlinux | less
     $ objdump vmlinux | less
     $ objdump vmlinux -D | less
     $ hexdump vmlinux | less
     $ od vmlinux | less

我们将使用带有一些开关的od命令来打开vmlinuz文件。

     $ od -A d -t x1 vmlinuz-5.0.9-301.fc30.x86_64 | less
<snip>
0000000 4d 5a ea 07 00 c0 07 8c c8 8e d8 8e c0 8e d0 31
0000016 e4 fb fc be 40 00 ac 20 c0 74 09 b4 0e bb 07 00
0000032 cd 10 eb f2 31 c0 cd 16 cd 19 ea f0 ff 00 f0 00
0000048 00 00 00 00 00 00 00 00 00 00 00 00 82 00 00 00
0000064 55 73 65 20 61 20 62 6f 6f 74 20 6c 6f 61 64 65
0000080 72 2e 0d 0a 0a 52 65 6d 6f 76 65 20 64 69 73 6b
0000096 20 61 6e 64 20 70 72 65 73 73 20 61 6e 79 20 6b
0000112 65 79 20 74 6f 20 72 65 62 6f 6f 74 2e 2e 2e 0d
0000128 0a 00 50 45 00 00 64 86 04 00 00 00 00 00 00 00
0000144 00 00 01 00 00 00 a0 00 06 02 0b 02 02 14 80 37
0000160 8e 00 00 00 00 00 80 86 26 02 f0 48 00 00 00 02
0000176 00 00 00 00 00 00 00 00 00 00 20 00 00 00 20 00
0000192 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000208 00 00 00 c0 b4 02 00 02 00 00 00 00 00 00 0a 00
0000224 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
*
0000256 00 00 00 00 00 00 06 00 00 00 00 00 00 00 00 00
0000272 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000288 00 00 00 00 00 00 00 00 00 00 80 39 8e 00 48 09
0000304 00 00 00 00 00 00 00 00 00 00 2e 73 65 74 75 70
0000320 00 00 e0 43 00 00 00 02 00 00 e0 43 00 00 00 02
0000336 00 00 00 00 00 00 00 00 00 00 00 00 00 00 20 00
0000352 50 60 2e 72 65 6c 6f 63 00 00 20 00 00 00 e0 45
0000368 00 00 20 00 00 00 e0 45 00 00 00 00 00 00 00 00
0000384 00 00 00 00 00 00 40 00 10 42 2e 74 65 78 74 00

0000400 00 00 80 f3 8d 00 00 46 00 00 80 f3 8d 00 00 46
0000416 00 00 00 00 00 00 00 00 00 00 00 00 00 00 20 00
0000432 50 60 2e 62 73 73 00 00 00 00 80 86 26 02 80 39
0000448 8e 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000464 00 00 00 00 00 00 80 00 00 c8 00 00 00 00 00 00
0000480 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff
0000496 ff 22 01 00 38 df 08 00 00 00 ff ff 00 00 55 aa
0000512 eb 66 48 64 72 53 0d 02 00 00 00 00 00 10 c0 37
0000528 00 01 00 80 00 00 10 00 00 00 00 00 00 00 00 00
0000544 00 00 00 00 50 5a 00 00 00 00 00 00 ff ff ff 7f
0000560 00 00 00 01 01 15 3f 00 ff 07 00 00 00 00 00 00
0000576 00 00 00 00 00 00 00 00 b1 03 00 00 11 f3 89 00
0000592 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00
0000608 00 c0 b4 02 90 01 00 00 8c d8 8e c0 fc 8c d2 39
0000624 c2 89 e2 74 16 ba 50 58 f6 06 11 02 80 74 04 8b
</snip>

# od -A d -t x1 /boot/vmlinuz-5.3.7-301.fc31.x86_64 | grep -i '1f 8b 08 00'
0018864 8f 1f 8b 08 00 00 00 00 00 02 03 ec fd 79 7c 54

所以,在0018864,实际的内核(vmlinux)开始,而vmlinuz文件在0000000.开始,这意味着从00000000018864,我们拥有的是文件的头,比如header.Smisc.c等。这将从vmlinuz.中提取实际的内核(vmlinux)你可以把一个头文件看作是vmlinux二进制文件上的一个帽,当这个帽可用时,它就变成了vmlinuz。在接下来的部分中,我们将看到内核例程如何提取vmlinuz

提取 _ 内核

让我们从arch/x86/boot/compressed/misc.c回到extract_kernel函数。

asmlinkage __visible void *extract_kernel(void *rmode, memptr heap,
                                          unsigned char *input_data,
                                          unsigned long input_len,
                                          unsigned char *output,
                                          unsigned long output_len)

如您所见,该函数将接受七个参数。

|

争吵

|

目的

|
| --- | --- |
| rmode | 由引导装载程序填充的指向boot_params结构的指针 |
| heap | 指向代表早期引导堆起始地址的文件的指针 |
| input_data | 指向压缩内核起点的指针,或者换句话说,指向arch/x86/boot/compressed/vmlinux.bin.bz2的指针 |
| input_len | 压缩内核的大小 |
| output | 未来解压缩内核的起始地址 |
| output_len | 解压缩内核的大小 |
| run_size | 运行内核所需的空间量,包括.bss.brk部分 |

除了内核,引导装载程序还将在内存中装载 initramfs。我们将在第五章中讨论 initramfs。因此,在提取内核映像之前,头文件或内核例程必须注意vmlinuz提取不会覆盖或重叠已经加载的 initramfs 映像。因此,extract_kernel函数也将负责计算 initramfs 地址空间,并相应地调整内核映像解压缩。一旦我们得到了头文件可以解压vmlinuz的正确地址,它将在那里提取内核。

340 asmlinkage __visible void *extract_kernel(void *rmode, memptr heap,
341                                   unsigned char *input_data,
342                                   unsigned long input_len,
343                                   unsigned char *output,
344                                   unsigned long output_len)
345 {
346         const unsigned long kernel_total_size = VO__end - VO__text;
347         unsigned long virt_addr = LOAD_PHYSICAL_ADDR;
348         unsigned long needed_size;
349
350         /* Retain x86 boot parameters pointer passed from startup_32/64\. */
351         boot_params = rmode;
352
353         /* Clear flags intended for solely in-kernel use. */
354         boot_params->hdr.loadflags &= ~KASLR_FLAG;
355
356         sanitize_boot_params(boot_params);
357
358         if (boot_params->screen_info.orig_video_mode == 7) {
359                 vidmem = (char *) 0xb0000;
360                 vidport = 0x3b4;
361         } else {
362                 vidmem = (char *) 0xb8000;
363                 vidport = 0x3d4;
364         }
365
366         lines = boot_params->screen_info.orig_video_lines;
367         cols = boot_params->screen_info.orig_video_cols; 

368
369         console_init();
370
371         /*
372          * Save RSDP address for later use. Have this after console_init()
373          * so that early debugging output from the RSDP parsing code can be
374          * collected.
375          */
376         boot_params->acpi_rsdp_addr = get_rsdp_addr();
377
378         debug_putstr("early console in extract_kernel\n");
379
380         free_mem_ptr     = heap;        /* Heap */
381         free_mem_end_ptr = heap + BOOT_HEAP_SIZE;
382
383         /*
384          * The memory hole needed for the kernel is the larger of either
385          * the entire decompressed kernel plus relocation table, or the
386          * entire decompressed kernel plus .bss and .brk sections.
387          *
388          * On X86_64, the memory is mapped with PMD pages. Round the
389          * size up so that the full extent of PMD pages mapped is
390          * included in the check against the valid memory table
391          * entries. This ensures the full mapped area is usable RAM
392          * and doesnt include any reserved areas.
393          */
394         needed_size = max(output_len, kernel_total_size);
395 #ifdef CONFIG_X86_64
396         needed_size = ALIGN(needed_size, MIN_KERNEL_ALIGN);
397 #endif
398
399         /* Report initial kernel position details. */
400         debug_putaddr(input_data);
401         debug_putaddr(input_len);
402         debug_putaddr(output);
403         debug_putaddr(output_len);
404         debug_putaddr(kernel_total_size);
405         debug_putaddr(needed_size);
406
407 #ifdef CONFIG_X86_64

408         /* Report address of 32-bit trampoline */
409         debug_putaddr(trampoline_32bit);
410 #endif
411
412         choose_random_location((unsigned long)input_data, input_len,
413                                 (unsigned long *)&output,
414                                 needed_size,
415                                 &virt_addr);
416
417         /* Validate memory location choices. */
418         if ((unsigned long)output & (MIN_KERNEL_ALIGN - 1))
419                 error("Destination physical address inappropriately aligned");
420         if (virt_addr & (MIN_KERNEL_ALIGN - 1))
421                 error("Destination virtual address inappropriately aligned");
422 #ifdef CONFIG_X86_64
423         if (heap > 0x3fffffffffffUL)
424                 error("Destination address too large");
425         if (virt_addr + max(output_len, kernel_total_size) > KERNEL_IMAGE_SIZE)
426                 error("Destination virtual address is beyond the kernel mapping area");
427 #else
428         if (heap > ((-__PAGE_OFFSET-(128<<20)-1) & 0x7fffffff))
429                 error("Destination address too large");
430 #endif
431 #ifndef CONFIG_RELOCATABLE
432         if ((unsigned long)output != LOAD_PHYSICAL_ADDR)
433                 error("Destination address does not match LOAD_PHYSICAL_ADDR");
434         if (virt_addr != LOAD_PHYSICAL_ADDR)
435                 error("Destination virtual address changed when not relocatable");
436 #endif
437
438         debug_putstr("\nDecompressing Linux... ");
439         __decompress(input_data, input_len, NULL, NULL, output, output_len,
440                         NULL, error);
441         parse_elf(output);
442         handle_relocations(output, output_len, virt_addr);
443         debug_putstr("done.\nBooting the kernel.\n");
444         return output;
445 }

解压缩方法将根据内核编译时使用的压缩算法来选择。解压缩方法可以在同一个misc.c文件中看到。

           <snip from misc.c>
 57 #ifdef CONFIG_KERNEL_GZIP
 58 #include "../../../../lib/decompress_inflate.c"
 59 #endif
 60
 61 #ifdef CONFIG_KERNEL_BZIP2
 62 #include "../../../../lib/decompress_bunzip2.c"
 63 #endif
 64
 65 #ifdef CONFIG_KERNEL_LZMA
 66 #include "../../../../lib/decompress_unlzma.c"
 67 #endif
 68
 69 #ifdef CONFIG_KERNEL_XZ
 70 #include "../../../../lib/decompress_unxz.c"
 71 #endif
 72
 73 #ifdef CONFIG_KERNEL_LZO
 74 #include "../../../../lib/decompress_unlzo.c"
 75 #endif
     </snip>

一旦内核在内存中解压缩,提取的内核的入口点将从extract_kernel函数中获得,CPU 将跳转到一个内核内部。

内核内部

内核做了很多事情,但是我将列出你作为一个学习引导的人最感兴趣的事情。

  • 如果架构是 64 位的,内核会将内核堆栈大小设置为 16 KB。这意味着每一个新的进程都将拥有自己的内核堆栈,大小为 16 KB。

  • page_size将被设置为 4 KB,这是英特尔 64 位架构的默认页面大小。

  • 内核将准备中断和异常处理机制,也称为中断描述符表 (IDT)。

  • 内核会设置页面错误处理机制。

  • 内核将收集 initramfs 文件的详细信息,如文件名、大小、地址、重定位地址、新根设备的主要和次要编号等。,从/arch/x86/kernel/setup.c开始。

  • 然后它从源代码文件init/initramfs.c中提取 initramfs。

  • 最后,它通过使用init/main.cstart_kernel函数启动 systemd。

您会注意到这是我们第一次来到arch目录之外。这意味着我们可以认为这段代码是独立于架构的。内核一旦启动,它会做无数的事情,在本书中几乎不可能涵盖所有的内容。就引导而言,内核的座右铭是从 initramfs .启动 systemd 由于 initramfs 已经被引导装载程序加载到内存中,提取 initramfs 内核需要 initramfs 文件细节,内核将从/arch/x86/kernel/setup.c获得这些细节。

      Initramfs file name,
      Initramfs file size,
      Initramfs files address,
      Initramfs files relocation address,
      Major and minor numbers on which initramfs will be mounted.

一旦内核收到 initramfs 文件的细节,它将从init/initramfs.c文件中提取 initramfs 档案。我们将在第五章中讨论内核如何提取内存中的 initramfs。要将 initramfs 挂载为根设备,需要虚拟文件系统,如procsysdev等。,所以内核相应地准备它们。

    err = register_filesystem(&proc_fs_type);
        if (err)
        return;

内核稍后将在init/do_mounts.c.do_mount_root函数的帮助下将提取的 initramfs 挂载为根。一旦 initramfs 被挂载到内存中,内核将从其中启动 systemd。systemd 将通过一个init/main.c文件的相同的start_kernel函数启动。

     asmlinkage void __init start_kernel(void)

基本上,一旦根文件系统准备好了,它将进入根文件系统并创建两个线程:PID 1 是一个 systemd 进程,PID 2 是一个 kthread。为了更好地理解,请参考图 4-4 所示的流程图。

img/493794_1_En_4_Fig4a_HTML.jpg img/493794_1_En_4_Fig4b_HTML.jpg

图 4-4

流程图,再次更新

图 4-5 显示了我们到目前为止讨论过的完整启动序列。

img/493794_1_En_4_Fig5_HTML.jpg

图 4-5

框图中的启动顺序

在我们继续讨论内核如何提取 initramfs 并从中运行 systemd 之前,我们需要了解 initramfs 的基础知识,比如我们为什么需要它,它的结构是什么,等等。一旦我们理解了 initramfs 的重要性和基础知识,我们将继续我们的引导顺序,讨论 systemd 在引导顺序中的作用。

五、initramfs

在本章中,我们将讨论为什么我们真的需要 initramfs,以及为什么它在引导过程中很重要。我们知道 initramfs 是由 bootloader 加载到内存中的,但是我们还没有讨论 initramfs 是如何提取的。本章将解决这个问题。我们还将看到提取、重建和定制 initramfs 的步骤。稍后,我们将看到 initramfs 的结构以及 initramfs 中系统的引导顺序。

为什么是 initramfs?

引导过程的目的是向用户展示驻留在根文件系统中的他们自己的文件。换句话说,找到、安装并向用户展示用户的根文件系统是内核的职责。为了实现这个目标,内核必须运行 systemd 二进制文件,它也驻留在用户的根文件系统中。现在这已经成为一个鸡和蛋的问题。要运行 systemd 进程,首先我们必须挂载根文件系统,要挂载根文件系统,我们必须从根文件系统运行 systemd。此外,除了实际的根文件系统之外,用户还可能拥有其他文件系统上的文件,如 NFS、CIFS 等。其他文件系统的列表也在根文件系统中(/etc/fstab)。

因此,为了解决这个先有鸡还是先有蛋的问题,开发人员提出了一个名为 initramfs(意思是“初始 RAM 文件系统”)的解决方案。initramfs 是一个临时的根文件系统(在主内存中),它将用于挂载实际的根文件系统(从硬盘或网络)。因此,initramfs 的全部目的是从 HDD/网络挂载用户的根文件系统。理想情况下,内核有足够的能力在没有 initramfs 的情况下从磁盘挂载根文件系统,但是现在用户的根文件系统可能在任何地方。它可能在 RAID、LVM 或多路径设备上。它可以是 XFS、BTRFS、ext4、ext3、NFS 等多种文件系统类型。它甚至可以在像 LUKS 这样的加密文件系统上。因此,对于一个内核来说,在自己的vmlinux二进制文件中包含所有这些场景几乎是不可能的。让我在这一部分提供一些真实的场景。

假设根文件系统在 NFS 上,并且没有 initramfs 的概念。这意味着内核必须自己从 NFS 挂载用户的根文件系统。在这种情况下,内核必须完成以下任务:

  1. 调出主网络接口。

  2. 调用 DHCP 客户端并从 DHCP 服务器获取 IP 地址。

  3. 找到 NFS 共享和相关的 NFS 服务器。

  4. 挂载 NFS 共享(根文件系统)。

要实现这些步骤,内核需要有以下二进制:NetworkManagerdhclientmount等。

现在让我们假设根文件系统在一个软件 RAID 设备上。然后内核必须完成以下任务:

  1. 先用mdadm --examine --scan找到 RAID 磁盘。

  2. 一旦软件 RAID 跨越的底层磁盘被识别,它必须用mdadm --assemble --scan组装 RAID。

  3. 为了实现这一点,内核需要有mountmdadm二进制文件以及软件 RAID 设备的一些配置文件。

现在让我们假设根文件系统在一个逻辑卷上。然后内核必须自己完成以下任务:

  1. pvs找到物理卷。

  2. vgscan找到卷组,然后用vgchange.激活它

  3. lvscan扫描 LVS。

  4. 最后,一旦填充了root lv,就将其作为根文件系统挂载。

  5. 为了实现这一点,内核需要有类似于pvscanpvslvscanvgscanlvsvgchange的二进制文件。

假设根文件系统位于一个加密的块设备上。然后内核必须完成以下任务:

  1. 从用户处收集密码和/或插入硬件令牌(如智能卡或 USB 安全加密狗)。

  2. 使用设备映射器创建解密目标。

为了实现所有这些,内核需要与 LUKS 相关的二进制文件。

对于内核来说,不可能包含所有这些根文件系统的可能性;因此,开发人员提出了 initramfs 概念,其唯一目的是挂载根文件系统。

内核仍然可以执行我们已经讨论过的所有步骤。例如,如果您从 LFS ( www.linuxfromscratch.org/ )构建一个简单的命令行 Linux 系统,您不需要 initramfs 来挂载根文件系统,因为内核本身就有足够的能力来挂载根文件系统。但是当你试图通过 BLFS 给它添加一个 GUI 时,你需要 initramfs。

因此,结论是内核可以自己挂载根文件系统,但是为此,内核必须保留所有讨论过的二进制文件、支持库、配置文件等。,在vmlinuz文件中。这会产生很多问题。

  • 它会破坏内核二进制的主要动机。

  • 内核二进制文件将会非常大。较大的二进制文件将很难维护。

  • 庞大的二进制文件很难在服务器上管理、升级、共享和处理(就 RPM 包而言)。

  • 方法不会按照接吻规则(保持简单,笨蛋)。

基础设施

为了理解 initramfs 结构,我们需要首先理解三种不同的文件系统。

拉弗

为了便于理解,我们将比较 ramfs 和内核的缓存机制。Linux 有一个独特的特性叫做页面缓存。每当您执行任何 I/O 事务时,它都会在页面中缓存这些事务。在内存中缓存页面总是好的。这将节省我们未来的 I/O 事务。每当系统遇到内存不足的情况时,内核就会从内存中丢弃这些缓存的页面。ramfs 就像我们的缓存。但是 ramfs 的问题是它没有后备存储;因此,它不能换出页面(swap 也是一个存储设备)。因此,很明显,内核将无法释放这些内存,因为没有地方保存这些页面。因此,ramfs 将继续增长,你不能真正限制它的大小。我们能做的是只允许根用户写入 ramfs 来缓解这种情况。

文件系统

tmpfs 就像 ramfs 一样,只是增加了一些东西。我们可以限制 tmpfs 的大小,这在 ramfs 中是做不到的。此外,tmpfs 页面可以使用交换空间。

根目录

rootfs 是一个 tmpfs,它是 ramfs 的一个实例。rootfs 的优点是您不能卸载它。这和你不能杀死systemd进程是一个道理。

initramfs 使用 ramfs 作为文件系统,一旦挂载了用户的根文件系统,initramfs 在内存中占用的空间就会被释放。

# dmesg | grep Free

[    0.813330] Freeing SMP alternatives memory: 36K
[    3.675187] Freeing initrd memory: 32548K    <<<=======<<<<<<===== NOTE
[    5.762702] Freeing unused decrypted memory: 2040K
[    5.767001] Freeing unused kernel image memory: 2272K
[    5.776841] Freeing unused kernel image memory: 2016K
[    5.783116] Freeing unused kernel image memory: 1580K

以前,Linux 使用initrd(初始 RAM 磁盘)而不是 initramfs,但是initrd现在已经过时了,因此我们将只列出几个要点来与 initramfs 进行比较。

init rd

  • 被格式化/视为块设备意味着initrd无法扩展。这意味着一旦您将initrd放入内存并将其视为块设备,您就不能增加或减少它的大小。

  • 我们将在缓存中浪费一些内存,因为initrd被认为是块设备,因为 Linux 内核被设计为将块设备内容保存在缓存中,以减少 I/O 事务。简而言之,内核不必要地缓存已经在内存中的initrd内容。

Initramfs 的缩写形式

  • initrd中,总会有文件系统驱动程序及其二进制文件的开销,比如mke2fsmke2fs命令用于创建 ext2/3/4 文件系统。这意味着一些 RAM 区域将首先被格式化,由mke2fs使用 ext2/3/4 文件系统,然后initrd将在其上提取,而 initramfs 就像 tmpfs 一样,您可以随时动态地增加或减少。

  • 数据块设备和缓存之间没有数据重复。

  • 要使用 initramfs 作为根文件系统,内核不需要任何驱动程序或类似于mke2fs的二进制文件,因为 initramfs 归档文件将按原样提取到主内存中。

# ls -lh /boot/initramfs-5.3.7-301.fc31.x86_64.img
-rw-------. 1 root root 32M Dec  9 10:19 /boot/initramfs-5.3.7-301.fc31.x86_64.img

我们可以使用lsinitrd工具查看 initramfs 的内容,或者我们可以在skipcpio工具的帮助下提取 initramfs。

#lsinitrd
<snip>
Image: /boot/initramfs-5.3.7-301.fc31.x86_64.img: 32M
========================================================================
Early CPIO image
========================================================================
drwxr-xr-x   3 root     root            0 Jul 25  2019 .
-rw-r--r--   1 root     root            2 Jul 25  2019 early_cpio
drwxr-xr-x   3 root     root            0 Jul 25  2019 kernel
drwxr-xr-x   3 root     root            0 Jul 25  2019 kernel/x86
drwxr-xr-x   2 root     root            0 Jul 25  2019 kernel/x86/microcode
-rw-r--r--   1 root     root       100352 Jul 25  2019 kernel/x86/microcode/GenuineIntel.bin
========================================================================
Version: dracut-049-27.git20181204.fc31.1

Arguments: -f

dracut modules:
bash
systemd
systemd-initrd
nss-softokn
i18n
network-manager
network
ifcfg
drm
plymouth
dm
kernel-modules
kernel-modules-extra
kernel-network-modules
lvm
qemu
qemu-net
resume
rootfs-block
terminfo
udev-rules
dracut-systemd
usrmount
base
fs-lib
shutdown
========================================================================
drwxr-xr-x  12 root     root            0 Jul 25  2019 .
crw-r--r--   1 root     root       5,   1 Jul 25  2019 dev/console
crw-r--r--   1 root     root       1,  11 Jul 25  2019 dev/kmsg
crw-r--r--   1 root     root       1,   3 Jul 25  2019 dev/null
crw-r--r--   1 root     root       1,   8 Jul 25  2019 dev/random
crw-r--r--   1 root     root       1,   9 Jul 25  2019 dev/urandom
lrwxrwxrwx   1 root     root            7 Jul 25  2019 bin -> usr/bin
drwxr-xr-x   2 root     root            0 Jul 25  2019 dev
drwxr-xr-x  11 root     root            0 Jul 25  2019 etc
drwxr-xr-x   2 root     root            0 Jul 25  2019 etc/cmdline.d
drwxr-xr-x   2 root     root            0 Jul 25  2019 etc/conf.d
-rw-r--r--   1 root     root          124 Jul 25  2019 etc/conf.d/systemd.conf
-rw-r--r--   1 root     root            0 Jul 25  2019 etc/fstab.empty
-rw-r--r--   1 root     root          240 Jul 25  2019 etc/group
-rw-r--r--   1 root     root           22 Jul 25  2019 etc/hostname
lrwxrwxrwx   1 root     root           25 Jul 25  2019 etc/initrd-release -> ../usr/lib/initrd-release
-rw-r--r--   1 root     root         8581 Jul 25  2019 etc/ld.so.cache
-rw-r--r--   1 root     root           28 Jul 25  2019 etc/ld.so.conf
drwxr-xr-x   2 root     root            0 Jul 25  2019 etc/ld.so.conf.d
-rw-r--r--   1 root     root           17 Jul 25  2019 etc/ld.so.conf.d/libiscsi-x86_64.conf
-rw-rw-r--   1 root     root           19 Jul 25  2019 etc/locale.conf
drwxr-xr-x   2 root     root            0 Jul 25  2019 etc/lvm
-rw-r--r--   1 root     root       102256 Jul 25  2019 etc/lvm/lvm.conf
-rw-r--r--   1 root     root         2301 Jul 25  2019 etc/lvm/lvmlocal.conf
-r--r--r--   1 root     root           33 Jul 25  2019 etc/machine-id
drwxr-xr-x   2 root     root            0 Jul 25  2019 etc/modprobe.d
</snip>

要提取 initramfs ,的内容,请使用来自/usr/lib/dracut/skipcpio/.skipcpio二进制文件,skipcpio由 dracut 工具提供。我们将在第六章中讲述 dracut。

#/usr/lib/dracut/skipcpio initramfs-5.3.7-301.fc31.x86_64.img | gunzip -c | cpio -idv

如果您查看提取的 initramfs 内容,您会惊讶地发现它看起来就像用户的根文件系统。请注意,我们已经将 initramfs 提取到了/root/boot目录中。

# ls -lh /root/boot/
total 44K
lrwxrwxrwx.  1 root root    7 Mar 26 18:03 bin -> usr/bin
drwxr-xr-x.  2 root root 4.0K Mar 26 18:03 dev
drwxr-xr-x. 11 root root 4.0K Mar 26 18:03 etc
lrwxrwxrwx.  1 root root   23 Mar 26 18:03 init -> usr/lib/systemd/systemd
lrwxrwxrwx.  1 root root    7 Mar 26 18:03 lib -> usr/lib
lrwxrwxrwx.  1 root root    9 Mar 26 18:03 lib64 -> usr/lib64
drwxr-xr-x.  2 root root 4.0K Mar 26 18:03 proc
drwxr-xr-x.  2 root root 4.0K Mar 26 18:03 root
drwxr-xr-x.  2 root root 4.0K Mar 26 18:03 run
lrwxrwxrwx.  1 root root    8 Mar 26 18:03 sbin -> usr/sbin
-rwxr-xr-x.  1 root root 3.1K Mar 26 18:03 shutdown
drwxr-xr-x.  2 root root 4.0K Mar 26 18:03 sys
drwxr-xr-x.  2 root root 4.0K Mar 26 18:03 sysroot
drwxr-xr-x.  2 root root 4.0K Mar 26 18:03 tmp
drwxr-xr-x.  8 root root 4.0K Mar 26 18:03 usr
drwxr-xr-x.  3 root root 4.0K Mar 26 18:03 var

您会发现binsbinusretcvarliblib64——类似于我们过去在用户的根文件系统中看到的目录。与此同时,您会注意到虚拟文件系统目录,如devrunprocsys等。因此,initramfs 就像用户的根文件系统。为了更好地理解 initramfs 的实现,让我们研究一下每个目录。

initramfs 实现

现在我们来看看 initramfs 的内容,以及 initramfs 是如何组织的。通过本节,您将认识到 initramfs 只不过是一个小的根文件系统。

容器

正常双星

我们可以在已经完成引导过程的系统上使用以下所有二进制文件。因为所有这些二进制文件都可以在 initramfs 中获得,所以当系统还在引导时,我们将能够在引导时使用所有这些命令。

cat, chown, cp, dmesg, echo, grep, gzip, less, ln, mkdir, mv, ps, rm, sed, sleep, umount, uname, vi, loadkeys, kbd_mode, flock, tr, true, stty, mount, sort etc.

[root@fedorab boot]# ls -la bin/

total 7208
drwxr-xr-x. 2 root root    4096 Jan 10 12:01 .
drwxr-xr-x. 8 root root    4096 Dec 19 14:30 ..
-rwxr-xr-x. 1 root root 1237376 Dec 19 14:30 bash
-rwxr-xr-x. 1 root root   50160 Dec 19 14:30 cat
-rwxr-xr-x. 1 root root   82688 Dec 19 14:30 chown
-rwxr-xr-x. 1 root root  177144 Dec 19 14:30 cp
-rwxr-xr-x. 1 root root   89344 Dec 19 14:30 dmesg
-rwxr-xr-x. 1 root root    2666 Dec 19 14:30 dracut-cmdline
-rwxr-xr-x. 1 root root     422 Dec 19 14:30 dracut-cmdline-ask
-rwxr-xr-x. 1 root root    1386 Dec 19 14:30 dracut-emergency
-rwxr-xr-x. 1 root root    2151 Dec 19 14:30 dracut-initqueue
-rwxr-xr-x. 1 root root    1056 Jan 10 12:01 dracut-mount
-rwxr-xr-x. 1 root root     517 Dec 19 14:30 dracut-pre-mount
-rwxr-xr-x. 1 root root     928 Dec 19 14:30 dracut-pre-pivot
-rwxr-xr-x. 1 root root     482 Dec 19 14:30 dracut-pre-trigger
-rwxr-xr-x. 1 root root    1417 Dec 19 14:30 dracut-pre-udev
-rwxr-xr-x. 1 root root   45112 Dec 19 14:30 echo
-rwxr-xr-x. 1 root root   76768 Dec 19 14:30 findmnt
-rwxr-xr-x. 1 root root   38472 Dec 19 14:30 flock
-rwxr-xr-x. 1 root root  173656 Dec 19 14:30 grep
-rwxr-xr-x. 1 root root  107768 Dec 19 14:30 gzip
-rwxr-xr-x. 1 root root   78112 Dec 19 14:30 journalctl
-rwxr-xr-x. 1 root root   17248 Dec 19 14:30 kbd_mode
-rwxr-xr-x. 1 root root  387504 Dec 19 14:30 kmod
-rwxr-xr-x. 1 root root  192512 Dec 19 14:30 less
-rwxr-xr-x. 1 root root   85992 Dec 19 14:30 ln
-rwxr-xr-x. 1 root root  222616 Dec 19 14:30 loadkeys
lrwxrwxrwx. 1 root root       4 Dec 19 14:30 loginctl -> true
-rwxr-xr-x. 1 root root  158056 Dec 19 14:30 ls
-rwxr-xr-x. 1 root root   99080 Dec 19 14:30 mkdir
-rwxr-xr-x. 1 root root   80264 Dec 19 14:30 mkfifo
-rwxr-xr-x. 1 root root   84560 Dec 19 14:30 mknod
-rwsr-xr-x. 1 root root   58984 Dec 19 14:30 mount
-rwxr-xr-x. 1 root root  169400 Dec 19 14:30 mv
-rwxr-xr-x. 1 root root   50416 Dec 19 14:30 plymouth
-rwxr-xr-x. 1 root root  143408 Dec 19 14:30 ps
-rwxr-xr-x. 1 root root   60376 Dec 19 14:30 readlink
-rwxr-xr-x. 1 root root   83856 Dec 19 14:30 rm
-rwxr-xr-x. 1 root root  127192 Dec 19 14:30 sed
-rwxr-xr-x. 1 root root   52272 Dec 19 14:30 setfont
-rwxr-xr-x. 1 root root   16568 Dec 19 14:30 setsid
lrwxrwxrwx. 1 root root       4 Dec 19 14:30 sh -> bash
-rwxr-xr-x. 1 root root   46608 Dec 19 14:30 sleep
-rwxr-xr-x. 1 root root  140672 Dec 19 14:30 sort
-rwxr-xr-x. 1 root root   96312 Dec 19 14:30 stat
-rwxr-xr-x. 1 root root   92576 Dec 19 14:30 stty
-rwxr-xr-x. 1 root root  240384 Dec 19 14:30 systemctl
-rwxr-xr-x. 1 root root   20792 Dec 19 14:30 systemd-cgls
-rwxr-xr-x. 1 root root   19704 Dec 19 14:30 systemd-escape
-rwxr-xr-x. 1 root root   62008 Dec 19 14:30 systemd-run
-rwxr-xr-x. 1 root root   95168 Dec 19 14:30 systemd-tmpfiles

-rwxr-xr-x. 1 root root  173752 Dec 19 14:30 teamd
-rwxr-xr-x. 1 root root   58400 Dec 19 14:30 tr
-rwxr-xr-x. 1 root root   45112 Dec 19 14:30 true
-rwxr-xr-x. 1 root root  442552 Dec 19 14:30 udevadm
-rwsr-xr-x. 1 root root   41912 Dec 19 14:30 umount
-rwxr-xr-x. 1 root root   45120 Dec 19 14:30 uname
-rwxr-xr-x. 1 root root 1353704 Dec 19 14:30 vi

特殊二进制文件

|

特殊二元

|

目的

|
| --- | --- |
| bash | initramfs 会在引导时给我们提供一个 shell。 |
| mknod | 我们将能够创造设备。 |
| udevadm | 我们将能够管理设备。dracut 使用的是udev,一个事件驱动的工具,它会启动某些程序,比如lvmmdadm等。,当符合某些udev规则时。例如,只要符合某些udev规则,存储卷和网卡设备文件就会出现在/dev下。 |
| kmod | 一个管理内核模块的工具。 |

网络二进制文件

在 bin 下只有一个与网络相关的二进制文件可用,那就是 teamd (initramfs 可以处理分组网络设备)。

钩住

我们将在第七章和第九章中讨论钩子。

dracut-cmdline               dracut-cmdline-ask
dracut-emergency             dracut -initqueue
dracut-mount                 dracut -pre-pivot
dracut - pre-trigger         dracut -pre-udev

二进制系统

|

二进制的

|

目的

|
| --- | --- |
| systemd | 这是替换init的每个进程的父进程。这是第一个进程,它在我们进入 initramfs 时运行。 |
| systemctl | Systemd 的服务经理。 |
| systemd-cgls | 这将列出现有的控制组(cgroups)。 |
| systemd-escape | 这将把字符串转换成 systemd 单位格式,也称为转义。 |
| systemd-run | 这可以将程序作为服务运行,但是在临时范围内。 |
| systemd-tmpfiles | 这将创建、删除和清理易变的和临时的文件和目录。 |
| journalctl | 一个处理 systemd 日志的工具。 |

命令

文件系统和存储相关的二进制文件

|

二进制的

|

目的

|
| --- | --- |
| blkid | 要读取设备属性 |
| chroot | 要更改根文件系统设备 |
| e2fsck | 检查 ext2/3/4 文件系统 |
| fsck, fsck.ext4 | 检查并修复文件系统 |
| swapoff | 如果您想停止交换设备 |
| dmsetup | 用于 LVM 管理的设备映射工具 |
| dmeventd | 设备映射器事件守护程序 |
| lvm | 一个 LVM 管理工具,将提供lvscanvgscanvgchangepvs等。,命令 |
| lvm_scan | 找到 LVM 设备的脚本 |

网络二进制文件

|

双星

|

目的

|
| --- | --- |
| dhclient | 要从 DHCP 服务器获取 IP 地址 |
| losetup | 设置loop装置 |
| Netroot | 网络上对根的支持 |
| NetworkManager | 管理网络设备的工具 |

特殊二进制文件

|

双星

|

目的

|
| --- | --- |
| depmod | 生成modules.dep(kmod的符号链接) |
| lsmod | 列出加载的模块(kmod的符号链接) |
| modinfo | 打印模块信息(符号链接kmod) |
| modprobe | 加载或插入模块(符号链接kmod) |
| rmmod | 移除加载的模块(kmod的符号链接) |
| init / systemd | 第一个过程 |
| kexec | Kdump 使用的 kexec 内核 |
| udevadm | Udev 经理 |

基本二进制文件

最后,下面是基本的二进制文件:

Halt, poweroff, reboot

 [root@fedorab boot]# ls -lah sbin/
total 13M
drwxr-xr-x. 2 root root 4.0K Dec 19 14:30 .
drwxr-xr-x. 8 root root 4.0K Dec 19 14:30 ..
-rwxr-xr-x. 1 root root 126K Dec 19 14:30 blkid
-rwxr-xr-x. 1 root root  50K Dec 19 14:30 chroot
lrwxrwxrwx. 1 root root   11 Dec 19 14:30 depmod -> ../bin/kmod
-rwxr-xr-x. 1 root root 2.9M Dec 19 14:30 dhclient
-r-xr-xr-x. 1 root root  45K Dec 19 14:30 dmeventd
-r-xr-xr-x. 1 root root 159K Dec 19 14:30 dmsetup
-rwxr-xr-x. 2 root root 340K Dec 19 14:30 e2fsck
-rwxr-xr-x. 1 root root  58K Dec 19 14:30 fsck
-rwxr-xr-x. 2 root root 340K Dec 19 14:30 fsck.ext4
lrwxrwxrwx. 1 root root   16 Dec 19 14:30 halt -> ../bin/systemctl
lrwxrwxrwx. 1 root root   22 Dec 19 14:30 init -> ../lib/systemd/systemd
-rwxr-xr-x. 1 root root 1.2K Dec 19 14:30 initqueue
lrwxrwxrwx. 1 root root   11 Dec 19 14:30 insmod -> ../bin/kmod
-rwxr-xr-x. 1 root root  197 Dec 19 14:30 insmodpost.sh
-rwxr-xr-x. 1 root root 203K Dec 19 14:30 kexec
-rwxr-xr-x. 1 root root  496 Dec 19 14:30 loginit
-rwxr-xr-x. 1 root root 117K Dec 19 14:30 losetup
lrwxrwxrwx. 1 root root   11 Dec 19 14:30 lsmod -> ../bin/kmod

-r-xr-xr-x. 1 root root 2.4M Dec 19 14:30 lvm
-rwxr-xr-x. 1 root root 3.5K Dec 19 14:30 lvm_scan
lrwxrwxrwx. 1 root root   11 Dec 19 14:30 modinfo -> ../bin/kmod
lrwxrwxrwx. 1 root root   11 Dec 19 14:30 modprobe -> ../bin/kmod
-rwxr-xr-x. 1 root root 2.7K Dec 19 14:30 netroot
-rwxr-xr-x. 1 root root 5.3M Dec 19 14:30 NetworkManager
-rwxr-xr-x. 1 root root  16K Dec 19 14:30 nologin
-rwxr-xr-x. 1 root root 150K Dec 19 14:30 plymouthd
lrwxrwxrwx. 1 root root   16 Dec 19 14:30 poweroff -> ../bin/systemctl
-rwxr-xr-x. 1 root root 1.4K Dec 19 14:30 rdsosreport
lrwxrwxrwx. 1 root root   16 Dec 19 14:30 reboot -> ../bin/systemctl
lrwxrwxrwx. 1 root root   11 Dec 19 14:30 rmmod -> ../bin/kmod
-rwxr-xr-x. 1 root root  25K Dec 19 14:30 swapoff
-rwxr-xr-x. 1 root root 6.0K Dec 19 14:30 tracekomem
lrwxrwxrwx. 1 root root   14 Dec 19 14:30 udevadm -> ../bin/udevadm

看到没有实际用户的根文件系统,我们也能使用和管理 Shell、网络、模块、设备等等,这难道不令人惊奇吗??换句话说,您并不真正需要用户的根文件系统,除非用户想要访问他们的私有文件。开玩笑的。

现在一个问题浮现在脑海中:我们可以在哪里以及如何使用所有这些命令?initramfs 将自动使用这些二进制文件或命令。或者,正确地说,这些二进制文件或命令将被 initramfs 的 systemd 用来挂载用户的实际根文件系统,但如果 systemd 无法这样做,它将为我们提供一个 shell,我们将能够使用这些命令并进一步排除故障。我们将在第七章第七章、第八章和第九章对此进行讨论。

等等

来自binsbin目录的二进制文件将有自己的配置文件,它们将存储在 initramfs 的etc目录中。

 [root@fedorab boot]# tree etc/
etc/
├── cmdline.d
├── conf.d
│   └── systemd.conf
├── fstab.empty
├── group
├── hostname
├── initrd-release -> ../usr/lib/initrd-release
├── ld.so.cache
├── ld.so.conf
├── ld.so.conf.d
│   └── libiscsi-x86_64.conf
├── locale.conf
├── lvm
│   ├── lvm.conf
│   └── lvmlocal.conf
├── machine-id
├── modprobe.d
│   ├── firewalld-sysctls.conf
│   ├── kvm.conf
│   ├── lockd.conf
│   ├── mlx4.conf
│   ├── nvdimm-security.conf
│   └── truescale.conf
├── mtab -> /proc/self/mounts
├── os-release -> initrd-release
├── passwd
├── plymouth
│   └── plymouthd.conf
├── sysctl.conf
├── sysctl.d
│   └── 99-sysctl.conf -> ../sysctl.conf
├── systemd
│   ├── journald.conf
│   └── system.conf
├── system-release -> ../usr/lib/fedora-release
├── udev
│   ├── rules.d
│   │   ├── 11-dm.rules
│   │   ├── 59-persistent-storage-dm.rules
│   │   ├── 59-persistent-storage.rules
│   │   ├── 61-persistent-storage.rules

│   │   └── 64-lvm.rules
│   └── udev.conf
├── vconsole.conf
└── virc

10 directories, 35 files

虚拟文件系统

虚拟文件系统是这样一种文件系统,其文件并不真正存在于磁盘上;相反,整个文件系统在内存中都是可用的。这是各有利弊的;例如,您获得了非常高的吞吐量,但是文件系统不能永久存储数据。initramfs 中有三个可用的虚拟文件系统,分别是devprocsys。在这里,我对文件系统做了一个简单的介绍,但是我们将在接下来的章节中详细讨论它们:

[root@fedorab boot]# ls -lah dev
total 8.0K
drwxr-xr-x.  2 root root  4.0K Dec 19 14:30 .
drwxr-xr-x. 12 root root  4.0K Dec 19 14:33 ..
crw-r--r--.  1 root root 5,  1 Dec 19 14:30 console
crw-r--r--.  1 root root 1, 11 Dec 19 14:30 kmsg
crw-r--r--.  1 root root 1,  3 Dec 19 14:30 null
crw-r--r--.  1 root root 1,  8 Dec 19 14:30 random
crw-r--r--.  1 root root 1,  9 Dec 19 14:30 urandom

[root@fedorab boot]# ls -lah proc/
total 8.0K
drwxr-xr-x.  2 root root 4.0K Dec 19 14:30 .
drwxr-xr-x. 12 root root 4.0K Dec 19 14:33 ..

[root@fedorab boot]# ls -lah sys/
total 8.0K
drwxr-xr-x.  2 root root 4.0K Dec 19 14:30 .
drwxr-xr-x. 12 root root 4.0K Dec 19 14:33 ..

偏差

到目前为止,只有五个默认的设备文件,但是当系统启动时,udev将完全填充这个目录。consolekmsgnullrandomurandom设备文件将由内核自己创建,或者换句话说,这些设备文件是使用mknod命令手工创建的,但是其余的设备文件将由udev填充。

过程和系统

一旦内核控制了引导过程,内核就会创建并填充这些目录。proc文件系统将保存所有进程的相关信息,如/proc/1/status,而sys将保存设备及其驱动程序的相关信息,如/sys/fs/ext4/sda5/errors_count.

usr, var

众所周知,现在usr是根文件系统中一个独立的文件系统层次结构。我们的/bin/sbin/lib/lib64不过是到usr/binusr/sbinusr/libusr/lib64的符号链接。

# ls -l bin
lrwxrwxrwx. 1 root root 7 Dec 21 12:19 bin -> usr/bin

# ls -l sbin
lrwxrwxrwx. 1 root root 8 Dec 21 12:19 sbin -> usr/sbin

# ls -la usr
total 40
drwxr-xr-x.  8 root root  4096 Dec 21 12:19 .
drwxr-xr-x. 12 root root  4096 Dec 21 12:19 ..
drwxr-xr-x.  2 root root  4096 Dec 21 12:19 bin
drwxr-xr-x. 12 root root  4096 Dec 21 12:19 lib
drwxr-xr-x.  4 root root 12288 Dec 21 12:19 lib64
drwxr-xr-x.  2 root root  4096 Dec 21 12:19 libexec
drwxr-xr-x.  2 root root  4096 Dec 21 12:19 sbin
drwxr-xr-x.  5 root root  4096 Dec 21 12:19 share

# ls -la var

total 12
drwxr-xr-x.  3 root root 4096 Dec 21 12:19 .
drwxr-xr-x. 12 root root 4096 Dec 21 12:19 ..
lrwxrwxrwx.  1 root root   11 Dec 21 12:19 lock -> ../run/lock
lrwxrwxrwx.  1 root root    6 Dec 21 12:19 run -> ../run
drwxr-xr-x.  2 root root 4096 Dec 21 12:19 tmp

lib,lib64

库差不多有 200 个,几乎都是由glibc提供的,比如libc.so.6

liblib64目录是usr/libusr/lib64的符号链接。

# ls -l lib
lrwxrwxrwx. 1 root root 7 Dec 21 12:19 lib -> usr/lib

# ls -l lib64
lrwxrwxrwx. 1 root root 9 Dec 21 12:19 lib64 -> usr/lib64

# ls -la lib/
total 128
drwxr-xr-x. 12 root root  4096 Dec 21 12:19 .
drwxr-xr-x.  8 root root  4096 Dec 21 12:19 ..
drwxr-xr-x.  3 root root  4096 Dec 21 12:19 dracut
-rwxr-xr-x.  1 root root 34169 Dec 21 12:19 dracut-lib.sh
-rw-r--r--.  1 root root    31 Dec 21 12:19 fedora-release
drwxr-xr-x.  6 root root  4096 Dec 21 12:19 firmware
-rwxr-xr-x.  1 root root  6400 Dec 21 12:19 fs-lib.sh
-rw-r--r--.  1 root root   238 Dec 21 12:19 initrd-release
drwxr-xr-x.  6 root root  4096 Dec 21 12:19 kbd
drwxr-xr-x.  2 root root  4096 Dec 21 12:19 modprobe.d
drwxr-xr-x.  3 root root  4096 Dec 21 12:19 modules
drwxr-xr-x.  2 root root  4096 Dec 21 12:19 modules-load.d
-rwxr-xr-x.  1 root root 25295 Dec 21 12:19 net-lib.sh
lrwxrwxrwx.  1 root root    14 Dec 21 12:19 os-release -> initrd-release
drwxr-xr-x.  2 root root  4096 Dec 21 12:19 sysctl.d
drwxr-xr-x.  5 root root  4096 Dec 21 12:19 systemd
drwxr-xr-x.  2 root root  4096 Dec 21 12:19 tmpfiles.d
drwxr-xr-x.  3 root root  4096 Dec 21 12:19 udev

# ls -la lib64/libc.so.6 

lrwxrwxrwx. 1 root root 12 Dec 21 12:19 lib64/libc.so.6 -> libc-2.30.so

# dnf whatprovides lib64/libc.so.6
glibc-2.30-5.fc31.x86_64 : The GNU libc libraries
Repo        : @System
Matched from:
Filename    : /lib64/libc.so.6

initramfs 引导

initramfs 中引导序列的基本流程很容易理解:

  1. 由于 initramfs 是一个根文件系统(temporary),它将创建运行进程所必需的环境。initramfs 将被挂载为根文件系统(临时的/),systemd 之类的程序将从这里启动。

  2. 之后,来自硬盘或网络的新用户的根文件系统将被挂载到 initramfs 中的一个临时目录中。

  3. 一旦用户的根文件系统被挂载到 initramfs 中,内核将启动init二进制文件,它是操作系统的第一个进程systemd的符号链接。

    # ls init -l
    lrwxrwxrwx. 1 root root 23 Dec 21 12:19 init -> usr/lib/systemd/systemd
    
    
  4. 一旦一切就绪,临时根文件系统(initramfs 根文件系统)将被卸载,systemd 将负责引导序列的其余部分。第七章将介绍系统引导。

我们可以交叉验证内核是否真的一提取 initramfs 就启动init/systemd进程。我们可以为此修改init脚本,但是问题是 systemd 是二进制的,而init曾经是一个脚本。我们可以很容易地编辑init,因为它是一个脚本文件,但是我们不能编辑 systemd 二进制文件。然而,为了更好地理解和验证我们的引导序列,看看内核一提取 initramfs,systemd 是否就被调用,我们将使用一个基于init的系统。这将是一个很好的例子,因为 systemd 将取代init系统。另外,init仍然是 systemd 的符号链接。我们将使用 Centos 6 系统,这是一个基于init的 Linux 发行版。

首先提取 initramfs。

# zcat  initramfs-2.6.32-573.el6.x86_64.img  |  cpio –idv

[root@localhost initramfs]# ls -lah
total 120K
drwxr-xr-x. 26 root root 4.0K Mar 27 12:56 .
drwxr-xr-x.  3 root root 4.0K Mar 27 12:56 ..
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 bin
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 cmdline
drwxr-xr-x.  3 root root 4.0K Mar 27 12:56 dev
-rw-r--r--.  1 root root   19 Mar 27 12:56 dracut-004-388.el6
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 emergency
drwxr-xr-x.  8 root root 4.0K Mar 27 12:56 etc
-rwxr-xr-x.  1 root root 8.8K Mar 27 12:56 init
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 initqueue
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 initqueue-finished
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 initqueue-settled
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 initqueue-timeout
drwxr-xr-x.  7 root root 4.0K Mar 27 12:56 lib
drwxr-xr-x.  3 root root 4.0K Mar 27 12:56 lib64
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 mount
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 netroot
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 pre-mount
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 pre-pivot
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 pre-trigger
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 pre-udev
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 proc
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 sbin
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 sys
drwxr-xr-x.  2 root root 4.0K Mar 27 12:56 sysroot
drwxrwxrwt.  2 root root 4.0K Mar 27 12:56 tmp
drwxr-xr-x.  8 root root 4.0K Mar 27 12:56 usr
drwxr-xr-x.  4 root root 4.0K Mar 27 12:56 var

打开一个init文件,并在其中添加以下横幅:

#vim init
   "We are inside the init process. Init is replaced by Systemd"
<snip>
#!/bin/sh
#
# Licensed under the GPLv2
#
# Copyright 2008-2009, Red Hat, Inc.
# Harald Hoyer <harald@redhat.com>
# Jeremy Katz <katzj@redhat.com>
echo "we are inside the init process. Init is replaced by Systemd"
wait_for_loginit()
{
    if getarg rdinitdebug; then
        set +x
        exec 0<>/dev/console 1<>/dev/console 2<>/dev/console
        # wait for loginit
        i=0
        while [ $i -lt 10 ]; do
.
.
.
</snip>

test.img名重新打包 initramfs。

[root@localhost initramfs]# find . | cpio -o -c | gzip -9 > /boot/test.img
163584 blocks

# ls -lh /boot/
total 66M
-rw-r--r--. 1 root root 105K Jul 23  2015 config-2.6.32-573.el6.x86_64
drwxr-xr-x. 3 root root 1.0K Aug  7  2015 efi
-rw-r--r--. 1 root root 163K Jul 20  2011 elf-memtest86+-4.10
drwxr-xr-x. 2 root root 1.0K Dec 21 16:12 grub
-rw-------. 1 root root  27M Dec 21 15:55 initramfs-2.6.32-573.el6.x86_64.img
-rw-------. 1 root root 5.3M Dec 21 16:03 initrd-2.6.32-573.el6.x86_64kdump.img
drwx------. 2 root root  12K Dec 21 15:54 lost+found
-rw-r--r--. 1 root root 162K Jul 20  2011 memtest86+-4.10
-rw-r--r--. 1 root root 202K Jul 23  2015 symvers-2.6.32-573.el6.x86_64.gz
-rw-r--r--. 1 root root 2.5M Jul 23  2015 System.map-2.6.32-573.el6.x86_64
-rw-r--r--. 1 root root  27M Mar 27 13:16 test.img
-rwxr-xr-x. 1 root root 4.1M Jul 23  2015 vmlinuz-2.6.32-573.el6.x86_64

使用新的test.img initramfs 启动,打开 initramfs 后,您会注意到我们的横幅正在打印。

<snip>
.
.
.
cpuidle: using governor ladder
cpuidle: using governor menu
EFI Variables Facility v0.08 2004-May-17
usbcore: registered new interface driver hiddev
usbcore: registered new interface driver usbhid
usbhid: v2.6:USB HID core driver
GRE over IPv4 demultiplexor driver
TCP cubic registered
Initializing XFRM netlink socket
NET: Registered protocol family 17
registered taskstats version 1
rtc_cmos 00:01: setting system clock to 2020-03-27 07:53:44 UTC (1585295624)
Initalizing network drop monitor service
Freeing unused kernel memory: 1296k freed
Write protecting the kernel read-only data: 10240k
Freeing unused kernel memory: 732k freed
Freeing unused kernel memory: 1576k freed
we are inside the init process. Init is replaced by Systemd
dracut: dracut-004-388.el6
dracut: rd_NO_LUKS: removing cryptoluks activation
device-mapper: uevent: version 1.0.3
device-mapper: ioctl: 4.29.0-ioctl (2014-10-28) initialised: dm-devel@redhat.com
udev: starting version 147
dracut: Starting plymouth daemon
.
.
</snip>

内核如何从内存中提取 initramfs?

让我们花一分钟时间,试着回忆一下到目前为止我们所学的内容。

  1. 引导加载程序首先运行。

  2. 引导装载程序复制内存中的内核和 initramfs。

  3. 内核会自行提取。

  4. 引导装载程序将 initramfs 的位置传递给内核。

  5. 内核在内存中提取 initramfs。

  6. 内核从提取的 initramfs 运行 systemd。

提取发生在内核的init/initramfs.c文件中。populate_rootfs函数负责提取。

populate_rootfs 函数:

<snip>
.
.
646 static int __init populate_rootfs(void)
647 {
648         /* Load the built in initramfs */
649         char *err = unpack_to_rootfs(__initramfs_start, __initramfs_size);
650         if (err)
651                 panic("%s", err); /* Failed to decompress INTERNAL initramfs */
652
653         if (!initrd_start || IS_ENABLED(CONFIG_INITRAMFS_FORCE))
654                 goto done;
655
656         if (IS_ENABLED(CONFIG_BLK_DEV_RAM))
657                 printk(KERN_INFO "Trying to unpack rootfs image as initramfs...\n");
658         else
659                 printk(KERN_INFO "Unpacking initramfs...\n");
660
661         err = unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start);
662         if (err) {
663                 clean_rootfs();
664                 populate_initrd_image(err);

665         }
666
667 done:
668         /*
669          * If the initrd region is overlapped with crashkernel reserved region,
670          * free only memory that is not part of crashkernel region.
671          */
672         if (!do_retain_initrd && initrd_start && !kexec_free_initrd())
673                 free_initrd_mem(initrd_start, initrd_end);
674         initrd_start = 0;
675         initrd_end = 0;
676
677         flush_delayed_fput();
678         return 0;
679 }
.
.
</snip>

unpack_to_rootfs 函数:

<snip>
.
.
443 static char * __init unpack_to_rootfs(char *buf, unsigned long len)
444 {
445         long written;
446         decompress_fn decompress;
447         const char *compress_name;
448         static __initdata char msg_buf[64];
449
450         header_buf = kmalloc(110, GFP_KERNEL);
451         symlink_buf = kmalloc(PATH_MAX + N_ALIGN(PATH_MAX) + 1, GFP_KERNEL);
452         name_buf = kmalloc(N_ALIGN(PATH_MAX), GFP_KERNEL);
453
454         if (!header_buf || !symlink_buf || !name_buf)
455                 panic("can't allocate buffers");
456
457         state = Start;
458         this_header = 0;
459         message = NULL;
460         while (!message && len) {
461                 loff_t saved_offset = this_header;
462                 if (*buf == '0' && !(this_header & 3)) {
463                         state = Start;
464                         written = write_buffer(buf, len);
465                         buf += written;
466                         len -= written;
467                         continue;

468                 }
469                 if (!*buf) {
470                         buf++;
471                         len--;
472                         this_header++;
473                         continue;
474                 }
475                 this_header = 0;
476                 decompress = decompress_method(buf, len, &compress_name);
477                 pr_debug("Detected %s compressed data\n", compress_name);
478                 if (decompress) {
479                         int res = decompress(buf, len, NULL, flush_buffer, NULL,
480                                    &my_inptr, error);
481                         if (res)
482                                 error("decompressor failed");
483                 } else if (compress_name) {
484                         if (!message) {
485                                 snprintf(msg_buf, sizeof msg_buf,
486                                          "compression method %s not configured",
487                                          compress_name);
488                                 message = msg_buf;
489                         }
490                 } else
491                         error("invalid magic at start of compressed archive");
492                 if (state != Reset)
493                         error("junk at the end of compressed archive");
494                 this_header = saved_offset + my_inptr;
495                 buf += my_inptr;
496                 len -= my_inptr;
497         }
498         dir_utime();
499         kfree(name_buf);
500         kfree(symlink_buf);
501         kfree(header_buf);
502         return message;
503 }
.
.
</snip>

populate_rootfs函数内部有一个unpack_to_rootfs函数。这是一个 worker 函数,它解包 initramfs,如果失败则返回 0,如果成功则返回 1。还要注意有趣的函数参数。

  • __initramfs_start:这是一个加载的 initramfs 的确切位置/地址(initramfs 将由 bootloader 加载,所以显然地址位置也是 bootloader 通过boot_protocol提供的)。

  • __initramfs_size:这是 initramfs 映像的大小。

内核如何以 Root 身份挂载 initramfs?

initramfs blob 只是一个(可选压缩的)cpio文件。内核通过在内存中创建一个 tmpfs/ramfs 文件系统作为根文件系统来提取它。所以,并没有一个固定的位置。内核只是在运行过程中为提取的文件分配内存。我们已经看到 GRUB 2/boot loader 将内核放在一个特定的位置,这个位置依赖于体系结构,但是 initramfs 映像提取并不发生在任何特定的位置。

现在,在我们进一步进行引导序列之前,我们需要理解 dracut 工具,它生成 initramfs。这个工具将让我们更好地理解 initramfs 和 systemd。

六、dracut

简单地说,dracut 是一个在基于 Fedora 的系统上创建 initramfs 文件系统的工具。基于 Debian 和 Ubuntu 的系统使用一个类似的工具叫做 update-initramfs 。如果您想要生成、重新生成或定制现有的 initramfs,那么您应该知道如何使用 dracut 工具。本章将解释 dracut 如何工作,以及如何生成和定制 initramfs。此外,您将了解一些与 initramfs 相关的最常见的“无法启动”问题。

入门指南

每个内核都有自己的 initramfs 文件,但是您可能想知道为什么在安装一个新的内核时从来不需要使用dracut命令来创建 initramfs。相反,您只是在/boot位置找到了各自的 initramfs。嗯,当你安装一个新的内核时,内核的rpm包的post-scripts命令调用 dracut 并为你创建 initramfs。让我们看看它在基于 Fedora 的系统上是如何工作的:

# rpm -q --scripts kernel-core-5.3.7-301.fc31.x86_64
postinstall scriptlet (using /bin/sh):

if [ `uname -i` == "x86_64" -o `uname -i` == "i386" ] &&
   [ -f /etc/sysconfig/kernel ]; then
  /bin/sed -r -i -e 's/^DEFAULTKERNEL=kernel-smp$/DEFAULTKERNEL=kernel/' /etc/sysconfig/kernel || exit $?
fi
preuninstall scriptlet (using /bin/sh):
/bin/kernel-install remove 5.3.7-301.fc31.x86_64 /lib/modules/5.3.7-301.fc31.x86_64/vmlinuz || exit $?
posttrans scriptlet (using /bin/sh):
/bin/kernel-install add 5.3.7-301.fc31.x86_64 /lib/modules/5.3.7-301.fc31.x86_64/vmlinuz || exit $?

可以看到,内核包的post-scripts命令调用了kernel-install脚本。kernel-install脚本执行所有在/usr/lib/kernel/install.d可用的脚本。

# vim /bin/kernel-install

 94 if ! [[ $MACHINE_ID ]]; then
 95     ENTRY_DIR_ABS=$(mktemp -d /tmp/kernel-install.XXXXX) || exit 1
 96     trap "rm -rf '$ENTRY_DIR_ABS'" EXIT INT QUIT PIPE
 97 elif [[ -d /efi/loader/entries ]] || [[ -d /efi/$MACHINE_ID ]]; then
 98     ENTRY_DIR_ABS="/efi/$MACHINE_ID/$KERNEL_VERSION"
 99 elif [[ -d /boot/loader/entries ]] || [[ -d /boot/$MACHINE_ID ]]; then
100     ENTRY_DIR_ABS="/boot/$MACHINE_ID/$KERNEL_VERSION"
101 elif [[ -d /boot/efi/loader/entries ]] || [[ -d /boot/efi/$MACHINE_ID ]]; then
102     ENTRY_DIR_ABS="/boot/efi/$MACHINE_ID/$KERNEL_VERSION"
103 elif mountpoint -q /efi; then
104     ENTRY_DIR_ABS="/efi/$MACHINE_ID/$KERNEL_VERSION"
105 elif mountpoint -q /boot/efi; then
106     ENTRY_DIR_ABS="/boot/efi/$MACHINE_ID/$KERNEL_VERSION"
107 else
108     ENTRY_DIR_ABS="/boot/$MACHINE_ID/$KERNEL_VERSION"
109 fi
110
111 export KERNEL_INSTALL_MACHINE_ID=$MACHINE_ID
112
113 ret=0
114
115 readarray -t PLUGINS <<<"$(
116     dropindirs_sort ".install" \
117         "/etc/kernel/install.d" \
118         "/usr/lib/kernel/install.d"
119 )"

在这里您可以看到由kernel-install执行的脚本:

# ls /usr/lib/kernel/install.d/ -lh
total 36K
-rwxr-xr-x. 1 root root  744 Oct 10 18:26 00-entry-directory.install
-rwxr-xr-x. 1 root root 1.9K Oct 19 07:46 20-grubby.install
-rwxr-xr-x. 1 root root 6.6K Oct 10 13:05 20-grub.install
-rwxr-xr-x. 1 root root  829 Oct 10 18:26 50-depmod.install
-rwxr-xr-x. 1 root root 1.7K Jul 25  2019 50-dracut.install
-rwxr-xr-x. 1 root root 3.4K Jul 25  2019 51-dracut-rescue.install
-rwxr-xr-x. 1 root root 3.4K Oct 10 18:26 90-loaderentry.install
-rwxr-xr-x. 1 root root 1.1K Oct 10 13:05 99-grub-mkconfig.install

如您所见,这执行了50-dracut.install脚本。这个特定的脚本执行dracut命令,并为特定的内核创建 initramfs。

 46         for ((i=0; i < "${#BOOT_OPTIONS[@]}"; i++)); do
 47             if [[ ${BOOT_OPTIONS[$i]} == root\=PARTUUID\=* ]]; then
 48                 noimageifnotneeded="yes"
 49                 break
 50             fi
 51         done
 52         dracut -f ${noimageifnotneeded:+--noimageifnotneeded} "$BOOT_DIR_ABS/$INITRD" "$KERNEL_VERSION"
 53         ret=$?
 54         ;;
 55     remove)
 56         rm -f -- "$BOOT_DIR_ABS/$INITRD"
 57         ret=$?
 58         ;;
 59 esac
 60 exit $ret

同样,还有脚本51-dracut-rescue.install,它将为救援内核生成 initramfs。

100         if [[ ! -f "$BOOT_DIR_ABS/$INITRD" ]]; then
101             dracut -f --no-hostonly -a "rescue" "$BOOT_DIR_ABS/$INITRD" "$KERNEL_VERSION"
102             ((ret+=$?))
103         fi
104
105         if [[ "${BOOT_DIR_ABS}" != "/boot" ]]; then
106             {
107                 echo "title      $PRETTY_NAME - Rescue Image"
108                 echo "version    $KERNEL_VERSION"
109                 echo "machine-id $MACHINE_ID"
110                 echo "options    ${BOOT_OPTIONS[@]} rd.auto=1"
111                 echo "linux      $BOOT_DIR/linux"
112                 echo "initrd     $BOOT_DIR/initrd"
113             } > $LOADER_ENTRY
114         else
115             cp -aT "${KERNEL_IMAGE%/*}/bls.conf" $LOADER_ENTRY
116             sed -i 's/'$KERNEL_VERSION'/0-rescue-'${MACHINE_ID}'/' $LOADER_ENTRY
117         fi

因此,每个内核都有自己的 initramfs 文件。

# ls -lh /boot | grep -e vmlinuz -e initramfs

-rw-------. 1 root root  80M Dec  2 18:32 initramfs-0-rescue-280526b3bc5e4c49ac83c8e5fbdfdb2e.img
-rw-------. 1 root root  28M Dec 23 06:37 initramfs-5.3.16-300.fc31.x86_64.img
-rw-------. 1 root root  30M Dec  2 18:33 initramfs-5.3.7-301.fc31.x86_64.img
-rwxr-xr-x. 1 root root 8.9M Dec  2 18:32 vmlinuz-0-rescue-280526b3bc5e4c49ac83c8e5fbdfdb2e
-rwxr-xr-x. 1 root root 8.9M Dec 13 23:51 vmlinuz-5.3.16-300.fc31.x86_64
-rwxr-xr-x. 1 root root 8.9M Oct 22 01:04 vmlinuz-5.3.7-301.fc31.x86_64

注意内核(vmlinuz)文件的大小及其相关的 initramfs 文件大小。initramfs 文件比内核大得多。

制作 initramfs 映像

首先使用以下命令检查您的系统上安装了哪个内核:

# rpm -qa | grep -i kernel-5

kernel-5.3.16-300.fc31.x86_64
kernel-5.3.7-301.fc31.x86_64

选择要为其生成新的 initramfs 映像的内核版本,并将其传递给 dracut。

# dracut /boot/new.img 5.3.7-301.fc31.x86_64 -v
<snip>
dracut: Executing: /usr/bin/dracut /boot/new.img 5.3.7-301.fc31.x86_64 -v
dracut: dracut module 'busybox' will not be installed, because command 'busybox' could not be found!
dracut: dracut module 'stratis' will not be installed, because command 'stratisd-init' could not be found!
dracut: dracut module 'biosdevname' will not be installed, because command 'biosdevname' could not be found!
dracut: dracut module 'busybox' will not be installed, because command 'busybox' could not be found!
dracut: dracut module 'stratis' will not be installed, because command 'stratisd-init' could not be found!
dracut: *** Including module: bash ***
dracut: *** Including module: systemd ***
dracut: *** Including module: systemd-initrd ***
dracut: *** Including module: nss-softokn ***
dracut: *** Including module: i18n ***
dracut: *** Including module: network-manager ***
dracut: *** Including module: network ***
dracut: *** Including module: ifcfg ***
dracut: *** Including module: drm ***
dracut: *** Including module: plymouth ***
.
.
</snip>

在前面的代码中,dracut 将在当前目录中为 64 位 Fedora 内核Kernel-5.3.7-301.fc31.x86_64创建一个名为new.img的 initramfs 文件。

# ls -lh new.img
-rw-------. 1 root root 28M Dec 23 08:16 new.img

如果没有提供内核版本,那么 dracut 将为引导系统的内核创建 initramfs。传递给 dracut 的内核版本必须与位于/lib/modules/位置的内核目录相匹配。

# ls /lib/modules/ -l
total 4
drwxr-xr-x. 6 root root 4096 Dec  9 10:18 5.3.7-301.fc31.x86_64

# ls /lib/modules/5.3.7-301.fc31.x86_64/ -l
total 18084
-rw-r--r--.  1 root root     249 Oct 22 01:04 bls.conf
lrwxrwxrwx.  1 root root      38 Oct 22 01:04 build -> /usr/src/kernels/5.3.7-301.fc31.x86_64
-rw-r--r--.  1 root root  213315 Oct 22 01:03 config
drwxr-xr-x.  5 root root    4096 Oct 24 04:44 extra
drwxr-xr-x. 13 root root    4096 Oct 24 04:43 kernel
-rw-r--r--.  1 root root 1127438 Dec  9 10:18 modules.alias
-rw-r--r--.  1 root root 1101059 Dec  9 10:18 modules.alias.bin
-rw-r--r--.  1 root root    1688 Oct 22 01:04 modules.block
-rw-r--r--.  1 root root    8324 Oct 22 01:04 modules.builtin
-rw-r--r--.  1 root root   10669 Dec  9 10:18 modules.builtin.bin
-rw-r--r--.  1 root root   60853 Oct 22 01:04 modules.builtin.modinfo
-rw-r--r--.  1 root root  415475 Dec  9 10:18 modules.dep
-rw-r--r--.  1 root root  574502 Dec  9 10:18 modules.dep.bin
-rw-r--r--.  1 root root     381 Dec  9 10:18 modules.devname
-rw-r--r--.  1 root root     153 Oct 22 01:04 modules.drm
-rw-r--r--.  1 root root      59 Oct 22 01:04 modules.modesetting
-rw-r--r--.  1 root root    2697 Oct 22 01:04 modules.networking
-rw-r--r--.  1 root root  139947 Oct 22 01:04 modules.order
-rw-r--r--.  1 root root     700 Dec  9 10:18 modules.softdep
-rw-r--r--.  1 root root  468520 Dec  9 10:18 modules.symbols
-rw-r--r--.  1 root root  572778 Dec  9 10:18 modules.symbols.bin
lrwxrwxrwx.  1 root root       5 Oct 22 01:04 source -> build
-rw-------.  1 root root 4426726 Oct 22 01:03 System.map
drwxr-xr-x.  2 root root    4096 Oct 22 01:02 updates
drwxr-xr-x.  2 root root    4096 Oct 24 04:43 vdso
-rwxr-xr-x.  1 root root 9323208 Oct 22 01:04 vmlinuz

正如我们所知,initramfs 是一个临时的根文件系统,它的主要目的是提供一个环境来帮助挂载用户的根文件系统。用户的根文件系统可以是系统的本地文件系统,也可以是网络设备,为了使用该设备,内核应该有该硬件的驱动程序(模块),并且在引导时从 initramfs 获取这些模块。

例如,假设用户的根文件系统是本地连接的硬盘,而硬盘是 SCSI 设备。因此,initramfs 必须将 SCSI 驱动程序添加到它的归档中。

# lsinitrd | grep -i scsi | awk '{ print $9 }'
etc/ld.so.conf.d/libiscsi-x86_64.conf
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/firmware/iscsi_ibft.ko.xz
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/iscsi_boot_sysfs.ko.xz
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/libiscsi.ko.xz
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/qla4xxx
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/qla4xxx/qla4xxx.ko.xz
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/scsi_transport_iscsi.ko.xz
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/scsi_transport_srp.ko.xz
usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/virtio_scsi.ko.xz
usr/lib/udev/scsi_id

在 SCSI 设备之上,用户可能已经配置了一个 RAID 设备。如果有,那么内核需要 RAID 设备驱动程序来识别和组装 RAID 设备。类似地,一些用户的硬盘可以通过 HBA 卡连接。在这种情况下,内核需要一个类似qlaXxxx-的模块。

# lsinitrd | grep -i qla

        usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/qla4xxx
        usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/qla4xxx/qla4xxx.ko.xz

请注意,这些天'/lib'是到'/usr/lib/'的符号链接。

在某些用户的情况下,硬盘可能来自以太网上的光纤通道。然后内核需要 FCOE 模块。在虚拟化环境中,HDD 可以是由虚拟机管理程序公开的虚拟磁盘。在这种情况下,要挂载用户的根文件系统,virtIO模块是必需的。这样,硬件和它们各自的模块的列表继续下去。

显然,内核无法将所有这些必需的模块文件(.ko)存储在自己的二进制文件(vmlinuz)中。因此,initramfs 的主要工作之一是存储挂载用户根文件系统所需的所有模块。这也是 initramfs 文件比内核文件大得多的原因之一。但是请记住,initramfs 不是模块的来源。模块总是由内核提供,并由 dracut 存档在 initramfs 中。内核(vmlinuz)是所有模块的来源,但是正如您所猜测的,如果内核将所有模块存储在它的vmlinuz二进制文件中,那么内核将会非常大。因此,随着一个kernel包,一个名为kernel-modules的新包被引入,这个包提供了所有出现在/lib/modules/<kernel-version-arch>位置的模块;dracut 只提取那些挂载用户根文件系统所必需的模块(.ko文件)。

# rpm -qa | grep -i kernel

        Kernel-headers-5.3.6-300.fc31.x86_64
        kernel-modules-extra-5.3.7-301.fc31.x86_64
        kernel-modules-5.3.7-301.fc31.x86_64
        kernel-core-5.3.16-300.fc31.x86_64
        kernel-core-5.3.7-301.fc31.x86_64
        kernel-5.3.16-300.fc31.x86_64
        abrt-addon-kerneloops-2.12.2-1.fc31.x86_64
        kernel-5.3.7-301.fc31.x86_64
        libreport-plugin-kerneloops-2.10.1-2.fc31.x86_64
        Kernel-modules-5.3.16-300.fc31.x86_64
# rpm -ql kernel-modules-5.3.7-301.fc31.x86_64 | wc -l
    1698

    # rpm -ql kernel-modules-5.3.7-301.fc31.x86_64
    <snip>
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/atm/atmtcp.ko.xz
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/atm/eni.ko.xz
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/atm/firestream.ko.xz
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/atm/he.ko.xz
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/atm/nicstar.ko.xz
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/atm/solos-pci.ko.xz
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/atm/suni.ko.xz
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/auxdisplay/cfag12864b.ko.xz
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/auxdisplay/cfag12864bfb.ko.xz
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/auxdisplay/charlcd.ko.xz
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/auxdisplay/hd44780.ko.xz
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/auxdisplay/ks0108.ko.xz
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/bcma/bcma.ko.xz
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/bluetooth/ath3k.ko.xz
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/bluetooth/bcm203x.ko.xz
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/bluetooth/bfusb.ko.xz
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/bluetooth/bluecard_cs.ko.xz
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/bluetooth/bpa10x.ko.xz
    .
    .
    </snip>

正如你所看到的,kernel-5.3.7-301附带的kernel-modules包提供了将近 1698 个模块。另外,kernel-module包将是kernel包的一个依赖项;因此,每当安装kernel时,kernel-modules就会被一个基于 Fedora 的操作系统拉过来安装。

Dracut 和模块

我们现在将回顾 dracut 模块。

dracut 如何选择模块?

为了理解 dracut 如何拉取 initramfs 中的模块,首先我们需要理解depmod命令。depmod分析位于/lib/modules/<kernel-version-arch>位置的所有内核模块,并列出所有模块及其依赖模块。它将这个列表保存在modules.dep文件中。(注意,在基于 Fedora 的系统上,最好将模块的位置称为/usr/lib/modules/<kernel_version>/*。)这里有个例子:

# vim /lib/modules/5.3.7-301.fc31.x86_64/modules.dep
<snip>
.
.
kernel/arch/x86/kernel/cpu/mce/mce-inject.ko.xz:
kernel/arch/x86/crypto/des3_ede-x86_64.ko.xz: kernel/crypto/des_generic.ko.xz
kernel/arch/x86/crypto/camellia-x86_64.ko.xz:
kernel/arch/x86/crypto/blowfish-x86_64.ko.xz: kernel/crypto/blowfish_common.ko.xz
kernel/arch/x86/crypto/twofish-x86_64.ko.xz: kernel/crypto/twofish_common.ko.xz
.
.
</snip>

在这段代码中,您可以看到名为des3_ede的模块需要模块des_generic才能正常工作。在另一个例子中,您可以看到blowfish模块有一个blowfish_comman模块作为依赖项。因此,dracut 读取modules.dep文件,并开始从/lib/modules/5.3.7-301.fc31.x86_64/kernel/位置提取 initramfs 映像中的内核模块。

# ls /lib/modules/5.3.7-301.fc31.x86_64/kernel/ -l
total 44
drwxr-xr-x.  3 root root 4096 Oct 24 04:43 arch
drwxr-xr-x.  4 root root 4096 Oct 24 04:43 crypto
drwxr-xr-x. 80 root root 4096 Oct 24 04:43 drivers
drwxr-xr-x. 43 root root 4096 Oct 24 04:43 fs
drwxr-xr-x.  4 root root 4096 Oct 24 04:43 kernel
drwxr-xr-x.  8 root root 4096 Oct 24 04:43 lib
drwxr-xr-x.  2 root root 4096 Oct 24 04:43 mm
drwxr-xr-x. 51 root root 4096 Oct 24 04:43 net
drwxr-xr-x.  3 root root 4096 Oct 24 04:43 security
drwxr-xr-x. 13 root root 4096 Oct 24 04:43 sound
drwxr-xr-x.  3 root root 4096 Oct 24 04:43 virt

内核提供了数以千计的模块,但是不需要在 initramfs 中添加每个模块。因此,在收集模块时,dracut 提取非常具体的模块。

# find /lib/modules/5.3.7-301.fc31.x86_64/ -name '*.ko.xz' | wc -l
3539

如果 dracut 提取每个模块,那么 initramfs 的大小将会很大。还有,为什么在不必要的时候拉每个模块?因此,dracut 只提取那些在该系统上安装用户根文件系统所必需的模块。

# lsinitrd | grep -i '.ko.xz'  | wc -l
221

正如您所看到的,initramfs 只有 221 个模块,而内核有将近 3,539 个模块。

如果我们在 initramfs 中包含 3,539 个模块,它会使 initramfs 变得很大,最终会降低引导性能,因为 initramfs 归档文件的加载和解压缩时间会很长。此外,我们需要理解 initramfs 的主要任务是挂载用户的根文件系统。因此,只包含那些挂载根文件系统所必需的模块是有意义的。例如,与蓝牙相关的模块没有必要添加到 initramfs 中,因为根文件系统永远不会来自蓝牙连接的设备。所以,尽管内核(kernel-modules)提供了几个bluetooth模块,但在 initramfs 中你不会找到任何与蓝牙相关的模块。

# find /lib/modules/5.3.7-301.fc31.x86_64/ -name 'bluetooth'
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/net/bluetooth
    /lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/bluetooth

# lsinitrd | grep -i blue
    <no_output>

默认情况下,dracut 将只在 initramfs 中添加特定于主机的模块。这是通过检查当前系统状态和系统当前使用的模块来实现的。特定于主机是每个主流 Linux 发行版的默认方法。Fedora 和类似 Ubuntu 的系统也创建了一个通用的 initramfs 映像,称为 rescue initramfs 映像。rescue initramfs 包括用户可以在其上创建根文件系统的设备的所有可能的模块。其思想是通用 initramfs 应该适用于所有系统。因此,与特定于主机的 initramfs 相比,救援 initramfs 的大小总是更大。dracut 有一组逻辑来决定需要哪些模块来挂载根文件系统。这是 dracut 的手册页所说的,但是记住在基于 Fedora 的 Linux 中,--hostonly是缺省的。

"如果您想创建更轻、更小的 initramfs 映像,您可能需要指定- hostonly 或-H 选项。使用这个选项,产生的映像将只包含那些 dracut 模块、内核模块和文件系统,它们是引导这个特定机器所需要的。这样做的缺点是,如果不重新创建 initramfs 映像,就不能将磁盘放在另一个控制器或机器上,也不能切换到另一个根文件系统。- hostonly 选项仅供专家使用,您必须保留碎片。至少保留一个通用映像(和相应的内核)的副本,作为拯救您的系统的后备。”

在第五章中,我们看到有许多二进制文件、模块和配置文件是由 dracut 选择并添加到 initramfs 中的,但是 dracut 如何从用户庞大的根文件系统中选择文件呢?

通过运行位置/usr/lib/dracut/modules.d中的脚本来选择文件。这是存放 dracut 所有脚本的地方。dracut 在生成 initramfs 的同时运行这些脚本,如下所示:

# ls /usr/lib/dracut/modules.d/ -l
total 288
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 00bash
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 00systemd
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 00warpclock
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 01fips
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 01systemd-initrd
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 02systemd-networkd
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 03modsign
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 03rescue
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 04watchdog
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 05busybox
drwxr-xr-x. 2 root root 4096 Oct 24 04:42 05nss-softokn
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 05rdma
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 10i18n
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 30convertfs
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 35network-legacy
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 35network-manager
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 40network
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 45ifcfg
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 45url-lib
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 50drm
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 50plymouth
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 80lvmmerge
drwxr-xr-x. 2 root root 4096 Oct 24 04:42 90bcache
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90btrfs
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90crypt
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90dm
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90dmraid
drwxr-xr-x. 2 root root 4096 Oct 24 04:44 90dmsquash-live
drwxr-xr-x. 2 root root 4096 Oct 24 04:44 90dmsquash-live-ntfs
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90kernel-modules

drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90kernel-modules-extra
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90kernel-network-modules
drwxr-xr-x. 2 root root 4096 Oct 24 04:44 90livenet
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90lvm
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90mdraid
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90multipath
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90qemu
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90qemu-net
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 90stratis
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 91crypt-gpg
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 91crypt-loop
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95cifs
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95debug
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95fcoe
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95fcoe-uefi
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95fstab-sys
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95iscsi
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95lunmask
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95nbd
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95nfs
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95resume
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95rootfs-block
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95ssh-client
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95terminfo
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95udev-rules
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 95virtfs
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 97biosdevname
drwxr-xr-x. 2 root root 4096 Jan  6 12:42 98dracut-systemd
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 98ecryptfs
drwxr-xr-x. 2 root root 4096 Oct 24 04:44 98ostree
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 98pollcdrom
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 98selinux
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 98syslog
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 98usrmount
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 99base
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 99earlykdump
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 99fs-lib
drwxr-xr-x. 2 root root 4096 Oct 24 04:44 99img-lib
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 99kdumpbase
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 99shutdown
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 99squash
drwxr-xr-x. 2 root root 4096 Oct 24 04:43 99uefi-lib

使用#dracut --list-modules可以查看相同的输出。

每当我们试图创建 initramfs 文件系统时,dracut 就会开始执行/usr/lib/dracut/modules.d/中每个目录下的module-setup.sh脚本文件。

# find /usr/lib/dracut/modules.d/ -name 'module-setup.sh'

/usr/lib/dracut/modules.d/95iscsi/module-setup.sh
/usr/lib/dracut/modules.d/98ecryptfs/module-setup.sh
/usr/lib/dracut/modules.d/30convertfs/module-setup.sh
/usr/lib/dracut/modules.d/90crypt/module-setup.sh
/usr/lib/dracut/modules.d/10i18n/module-setup.sh
/usr/lib/dracut/modules.d/99earlykdump/module-setup.sh
/usr/lib/dracut/modules.d/95nbd/module-setup.sh
.
.
.
/usr/lib/dracut/modules.d/04watchdog/module-setup.sh
/usr/lib/dracut/modules.d/90lvm/module-setup.sh
/usr/lib/dracut/modules.d/35network-legacy/module-setup.sh
/usr/lib/dracut/modules.d/01systemd-initrd/module-setup.sh
/usr/lib/dracut/modules.d/99squash/module-setup.sh
/usr/lib/dracut/modules.d/05busybox/module-setup.sh
/usr/lib/dracut/modules.d/50drm/module-setup.sh

这个module-setup.sh脚本将挑选特定于该主机的模块、二进制文件和配置文件。例如,将从00bash目录运行的第一个module-setup.sh脚本将在 initramfs 中包含bash二进制文件。

# vim /usr/lib/dracut/modules.d/00bash/module-setup.sh
  1 #!/usr/bin/bash
  2
  3 # called by dracut
  4 check() {
  5     require_binaries /bin/bash
  6 }
  7
  8 # called by dracut
  9 depends() {
 10     return 0
 11 }
 12
 13 # called by dracut
 14 install() {
 15     # If another shell is already installed, do not use bash
 16     [[ -x $initdir/bin/sh ]] && return
 17
 18     # Prefer bash as /bin/sh if it is available.
 19     inst /bin/bash && ln -sf bash "${initdir}/bin/sh"
 20 }
 21

如您所见,脚本文件正在 initramfs 中添加/bin/bash二进制文件。再来看另一个例子,这个plymouth的。

# vim /usr/lib/dracut/modules.d/50plymouth/module-setup.sh
  1 #!/usr/bin/bash
  2
  3 pkglib_dir() {
  4     local _dirs="/usr/lib/plymouth /usr/libexec/plymouth/"
  5     if type -P dpkg-architecture &>/dev/null; then
  6         _dirs+=" /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH)/plymouth"
  7     fi
  8     for _dir in $_dirs; do
  9         if [ -x $_dir/plymouth-populate-initrd ]; then
 10             echo $_dir
 11             return
 12         fi
 13     done
 14 }
 15
 16 # called by dracut
 17 check() {
 18     [[ "$mount_needs" ]] && return 1
 19     [ -z $(pkglib_dir) ] && return 1
 20
 21     require_binaries plymouthd plymouth plymouth-set-default-theme

 22 }
 23
 24 # called by dracut
 25 depends() {
 26     echo drm
 27 }
 28
 29 # called by dracut
 30 install() {
 31     PKGLIBDIR=$(pkglib_dir)
 32     if grep -q nash ${PKGLIBDIR}/plymouth-populate-initrd \
 33         || [ ! -x ${PKGLIBDIR}/plymouth-populate-initrd ]; then
 34         . "$moddir"/plymouth-populate-initrd.sh
 35     else
 36         PLYMOUTH_POPULATE_SOURCE_FUNCTIONS="$dracutfunctions" \
 37             ${PKGLIBDIR}/plymouth-populate-initrd -t "$initdir"
 38     fi
 39
 40     inst_hook emergency 50 "$moddir"/plymouth-emergency.sh
 41
 42     inst_multiple readlink
 43
 44     if ! dracut_module_included "systemd"; then
 45         inst_hook pre-trigger 10 "$moddir"/plymouth-pretrigger.sh
 46         inst_hook pre-pivot 90 "$moddir"/plymouth-newroot.sh
 47     fi
 48 }

简单地 grepping require_binaries将显示 dracut 将添加到通用 initramfs 中的所有二进制文件。

# grep -ir "require_binaries" /usr/lib/dracut/modules.d/
/usr/lib/dracut/modules.d/90mdraid/module-setup.sh:    require_binaries mdadm expr || return 1
/usr/lib/dracut/modules.d/80lvmmerge/module-setup.sh:    require_binaries lvm dd swapoff || return 1
/usr/lib/dracut/modules.d/95cifs/module-setup.sh:    require_binaries mount.cifs || return 1
/usr/lib/dracut/modules.d/91crypt-gpg/module-setup.sh:    require_binaries gpg || return 1
/usr/lib/dracut/modules.d/91crypt-gpg/module-setup.sh:       require_binaries gpg-agent &&
/usr/lib/dracut/modules.d/91crypt-gpg/module-setup.sh:       require_binaries gpg-connect-agent &&
/usr/lib/dracut/modules.d/91crypt-gpg/module-setup.sh:       require_binaries /usr/libexec/scdaemon &&
/usr/lib/dracut/modules.d/45url-lib/module-setup.sh:    require_binaries curl || return 1
/usr/lib/dracut/modules.d/90stratis/module-setup.sh:    require_binaries stratisd-init thin_check thin_repair mkfs.xfs xfs_admin xfs_growfs || return 1
/usr/lib/dracut/modules.d/90multipath/module-setup.sh:    require_binaries multipath || return 1
/usr/lib/dracut/modules.d/95iscsi/module-setup.sh:    require_binaries iscsi-iname iscsiadm iscsid || return 1
/usr/lib/dracut/modules.d/95ssh-client/module-setup.sh:    require_binaries ssh scp  || return 1
/usr/lib/dracut/modules.d/35network-manager/module-setup.sh:    require_binaries sed grep || return 1
/usr/lib/dracut/modules.d/90dmsquash-live-ntfs/module-setup.sh:    require_binaries ntfs-3g || return 1
/usr/lib/dracut/modules.d/91crypt-loop/module-setup.sh:    require_binaries losetup || return 1
/usr/lib/dracut/modules.d/05busybox/module-setup.sh:    require_binaries busybox || return 1
/usr/lib/dracut/modules.d/99img-lib/module-setup.sh:    require_binaries tar gzip dd bash || return 1
/usr/lib/dracut/modules.d/90dm/module-setup.sh:    require_binaries dmsetup || return 1
/usr/lib/dracut/modules.d/03modsign/module-setup.sh:    require_binaries keyctl || return 1
/usr/lib/dracut/modules.d/97biosdevname/module-setup.sh:    require_binaries biosdevname || return 1
/usr/lib/dracut/modules.d/95nfs/module-setup.sh:    require_binaries rpc.statd mount.nfs mount.nfs4 umount || return 1
/usr/lib/dracut/modules.d/90dmraid/module-setup.sh:    require_binaries dmraid || return 1
/usr/lib/dracut/modules.d/95fcoe/module-setup.sh:    require_binaries dcbtool fipvlan lldpad ip readlink fcoemon fcoeadm || return 1
/usr/lib/dracut/modules.d/00warpclock/module-setup.sh:    require_binaries /sbin/hwclock || return 1
/usr/lib/dracut/modules.d/35network-legacy/module-setup.sh:    require_binaries ip dhclient sed awk grep || return 1
/usr/lib/dracut/modules.d/00bash/module-setup.sh:    require_binaries /bin/bash
/usr/lib/dracut/modules.d/95nbd/module-setup.sh:    require_binaries nbd-client || return 1
/usr/lib/dracut/modules.d/90btrfs/module-setup.sh:    require_binaries btrfs || return 1
/usr/lib/dracut/modules.d/00systemd/module-setup.sh:    if require_binaries $systemdutildir/systemd; then
/usr/lib/dracut/modules.d/10i18n/module-setup.sh:    require_binaries setfont loadkeys kbd_mode || return 1
/usr/lib/dracut/modules.d/90lvm/module-setup.sh:    require_binaries lvm || return 1
/usr/lib/dracut/modules.d/50plymouth/module-setup.sh:    require_binaries plymouthd plymouth plymouth-set-default-theme
/usr/lib/dracut/modules.d/95fcoe-uefi/module-setup.sh:    require_binaries dcbtool fipvlan lldpad ip readlink || return 1

同样,dracut 并不包括从/usr/lib/dracut/modules.d开始的每个模块。它只包括特定于主机的模块。在下一节中,您将学习如何在 initramfs 中添加或省略特定的模块。

自定义 initramfs

Dracut 也有自己的模块。内核模块和 dracut 模块是不同的。Dracut 收集特定于主机的二进制文件、相关库、配置文件和硬件设备模块,并将它们分组到名称 dracut modules 下。内核模块由硬件设备的.ko文件组成。您可以从/usr/lib/dracut/modules.d/dracut --list-modules命令中看到 dracut 模块列表。

# dracut --list-modules | xargs -n6
bash systemd warpclock fips systemd-initrd systemd-networkd
modsign rescue watchdog busybox nss-softokn rdma
i18n convertfs network-legacy network-manager network ifcfg
url-lib drm plymouth lvmmerge bcache btrfs
crypt dm dmraid dmsquash-live dmsquash-live-ntfs kernel-modules
kernel-modules-extra kernel-network-modules livenet lvm mdraid multipath
qemu qemu-net stratis crypt-gpg crypt-loop cifs
debug fcoe fcoe-uefi fstab-sys iscsi lunmask
nbd nfs resume rootfs-block ssh-client terminfo
udev-rules virtfs biosdevname dracut-systemd ecryptfs ostree
pollcdrom selinux syslog usrmount base earlykdump
fs-lib img-lib kdumpbase shutdown squash uefi-lib

如果您想在 initramfs 中添加或省略特定的 dracut 模块(不是硬件设备模块),那么dracut.conf在这里起着至关重要的作用。注意dracut.conf是 dracut 的配置文件,不是 initramfs 的;因此,它在 initramfs 中不可用。

# lsinitrd | grep -i 'dracut.conf'
    <no output>

生成 initramfs 时,dracut 将参考dracut.conf file。默认情况下,它将是一个空文件。

# cat /etc/dracut.conf
    # PUT YOUR CONFIG IN separate files
    # in /etc/dracut.conf.d named "<name>.conf"
    # SEE man dracut.conf(5) for options

dracut.conf提供了各种选项,可以用来添加或省略模块。

假设你想省略plymouth-相关文件(二进制、配置文件、模块等。)来自 initramfs 然后你可以在dracut.conf中增加一个omit_dracutmodules+=plymouth或者使用dracut二进制的omit ( -o)开关。这里有一个例子:

# lsinitrd | grep -i plymouth | wc -l
    118

在当前启动的内核中有将近 118 个plymouth-相关文件。我们现在尽量省略plymouth-相关文件。

# dracut -o plymouth /root/new.img

# lsinitrd /root/new.img | grep -i plymouth | wc -l
    4

正如您可以清楚地看到的,所有与 dracut 相关的模块都已经从我们新构建的 initramfs 中删除了。因此,与plymouth相关的二进制文件、配置文件、库和硬件设备模块(如果有的话)将不会被 initramfs 中的 dracut 捕获。在dracut.conf中增加omit_dracutmodules+=plymouth也可以达到同样的效果。

# cat /etc/dracut.conf | grep -v '#'
    omit_dracutmodules+=plymouth

# dracut /root/new.img --force

# lsinitrd /root/new.img | grep -i plymouth
-rw-r--r--   1 root     root          454 Jul 25  2019 usr/lib/systemd/system/systemd-ask-password-plymouth.path
-rw-r--r--   1 root     root          435 Jul 25  2019 usr/lib/systemd/system/systemd-ask-password-plymouth.service
drwxr-xr-x   2 root     root            0 Jul 25  2019 usr/lib/systemd/system/systemd-ask-password-plymouth.service.wants
lrwxrwxrwx   1 root     root           33 Jul 25  2019 usr/lib/systemd/system/systemd-ask-password-plymouth.service.wants/systemd-vconsole-setup.service -> ../systemd-vconsole-setup.service

以下来自man页面:

省略 dracut 模块

有时候出于速度、大小或功能的原因,你并不希望包含 dracut 模块。为此,可以在 dracut.conf 或/etc/dracut.conf.d/myconf.conf 配置文件中指定 omit_dracutmodules 变量(请参见 dracut.conf(5)),或者在命令行中使用-o 或- omit 选项:# dracut-o " multipath LVM " no-multipath-LVM . img

就像我们省略 dracut 模块一样,我们可以添加/usr/lib/dracut/modules.d中可用的任何模块。我们可以使用 dracut 的--add开关,也可以使用add_dracutmodules+= in dracut.conf。例如,您可以看到我们没有将 NFS 模块/文件/二进制文件添加到我们的new.img initramfs 中,因为我的测试系统没有从 NFS 引导,也没有在其中使用任何 NFS 挂载点。显然,dracut 会从/usr/lib/dracut/modules.d开始跳过nfs模块。所以,让我们把它添加到我们的 initramfs 中。

#lsinitrd | grep -i nfs
<no_output>

# cat /etc/dracut.conf
    # PUT YOUR CONFIG IN separate files
    # in /etc/dracut.conf.d named "<name>.conf"
    # SEE man dracut.conf(5) for options

    #omit_dracutmodules+=plymouth
    add_dracutmodules+=nfs

# dracut /root/new.img --force
# lsinitrd /root/new.img | grep -i nfs | wc -l
    33

我们也可以通过使用带有--add开关的dracut命令来实现这一点。

# lsinitrd /root/new.img | grep -i nfs
# dracut --add nfs /root/new.img --force
# lsinitrd /root/new.img | grep -i nfs
Arguments: --add 'nfs' --force
nfs
-rw-r--r--   1 root     root           15 Jul 25  2019 etc/modprobe.d/nfs.conf
drwxr-xr-x   2 root     root            0 Jul 25  2019 usr/lib64/libnfsidmap
-rwxr-xr-x   1 root     root        50416 Jul 25  2019 usr/lib64/libnfsidmap/nsswitch.so
-rwxr-xr-x   1 root     root        54584 Jul 25  2019 usr/lib64/libnfsidmap.so.1.0.0
lrwxrwxrwx   1 root     root           20 Jul 25  2019 usr/lib64/libnfsidmap.so.1 -> libnfsidmap.so.1.0.0
-rwxr-xr-x   1 root     root        42744 Jul 25  2019 usr/lib64/libnfsidmap/sss.so
-rwxr-xr-x   1 root     root        46088 Jul 25  2019 usr/lib64/libnfsidmap/static.so
-rwxr-xr-x   1 root     root        62600 Jul 25  2019 usr/lib64/libnfsidmap/umich_ldap.so
-rwxr-xr-x   1 root     root          849 Oct  8  2018 usr/lib/dracut/hooks/cleanup/99-nfsroot-cleanup.sh
-rwxr-xr-x   1 root     root         3337 Oct  8  2018 usr/lib/dracut/hooks/cmdline/90-parse-nfsroot.sh
-rwxr-xr-x   1 root     root          874 Oct  8  2018 usr/lib/dracut/hooks/pre-udev/99-nfs-start-rpc.sh
drwxr-xr-x   5 root     root            0 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs
drwxr-xr-x   2 root     root            0 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs/blocklayout
-rw-r--r--   1 root     root        16488 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs/blocklayout/blocklayoutdriver.ko.xz
drwxr-xr-x   2 root     root            0 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs_common
-rw-r--r--   1 root     root         2584 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs_common/grace.ko.xz
-rw-r--r--   1 root     root         3160 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs_common/nfs_acl.ko.xz
drwxr-xr-x   2 root     root            0 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs/filelayout
-rw-r--r--   1 root     root        11220 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs/filelayout/nfs_layout_nfsv41_files.ko.xz
drwxr-xr-x   2 root     root            0 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs/flexfilelayout
-rw-r--r--   1 root     root        20872 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs/flexfilelayout/nfs_layout_flexfiles.ko.xz
-rw-r--r--   1 root     root       109684 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs/nfs.ko.xz
-rw-r--r--   1 root     root        18028 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs/nfsv3.ko.xz
-rw-r--r--   1 root     root       182756 Jul 25  2019 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/fs/nfs/nfsv4.ko.xz
-rwxr-xr-x   1 root     root         4648 Oct  8  2018 usr/lib/nfs-lib.sh
-rwsr-xr-x   1 root     root       187680 Jul 25  2019 usr/sbin/mount.nfs
lrwxrwxrwx   1 root     root            9 Jul 25  2019 usr/sbin/mount.nfs4 -> mount.nfs
-rwxr-xr-x   1 root     root          719 Oct  8  2018 usr/sbin/nfsroot
drwxr-xr-x   4 root     root            0 Jul 25  2019 var/lib/nfs
drwxr-xr-x   2 root     root            0 Jul 25  2019 var/lib/nfs/rpc_pipefs
drwxr-xr-x   3 root     root            0 Jul 25  2019 var/lib/nfs/statd
drwxr-xr-x   2 root     root            0 Jul 25  2019 var/lib/nfs/statd/sm

就像我们在 initramfs 中添加了额外的nfs dracut 模块一样,通过在dracut.conf中添加dracutmodules+=,我们可以在 initramfs 中只有nfs模块。这意味着生成的 initramfs 中只有nfs模块。来自/usr/lib/dracut/modules.d/的其余模块将被丢弃。

# cat /etc/dracut.conf
    #omit_dracutmodules+=plymouth
    #add_dracutmodules+=nfs
    dracutmodules+=nfs

# dracut /root/new.img —force

# lsinitrd /root/new.img

Image: /root/new.img: 20M
========================================================================
Early CPIO image
========================================================================
drwxr-xr-x  3 root     root       0 Jul 25  2019 .
-rw-r—r--   1 root     root       2 Jul 25  2019 early_cpio
drwxr-xr-x  3 root     root       0 Jul 25  2019 kernel
drwxr-xr-x  3 root     root       0 Jul 25  2019 kernel/x86
drwxr-xr-x  2 root     root       0 Jul 25  2019 kernel/x86/microcode
-rw-r—r--   1 root     root       100352 Jul 25  2019 kernel/x86/microcode/GenuineIntel.bin
========================================================================
Version:

Arguments: --force

dracut modules:
nss-softokn
network-manager
network
kernel-network-modules
nfs
=======================================================================

如您所见,只添加了nfs模块及其依赖模块,如network dracut 模块。另外,请注意 initramfs 两个版本之间的大小差异。

# ls -lh initramfs-5.3.16-300.fc31.x86_64.img
    -rw-------. 1 root root 28M Dec 23 06:37 initramfs-5.3.16-300.fc31.x86_64.img

# ls -lh /root/new.img
    -rw-------. 1 root root 20M Dec 24 11:05 /root/new.img

使用 dracut 的-m--modules开关也可以达到同样的效果。

# dracut -m nfs /root/new.img --force

如果你只想添加硬件设备模块,那么请注意硬件设备模块是指/lib/modules/<kernel-version>/drivers/<module-name>kernel-modules包提供的*.ko文件。那么 dracut 的--add开关或add_dracutmodules+=开关将不起作用,因为这两个开关添加的是 dracut 模块,而不是内核模块(.ko)文件。因此,要添加内核模块,我们需要使用 dracut 的--add-drivers开关或dracut.conf中的drivers+=add_drivers+=。这里有一个例子:

# lsinitrd /root/new.img | grep -i ath3k

名为ath3k的蓝牙相关模块不在我们的 initramfs 中,但它是内核提供的模块之一。

#ls -lh /lib/modules/5.3.16-300.fc31.x86_64/kernel/drivers/bluetooth/ath3k.ko.xz

让我们添加它,如下所示:

# dracut --add-drivers ath3k /root/new.img --force

现在已经添加了,如下所示:

# lsinitrd /root/new.img | grep -i ath3k
Arguments: --add-drivers 'ath3k' --force
-rw-r--r-- 1 root  root 246804 Jul 25 03:54 usr/lib/firmware/ath3k-1.fw
-rw-r--r-- 1 root  root   5652 Jul 25 03:54 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/bluetooth/ath3k.ko.xz

如您所见,initramfs 中添加了ath3k.ko模块。

dracut 模块还是内核模块?

让我们看看何时添加 dracut 模块,何时添加内核模块。这里有一个场景:您的主机根文件系统在一个普通的 SCSI 设备上。因此,很明显,您的 initramfs 既没有一个multipath.ko内核模块,也没有一个类似于multipath.conf的配置文件。

  1. 突然,您决定将您的根文件系统从普通的本地磁盘转移到 SAN(我绝不会建议在生产系统上进行这样的改变),SAN 通过多路径设备连接。

  2. 要获得多路径设备的整个环境,您需要在这里添加多路径 dracut 模块,以便将多路径的整个环境拉入 initramfs。

  3. 几天后,您在同一系统上添加了新的 NIC 卡,NIC 卡供应商已经为其提供了驱动程序。驱动程序只不过是一个.ko文件(内核对象)。要在 initramfs 中添加这个模块,您必须选择添加kernel module选项。这将只添加网卡的驱动程序,而不是整个环境。

但是,如果您想在 initramfs 中添加某个特定的文件,它既不是内核模块,也不是 dracut 模块,该怎么办呢?dracut 提供了dracut.confinstall_items+=--include变量,通过它们我们可以添加特定的文件。这些文件可以是从普通文本到二进制文件等任何文件。

#lsinitrd /root/new.img | grep -i date
    <no_output>

默认情况下,date二进制文件不存在于 initramfs 中。但是要添加二进制,我们可以使用一个install_itsems+开关。

# cat /etc/dracut.conf
    # PUT YOUR CONFIG IN separate files
    # in /etc/dracut.conf.d named "<name>.conf"
    # SEE man dracut.conf(5) for options

    #omit_dracutmodules+=plymouth
    #add_dracutmodules+=nfs
    #dracutmodules+=nfs
    install_items+=date

# dracut /root/new.img --force

# lsinitrd /root/new.img | grep -i date
-rwxr-xr-x   1 root     root       122456 Jul 25 02:36 usr/bin/date

正如你所看到的,已经添加了date二进制文件,但最重要的是它不仅添加了二进制文件;相反,它还添加了运行date命令所必需的库。同样可以通过dracut命令的--install开关实现。但是这有一个限制;它不能添加用户自定义的二进制文件。为此,我们需要使用 dracut 的--include开关。使用--include,您可以在 initramfs 中包含普通文件、目录甚至二进制文件。对于二进制文件,如果您的二进制文件需要一个支持库,那么您必须指定该库的名称及其绝对路径。

“无法启动”问题 4 (initramfs)

问题:一个 Linux 生产系统经过四个月的定期维护后重新启动,它已经停止启动。它一直在屏幕上显示以下错误信息:

<snip>
.
dracut-initqueue[444]: warning: dracut-initqueue timeout - starting timeout scripts
dracut-initqueue[444]: warning: dracut-initqueue timeout - starting timeout scripts
dracut-initqueue[444]: warning: dracut-initqueue timeout - starting timeout scripts
dracut-initqueue[444]: warning: dracut-initqueue timeout - starting timeout scripts
.
</snip>

解决方法:以下是解决问题的步骤:

img/493794_1_En_6_Fig1_HTML.jpg

图 6-1

GRUB 闪屏

  1. 错误信息开始时会说它无法到达交换设备,然后该过程超时。

    [TIME] Timed out waiting for device /dev/mapper/fedora_localhost--live-swap

    这是一条非常重要的信息,因为它告诉您这个系统的文件系统有问题。

  2. 交换设备基于 HDD,并且交换文件系统已经在其上创建。现在交换设备本身丢失了。因此,要么底层磁盘本身不可访问,要么交换文件系统已损坏。了解了这一点,我们现在可以只关注存储方面。隔离问题很重要,因为“无法启动”问题有数千种可能导致系统停止启动的情况。

  3. 我们要么以救援模式启动,要么使用相同发行版和版本的实时映像。这是一个 Fedora 31 系统,如图 6-1 所示,我会使用 GRUB 的 rescue 选项。

  4. 一旦我们引导进入救援模式,我们将挂载用户的根文件系统并chroot到其中。为什么救援模式能够启动,而普通内核却不能在同一系统上启动?这是一个有效的问题,答案将在第十章中给出。

  5. 因为我们能够在救援内核中挂载根文件系统,但是不能在普通内核中挂载根文件系统,这意味着 initramfs 映像有问题。也许是一些模块是必要的处理硬盘丢失。我们来验证一下这个理论。

  6. 这是一个虚拟化系统,这意味着它有一个虚拟磁盘。这个从/dev目录就能看出来。

  7. 为了处理虚拟磁盘,我们需要在 initramfs 中有一个virtio_blk模块。

#ls /dev/vd*
vda vda1 vda2

#lsinitrd /boot/new.img | grep -i virt
Arguments: --omit-drivers virtio_blk
-rw-r--r-- 1 root  root   14132 Jul 25 03:54
    usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/char/virtio_console.ko.xz
-rw-r--r-- 1 root  root   25028 Jul 25 03:54
    usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/net/virtio_net.ko.xz
-rw-r--r-- 1 root  root   7780 Jul 25 03:54
    usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/virtio_scsi.ko.xz
-rw-r--r-- 1 root  root   499 Feb 26  2018 usr/lib/sysctl.d/60-libvirtd.conf

如你所见,virtio_blk模块不见了。

  1. 由于virtio_blk缺失,显然内核无法检测和访问vda磁盘,用户在这里拥有根文件系统和交换文件系统。

  2. 要解决这个问题,我们需要在 initramfs 中添加缺失的virtio_blk模块。

img/493794_1_En_6_Fig2_HTML.jpg

图 6-2

Fedora 的登录屏幕

  1. 我们将使用我们的new.img initramfs 进行引导。如何借助 GRUB 命令提示符手动引导系统已经在“无法引导”问题 1 中讨论过了。

  2. 在添加了丢失的virtio_blk模块后,“无法启动”的问题已经被修复。在图 6-2 中可以看到成功启动的系统。

#dracut --add-drivers=virtio_blk /boot/new.img --force

# lsinitrd | grep -i virtio_blk
    -rw-r--r--   1 root     root         8356 Jul 25 03:54 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/block/virtio_blk.ko.xz

“无法启动”问题 5 (initramfs)

问题:图 6-3 显示了屏幕上可见的内容。

img/493794_1_En_6_Fig3_HTML.jpg

图 6-3

控制台消息

解决方法:以下是解决问题的步骤:

  1. 这很容易理解和解决。

  2. 错误消息是不言自明的;initramfs 文件本身缺失。

  3. 要么是 initramfs 本身丢失,要么是因为/boot/loader/entries/*文件中有一个错误的条目。在这种情况下,initramfs 本身是缺失的。

  4. 因此,我们需要以救援模式引导,并挂载用户的根文件系统。

  5. 要么重新安装内核的rpm包,让包的postscripts部分重新生成丢失的 initramfs,并相应地更新 BLS 条目。

  6. 或者您可以在dracut命令的帮助下重新生成 initramfs。

内核命令行选项

正如我们已经看到的,GRUB 接受内核命令行参数,并将它们传递给内核。内核有数百个命令行参数,任何人都几乎不可能涵盖每一个参数。因此,我们将只关注那些在引导操作系统时必需的参数。如果你对所有的内核命令行参数感兴趣,那么访问下面的页面: https://www.kernel.org/doc/html/v4.14/admin-guide/kernel-parameters.html

该页面上的参数列表是 4 系列内核的,但是大多数参数解释也适用于 5 系列内核。最好的选择是总是在/usr/share/doc/查看内核文档。

  • 这是主内核的命令行参数之一。引导的最终目的是挂载用户的根文件系统。root内核命令行参数提供了用户根文件系统的名称,内核应该挂载这个文件系统。

  • 从 initramfs 运行的 systemd 代表内核挂载用户的根文件系统。

  • 如果用户的根文件系统不可用,或者如果内核不能挂载它,那么对于内核来说,这将被认为是一种紧急情况。

初始化

  • 内核从 initramfs 运行 systemd,这成为第一个进程。它也被称为 PID-1,是所有进程的父进程。

  • 但是如果您是一名开发人员,并且希望运行自己的二进制文件而不是 systemd,那么您可以使用init内核命令行参数。这里有一个例子:

    init=/sbin/yogesh

正如你在图 6-4 中看到的,这将运行yogesh二进制文件而不是 systemd。

img/493794_1_En_6_Fig4_HTML.jpg

图 6-4

内核命令行参数

但是yogesh在实际的根文件系统上不可用;因此,如图 6-5 所示,将无法启动。

img/493794_1_En_6_Fig5_HTML.jpg

图 6-5

应急 Shell

img/493794_1_En_6_Fig6_HTML.jpg

图 6-6

rdsosreport.txt 文件

  • 系统把我们扔进了紧急壳里。关于调试 Shell 的详细讨论,请参考第八章。

  • /run/initramfs/rdsosreport.txt中提到了让我们进入紧急 Shell 的原因以及“无法启动”问题的原因。图 6-6 显示了rdsosreport.txt文件的一个片段。

  • 这里要注意的有趣部分是,我们的/sbin/yogesh二进制文件将在chroot调用实际根文件系统时被调用。我们还没有讨论chroot;你可以在第十章中找到详细的讨论。

Romania 罗马尼亚

  • 这是对root内核命令行参数的支持参数。ro代表“只读”文件系统。用户的根文件系统将挂载在 initramfs 中,如果已经传递了ro内核命令行参数,它将以只读模式挂载。ro是每个主要 Linunx 发行版的默认选择。

rhgb 和 quite

img/493794_1_En_6_Fig7_HTML.jpg

图 6-7

普利茅斯银幕

  • 几乎每个 Linux 发行版都在引导时显示动画,以使引导过程更加激动人心,但是分析引导序列所需的重要控制台消息将隐藏在动画后面。要停止动画并在屏幕上查看详细的控制台消息,请移除rhgbquite参数。

  • rhgbquite通过后,如图 6-7 所示,将显示plymouth动画。

img/493794_1_En_6_Fig8_HTML.jpg

图 6-8

控制台消息

  • rhgbquite被移除时,如图 6-8 所示,控制台消息将暴露给用户。

  • 您也可以在动画(plymouth)屏幕上按 Escape 键,并可以看到控制台消息,但为此,您必须亲自出现在制作系统前,这是不太可能的。

防火墙

  • 有时,为了解决“无法启动”的问题,您希望完全摆脱 SELinux。这时可以通过selinux=0内核命令行参数。这将完全禁用 SELinux。

这些是一些直接影响引导顺序的内核命令行参数。与内核命令行参数一样,GRUB 也可以接受 dracut 命令行参数,这些参数将被 initramfs 接受,或者更准确地说,被 initramfs 的 systemd 接受。

dracut 命令行选项

用外行人的话来说,可以把以rd.开头的命令行参数看作是 initramfs 能够理解的 dracut 命令行参数。

rd.auto (rd.auto=1)

  • 根据手册页,这允许自动装配特殊设备,如 cryptoLUKS、dmraid、mdraid 或 lvm。默认为关闭。

  • 考虑一个类似前面的场景,您的系统没有配置mdraid (s/w raid),但是现在您最近已经实现了它,并且您希望在引导时激活该设备。换句话说,在创建 initramfs 时,机器的存储状态发生了变化。现在,在不重新生成新的 initramfs 的情况下,您希望在引导时激活新的配置(LVM 或 LUKS)。

rd.hostonly=0

  • 根据手册页,这将删除在构建 initramfs 映像的主机系统的配置中编译的所有内容。这有助于引导,如果任何磁盘布局已经改变,特别是结合rd.auto或指定布局的其他参数。

  • 假设您的显卡提供商(如 Nvidia)为您提供了 initramfs 中的特殊驱动程序/模块,但是这些模块已经开始产生问题。由于图形驱动程序将在引导的早期阶段加载,您希望避免使用该模块;相反,你想使用一个通用的驱动程序(vesa)。在这种情况下,您可以将rd.hostonly=0.与该参数一起使用,initramfs 将加载通用驱动程序,并将避免特定于主机的 Nvidia 驱动程序。

rd.fstab = 0

  • 根据手册页,如果您不想使用在真正根目录的/etc/fstab中找到的根文件系统的特殊挂载选项,请使用这个参数。

rd.skipfsck

img/493794_1_En_6_Fig9_HTML.jpg

图 6-9

控制台消息

  • 根据手册页,这跳过了rootfs/usrfsck。如果您将/usr挂载为只读,并且init系统在重新挂载之前执行fsck,您可能想要使用这个选项来避免重复。

  • 大多数 Linux 管理员对fsck以及它如何与ro内核命令行参数结合有误解。我们大多数人认为内核首先在ro模式下挂载实际的根文件系统,然后在其上执行fsck,这样fsck操作就不会破坏根文件系统数据。一旦fsck成功,它将参照/etc/fstab以读写模式重新挂载根文件系统。

    但是这种理解有一个基本的缺陷,那就是无论ro还是rw模式,fsck都不能在挂载的文件系统上执行。

  • 以下 Fedora 系统的用户根文件系统位于 sda5 设备上,并且当前以只读模式挂载,因此fsck将失败,因为文件系统已挂载:

    # fsck.ext4 /dev/sda5
        e2fsck 1.45.3 (14-Jul-2019)
        /dev/sda5 is mounted.
        e2fsck: Cannot continue, aborting.
    
    
  • 因此,证明了用户的根文件系统在ro模式下挂载的目的不是为了执行fsck。那么将ro命令行参数传递给内核的原因是什么呢?让我们通过引导序列来讨论它。

  • 内核提取 initramfs 并将命令行参数如rootro传递给 systemd,systemd 将从 initramfs 开始。

  • systemd 将找到实际的根文件系统。

  • 一旦根文件系统(设备)被识别,systemd 将对其执行fsck

  • 如果fsck成功,那么 systemd 将把根文件系统作为ro(根据传递的内核命令行参数)装入 initramfs 本身。它将以只读方式安装在 initramfs 的/sysroot目录中。

  • 如图 6-9 所示,内核已经提取了 initramfs 并从中启动 systemd(我已经去掉了rhgbquite参数)。

Systemd 随后扫描连接的存储设备以查找根文件系统,并找到了一个。在挂载用户的根文件系统之前,它首先在其上执行了fsck,然后将它挂载到目录sysroot上的 initramfs 中。用户的根文件系统将以只读模式装载。

img/493794_1_En_6_Fig11_HTML.jpg

图 6-11

fsck 控制台消息

  • The reason for mounting it in read-only mode is simple to understand. Suppose the system fails to boot, but it has managed to mount the user’s root filesystem on sysroot and has provided us with a shell to fix the “can’t boot” issue. Users might accidentally corrupt or even delete the user’s root filesystem that is mounted under sysroot. So, to prevent the user’s root filesystem from such accidents, it is preferred to mount it in read-only mode.

    #switch_root:/# ls -ld /sysroot/
        dr-xr-xr-x 19 root 0 4096 Sep 10  2017 /sysroot/
    
    

    如何使用调试 Shell 以及 initramfs 如何提供它们将在第八章中讨论。

  • Figure 6-10 shows systemd continuing its booting sequence and leaving the initramfs environment.

    img/493794_1_En_6_Fig10_HTML.jpg

    图 6-10

    控制台消息

    如图 6-10 所示,交换机根离开当前的 initramfs 环境,并将根从 initramfs 的临时根文件系统更改为/sysroot,其中安装了用户的根文件系统。(切换根过程将在第九章中讨论。)

  • 在进入用户的根文件系统之后,用户的根文件系统的 systemd 读取/etc/fstab并在挂载点上采取适当的动作。例如,在这个 Fedora 系统上,有用户的根文件系统条目和/boot条目(引导在单独的分区上):

    #cat /etc/fstab
    
    /dev/mapper/fedora_localhost--live-root  /     ext4    defaults    1 1
    UUID=eea3d947-0618-4d8c-b083-87daf15b2679 /boot  ext4    defaults  1 2
    /dev/mapper/fedora_localhost--live-swap none   swap    defaults     0 0
    
    
  • 如图 6-11 所示,在这个阶段,systemd 在挂载之前只会在引导设备上执行fsck。请注意,它不是在用户的根文件系统上执行fsck,因为它已经在 initramfs 环境中执行过了。此外,用户的根文件系统是当前挂载的,我们都知道在交换设备上做fsck是没有意义的。

  • 如果有任何其他像/usr这样的额外挂载点,它也会在那个设备上执行fsck

  • fsck取决于/etc/fstab的第五个参数。如果为 1,那么fsck将在引导时执行。此fstab设置不适用于用户的根文件系统,因为fsck将在 initramfs 内的用户根文件系统上强制执行,这是在读取/etc/fstab文件之前。

  • rd.skipfsck仅适用于 root 和用户的根文件系统。它不适用于任何其他文件系统,如/boot

rd.driver.blacklist、rd.driver.pre 和 rd.driver.post

这是来自rd.driver.blacklist的手册页:

rd.driver.blacklist=<drivername>[,<drivername>,...]

不加载内核模块<驱动名>。可以多次指定该参数。

rd.driver.blacklist是最重要的 dracut 命令行参数之一。顾名思义,它会将指定的模块列入黑名单。让我们尝试将对虚拟客户系统相当重要的virtio相关驱动程序列入黑名单。

# lsmod | grep -i virt
    virtio_balloon         24576  0
    virtio_net             57344  0
    virtio_console         40960  2
    virtio_blk             20480  3
    net_failover           20480  1 virtio_net

它在 initramfs 中也是可用的。

# lsinitrd | grep -i virtio
-rw-r--r-- 1 root  root  8356 Jul 25 03:54 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/block/virtio_blk.ko.xz
-rw-r--r--   1 root     root        14132 Jul 25 03:54 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/char/virtio_console.ko.xz
-rw-r--r--   1 root     root        25028 Jul 25 03:54 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/net/virtio_net.ko.xz
-rw-r--r--   1 root     root         7780 Jul 25 03:54 usr/lib/modules/5.3.7-301.fc31.x86_64/kernel/drivers/scsi/virtio_scsi.ko.xz

请记住,要将模块列入黑名单,如图 6-12 所示,您需要确保所有其他相关模块也必须列入黑名单;否则,依赖模块将会拉出黑名单中的模块。例如,在这种情况下,virtio_balloonvirtio_netvirtio_consolevirtio_blkvirtio_pci模块相互依赖。这意味着如果我们只将virtio_blk列入黑名单,其他依赖模块仍将加载virtio_blk模块。

img/493794_1_En_6_Fig12_HTML.jpg

图 6-12

内核命令行参数

virtio相关的驱动程序很重要。虚拟磁盘和虚拟机管理程序网络就是通过这个驱动程序向来宾操作系统公开的。由于我们将它们列入黑名单,客户操作系统将停止启动。您可以在图 6-13 中看到“无法启动”控制台信息。

img/493794_1_En_6_Fig13_HTML.jpg

图 6-13

控制台消息

因此,将virtio模块列入黑名单是成功的,但是这种方法有两个问题。

  • rd.driver.blacklist只会阻塞从 initramfs 加载的模块。

  • 我们每次都需要手动提供模块列表给rd.driver.blacklist

如果模块不在 initramfs 中,那么您就不能真正阻止它加载。例如,bluetooth模块不是从 initramfs 加载的,而是内核在 initramfs 环境之后加载的。

# lsmod | grep -i bluetooth

    bluetooth             626688  37 btrtl,btintel,btbcm,bnep,btusb,rfcomm
    ecdh_generic           16384  1 bluetooth
    rfkill                 28672  5 bluetooth

# lsinitrd | grep -i bluetooth
    <no_output>

为了阻止内核加载bluetooth模块,我们需要告诉modprobe命令阻止模块加载。modprobe是一个代表内核加载或删除模块的二进制文件。

创建一个新的blacklist.conf文件。(可以选择任意名称,但必须有一个.conf后缀)并将该模块列入黑名单。

#cat /etc/modprobe.d/blacklist.conf
    blacklist bluetooth

但是重启后,你会发现bluetooth又被内核加载了。

#lsmod | grep -i bluetooth
    bluetooth             626688  37 btrtl,btintel,btbcm,bnep,btusb,rfcomm
    ecdh_generic           16384  1 bluetooth
    rfkill                 28672  5 bluetooth

这是因为bluetooth模块是多个其他模块的依赖,如btrtlbtintelbtbcmbnepbtusbrfcommrfkill。因此,modprobe已经加载了bluetooth作为其他模块的依赖项。在这种情况下,我们需要通过在blacklist.conf文件中添加install bluetooth /bin/true行来欺骗modprobe命令,如下所示:

# cat /etc/modprobe.d/blacklist.conf
    install bluetooth /bin/true

重启后,你会发现bluetooth模块已经被封锁。

# lsmod | grep -i bluetooth
    <no_output>

也可以用/bin/false代替/bin/true

经过对rd.driver.blacklist的解释,rd.driver.prerd.driver.post dracut 命令行参数更容易理解,手册页也一目了然,如下所示:

rd.driver.pre=<drivername>[,<drivername>,...]

强制加载内核模块<驱动名>。可以多次指定该参数。

rd.driver.post=<drivername>[,<drivername>,...]

在所有自动加载模块加载完毕后,强制加载内核模块<驱动名>。可以多次指定该参数。

研发调试

这来自手册页:

为 dracut shell 设置-x。如果 systemd 在 initramfs 中是活动的,则所有输出都将记录到 systemd 日志中,您可以使用“journalctl -ab”来检查该日志。如果 systemd 未处于活动状态,日志将被写入 dmesg 和/run/initramfs/init.log。如果设置了“quiet ”,它还会记录到控制台。

rd.debug将启用 systemd 的调试日志记录,这将在控制台和 systemd 日志中记录大量消息。rd.debug提供的详细信息将有助于识别与 systemd 相关的“无法启动”问题。

rd.memdebug= [0-4]

这来自手册页:打印不同点的内存使用信息,设置详细级别从 0 到 4。级别越高意味着调试输出越多:

  • 这将在屏幕上打印所有与内存子系统相关的信息,如meminfoslabinfo文件内容。
       0 - no output
       1 - partial /proc/meminfo
       2 - /proc/meminfo
       3 - /proc/meminfo + /proc/slabinfo
       4 - /proc/meminfo + /proc/slabinfo + tracekomem

lvm、raid 和多路径相关的 dracut 命令行参数

这来自手册页:

  • 在这些参数中,您必须至少观察过 GRUB 传递的rd.lvm.lv选项。rd.lvm.lv的目的是在引导的早期阶段激活给定的 LVM 设备。默认情况下,主要的 Linux 发行商只激活根和交换(如果配置的话)LV 设备。在引导时只激活根文件系统可以加快引导过程。在将根从 initramfs 切换到实际的根文件系统之后,systemd 可以按照/etc/fstab中的列表激活剩余的卷组。

  • 类似地,dracut 提供了多路径和 RAID 相关的命令行参数,这些参数也是不言自明的。

    MD RAID
           rd.md=0
    disable MD RAID detection
    
           rd.md.imsm=0
    disable MD RAID for imsm/isw raids, use DM RAID instead
    
           rd.md.ddf=0
    disable MD RAID for SNIA ddf raids, use DM RAID instead
    
           rd.md.conf=0
    ignore mdadm.conf included in initramfs
    
           rd.md.waitclean=1
    wait for any resync, recovery, or reshape activity to finish before continuing
    
           rd.md.uuid=<md raid uuid>
    only activate the raid sets with the given UUID. This parameter can be specified multiple times.
    
       DM RAID
           rd.dm=0
    disable DM RAID detection
    
           rd.dm.uuid=<dm raid uuid>
    only activate the raid sets with the given UUID. This parameter can be specified multiple times.
    
       MULTIPATH
           rd.multipath=0
    disable multipath detection
    
    
  • dracut 为网络、NFS、CIFS、iSCSI、FCoE 等提供了 n 个命令行参数。这也意味着这些是您可以放置根文件系统的各种选项,但是几乎不可能涵盖每一个 dracut 命令行参数。另外,我不赞成从所有这些复杂的结构中引导系统。我相信让用户的根文件系统总是在本地磁盘上,这样引导过程会很容易,主要是因为在“不能引导”的情况下,更简单的引导序列可以更快地修复。

       rd.lvm=0
disable LVM detection

       rd.lvm.vg=<volume group name>
only activate the volume groups with the given name. rd.lvm.vg can be specified multiple times on the kernel command line.

       rd.lvm.lv=<logical volume name>
only activate the logical volumes with the given name. rd.lvm.lv can be specified multiple times on the kernel command line.

       rd.lvm.conf=0
remove any /etc/lvm/lvm.conf, which may exist in the initramfs

研发突破和研发 Shell

|

参数

|

目的

|
| --- | --- |
| cmdline | 这个钩子收集内核命令行参数。 |
| pre-udev | 该钩子在启动udev处理器之前启动。 |
| pre-trigger | 在这个钩子中,你可以用'udevadm' control --property=KEY=value设置udev环境变量或者控制udev的进一步执行。 |
| pre-mount | 这个钩子在/sysroot挂载用户的根文件系统之前开始。 |
| mount | 钩子将在/sysroot挂载根文件系统后启动。 |
| pre-pivot | 钩子将在切换到实际的根文件系统之前执行。 |

  • rd.shell将在引导序列结束时为我们提供 shell,使用rd.break,我们可以打破引导序列。但是要理解这些参数,我们需要很好地理解 systemd。因此,在讨论rd.break和 dracut 钩子之前,我们将在下一章首先讨论 systemd。以下是rd.break接受的参数:

七、系统:第一部分

以下是我们目前所知的引导顺序:

  1. 引导加载程序在内存中加载内核和 initramfs。

  2. 内核将被加载到特定的位置(特定于架构的位置),而 initramfs 将被加载到任何可用的位置。

  3. 内核在vmlinuz文件头的帮助下提取自己。

  4. 内核在主内存(init/initramfs.c)中提取 initramfs,并将其作为临时根文件系统(/)安装在主内存中。

  5. 内核从临时根文件系统启动(init/main.c)systemd 作为第一个进程,使用 PID-1。

  6. systemd 将找到用户的根文件系统并将chroot放入其中。

本章将介绍从 initramfs 派生的 systemd 如何挂载用户的根文件系统,我们还将看到 initramfs 中 systemd 的详细引导顺序。但在此之前,我们需要将 systemd 理解为一个过程。

我将让 systemd 的手册页在这里进行讨论:

  • “找到并挂载根文件系统后, initrd 将控制权移交给存储在根文件系统中的主机的系统管理器(如 systemd(1)),该系统管理器随后负责探测所有剩余的硬件,挂载所有必需的文件系统并生成所有配置的服务。”

结构

systemd 最初是在 Fedora 15 中引入的。我们都知道 systemd 是对init脚本的替代(/sbin/init现在是/usr/lib/systemd/systemd的符号链接),它惊人地减少了引导时间。然而,在现实中,systemd 远不止是init的替代品。这就是 systemd 所做的:

|

单位

|

目的

|
| --- | --- |
| systemd.service | 来管理服务 |
| systemd.socket | 创建和管理套接字 |
| systemd.device | 根据udev的输入创建和使用设备 |
| systemd.mount | 要挂载文件系统 |
| systemd.automount | 要自动挂载文件系统 |
| systemd.swap | 制造和管理交换设备 |
| systemd.target | 服务组而不是运行级别 |
| systemd.path | 关于由 systemd 监控的路径的信息,用于基于路径的激活 |
| systemd.timer | 对于基于时间的激活 |
| systemd.slice | 服务单元的资源管理,如 CPU、内存、I/O |

  1. 它用journalctl维护日志。

  2. 它广泛使用 cgroups 版本 1 和 2。

  3. 它减少了启动时间。

  4. 它管理单元。是 systemd 处理的一种类型的单元。以下是 systemd 提供和管理的单元:

单元文件将从以下三个位置存储和加载:

|

小路

|

描述

|
| --- | --- |
| /etc/systemd/system | 本地配置 |
| /run/systemd/system | 运行时间单位 |
| /usr/lib/systemd/system | 已安装软件包的单位 |

/etc/systemd/system是一个管理位置,而/usr/lib/systemd/system是一个应用程序供应商位置。这意味着如果相同的单元文件出现在两个位置,管理员的位置将优先于应用程序供应商的位置。请注意,在本章中,所有的命令都是从 initramfs 被解压缩的目录中执行的。

#  tree etc/systemd/
       etc/systemd/
       ├── journald.conf
       └── system.conf
0 directories, 2 files

#ls usr/lib/systemd/system | column

basic.target                       plymouth-switch-root.service
cryptsetup.target                  poweroff.target
ctrl-alt-del.target                poweroff.target.wants
default.target                     reboot.target
dracut-cmdline-ask.service         reboot.target.wants
dracut-cmdline.service             remote-fs-pre.target
dracut-emergency.service           remote-fs.target
dracut-initqueue.service           rescue.service
dracut-mount.service               rescue.target
dracut-pre-mount.service           rescue.target.wants
dracut-pre-pivot.service           rpcbind.target
dracut-pre-trigger.service         shutdown.target
dracut-pre-udev.service            sigpwr.target
emergency.service                  slices.target
emergency.target                   sockets.target
emergency.target.wants             sockets.target.wants
final.target                       swap.target
halt.target                        sysinit.target
halt.target.wants                  sysinit.target.wants
initrd-cleanup.service             sys-kernel-config.mount
initrd-fs.target                   syslog.socket
initrd-parse-etc.service           systemd-ask-password-console.path
initrd-root-device.target          systemd-ask-password-console.service
initrd-root-fs.target              systemd-ask-password-console.service.wants
initrd-switch-root.service         systemd-ask-password-plymouth.path
initrd-switch-root.target          systemd-ask-password-plymouth.service
initrd-switch-root.target.wants    systemd-ask-password-plymouth.service.wants
initrd.target                      systemd-fsck@.service
initrd.target.wants                systemd-halt.service
initrd-udevadm-cleanup-db.service  systemd-journald-audit.socket
kexec.target                       systemd-journald-dev-log.socket
kexec.target.wants                 systemd-journald.service
kmod-static-nodes.service          systemd-journald.socket
local-fs-pre.target                systemd-kexec.service
local-fs.target                    systemd-modules-load.service
multi-user.target                  systemd-poweroff.service
multi-user.target.wants            systemd-random-seed.service
network-online.target              systemd-reboot.service
network-pre.target                 systemd-sysctl.service
network.target                     systemd-tmpfiles-setup-dev.service
nss-lookup.target                  systemd-tmpfiles-setup.service
nss-user-lookup.target             systemd-udevd-control.socket
paths.target                       systemd-udevd-kernel.socket
plymouth-halt.service              systemd-udevd.service
plymouth-kexec.service             systemd-udev-settle.service
plymouth-poweroff.service          systemd-udev-trigger.service
plymouth-quit.service              systemd-vconsole-setup.service
plymouth-quit-wait.service         timers.target
plymouth-reboot.service            umount.target
plymouth-start.service

第三个位置/run/systemd/system是一个临时位置,将由 systemd 在内部用来管理单元。例如,它将在创建套接字时被广泛使用。事实上,/run是 systemd 引入的一个单独的文件系统,用于存储运行时数据。到目前为止,initramfs 的/run目录是空的,这很明显,因为 initramfs 没有被使用。

#ls run/
    <no_output>

此外,预计 initramfs 中的单元文件比用户根文件系统中的单元文件要少。dracut 将只收集那些挂载用户根文件系统所必需的 systemd 单元文件。例如,在 initramfs 中添加与 systemd 单元文件相关的httpdmysql是没有意义的。我们来试着了解一下 systemd 的service单元文件之一,如下图:

# cat /usr/lib/systemd/system/sshd.service
[Unit]
Description=OpenSSH server daemon
Documentation=man:sshd(8) man:sshd_config(5)
After=network.target sshd-keygen.target
Wants=sshd-keygen.target

[Service]
Type=notify
EnvironmentFile=-/etc/crypto-policies/back-ends/opensshserver.config
EnvironmentFile=-/etc/sysconfig/sshd-permitrootlogin
EnvironmentFile=-/etc/sysconfig/sshd
ExecStart=/usr/sbin/sshd -D $OPTIONS $CRYPTO_POLICY $PERMITROOTLOGIN
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=on-failure
RestartSec=42s

[Install]
WantedBy=multi-user.target

这个sshd服务单元文件将不是 initramfs 的一部分,因为您不需要一个ssh服务来挂载用户的根文件系统。service单元文件分为三部分:[unit][service][install]

  • [unit]:
After=network.target sshd-keygen.target

只有在network.target(列出的单位)和sshd-keygen(列出的单位)成功启动后,sshd服务才会启动。如果其中任何一个失败,那么sshd服务也会失败。

Wants=sshd-keygen.target

这是Requires.的一个不太严重的版本,如果wants中提到的任何单元出现故障,那么sshd服务(或该特定服务)也将启动,而在Requires中,只有在Requires中提到的单元已经成功启动时,sshd服务才会启动。BeforeAfter相反WantsAfterBeforeRequires都是相互独立工作的。通常将WantsAfter一起使用。

Conflicts=

这可用于列出与当前单位冲突的单位。启动此设备可能会停止列出的冲突设备。

OnFailure=

当任何给定单元达到故障状态时,单元将启动。

  • 【服务】:
ExecStart=/usr/sbin/sshd

启动一个sshd服务单元只是启动在ExecStart.之后提到的二进制

  • 【安装】:

systemd 不使用单元文件的Install部分。而是由systemctl enabledisable命令使用。它将被systemctl用来创建或破坏符号链接。

systemd 如何减少启动时间?

systemd 的创造者 Lennart Poettering 在他的博客 http://0pointer.de/blog/projects/systemd.html 中给出了 systemd 如何减少启动时间的经典例子。如果你真的想深入 systemd 世界,这个博客是最好的资源之一。

有四个守护进程:syslogdbusavahibluetooth

每个守护进程都需要记录消息。因此,syslog是其他所有守护进程的要求。avahi需要syslogdbus来运行。bluetooth需要dbussyslog但不需要avahi运行。使用Sysv/init脚本模型,会发生这样的情况:

img/493794_1_En_7_Fig1_HTML.jpg

图 7-1

初始模型

  1. syslog会先开始。

  2. 当它完全准备好时,dbus服务将被启动。

  3. dbus之后,avahi将被启动。

  4. 最后会启动bluetooth服务。见图 7-1 。

bluetoothavahi互不依赖,但是bluetooth要等到avahi启动。类似 Ubuntu 的发行版使用upstart而不是init,这在一定程度上改善了引导时间。在upstart中,互不依赖的服务将并行启动,意味着avahibluetooth将一起启动。请参考图 7-2 。

img/493794_1_En_7_Fig2_HTML.jpg

图 7-2

暴发户模式

systemd,中,所有的服务都是借助sockets .同时启动的下面是一个例子:

  1. systemd 将为syslog(已经被替换为journald)创建一个套接字。

  2. 套接字/dev/log是到/run/systemd/journal/dev-log的符号链接。

    # file /dev/log
          /dev/log: symbolic link to /run/systemd/journal/dev-log
    
    # file /run/systemd/journal/dev-log
          /run/systemd/journal/dev-log: socket
    
    

如前所述,run文件系统将被 systemd 用来创建套接字文件。

  1. 对于dbus,套接字是在/run/dbus/system_bus_socket.创建运行的,dbus需要journald才能运行,但由于系统仍在引导,journald / syslog尚未完全启动,dbus会将其消息记录到journald的套接字/dev/log中,每当journald服务完全就绪时,它就会从套接字中获取消息。

  2. It’s the same for the bluetooth service ; it needs the dbus service to be running to start. So, systemd will create a /run/dbus/system_bus_socket socket before the dbus service starts. The bluetooth service will not wait for dbus to start. You can refer to Figure 7-3 for a better understanding.

    img/493794_1_En_7_Fig3_HTML.jpg

    图 7-3

    系统模型

  3. 如果systemd创建的套接字用完了缓冲区,那么bluetooth服务将被阻塞,直到套接字可用。这种套接字方法将大大减少启动时间。

这种基于套接字的方法最初是在 macOS 中尝试的。当时叫launchd。Lennart Poettering 从中获得了灵感。

系统 d-分析

systemd 提供了systemd-analyze工具来检查系统启动所需的时间。

# systemd-analyze
Startup finished in 1.576s (kernel) + 1.653s (initrd) + 11.574s (userspace) = 14.805s
graphical.target reached after 11.561s in userspace

你可以看到,我的 Fedora 系统初始化内核用了 1.5 秒;然后,它在 initramfs 中花了 1.6 秒,花了将近 11 秒来启动服务或初始化用户空间。总时间几乎是 15 秒。总时间是从引导装载程序到图形目标计算出来的。

以下是一些重要的注意事项:

  • 总时间不包括 GNOME、KDE、Cinnamon 等桌面环境所花费的时间。这也是有意义的,因为桌面环境不是由 systemd 处理的,所以 systemd 工具不能计算桌面环境所花费的时间。

  • 此外,由于 systemd 的套接字方法,服务可能在总时间(14.805 秒)后仍在启动。

因此,为了提供更多的洞察力和干净的数据,systemd-analyse提供了一个blame工具。

# systemd-analyze blame
          31.202s dnf-makecache.service
          10.517s pmlogger.service
          9.264s NetworkManager-wait-online.service
          4.977s plymouth-switch-root.service
          2.994s plymouth-quit-wait.service
          1.674s systemd-udev-settle.service
          1.606s lightdm.service
          1.297s pmlogger_check.service
           938ms docker.service
           894ms dracut-initqueue.service
           599ms pmcd.service
           590ms lvm2-monitor.service
           568ms abrtd.service
           482ms firewalld.service
           461ms systemd-logind.service
           430ms lvm2-pvscan@259:3.service
           352ms initrd-switch-root.service
           307ms bolt.service
           290ms systemd-machined.service
           288ms registries.service
           282ms udisks2.service
           269ms libvirtd.service
           255ms sssd.service
           209ms systemd-udevd.service
           183ms systemd-journal-flush.service
           180ms docker-storage-setup.service
           169ms systemd-journald.service
           156ms polkit.service
           .
           .
           </snip>

blame输出很容易被误解;即,两个服务可能同时初始化,因此初始化两个服务所花费的时间比两个单独时间的总和少得多。为了获得更精确的数据,您可以使用systemd-analyseplot工具,它将生成图表并提供更多关于启动时间的细节。生成的绘图图像如图 7-4 所示。

img/493794_1_En_7_Fig4_HTML.jpg

图 7-4

生成的绘图图像

# systemd-analyze plot > plot.svg

# eog plot.svg

以下是systemd-analyse提供的一些其他工具,可以用来识别引导时间。

|

系统分析

|

描述

|
| --- | --- |
| time | 打印在内核中花费的时间 |
| blame | 打印按时间排序的运行单元列表至init |
| critical-chain [UNIT...] | 打印时间关键型单位链的树 |
| plot | 输出显示服务初始化的 SVG 图形 |
| dot [UNIT...] | 以dot(1)格式输出依赖图 |
| log-level [LEVEL] | 获取/设置管理器的日志记录阈值 |
| log-target [TARGET] | 获取/设置管理器的日志记录目标 |
| dump | 服务管理器的输出状态序列化 |
| cat-config | 显示配置文件和插件 |
| unit-files | 列出设备的文件和符号链接 |
| units-paths | 列出设备的装载目录 |
| exit-status [STATUS...] | 列出退出状态定义 |
| syscall-filter [NAME...] | 打印 seccomp 过滤器中的系统调用列表 |
| condition... | 评估条件和资产 |
| verify FILE... | 检查单元文件的正确性 |
| service-watchdogs [BOOL] | 获取/设置服务监控器状态 |
| calendar SPEC... | 验证重复的日历时间事件 |
| timestamp... | 验证时间戳 |
| timespan SPAN... | 验证时间跨度 |
| security [UNIT...] | 分析单元的安全性 |

“无法启动”问题 6 (systemd)

问题:系统成功启动,但启动时nagios服务无法启动。

以下是解决此问题的步骤:

  1. 我们需要首先隔离问题。当 GRUB 出现在屏幕上时,删除rhgb quiet内核命令行参数。

  2. 详细日志显示系统能够启动,但是nagios服务在启动时无法启动。可以看到,负责网络的 systemd 的NetworkManager服务已经成功启动。这意味着这不是网络通信问题。

    13:23:52   systemd: Starting Network Manager...
    13:23:52   systemd: Started Kernel Samepage Merging (KSM) Tuning Daemon.
    13:23:52   systemd: Started Install ABRT coredump hook.
    13:23:52   abrtd: Init complete, entering main loop
    13:23:52   systemd: Started Load CPU microcode update.
    13:23:52   systemd: Started Authorization Manager.
    13:23:53   NetworkManager[1356]: <info>  [1534389833.1078] NetworkManager is starting... (for the first time)
    13:23:53   NetworkManager[1356]: <info>  [1534389833.1079] Read config: /etc/NetworkManager/NetworkManager.conf (lib: 00-server.conf, 10-slaves-order.conf)
    13:23:53   NetworkManager[1356]: <info>  [1534389833.1924] manager[0x558b0496a0c0]: monitoring kernel firmware directory '/lib/firmware'.
    13:23:53   NetworkManager[1356]: <info>  [1534389833.2051] dns-mgr[0x558b04971150]: init: dns=default, rc-manager=file
    13:23:53   systemd: Started Network Manager.
    
    
  3. nagios服务试图在NetworkManager服务之后立即执行。这意味着nagios在其服役单位档案中一定提到过after=Network.target。但是nagios服务无法启动。

    13:24:03   nagios: Nagios 4.2.4 starting... (PID=5006)
    13:24:03   nagios: Local time is Thu  13:24:03 AEST 2018
    13:24:03   nagios: LOG VERSION: 2.0
    13:24:03   nagios: qh: Socket '/usr/local/nagios/var/rw/nagios.qh' successfully initialized
    13:24:03   nagios: qh: core query handler registered
    13:24:03   nagios: nerd: Channel hostchecks registered successfully
    13:24:03   nagios: nerd: Channel servicechecks registered successfully
    13:24:03   nagios: nerd: Channel opathchecks registered successfully
    13:24:03   nagios: nerd: Fully initialized and ready to rock!  Nagios Can't ping devices (not 100% packet loss at the end of each line)
    13:24:04   nagios: HOST ALERT:  X ;DOWN;SOFT;1;CRITICAL -  X: Host unreachable @  X. rta nan, lost 100%
    
    

解决:奇怪的是nagios错误信息说启动失败是因为无法连接网络,但是根据NetworkManager显示已经成功启动,系统已经放入网络。

这个问题显然是由 systemd 的“加速启动过程”方法造成的。要将系统放入网络,systemd 必须做大量的工作:初始化网卡、激活链接、将 IP 放在网卡上、检查是否有任何重复的 IP 可用、开始在网络上通信等。显然,要完成这一切,systemd 需要一些时间。在我的测试系统上,完全填充网络花了将近 20 秒。当然,systemd 不能在这段时间内暂停引导序列。如果 systemd 等待网络完全填充,那么 systemd 加快引导过程的创新的一个主要方面将被破坏。

NetworkManager的帮助下,systemd 将尽最大努力确保我们在网络上,但它不会等待用户指定的网络生成,也不会等到拓扑的每个规则都实现。

在某些情况下,像这种“无法启动”的问题,有可能是NetworkManager已经告诉 systemd 初始化nagios,这是依赖于network.target,但网络尚未完全启动,所以nagios可能无法联系其服务器。

  1. 为了解决这样的问题,systemd 建议启用NetworkManager-wait-online.service.这项服务会让NetworkManager一直等到网络完全恢复。一旦网络被完全填充,NetworkManager将向 systemd 发送信号,启动依赖于network.target的服务。
# cat /usr/lib/systemd/system/NetworkManager-wait-online.service
[Unit]
Description=Network Manager Wait Online
Documentation=man:nm-online(1)
Requires=NetworkManager.service
After=NetworkManager.service
Before=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/bin/nm-online -s -q --timeout=30
RemainAfterExit=yes

[Install]
WantedBy=network-online.target

这只是调用nm-online二进制文件,并将-s开关传递给它。这项服务将保持NetworkManager最多 30 秒。

这是手册页对 nm-online 的描述:

“等待 NetworkManager 启动完成,而不是专门等待网络连接。一旦 NetworkManager 激活(或试图激活)了当前网络状态下可用的每个自动激活连接,启动即被视为完成。(这通常只在启动时有用;启动完成后,不管当前网络状态如何,nm-online -s 都会立即返回。)”

img/493794_1_En_7_Fig5_HTML.jpg

图 7-5

启用网络管理器-等待-在线-服务后的图

  1. 启用NetworkManager-wait-online-service后,问题已解决,但开机时间略有减少。如图 7-5 所示,大部分开机时间已经被NetworkManager-wait-online-service吃光了,这是意料之中的。

systemd 提供了另一个工具bootchart,它基本上是一个守护进程,通过它可以对 Linux 引导过程进行性能分析。它将在启动时收集数据并制作图表。你可以认为bootchartsystemd-analyze剧情的高级版本。要使用这个工具,如图 7-6 所示,您需要将systemd-bootchart二进制文件的完整路径传递给init内核命令行参数。

img/493794_1_En_7_Fig6_HTML.jpg

图 7-6

内核命令行参数

在成功的引导过程后,如图 7-7 所示,工具将在/run/log/bootchart*.创建一个详细的图形图像。一旦图像生成,systemd-bootchart将控制权移交给 systemd,systemd 将继续引导过程。

img/493794_1_En_7_Fig7_HTML.jpg

图 7-7

引导图表

既然我们现在了解了 systemd 的基础知识,我们可以继续我们暂停的引导序列。到目前为止,我们已经到了内核已经提取 RAM 中的 initramfs 并从中启动 systemd 二进制文件的阶段。一旦 systemd 进程启动,它将遵循常规的引导顺序。

initramfs 中的系统流

systemd 将从 initramfs 启动,并遵循图 7-8 所示的引导顺序。Harald Hoyer(他创建了 dracut initramfs,并且是 systemd 的主要开发人员)创建了这个流程图,它也可以在 systemd 手册页中找到。

img/493794_1_En_7_Fig8a_HTML.jpg

img/493794_1_En_7_Fig8b_HTML.jpg

图 7-8

引导流程图

此流程图来自 dracut 的手册页。systemd 在引导过程中的最终目的是在 initramfs ( sysroot)中挂载用户的根文件系统,然后切换到其中。一旦 systemd 将switch_rooted放入新的(用户的)根文件系统,它将离开 initramfs 环境,并通过启动httpdmysql等用户空间服务继续引导过程。如果用户以图形模式启动系统,它还会绘制一个桌面/GUI。本书的范围包括引导序列,直到 systemd 挂载用户的根文件系统,然后切换到它。没有介绍switch_root之后的引导序列有几个原因。我将在此提及原因,这些原因非常重要:

  • 引导的最终目标是挂载用户的根文件系统并呈现给用户,这一点在本书中有详细介绍。

  • initramfs 之后 systemd 执行的活动很容易理解,因为 systemd 执行类似的活动,但是是在新的根文件系统环境下。

  • 生产系统通常不在图形模式下运行。

  • Linux 有几个桌面,如 GNOME、KDE、Cinnamon、Unity 等。每个用户都有自己喜欢的桌面,几乎不可能在启动时记录每个桌面的每一步。

因此,在这种理解下,本章我们将讨论到basic.target.的启动序列,请参考图 7-9 。

img/493794_1_En_7_Fig9_HTML.jpg

图 7-9

直到 basic.target 的引导序列

系统日志。socket

每个进程都必须记录它的消息。事实上,只有当进程、服务或守护进程能够在操作系统日志记录机制中记录其消息时,它才会启动。现在的 OS 日志机制是journald。因此,显然必须首先启动journald服务,但是我们知道,systemd 不会等到服务完全启动。为了加速这个过程,它使用了套接字方法。因此,systemd 必须首先启动journald套接字。journald服务创建以下四个套接字并监听消息:

  • systemd-journald.socket

  • systemd-journald-dev-log.socket

  • systemd-journald-audit.socket

  • syslog.socket

守护程序、应用程序和每个进程都将使用这些套接字来记录它们的消息。

# # vim usr/lib/systemd/system/systemd-journald.socket

#  SPDX-License-Identifier: LGPL-2.1+
#
#  This file is part of systemd.
#
#  systemd is free software; you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation; either version 2.1 of the License, or
#  (at your option) any later version.

[Unit]
Description=Journal Socket
Documentation=man:systemd-journald.service(8) man:journald.conf(5)
DefaultDependencies=no
Before=sockets.target

# Mount and swap units need this. If this socket unit is removed by an
# isolate request the mount and swap units would be removed too,
# hence let's exclude this from isolate requests.
IgnoreOnIsolate=yes

[Socket]
ListenStream=/run/systemd/journal/stdout
ListenDatagram=/run/systemd/journal/socket
SocketMode=0666
PassCredentials=yes
PassSecurity=yes
ReceiveBuffer=8M
Service=systemd-journald.service

# cat usr/lib/systemd/system/systemd-journald-dev-log.socket | grep -v '#'
[Unit]
Description=Journal Socket (/dev/log)
Documentation=man:systemd-journald.service(8) man:journald.conf(5)
DefaultDependencies=no
Before=sockets.target

IgnoreOnIsolate=yes

[Socket]
Service=systemd-journald.service
ListenDatagram=/run/systemd/journal/dev-log
Symlinks=/dev/log
SocketMode=0666
PassCredentials=yes
PassSecurity=yes

ReceiveBuffer=8M
SendBuffer=8M

我们已经讨论了套接字的工作方式,尤其是/dev/log套接字。引导序列的下一步是dracut-cmdline.service .

dracut-cmdline.service

初始化journald套接字后,systemd 通过usr/lib/systemd/system/dracut-cmdline.service收集内核命令行参数,如rootrflagsfstype变量。这也被称为 initramfs 的一个 cmdline 钩子,我们在第六章的最后提到过。可以通过将cmdline值传递给rd.break(一个 dracut 命令行参数)来调用钩子。我们将通过使用cmdline钩子来探索引导过程的这个阶段。我们需要在启动时将rd.break=cmdline dracut 命令行参数传递给内核。

在 initramfs 内部,systemd 从usr/lib/systemd/system/dracut-cmdline.service调用这个钩子。

# cat usr/lib/systemd/system/dracut-cmdline.service

#  This file is part of dracut.
#
# See dracut.bootup(7) for details

[Unit]
Description=dracut cmdline hook
Documentation=man:dracut-cmdline.service(8)
DefaultDependencies=no
Before=dracut-pre-udev.service
After=systemd-journald.socket
Wants=systemd-journald.socket
ConditionPathExists=/usr/lib/initrd-release
ConditionPathExistsGlob=|/etc/cmdline.d/*.conf
ConditionDirectoryNotEmpty=|/lib/dracut/hooks/cmdline
ConditionKernelCommandLine=|rd.break=cmdline
ConditionKernelCommandLine=|resume
ConditionKernelCommandLine=|noresume
Conflicts=shutdown.target emergency.target
[Service]
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
Type=oneshot
ExecStart=-/bin/dracut-cmdline
StandardInput=null
StandardOutput=syslog
StandardError=syslog+console
KillMode=process
RemainAfterExit=yes

# Bash ignores SIGTERM, so we send SIGHUP instead, to ensure that bash
# terminates cleanly.
KillSignal=SIGHUP

如您所见,systemd 调用了一个dracut-cmdline脚本。该脚本在 initramfs 本身中可用,它将收集内核命令行参数。

# vim bin/dracut-cmdline
 24 # Get the "root=" parameter from the kernel command line, but differentiate
 25 # between the case where it was set to the empty string and the case where it
 26 # wasn't specified at all.
 27 if ! root="$(getarg root=)"; then
 28     root_unset='UNSET'
 29 fi
 30
 31 rflags="$(getarg rootflags=)"
 32 getargbool 0 ro && rflags="${rflags},ro"
 33 getargbool 0 rw && rflags="${rflags},rw"
 34 rflags="${rflags#,}"
 35
 36 fstype="$(getarg rootfstype=)"
 37 if [ -z "$fstype" ]; then
 38     fstype="auto"
 39 fi
 40
 41 export root
 42 export rflags
 43 export fstype
 44
 45 make_trace_mem "hook cmdline" '1+:mem' '1+:iomem' '3+:slab' '4+:komem'
 46 # run scriptlets to parse the command line
 47 getarg 'rd.break=cmdline' -d 'rdbreak=cmdline' && emergency_shell -n cmdline "Break before cmdline"
 48 source_hook cmdline
 49
 50 [ -f /lib/dracut/parse-resume.sh ] && . /lib/dracut/parse-resume.sh
 51
 52 case "${root}${root_unset}" in
 53     block:LABEL=*|LABEL=*)

 54         root="${root#block:}"
 55         root="$(echo $root | sed 's,/,\\x2f,g')"
 56         root="block:/dev/disk/by-label/${root#LABEL=}"
 57         rootok=1 ;;
 58     block:UUID=*|UUID=*)
 59         root="${root#block:}"
 60         root="block:/dev/disk/by-uuid/${root#UUID=}"
 61         rootok=1 ;;
 62     block:PARTUUID=*|PARTUUID=*)
 63         root="${root#block:}"
 64         root="block:/dev/disk/by-partuuid/${root#PARTUUID=}"
 65         rootok=1 ;;
 66     block:PARTLABEL=*|PARTLABEL=*)
 67         root="${root#block:}"
 68         root="block:/dev/disk/by-partlabel/${root#PARTLABEL=}"
 69         rootok=1 ;;
 70     /dev/*)
 71         root="block:${root}"
 72         rootok=1 ;;
 73     UNSET|gpt-auto)
 74         # systemd's gpt-auto-generator handles this case.
 75         rootok=1 ;;
 76 esac
 77
 78 [ -z "${root}${root_unset}" ] && die "Empty root= argument"
 79 [ -z "$rootok" ] && die "Don't know how to handle 'root=$root'"
 80
 81 export root rflags fstype netroot NEWROOT
 82
 83 export -p > /dracut-state.sh
 84
 85 exit 0

基本上,有三个参数(内核命令行参数)将在这个钩子中导出:

  • root =用户的根文件系统名称

  • rflags =用户的根文件系统标志(rorw)

  • fstype =自动(是否自动安装)

让我们看看这些参数是如何被 initramfs 发现的(或者在 initramfs 的 cmdline 钩子中)。getarg命名函数将用于获取这三个内核命令行参数。

root="$(getarg root=)
rflags="$(getarg rootflags=)
fstype="$(getarg rootfstype=)"
.
.
export root
export rflags
export fstype

在 initramfs 的usr/lib/dracut-lib.sh文件中定义了getarg函数。

#vim usr/lib/dracut-lib.sh
 201 getarg() {
 202     debug_off
 203     local _deprecated _newoption
 204     while [ $# -gt 0 ]; do
 205         case $1 in
 206             -d) _deprecated=1; shift;;
 207             -y) if _dogetarg $2 >/dev/null; then
 208                     if [ "$_deprecated" = "1" ]; then
 209                         [ -n "$_newoption" ] && warn "Kernel command line option '$2' is deprecated, use '$_newoption' instead." || warn "Option '$2' is deprecated."
 210                     fi
 211                     echo 1
 212                     debug_on
 213                     return 0
 214                 fi
 215                 _deprecated=0
 216                 shift 2;;
 217             -n) if _dogetarg $2 >/dev/null; then

 218                     echo 0;
 219                     if [ "$_deprecated" = "1" ]; then
 220                         [ -n "$_newoption" ] && warn "Kernel command line option '$2' is deprecated, use '$_newoption=0' instead." || warn "Option '$2' is deprecated."
 221                     fi
 222                     debug_on
 223                     return 1

 224                 fi
 225                 _deprecated=0
 226                 shift 2;;
 227             *)  if [ -z "$_newoption" ]; then
 228                     _newoption="$1"
 229                 fi
 230                 if _dogetarg $1; then
 231                     if [ "$_deprecated" = "1" ]; then
 232                         [ -n "$_newoption" ] && warn "Kernel command line option '$1' is deprecated, use '$_newoption' instead." || warn "Option '$1' is deprecated."
 233                     fi
 234                     debug_on
 235                     return 0;
 236                 fi
 237                 _deprecated=0
 238                 shift;;
 239         esac
 240     done
 241     debug_on
 242     return 1
 243 }

getarg函数正在从同一个文件中调用_dogetarg函数。

 165 _dogetarg() {
 166     local _o _val _doecho
 167     unset _val
 168     unset _o
 169     unset _doecho
 170     CMDLINE=$(getcmdline)
 171
 172     for _o in $CMDLINE; do
 173         if [ "${_o%%=*}" = "${1%%=*}" ]; then
 174             if [ -n "${1#*=}" -a "${1#*=*}" != "${1}" ]; then
 175                 # if $1 has a "=<value>", we want the exact match
 176                 if [ "$_o" = "$1" ]; then
 177                     _val="1";
 178                     unset _doecho
 179                 fi
 180                 continue
 181             fi
 182
 183             if [ "${_o#*=}" = "$_o" ]; then
 184                 # if cmdline argument has no "=<value>", we assume "=1"
 185                 _val="1";
 186                 unset _doecho
 187                 continue
 188             fi
 189
 190             _val="${_o#*=}"
 191             _doecho=1
 192         fi
 193     done
 194     if [ -n "$_val" ]; then
 195         [ "x$_doecho" != "x" ] && echo "$_val";
 196         return 0;
 197     fi
 198     return 1;
 199 }

然后,_dogetarg()函数调用getcmdline命名函数,该函数从/proc/cmdline收集实际的内核命令行参数。

 137 getcmdline() {
 138     local _line
 139     local _i
 140     local CMDLINE_ETC_D
 141     local CMDLINE_ETC
 142     local CMDLINE_PROC
 143     unset _line
 144
 145     if [ -e /etc/cmdline ]; then
 146         while read -r _line || [ -n "$_line" ]; do
 147             CMDLINE_ETC="$CMDLINE_ETC $_line";
 148         done </etc/cmdline;
 149     fi
 150     for _i in /etc/cmdline.d/*.conf; do
 151         [ -e "$_i" ] || continue
 152         while read -r _line || [ -n "$_line" ]; do
 153             CMDLINE_ETC_D="$CMDLINE_ETC_D $_line";
 154         done <"$_i";
 155     done
 156     if [ -e /proc/cmdline ]; then
 157         while read -r _line || [ -n "$_line" ]; do
 158             CMDLINE_PROC="$CMDLINE_PROC $_line"
 159         done </proc/cmdline;
 160     fi
 161     CMDLINE="$CMDLINE_ETC_D $CMDLINE_ETC $CMDLINE_PROC"
 162     printf "%s" "$CMDLINE"
 163 }

以下是到目前为止的引导顺序:

  1. 引导装载程序从用户那里收集内核命令行参数,并将它们存储在自己的配置文件中(grub.cfg)。

  2. 它通过填充内核头将这些命令行参数传递给内核。

  3. 内核提取自身并复制内核头中的内核命令行参数。

  4. 内核提取内存中的 initramfs,并将其用作临时根文件系统。

  5. 在相同的过程中,内核准备虚拟文件系统,如procsysdevdevptsshm等。

  6. 内核将命令行参数存储在/proc/cmdline文件中。

  7. systemd 通过读取/proc/cmdline文件收集内核命令行参数,并将它们存储在rootrootfsfstype变量中。

我们可以通过使用cmdline钩子来验证这个程序。

回到/bin/dracut-cmdline脚本,让我们来看看:

 41 export root
 42 export rflags
 43 export fstype
 44
 45 make_trace_mem "hook cmdline" '1+:mem' '1+:iomem' '3+:slab' '4+:komem'
 46 # run scriptlets to parse the command line
 47 getarg 'rd.break=cmdline' -d 'rdbreak=cmdline' && emergency_shell -n cmdline "Break before cmdline"
 48 source_hook cmdline
 49
 50 [ -f /lib/dracut/parse-resume.sh ] && . /lib/dracut/parse-resume.sh

条件是如果用户已经在 GRUB 的内核部分传递了rd.break=cmdline参数,那么就执行emergency_shell函数。图 7-10 显示了该情况。

img/493794_1_En_7_Fig10_HTML.jpg

图 7-10

条件

如果用户已经通过了rd.break=cmdline,那么脚本调用名为emergency_shell.的函数顾名思义,它会提供调试 shell,如果调试 shell 已经成功启动,那么它调用另一个名为source_hook的函数,并向其传递cmdline参数。谁写这个代码给用户提供调试 Shell,谁就是天才程序员!

我们在这个阶段不会讨论紧急 shell 函数,因为我们需要先了解 systemd。因此,我们将在第八章中更详细地讨论它。

图 7-11 显示了dracut-cmdline.service单元工作的流程图。

img/493794_1_En_7_Fig11_HTML.jpg

图 7-11

dracut-cmdline.service 流程图

更进一步,用户的根文件系统名称可能只是/dev/sda5,但是同一个 sda5 设备可能通过uuidpartuuidlabel被引用。最后,sda5 的每隔一个引用都要到达/dev/sda5;因此,内核在/dev/disk/下为所有这些不同的设备名准备了符号链接文件。请参见图 7-12 。

img/493794_1_En_7_Fig12_HTML.jpg

图 7-12

/dev/disk 目录内容

同一个/bin/dracut-cmdline脚本将 mear sda5 根文件系统名称转换为/dev/disk/by-uuid/6588b8f1-7f37-4162-968c-8f99eacdf32e

 52 case "${root}${root_unset}" in
 53     block:LABEL=*|LABEL=*)
 54         root="${root#block:}"
 55         root="$(echo $root | sed 's,/,\\x2f,g')"
 56         root="block:/dev/disk/by-label/${root#LABEL=}"
 57         rootok=1 ;;
 58     block:UUID=*|UUID=*)
 59         root="${root#block:}"
 60         root="block:/dev/disk/by-uuid/${root#UUID=}"
 61         rootok=1 ;;
 62     block:PARTUUID=*|PARTUUID=*)
 63         root="${root#block:}"
 64         root="block:/dev/disk/by-partuuid/${root#PARTUUID=}"
 65         rootok=1 ;;
 66     block:PARTLABEL=*|PARTLABEL=*)
 67         root="${root#block:}"
 68         root="block:/dev/disk/by-partlabel/${root#PARTLABEL=}"
 69         rootok=1 ;;
 70     /dev/*)
 71         root="block:${root}"
 72         rootok=1 ;;
 73     UNSET|gpt-auto)
 74         # systemd's gpt-auto-generator handles this case.
 75         rootok=1 ;;
 76 esac
 77
 78 [ -z "${root}${root_unset}" ] && die "Empty root= argument"
 79 [ -z "$rootok" ] && die "Don't know how to handle 'root=$root'"
 80
 81 export root rflags fstype netroot NEWROOT
 82
 83 export -p > /dracut-state.sh
 84
 85 exit 0

让我们看看cmdline钩子的作用。如图 7-13 所示,在 GRUB 的内核节上传递rd.break=cmdline

img/493794_1_En_7_Fig13_HTML.jpg

图 7-13

内核命令行参数

内核将提取 initramfs,systemd 进程将启动,systemd 将初始化journald套接字,正如你在图 7-14 中看到的,systemd 将把我们放到一个命令行 shell 中,因为我们告诉 systemd 在执行dracut-cmdline钩子之前中断(钩子)引导序列。

img/493794_1_En_7_Fig14_HTML.jpg

图 7-14

命令行挂钩

目前,我们在 initramfs 内部,由于dracut-cmdline.service还没有启动,systemd 还没有从/proc/cmdline收集到内核命令行参数,如rootrsflagsfstype,我们已经在systemd-journal.socket.之后暂停(dracut hooked)了 systemd 的引导序列。请参见图 7-15 以便更好地理解。另外,/dev/disk下的符号链接还没有被 dracut 创建。

img/493794_1_En_7_Fig15_HTML.jpg

图 7-15

命令行挂钩

因为 systemd 还没有收集用户根文件系统的名称,所以毫无疑问,您不会发现用户的根文件系统挂载在 initramfs 中。sysroot是 initramfs 中的一个目录,systemd 在这里挂载用户的根文件系统。参见图 7-16 。

img/493794_1_En_7_Fig16_HTML.jpg

图 7-16

sysroot 目录

但是如果我们不向rd.break传递任何参数或者简单地退出当前的 cmdline shell,我们将被丢弃到switch_root shell。switch_root shell 是 initramfs 中 systemd 引导序列的最后阶段。在图 7-17 中,你可以看到我们正在没有任何争论的情况下通过rd.break

img/493794_1_En_7_Fig17_HTML.jpg

图 7-17

rd.break 内核命令行参数

如图 7-18 所示,在switch_root shell 中,由于dracut-cmdline.service已经执行,你会发现内核命令行参数已经被 systemd 收集。此外,用户的根文件系统已经挂载在sysroot下的 initramfs 中。

img/493794_1_En_7_Fig18_HTML.jpg

图 7-18

开关根挂钩

如果我们从这个阶段退出,switch_root ( pivot_root)将由 systemd 执行,它将离开 initramfs 环境。稍后 systemd 将执行剩余的引导程序,如图 7-19 所示,最终我们将得到桌面。

img/493794_1_En_7_Fig19_HTML.jpg

图 7-19

Fedora 的登录屏幕

回到我们到目前为止的引导顺序,我们已经到达了预udev阶段。对此可参考图 7-20 。

img/493794_1_En_7_Fig20_HTML.jpg

图 7-20

到目前为止介绍的引导顺序

dracut-预 udev.service

下一个 systemd 将处理连接的设备。为此,systemd 必须启动udev守护进程,但是在启动udev服务之前,它会检查用户是否希望在udev开始之前停止引导过程。如果用户已经传递了rd.break=pre-udev dracut命令行参数,systemd 将在执行udev守护进程之前停止引导序列。

# cat usr/lib/systemd/system/dracut-pre-udev.service | grep -v '#'

[Unit]
Description=dracut pre-udev hook
Documentation=man:dracut-pre-udev.service(8)
DefaultDependencies=no
Before=systemd-udevd.service dracut-pre-trigger.service
After=dracut-cmdline.service
Wants=dracut-cmdline.service
ConditionPathExists=/usr/lib/initrd-release
ConditionDirectoryNotEmpty=|/lib/dracut/hooks/pre-udev
ConditionKernelCommandLine=|rd.break=pre-udev
ConditionKernelCommandLine=|rd.driver.blacklist
ConditionKernelCommandLine=|rd.driver.pre
ConditionKernelCommandLine=|rd.driver.post
ConditionPathExistsGlob=|/etc/cmdline.d/*.conf
Conflicts=shutdown.target emergency.target

[Service]
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
Type=oneshot
ExecStart=-/bin/dracut-pre-udev
StandardInput=null
StandardOutput=syslog
StandardError=syslog+console
KillMode=process
RemainAfterExit=yes

KillSignal=SIGHUP

它会把我们扔在一个前贝壳上。注意afterbeforewants变量。执行dracut-pre-udev.service只是从 initramfs 中启动一个/bin/dracut-pre-udev二进制文件。在图 7-21 中,我们已经将rd.break=pre-udev作为内核命令行参数进行了传递。

img/493794_1_En_7_Fig21_HTML.jpg

图 7-21

传递 udev 之前的内核命令行参数

为了理解pre-udev钩子,你可以简单地列出/dev的内容,在图 7-22 中你会注意到没有名为 sda 的设备文件。sda 是我们的硬盘,在那里我们有我们的根文件系统。

img/493794_1_En_7_Fig22_HTML.jpg

图 7-22

前 udev 挂钩

没有sda设备文件的原因是因为udev守护进程还没有启动。守护进程将由/usr/lib/systemd/system/systemd-udevd.service单元文件启动,该文件将在pre-udev钩子之后启动。

# cat usr/lib/systemd/system/systemd-udevd.service | grep -v '#'

[Unit]
Description=udev Kernel Device Manager
Documentation=man:systemd-udevd.service(8) man:udev(7)
DefaultDependencies=no
After=systemd-sysusers.service systemd-hwdb-update.service
Before=sysinit.target
ConditionPathIsReadWrite=/sys

[Service]
Type=notify
OOMScoreAdjust=-1000
Sockets=systemd-udevd-control.socket systemd-udevd-kernel.socket
Restart=always
RestartSec=0
ExecStart=/usr/lib/systemd/systemd-udevd
KillMode=mixed
WatchdogSec=3min
TasksMax=infinity
PrivateMounts=yes
ProtectHostname=yes
MemoryDenyWriteExecute=yes
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
RestrictRealtime=yes
RestrictSUIDSGID=yes
SystemCallFilter=@system-service @module @raw-io
SystemCallErrorNumber=EPERM
SystemCallArchitectures=native
LockPersonality=yes
IPAddressDeny=any

让我们试着了解一下udev是如何工作的,它是如何在/dev下创建设备文件的。

是内核检测连接到系统的硬件;更准确地说,是内核内部编译的驱动程序或后来插入的模块将检测硬件,并用sysfs ( /sys挂载点)注册它们的对象。因为有了/sys挂载点,这些数据对用户空间和udev.这样的工具变得可用,所以是内核通过驱动程序检测硬件并在/dev中创建一个设备文件,这是一个devfs文件系统。此后,内核向udevd发送一个uevent,然后udevd改变设备文件的名称、所有者或组,或者根据这里定义的规则设置适当的权限:

     /etc/udev/rules.d,
     /lib/udev/rules.d, and
     /run/udev/rules.d

# ls etc/udev/rules.d/
     59-persistent-storage.rules  61-persistent-storage.rules

# ls lib/udev/rules.d/
     50-udev-default.rules        70-uaccess.rules    75-net-description.rules  85-nm-unmanaged.rules
     60-block.rules               71-seat.rules       80-drivers.rules          90-vconsole.rules
     60-persistent-storage.rules  73-seat-late.rules  80-net-setup-link.rules   99-systemd.rules

与用户根文件系统上的可用的udev规则相比,initramfs 的udev规则文件很少。基本上,它只有那些管理用户根文件系统设备所必需的规则。一旦udevd处于控制状态,它将基于lib/udev/rules.d/99-systemd.rules调用各自的 systemd 单元。这里有一个例子:

# cat lib/udev/rules.d/99-systemd.rules
SUBSYSTEM=="net", KERNEL!="lo", TAG+="systemd", ENV{SYSTEMD_ALIAS}+="/sys/subsystem/net/devices/$name"
SUBSYSTEM=="bluetooth", TAG+="systemd", ENV{SYSTEMD_ALIAS}+="/sys/subsystem/bluetooth/devices/%k"

SUBSYSTEM=="bluetooth", TAG+="systemd", ENV{SYSTEMD_WANTS}+="bluetooth.target", ENV{SYSTEMD_USER_WANTS}+="bluetooth.target"
ENV{ID_SMARTCARD_READER}=="?*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="smartcard.target", ENV{SYSTEMD_USER_WANTS}+="smartcard.target"
SUBSYSTEM=="sound", KERNEL=="card*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="sound.target", ENV{SYSTEMD_USER_WANTS}+="sound.target"

SUBSYSTEM=="printer", TAG+="systemd", ENV{SYSTEMD_WANTS}+="printer.target", ENV{SYSTEMD_USER_WANTS}+="printer.target"
SUBSYSTEM=="usb", KERNEL=="lp*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="printer.target", ENV{SYSTEMD_USER_WANTS}+="printer.target"
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ENV{ID_USB_INTERFACES}=="*:0701??:*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="printer.target", ENV{SYSTEMD_USER_WANTS}+="printer.target"

SUBSYSTEM=="udc", ACTION=="add", TAG+="systemd", ENV{SYSTEMD_WANTS}+="usb-gadget.target"

该规则被加上了systemd标签。这意味着无论何时检测到一个bluetooth设备,udevd将调用 systemd 的bluetooth.target.,bluetooth.target将执行/usr/libexec/bluetooth/bluetoothd二进制,这将负责剩下的bluetooth设备处理。因此,udevd操作bluetooth装置的完整顺序如下:

  1. 如果用户在启动时将蓝牙设备连接到系统,则内核或内核中编译的驱动程序或稍后插入的模块将检测蓝牙设备并将其对象注册到/sys

  2. 稍后,内核将在/dev挂载点创建一个设备文件。设备文件创建完成后,内核会发送一个ueventudevd

  3. udevd将从 initramfs 引用lib/udev/rules.d/99-systemd.rules并将调用 systemd。根据标签,systemd 应该处理剩下的部分。

  4. systemd 将执行bluetooth.target,?? 将执行bluetoothd二进制,蓝牙硬件将准备好使用。

当然,蓝牙不是启动时必需的硬件。我举这个例子只是为了便于理解。

因此,我们已经到达systemd-udev.service. systemd 将继续其引导序列,并将执行dracut-pre-trigger.service.您可以在图 7-23 中看到引导序列。

img/493794_1_En_7_Fig23_HTML.jpg

图 7-23

到目前为止介绍的引导顺序

dracut-预触发服务

如果用户传递了rd.break=pre-trigger dracut 命令行参数,systemd 的 initramfs 引导序列将被中断(挂钩)。你可以在图 7-24 中看到,我们已经将pre-trigger作为参数传递给了rd.break内核命令行参数。

img/493794_1_En_7_Fig24_HTML.jpg

图 7-24

rd.break =预触发内核命令行参数

它将把我们放在一个pre-trigger shell 上,这是在启动udevd服务之后。首先让我们看看它是如何落在一个pre-trigger壳上的。

# cat usr/lib/systemd/system/dracut-pre-trigger.service | grep -v '#'
[Unit]
Description=dracut pre-trigger hook
Documentation=man:dracut-pre-trigger.service(8)
DefaultDependencies=no
Before=systemd-udev-trigger.service dracut-initqueue.service
After=dracut-pre-udev.service systemd-udevd.service systemd-tmpfiles-setup-dev.service
Wants=dracut-pre-udev.service systemd-udevd.service
ConditionPathExists=/usr/lib/initrd-release
ConditionDirectoryNotEmpty=|/lib/dracut/hooks/pre-trigger
ConditionKernelCommandLine=|rd.break=pre-trigger
Conflicts=shutdown.target emergency.target

[Service]
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
Type=oneshot
ExecStart=-/bin/dracut-pre-trigger
StandardInput=null
StandardOutput=syslog
StandardError=syslog+console
KillMode=process
RemainAfterExit=yes

KillSignal=SIGHUP

请注意服务单元文件的AfterBeforewants部分。如果这个ConditionDirectoryNotEmpty=|/lib/dracut/hooks/pre-trigger目录存在,并且如果用户将rd.break=pre-trigger作为命令行参数传递,这个服务文件将从 initramfs 执行/bin/dracut-pre-trigger

[root@fedorab boot]# cat bin/dracut-pre-trigger
#!/usr/bin/sh

export DRACUT_SYSTEMD=1
if [ -f /dracut-state.sh ]; then
    . /dracut-state.sh 2>/dev/null
fi
type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh
source_conf /etc/conf.d
make_trace_mem "hook pre-trigger" '1:shortmem' '2+:mem' '3+:slab' '4+:komem'
source_hook pre-trigger
getarg 'rd.break=pre-trigger' 'rdbreak=pre-trigger' && emergency_shell -n pre-trigger "Break pre-trigger"
udevadm control --reload >/dev/null 2>&1 || :
export -p > /dracut-state.sh
exit 0

如您所见,它正在通过getarg函数检查传递的 dracut 命令行参数(rd.break=pre-trigger)。我们在本章前面已经看到了getarg是如何工作的。如果用户已经通过了rd.break=pre-trigger,,那么它将调用emergency_shell函数,并将pre-trigger作为参数传递给它。emergency_shell函数写在dracut-lib.sh文件中。这个函数将为我们提供pre-triggerShell。第八章介绍了提供应急 Shell 的程序。

顾名思义,正如你在图 7-25 中看到的,我们已经在udev触发之前停止了引导序列。因此,sda 磁盘在dev下还不可用。

img/493794_1_En_7_Fig25_HTML.jpg

图 7-25

预触发钩

这是因为udevadm触发器还没有被执行。服务dracut-pre-trigger.service只执行重新加载udev规则.udevadm control --reload,,如图 7-26 所示,服务systemd-udev.service已经启动,但systemd-udev-trigger服务尚未启动。

img/493794_1_En_7_Fig26_HTML.jpg

图 7-26

预触发钩

systemd-udev-trigger.service

图 7-27 显示了我们已经到达的引导阶段。

img/493794_1_En_7_Fig27_HTML.jpg

图 7-27

到目前为止的引导顺序

正如我们已经看到的,在 pre-?? 中,/dev没有被填充,因为systemd-udevd.service本身没有启动。与pre-trigger一样:/dev没有被填充,但是udevd服务已经开始。通过使用udevd守护进程提供的环境,udevd服务将创建一个环境来启动/运行各种udev工具,如udevadm.,如图 7-28 所示,在pre-trigger中,我们将能够执行udevadm,这是我们在pre-udev shell 中无法使用的。

img/493794_1_En_7_Fig28_HTML.jpg

图 7-28

预触发钩

正如您在pre-trigger开关内部看到的,sda 设备尚未创建。但是由于我们已经准备好了一个udevadm环境,我们可以通过它发现设备。如图 7-29 所示,我们将首先挂载内核配置文件系统。

img/493794_1_En_7_Fig29_HTML.jpg

图 7-29

预触发钩

pre-trigger:/ # udevadm trigger --type=subsystems --action=add

然后我们将触发udevadm来添加设备。

pre-trigger:/ # udevadm trigger --type=devices --action=add

如图 7-29 所示,sda 设备已经创建完毕。systemd 将通过systemd-udev-trigger.service发出相同的命令,这些命令将在/dev下发现并创建存储设备文件。

# cat usr/lib/systemd/system/systemd-udev-trigger.service  | grep -v ‘#’

[Unit]
Description=udev Coldplug all Devices
Documentation=man:udev(7) man:systemd-udevd.service(8)
DefaultDependencies=no
Wants=systemd-udevd.service
After=systemd-udevd-kernel.socket systemd-udevd-control.socket
Before=sysinit.target
ConditionPathIsReadWrite=/sys

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/udevadm trigger –type=subsystems –action=add
ExecStart=/usr/bin/udevadm trigger –type=devices –action=add

但是在图 7-30 中可以看到,由于udev环境缺失,相同的udevadm命令在pre-udev钩子中不会成功。

img/493794_1_En_7_Fig30_HTML.jpg

图 7-30

前 udev hook 中的 udevadm

这就是dracut-pre-trigger.servicepre-trigger挂钩的重要性。

图 7-31 中给出的流程图将帮助您理解迄今为止 systemd 在 initramfs 中采取的步骤。读完第八章后,流程图会更容易理解。我强烈推荐在看完第八章后重温这一章。

img/493794_1_En_7_Fig31_HTML.jpg

图 7-31

流程图

本地文件系统目标

如图 7-32 所示,我们已经到了启动的local-fs-target阶段。

img/493794_1_En_7_Fig32_HTML.jpg

图 7-32

到目前为止介绍的引导顺序

所以,systemd 到目前为止已经达到了local-fs.target.,systemd 一直在执行一个又一个的服务,只是因为存储设备没有准备好。由于udevadm触发成功,存储设备已被填充,现在是准备挂载点的时候了,这将由local-fs.target完成。在进入local-fs.target之前,它会确保运行local-fs.pre.target

# cat usr/lib/systemd/system/local-fs-pre.target

[Unit]
Description=Local File Systems (Pre)
Documentation=man:systemd.special(7)
RefuseManualStart=yes

#cat usr/lib/systemd/system/local-fs.target

[Unit]
Description=Local File Systems
Documentation=man:systemd.special(7)
DefaultDependencies=no
Conflicts=shutdown.target
After=local-fs-pre.target
OnFailure=emergency.target
OnFailureJobMode=replace-irreversibly

systemd-fstab-generator将由local-fs.target导航。

man page - systemd.special

systemd-fstab-generator(3) 自动将类型 Before= 的依赖项添加到所有引用该目标单元的本地挂载点的挂载单元。此外,对于 /etc/fstab 中列出的那些设置了自动挂载选项的挂载,它会将 Wants= 类型的依赖项添加到该目标单元。

将从 initramfs 中调用systemd-fstab-generator二进制文件。

# file usr/lib/systemd/system-generators/systemd-fstab-generator

usr/lib/systemd/system-generators/systemd-fstab-generator: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=e16e9d4188e2cab491f551b5f703a5caa645764b, for GNU/Linux 3.2.0, stripped

事实上,systemd 在引导序列的早期运行所有的生成器。

# ls -l usr/lib/systemd/system-generators
     total 92
     -rwxr-xr-x. 1 root root  3750 Dec 21 12:19 dracut-rootfs-generator
     -rwxr-xr-x. 1 root root 45640 Dec 21 12:19 systemd-fstab-generator
     -rwxr-xr-x. 1 root root 37032 Dec 21 12:19 systemd-gpt-auto-generator

systemd-fstab-generator就是其中之一。systemd-fstab-generator的主要任务是读取内核命令行,在/tmp目录下或者/run/systemd/generator/下创建 systemd 挂载单元文件(继续读,这个都有意义)。如你所见,它是一个二进制文件,这意味着我们需要检查 systemd 的 C 源代码,以了解它是做什么的。systemd-fstab-generator要么没有输入,要么有三个输入。

# usr/lib/systemd/system-generators/systemd-fstab-generator /dev/sda5
This program takes zero or three arguments.

当然,三个输入是根文件系统名称、文件系统类型和根文件系统标志。在写这本书的时候,systemd 的最新版本是版本 244,所以我们在这里用这个来解释。之前显示的错误信息来自src/shared/generator.h

# vim systemd-244/src/shared/generator.h
 57 /* Similar to DEFINE_MAIN_FUNCTION, but initializes logging and assigns positional arguments. */
 58 #define DEFINE_MAIN_GENERATOR_FUNCTION(impl)                            \
 59         _DEFINE_MAIN_FUNCTION(                                          \
 60                 ({                                                      \
 61                         log_setup_generator();                          \
 62                         if (argc > 1 && argc != 4)                      \
 63                                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), \
 64                                                 "This program takes zero or three arguments."); \
 65                 }),                                                     \
 66                 impl(argc > 1 ? argv[1] : "/tmp",                       \
 67                      argc > 1 ? argv[2] : "/tmp",                       \

systemd-fstab-generator二进制由src/fstab-generator/fstab-generator.c构成。

# vim systemd-244/src/fstab-generator/fstab-generator.c

868 static int run(const char *dest, const char *dest_early, const char *dest_late) {
869         int r, r2 = 0, r3 = 0;
870
871         assert_se(arg_dest = dest);
872         assert_se(arg_dest_late = dest_late);
873
874         r = proc_cmdline_parse(parse_proc_cmdline_item, NULL, 0);
875         if (r < 0)
876                 log_warning_errno(r, "Failed to parse kernel command line, ignoring: %m");
877
878         (void) determine_root();
879
880         /* Always honour root= and usr= in the kernel command line if we are in an initrd */
881         if (in_initrd()) {
882                 r = add_sysroot_mount();
883
884                 r2 = add_sysroot_usr_mount();
885
886                 r3 = add_volatile_root();
887         } else
888                 r = add_volatile_var();
889
890         /* Honour /etc/fstab only when that's enabled */
891         if (arg_fstab_enabled) {
892                 /* Parse the local /etc/fstab, possibly from the initrd */
893                 r2 = parse_fstab(false);
894
895                 /* If running in the initrd also parse the /etc/fstab from the host */
896                 if (in_initrd())
897                       r3 = parse_fstab(true);
898                 else
899                       r3 = generator_enable_remount_fs_service(arg_dest);
900         }
901
902         return r < 0 ? r : r2 < 0 ? r2 : r3;
903 }
904
905 DEFINE_MAIN_GENERATOR_FUNCTION(run);

如您所见,首先它通过函数proc_cmdline_parse解析命令行参数。

root        = root filesystem name
rootfstype  = root filesystem type
rootflags   = ro, rw or auto etc.

systemd-fstab-generator运行两次:在 initramfs 内部和在 initramfs 外部。一旦 systemd 退出 initramfs(在sysroot中挂载用户的根文件系统之后),systemd-fstab-generator将收集 usr 文件系统的命令行参数(如果它是一个单独的分区,并且它的条目在etc/fstab中可用)。

'usr' filesystem name
'usr' filesystem type
'usr' filesystem flags

为了便于理解,我们将考虑以下情况:

Inside of initramfs:   Before mounting the user's root filesystem in /sysroot
Outside of initramfs:   After mounting the user's root filesystem in /sysroot

因此,当 systemd 在 initramfs 内运行时,systemd-fstab-generator二进制文件将收集用户的根文件系统相关的命令行参数,当 systemd 在 initramfs 外运行时,它将收集 usr 文件系统相关的命令行参数。systemd 是在 initramfs 内部还是外部运行将通过in_initrd函数来检查。函数写在文件src/basic/util.c .中,检查它如何验证它是在 initramfs 环境内部还是外部很有趣。

# vim systemd-244/src/basic/util.c
 54 bool in_initrd(void) {
 55         struct statfs s;
 56         int r;
 57
 58         if (saved_in_initrd >= 0)
 59                 return saved_in_initrd;
 60
 61         /* We make two checks here:
 62          *
 63          * 1\. the flag file /etc/initrd-release must exist
 64          * 2\. the root file system must be a memory file system
 65          *
 66          * The second check is extra paranoia, since misdetecting an
 67          * initrd can have bad consequences due the initrd
 68          * emptying when transititioning to the main systemd.
 69          */
 70
 71         r = getenv_bool_secure("SYSTEMD_IN_INITRD");
 72         if (r < 0 && r != -ENXIO)
 73                 log_debug_errno(r, "Failed to parse $SYSTEMD_IN_INITRD, ignoring: %m");
 74
 75         if (r >= 0)
 76                 saved_in_initrd = r > 0;
 77         else
 78                 saved_in_initrd = access("/etc/initrd-release", F_OK) >= 0 &&
 79                                   statfs("/", &s) >= 0 &&
 80                                   is_temporary_fs(&s);
 81
 82         return saved_in_initrd;
 83 }

它检查/etc/initrd-release文件是否可用。如果这个文件不存在,这意味着我们在 initramfs 之外。这个函数然后调用statfs函数,它将提供文件系统的详细信息,如下所示:

struct statfs {
               __fsword_t f_type;    /* Type of filesystem (see below) */
               __fsword_t f_bsize;   /* Optimal transfer block size */
               fsblkcnt_t f_blocks;  /* Total data blocks in filesystem */
               fsblkcnt_t f_bfree;   /* Free blocks in filesystem */
               fsblkcnt_t f_bavail;  /* Free blocks available to
                                        unprivileged user */
               fsfilcnt_t f_files;   /* Total file nodes in filesystem */
               fsfilcnt_t f_ffree;   /* Free file nodes in filesystem */
               fsid_t     f_fsid;    /* Filesystem ID */
               __fsword_t f_namelen; /* Maximum length of filenames */
               __fsword_t f_frsize;  /* Fragment size (since Linux 2.6) */
               __fsword_t f_flags;   /* Mount flags of filesystem
                                        (since Linux 2.6.36) */
               __fsword_t f_spare[xxx];
                               /* Padding bytes reserved for future use */
           };

然后它调用is_temporary_fs()函数,这个函数写在/src/basic/stat-util.c里面。

190  bool is_temporary_fs(const struct statfs *s) {
191         return is_fs_type(s, TMPFS_MAGIC) ||
192                 is_fs_type(s, RAMFS_MAGIC);
193 }

如您所见,它检查根文件系统是否分配了 ramfs 幻数。如果是,那么我们在 initramfs 内部。在我们的例子中,我们在 initramfs 环境中,所以这个函数将返回true并从src/fstab-generator/fstab-generator.c继续前进,只创建根文件系统的-.mount ( sysroot.mount)单元文件.。如果我们在 initramfs 之外(在用用户的根文件系统挂载sysroot之后),它将为 usr 文件系统创建一个-.mount单元文件。简而言之,首先它检查我们是否在 initramfs 中。如果我们是,那么它为根文件系统创建挂载单元文件,如果我们是外部的,那么它为 usr(如果它是一个单独的文件系统)文件系统创建挂载单元文件。为了看到这一点,我们将进入switch_root(钩子)阶段,以便能够手动运行systemd-fstab-generator二进制文件。

img/493794_1_En_7_Fig34_HTML.jpg

图 7-34

sysroot.mount 文件

  1. 首先我已经删除了/tmp目录的内容。这是因为fstab生成器在/tmp.中生成挂载单元文件

  2. Run the systemd-fstab-generator binary, and as you can see in Figure 7-33, it has created a couple of files in /tmp.

    img/493794_1_En_7_Fig33_HTML.jpg

    图 7-33

    系统 d-fstab-generato0072

  3. 它创建了一个sysroot.mount单元文件。顾名思义,创建它是为了挂载用户的根文件系统。读取/proc/cmdline.已创建单元文件,参见图 7-34 查看sysroot.mount文件内容。

根文件系统将从 sda5(通过使用 UUID)挂载到sysroot目录。

img/493794_1_En_7_Fig35_HTML.jpg

图 7-35

systemd-fsck-root.service 文件内容

  1. 检查sysroot.mount单元文件的requires部分。它说在挂载根文件系统之前,必须首先执行systemd-fsck-root.service。图 7-35 为systemd-fsck-root.service档。

因此在引导时,如果您在 initramfs 中,那么systemd-fstab-generator将为用户的根文件系统生成挂载单元文件,并且相应的fsck服务文件也将被生成。

在 initramfs 引导序列的最后,systemd 将从/tmp目录中引用这些文件,将首先在根设备上执行fsck,并将根文件系统挂载到 sysroot 上(在 initramfs 内部);最终switch_root将会上演。

现在您必须明白,尽管二进制文件的名称是systemd-fstab-generator,但它并没有真正创建/etc/fstab文件。相反,它的工作是在/tmprun/systemd/generator/目录下为root(在 initramfs 内)和usr(在 initramfs 外)创建 systemd 挂载单元。这个系统只有root挂载点,所以它只为根文件系统创建了 systemd 单元文件。在 initramfs 内部,它调用add_sysroot_mount来挂载用户的根文件系统。一旦它被挂载,根文件系统 systemd 就调用add_sysroot_usr_mount函数。这些函数调用add_mount命名的函数,这又使 systemd 挂载单元文件。下面是src/fstab-generator/fstab-generator.cadd_mount函数的一个片段:

# vim systemd-244/src/fstab-generator/fstab-generator.c
341      r = unit_name_from_path(where, ".mount", &name);
342         if (r < 0)
343                 return log_error_errno(r, "Failed to generate unit name: %m");
344
345         r = generator_open_unit_file(dest, fstab_path(), name, &f);
346         if (r < 0)
347                 return r;
348
349         fprintf(f,
350                 "[Unit]\n"
351                 "SourcePath=%s\n"
352                 "Documentation=man:fstab(5) man:systemd-fstab-generator(8)\n",
353                 source);
354
355         /* All mounts under /sysroot need to happen later, at initrd-fs.target time. IOW, it's not
356          * technically part of the basic initrd filesystem itself, and so shouldn't inherit the default
357          * Before=local-fs.target dependency. */
358         if (in_initrd() && path_startswith(where, "/sysroot"))
359                 fprintf(f, "DefaultDependencies=no\n");

当前系统只有一个根分区。为了帮助您更好地理解这一点,这里我准备了一个测试系统,它有rootbootusrvaropt作为独立的文件系统:

UUID = f7ed74b5-9085-4f42-a1c4-a569f790fdad    /       ext4   defaults   1  1
UUID = 06609f65-5818-4aee-a9c5-710b76b36c68    /boot   ext4   defaults   1  2
UUID = 68fa7990-edf9-4a03-9011-21903a676322    /opt    ext4   defaults   1  2
UUID = 6fa78ab3-6c05-4a2f-9907-31be6d2a1071    /usr    ext4   defaults   1  2
UUID = 9c721a59-b62d-4d60-9988-adc8ed9e8770    /var    ext4   defaults   1  2

我们将置身于 initramfs 的pre-pivotShell 中(我们还没有讨论过)。图 7-36 显示我们已经将rd.break=pre-pivot命令行参数传递给内核。

img/493794_1_En_7_Fig36_HTML.jpg

图 7-36

内核命令行参数

如图 7-37 所示,在pre-pivot钩子中,root文件系统将与usr文件系统一起被挂载,因为pre-pivot钩子在sysroot上挂载用户的根文件系统后停止了引导序列。但是optvarboot不会被安装。

img/493794_1_En_7_Fig37_HTML.jpg

图 7-37

预枢转钩

即使运行systemd-fstab-generator,你也会发现只创建了usrroot挂载单元文件。在图 7-38 中可以看到systemd-fstab-generator输出。

img/493794_1_En_7_Fig38_HTML.jpg

图 7-38

预旋转挂钩中的 systemd-fstab-generator

这证明了在 initramfs 环境中,只有rootusr会被挂载。其余的挂载点将在 initramfs 之后或者切换到 root 之后挂载。因为var文件系统还没有安装,所以journalctl日志将从/run文件系统中维护,正如我们所知,这是一个临时文件系统。这清楚地表明,在 initramfs 环境中,您不能访问 journald 的永久日志,它们位于/var/log。请参考图 7-39 、 7-40 和 7-41 更好地理解这一点。

img/493794_1_En_7_Fig41_HTML.jpg

图 7-41

预透视挂钩中的日志行为

img/493794_1_En_7_Fig40_HTML.jpg

图 7-40

journalctl 提供的日志来自/run

img/493794_1_En_7_Fig39_HTML.jpg

图 7-39

预透视挂钩中的 journalctl 命令

你注意到一件事了吗?dracut-cmdline服务正在读取内核命令行参数,usr相关的命令行参数在/proc/cmdline中不可用。那么,systemd 是如何挂载usr文件系统的呢?另外,在生成 initramfs 时,dracut 不会复制其中的etc/fstab文件。

# lsinitrd | grep -i fstab
-rw-r--r--  1 root root       0 Jul 25 03:54 etc/fstab.empty
-rwxr-xr-x  1 root root   45640 Jul 25 03:54 usr/lib/systemd/system-generators/systemd-fstab-generator

# lsinitrd -f etc/fstab.empty
     <no_output>

那么,当 systemd 没有条目时,它如何在 initramfs 中挂载usr文件系统呢?

systemd-fstab-generatorlocal-fs.target期间运行时,它只为 root 创建挂载单元文件;然后它继续引导序列并在sysroot上挂载根文件系统。一旦挂载了根文件系统,它就从/etc/sysroot/etc/fstab中读取usr条目,并创建一个usr.mount单元文件,最后挂载它。让我们交叉验证这种理解:

  1. 放下pre-pivot挂钩。

  2. 从安装的/sysroot.中删除/etc/fstab

  3. 运行systemd-fstab-generator.

  4. 参见图 7-42 。

因为root文件系统名称将由dracut-cmdlineproc/cmdline, systemd-fstab-generator中取出,将成为sysroot.mount。但是由于sysroot中缺少fstab文件,它会将usr视为一个不可用的独立分区,并且它会跳过创建usr.mount单元文件,即使usr是一个独立的挂载点。

img/493794_1_En_7_Fig42_HTML.jpg

图 7-42

systemd-fstab-生成器行为

如果您希望在/sysroot中有类似于optvar的独立挂载点,或者您希望它们在 initramfs 环境中,该怎么办?systemd 的手册页对此有一个答案,如下所示:

x-initrd . mount

要在 initramfs 中挂载的附加文件系统。参见systemd.special(7).

中的 initrd-fs.target 描述 initrd-fs . target

systemd-fstab-generator(3)自动将 Before= 类型的依赖项添加到sysroot-usr.mount``sysroot-usr.mount中,以及在 /etc/fstab 中找到的所有具有 x-initrd.mount 而没有 noauto mount 选项的挂载点??

所以,我们需要使用/etc/fstab中的x-initrd.mount [systemd.mount]选项。例如,这里我已经通过相同的pre-pivot环境启用了 initramfs 中的var挂载点:

pre-pivot:/# vi /sysroot/etc/fstab

UUID=f7ed74b5-9085-4f42-a1c4-a569f790fdad  /      ext4  defaults   1  1
UUID=06609f65-5818-4aee-a9c5-710b76b36c68  /boot  ext4  defaults   1  2
UUID=68fa7990-edf9-4a03-9011-21903a676322  /opt   ext4  defaults   1  2
UUID=6fa78ab3-6c05-4a2f-9907-31be6d2a1071  /usr   ext4  defaults   1  2
UUID=9c721a59-b62d-4d60-9988-adc8ed9e8770  /var   ext4  defaults,x-initrd.mount   1  2

如图 7-43 所示,var挂载单元文件已经创建,但是fsck只对root文件系统可用。请参考图 7-44 中的流程图,以帮助您更好地理解这一点。

img/493794_1_En_7_Fig44_HTML.jpg

图 7-44

流程图

img/493794_1_En_7_Fig43_HTML.jpg

图 7-43

systemd-fstab-generator 的工作原理

交换目标

如图 7-45 所示,我们已经到了启动的swap.target阶段。

img/493794_1_En_7_Fig45_HTML.jpg

图 7-45

到目前为止的引导顺序

这将与local-fs.target并行执行。local-fs-.targetrootusr创建挂载点,而swap.target为交换设备创建挂载单元文件。一旦根文件系统挂载文件准备好了,就根据它挂载sysrootsystemd-fstab-generator将读取fstab,如果交换设备条目存在,它将生成swap.mount单元文件。这意味着只有在切换到用户的根文件系统(switch_rootsysroot)之后,才会创建swap.mount文件。在此阶段不会创建swap.mount

dracut-initqueue .服务

该服务创建实际的rootswapusr设备。我们用一个例子来理解这个。

通过pre-udev钩子,我们看到了类似 sda 的设备是不可用的。因为udevd服务本身还没有启动,所以两个udevadm命令都不起作用。参见图 7-46 。

img/493794_1_En_7_Fig46_HTML.jpg

图 7-46

预 udev 挂钩的工作原理

使用pre-trigger钩子,sda 设备没有创建,但是udevd服务已经启动;因此,如图 7-47 和图 7-48 所示,您可以使用类似udevadm的工具在/dev下创建sda器件,但不会在其上创建类似lvmraid的器件。这种设备也称为dm(设备映射器)设备。因此,如果根在lvm上,pre-trigger服务将不能为根创建设备文件,因此像/dev/fedora_localhost-live/这样的设备将不会被创建。

img/493794_1_En_7_Fig48_HTML.jpg

图 7-48

sda 装置已在预触发挂钩下创建

img/493794_1_En_7_Fig47_HTML.jpg

图 7-47

预触发钩

服务dracut-initqueue.service尚未启动。我们先看看单位档案到底是怎么说的。

# cat usr/lib/systemd/system/dracut-initqueue.service | grep -v '#'

[Unit]
Description=dracut initqueue hook
Documentation=man:dracut-initqueue.service(8)
DefaultDependencies=no
Before=remote-fs-pre.target
Wants=remote-fs-pre.target
After=systemd-udev-trigger.service
Wants=systemd-udev-trigger.service
ConditionPathExists=/usr/lib/initrd-release
ConditionPathExists=|/lib/dracut/need-initqueue
ConditionKernelCommandLine=|rd.break=initqueue
Conflicts=shutdown.target emergency.target

[Service]
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
Type=oneshot
ExecStart=-/bin/dracut-initqueue
StandardInput=null
StandardOutput=syslog
StandardError=syslog+console
KillMode=process
RemainAfterExit=yes
KillSignal=SIGHUP

正如你所看到的,这个服务只是启动了/bin/dracut-initqueue脚本,如果我们打开这个脚本,你会发现它实际上是在执行udevadm settle命令,其timeout值为 0。

 # vim bin/dracut-initqueue
 22 while :; do
 23
 24     check_finished && break
 25
 26     udevadm settle --exit-if-exists=$hookdir/initqueue/work
 27
 28     check_finished && break
 29
 30     if [ -f $hookdir/initqueue/work ]; then
 31         rm -f -- "$hookdir/initqueue/work"
 32     fi
 33
 34     for job in $hookdir/initqueue/*.sh; do
 35         [ -e "$job" ] || break
 36         job=$job . $job
 37         check_finished && break 2
 38     done
 39
 40     udevadm settle --timeout=0 >/dev/null 2>&1 || continue
 41
 42     for job in $hookdir/initqueue/settled/*.sh; do
 43         [ -e "$job" ] || break
 44         job=$job . $job
 45         check_finished && break 2
 46     done
 47
 48     udevadm settle --timeout=0 >/dev/null 2>&1 || continue
 49
 50     # no more udev jobs and queues empty.
 51     sleep 0.5

这将最终运行来自lib/dracut/hooks/initqueue/timeout/.lvm_scan命令。注意图 7-49 中传递的rootrd.break内核命令行参数。

img/493794_1_En_7_Fig49_HTML.jpg

图 7-49

内核命令行参数

如图 7-50 所示,lvm_scan命令被写入其中一个文件。

img/493794_1_En_7_Fig50_HTML.jpg

图 7-50

initqueue 挂钩

因此,这里我们有两个选择:或者我们可以只执行/bin/dracut-initqueue或者如图 7-51 所示,我们可以从pre-trigger钩子或者从initqueue钩子执行lvm_scan命令。

img/493794_1_En_7_Fig51_HTML.jpg

图 7-51

initqueue 挂钩中的 lvm_scan 命令

既然我们已经讨论了 initramfs 的 LVM 部分,现在是时候来看看最常见和最关键的“无法启动”问题了。

“无法启动”问题 7 (systemd + Root LVM)

问题:我们将标准根设备名称从/dev/mapper/fedora_localhost--live-root更改为/dev/mapper/root_vg-root。我们在/etc/fstab中做了适当的输入,但是重启后,系统无法启动。图 7-52 显示了屏幕上可见的内容。

img/493794_1_En_7_Fig52_HTML.jpg

图 7-52

控制台消息

由于我们现在对dracut-initqueue有了更好的理解,我们可以看到错误消息清楚地表明 systemd 无法组装root lvm设备。

  1. 让我们首先通过回忆执行的步骤来隔离问题。原root lv名如下:

  2. root volume group名称已被更改。

    # vgrename  fedora_localhost-live  root_vg
    
    The volume group Fedora_localhost-live was successfully renamed to root_vg.
    
    
  3. root lvm/etc/fstab条目已被适当更改。

#cat /etc/fstab

/dev/mapper/fedora_localhost--live-root     /        ext4  defaults 1  1
UUID=eea3d947-0618-4d8c-b083-87daf15b2679  /boot  ext4  defaults 1  2
/dev/mapper/fedora_localhost--live-swap        none   ext4  defaults 0  0

/dev/mapper/root_vg-root /            ext4    defaults   1 1
UUID=eea3d947-0618-4d8c-b083-87daf15b2679 /boot ext4  defaults  1 2
/dev/mapper/root_vg-swap none         swap    defaults      0 0

但是重启后,systemd 开始抛出dracut-initqueue timeout错误信息。

这些步骤看起来都被正确遵循了,但是我们需要进一步调查以了解为什么dracut-initqueue不能组装 LVM。

如果我们在错误屏幕上等待一段时间,如图 7-53 所示,systemd 将自动让我们进入紧急 Shell。我们将在第八章中详细了解 systemd 是如何让我们进入紧急状态的。

img/493794_1_En_7_Fig53_HTML.jpg

图 7-53

应急 Shell

如图 7-54 所示,我们将扫描当前可用的 LVs,并将挂载root vg以验证其内容。

img/493794_1_En_7_Fig54_HTML.jpg

图 7-54

激活 LVs

如您所见,root_vg(重命名为vg)可用,我们也可以激活它。这显然意味着 LVM 元数据没有损坏,LVM 设备没有任何完整性问题。如图 7-55 所示,我们将root_vg挂载到一个临时目录,并从应急 shell 本身交叉验证其fstab条目。

img/493794_1_En_7_Fig55_HTML.jpg

图 7-55

挂载根文件系统

vg是完整的,fstab条目是正确的,我们能够挂载根vg。那还缺什么?

缺少的部分是 GRUB 中没有调整内核命令行参数。见图 7-56 。

img/493794_1_En_7_Fig56_HTML.jpg

图 7-56

内核命令行参数

为了启动,我们需要中断 GRUB 闪屏,并需要更改图 7-57 中所示的内核命令行参数。

img/493794_1_En_7_Fig57_HTML.jpg

图 7-57

旧的内核命令行参数

新的见图 7-58 。

img/493794_1_En_7_Fig58_HTML.jpg

图 7-58

新的内核命令行参数

系统启动后,将/etc/default/grub更改为:

# cat /etc/default/grub
GRUB_TIMEOUT=10
GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"
GRUB_DEFAULT=saved
GRUB_DISABLE_SUBMENU=true
GRUB_TERMINAL_OUTPUT="console"
GRUB_CMDLINE_LINUX="resume=/dev/mapper/fedora_localhost--live-swap rd.lvm.lv=fedora_localhost-live/root rd.lvm.lv=fedora_localhost-live/swap console=ttyS0,115200 console=tty0"
GRUB_DISABLE_RECOVERY="true"
GRUB_ENABLE_BLSCFG=true

致以下内容:

# cat /etc/default/grub
GRUB_TIMEOUT=10
GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"
GRUB_DEFAULT=saved
GRUB_DISABLE_SUBMENU=true
GRUB_TERMINAL_OUTPUT="console"
GRUB_CMDLINE_LINUX="resume=/dev/mapper/root_vg-swap rd.lvm.lv=root_vg/root rd.lvm.lv=root_vg/swap console=ttyS0,115200 console=tty0"
GRUB_DISABLE_RECOVERY="true"
GRUB_ENABLE_BLSCFG=true

由于 Fedora 使用来自/boot/loader/entries的 BLS 条目,因此没有必要更改/etc/default/grub文件。

由此改变/boot/grub2/grubenv:

# cat /boot/grub2/grubenv
saved_entry=2058a9f13f9e489dba29c477a8ae2493-5.3.7-301.fc31.x86_64
menu_auto_hide=1
boot_success=0
kernelopts=root=/dev/mapper/fedora_localhost--live-root ro resume=/dev/mapper/fedora_localhost--live-swap rd.lvm.lv=fedora_localhost-live/root rd.lvm.lv=fedora_localhost-live/swap console=ttyS0,115200 console=tty0
boot_indeterminate=9

致以下内容:

# cat /boot/grub2/grubenv
saved_entry=2058a9f13f9e489dba29c477a8ae2493-5.3.7-301.fc31.x86_64
menu_auto_hide=1
boot_success=0
kernelopts=root=/dev/root_vg/root ro resume=/dev/mapper/root_vg-swap rd.lvm.lv=root_vg/root rd.lvm.lv=root_vg/swap console=ttyS0,115200 console=tty0
boot_indeterminate=9

这修复了“无法启动”的问题。

普利茅斯

现在是时候谈谈一个叫做plymouth的有趣服务了。早期的 Linux 会直接在控制台上显示引导信息,这对于桌面用户来说有点无聊。于是,plymouth被引入,如下图所示:

# cat usr/lib/systemd/system/plymouth-start.service
[Unit]
Description=Show Plymouth Boot Screen
DefaultDependencies=no
Wants=systemd-ask-password-plymouth.path systemd-vconsole-setup.service
After=systemd-vconsole-setup.service systemd-udev-trigger.service systemd-udevd.service
Before=systemd-ask-password-plymouth.service
ConditionKernelCommandLine=!plymouth.enable=0
ConditionVirtualization=!container

[Service]
ExecStart=/usr/sbin/plymouthd --mode=boot --pid-file=/var/run/plymouth/pid --attach-to-session
ExecStartPost=-/usr/bin/plymouth show-splash
Type=forking
KillMode=none
SendSIGKILL=no

可以看到,从/usr/lib/systemd/system/plymouth-start.service单元文件中,plymouth紧接在systemd-udev-trigger.service之后,dracut-initqueue.service之前开始,如图 7-59 所示。

img/493794_1_En_7_Fig59_HTML.jpg

图 7-59

引导序列

如图 7-60 所示,plymouth将在整个启动过程中处于活动状态。

img/493794_1_En_7_Fig60_HTML.jpg

图 7-60

普利茅斯

plymouth是一个在启动时显示动画的工具。例如,在 Fedora 中,它不显示如图 7-61 所示的控制台消息。

img/493794_1_En_7_Fig61_HTML.jpg

图 7-61

当普利茅斯不可用时

plymouth向您展示如图 7-62 所示的动画。

img/493794_1_En_7_Fig62_HTML.jpg

图 7-62

普利茅斯银幕

安装普利茅斯

如果你想安装不同主题的plymouth,那么你可以这样做:

  1. gnome-look.org 下载plymouth-theme,也可以使用以下:

  2. 将下载的主题解压到以下位置:/usr/share/plymouth/themes/

# dnf install plymouth-theme*

  1. plymouth从 initramfs 环境中运行时,您需要重新构建 initramfs。例如,必须为新的plymouth主题更新它的配置文件。
# ls -l /usr/share/plymouth/themes/
total 52
drwxr-xr-x. 2 root root 4096 Apr 26  2019 bgrt
drwxr-xr-x  3 root root 4096 Mar 30 09:15 breeze
drwxr-xr-x  2 root root 4096 Mar 30 09:15 breeze-text
drwxr-xr-x. 2 root root 4096 Mar 30 09:15 charge
drwxr-xr-x. 2 root root 4096 Apr 26  2019 details
drwxr-xr-x  2 root root 4096 Mar 30 09:15 fade-in
drwxr-xr-x  2 root root 4096 Mar 30 09:15 hot-dog
drwxr-xr-x  2 root root 4096 Mar 30 09:15 script
drwxr-xr-x  2 root root 4096 Mar 30 09:15 solar
drwxr-xr-x  2 root root 4096 Mar 30 09:15 spinfinity
drwxr-xr-x. 2 root root 4096 Apr 26  2019 spinner
drwxr-xr-x. 2 root root 4096 Apr 26  2019 text
drwxr-xr-x. 2 root root 4096 Apr 26  2019 tribar

# cat /etc/plymouth/plymouthd.conf
# Administrator customizations go in this file
#[Daemon]
#Theme=fade-in
[Daemon]
Theme=hot-dog

重启后,如图 7-63 所示,你会看到一个新的plymouth主题,名为hot-dog

img/493794_1_En_7_Fig63_HTML.jpg

图 7-63

热狗普利茅斯主题

管理普利茅斯

由于plymouth在早期启动,dracut 确实提供了一些命令行选项来管理plymouth的行为。

      plymouth.enable=0
           disable the plymouth bootsplash completely.

     rd.plymouth=0
           disable the plymouth bootsplash only for the initramfs.

之前显示的热狗图像被称为启动画面 .要查看安装/选择的启动画面,您可以使用以下内容:

#plymouth --show-splash

plymouth的另一个主要动机是在一个简单的文本文件中维护所有的引导时消息,用户可以在引导后检查。日志将被存储在/var/log/boot.log,但是记住这个文件是由plymouth维护的。这意味着只有在启动plymouth后,您才能找到启动信息。但同时,我们需要记住,plymouth确实在 initramfs 的早期阶段就开始了(就在udevd开始之后)。

# less /varlog/boot.log
<snip>
------------ Sat Jul 06 01:43:12 IST 2019 ------------
[ESC[0;32m  OK  ESC[0m] Started ESC[0;1;39mShow Plymouth Boot ScreenESC[0m.
[ESC[0;32m  OK  ESC[0m] Reached target ESC[0;1;39mPathsESC[0m.
[ESC[0;32m  OK  ESC[0m] Started ESC[0;1;39mForward Password R...s to Plymouth Directory WatchESC[0m.
[ESC[0;32m  OK  ESC[0m] Found device ESC[0;1;39m/dev/mapper/fedora_localhost--live-rootESC[0m.
[ESC[0;32m  OK  ESC[0m] Reached target ESC[0;1;39mInitrd Root DeviceESC[0m.
[ESC[0;32m  OK  ESC[0m] Found device ESC[0;1;39m/dev/mapper/fedora_localhost--live-swapESC[0m.
         Starting ESC[0;1;39mResume from hiber...fedora_localhost--live-swapESC[0m...
[ESC[0;32m  OK  ESC[0m] Started ESC[0;1;39mResume from hibern...r/fedora_localhost--live-swapESC[0m.
[ESC[0;32m  OK  ESC[0m] Reached target ESC[0;1;39mLocal File Systems (Pre)ESC[0m.
[ESC[0;32m  OK  ESC[0m] Reached target ESC[0;1;39mLocal File SystemsESC[0m.
         Starting ESC[0;1;39mCreate Volatile Files and DirectoriesESC[0m...
[ESC[0;32m  OK  ESC[0m] Started ESC[0;1;39mCreate Volatile Files and DirectoriesESC[0m.
[ESC[0;32m  OK  ESC[0m] Reached target ESC[0;1;39mSystem InitializationESC[0m.
[ESC[0;32m  OK  ESC[0m] Reached target ESC[0;1;39mBasic SystemESC[0m.
[ESC[0;32m  OK  ESC[0m] Started ESC[0;1;39mdracut initqueue hookESC[0m.
[ESC[0;32m  OK  ESC[0m] Reached target ESC[0;1;39mRemote File Systems (Pre)ESC[0m.

[ESC[0;32m  OK  ESC[0m] Reached target ESC[0;1;39mRemote File SystemsESC[0m.
         Starting ESC[0;1;39mFile System Check...fedora_localhost--live-rootESC[0m...
[ESC[0;32m  OK  ESC[0m] Started ESC[0;1;39mFile System Check ...r/fedora_localhost--live-rootESC[0m.
         Mounting ESC[0;1;39m/sysrootESC[0m...
[ESC[0;32m  OK  ESC[0m] Mounted ESC[0;1;39m/sysrootESC[0m.
[ESC[0;32m  OK  ESC[0m] Reached target ESC[0;1;39mInitrd Root File SystemESC[0m.
         Starting ESC[0;1;39mReload Configuration from the Real RootESC[0m...
[ESC[0;32m  OK  ESC[0m] Started ESC[0;1;39mReload Configuration from the Real RootESC[0m.
[ESC[0;32m  OK  ESC[0m] Reached target ESC[0;1;39mInitrd File SystemsESC[0m.
[ESC[0;32m  OK  ESC[0m] Reached target ESC[0;1;39mInitrd Default TargetESC[0m.
         Starting ESC[0;1;39mdracut pre-pivot and cleanup hookESC[0m...
[ESC[0;32m  OK  ESC[0m] Started ESC[0;1;39mdracut pre-pivot and cleanup hookESC[0m.
         Starting ESC[0;1;39mCleaning Up and Shutting Down DaemonsESC[0m...
[ESC[0;32m  OK  ESC[0m] Stopped target ESC[0;1;39mTimersESC[0m.
[ESC[0;32m  OK  ESC[0m] Stopped ESC[0;1;39mdracut pre-pivot and cleanup hookESC[0m.
[ESC[0;32m  OK  ESC[0m] Stopped target ESC[0;1;39mInitrd Default TargetESC[0m.
[ESC[0;32m  OK  ESC[0m] Stopped target ESC[0;1;39mRemote File SystemsESC[0m.
[ESC[0;32m  OK  ESC[0m] Stopped target ESC[0;1;39mRemote File Systems (Pre)ESC[0m.
[ESC[0;32m  OK  ESC[0m] Stopped ESC[0;1;39mdracut initqueue hookESC[0m.
         Starting ESC[0;1;39mPlymouth switch root serviceESC[0m...
[ESC[0;32m  OK  ESC[0m] Stopped target ESC[0;1;39mInitrd Root DeviceESC[0m.
[ESC[0;32m  OK  ESC[0m] Stopped target ESC[0;1;39mBasic SystemESC[0m.
[ESC[0;32m  OK  ESC[0m] Stopped target ESC0;1;39mSystem InitializationESC[0m.
.
.
</snip>

结构

plymouth从 initramfs/systemd 获取输入,以了解引导程序的哪个阶段已经完成(占引导程序的百分比),并相应地在屏幕上显示动画或进度条。有两个二进制文件负责plymouth的工作。

      /bin/plymouth            (Interface to plymouthd)
   /usr/sbin/plymouthd  (main binary which shows splash and logs boot messages in boot.log file)

systemd 所依赖的 initramfs 中提供了各种 plymouth 服务。

# ls -l usr/lib/systemd/system/ -l | grep -i plymouth

-rw-r--r--. 1 root root  384 Dec 21 12:19 plymouth-halt.service
-rw-r--r--. 1 root root  398 Dec 21 12:19 plymouth-kexec.service
-rw-r--r--. 1 root root  393 Dec 21 12:19 plymouth-poweroff.service
-rw-r--r--. 1 root root  198 Dec 21 12:19 plymouth-quit.service
-rw-r--r--. 1 root root  204 Dec 21 12:19 plymouth-quit-wait.service
-rw-r--r--. 1 root root  386 Dec 21 12:19 plymouth-reboot.service
-rw-r--r--. 1 root root  547 Dec 21 12:19 plymouth-start.service
-rw-r--r--. 1 root root  295 Dec 21 12:19 plymouth-switch-root.service
-rw-r--r--. 1 root root  454 Dec 21 12:19 systemd-ask-password-plymouth.path
-rw-r--r--. 1 root root  435 Dec 21 12:19 systemd-ask-password-plymouth.service
drwxr-xr-x. 2 root root 4096 Dec 21 12:19 systemd-ask-password-plymouth.service.wants

systemd 在 initramfs 中运行时,会在引导阶段不时调用这些服务。如您所见,每个服务都在调用plymouthd二进制文件,并根据当前的引导阶段传递开关。例如,plymouth-start.service简单的用模式boot.启动plymouthd二进制只有两种模式;一个是boot,另一个是shutdown.

# cat usr/lib/systemd/system/plymouth*  | grep -i execstart

ExecStart=/usr/sbin/plymouthd --mode=shutdown --attach-to-session
ExecStartPost=-/usr/bin/plymouth show-splash
ExecStart=/usr/sbin/plymouthd --mode=shutdown --attach-to-session
ExecStartPost=-/usr/bin/plymouth show-splash
ExecStart=/usr/sbin/plymouthd --mode=shutdown --attach-to-session
ExecStartPost=-/usr/bin/plymouth show-splash
ExecStart=-/usr/bin/plymouth quit                                    <<---
ExecStart=-/usr/bin/plymouth --wait
ExecStart=/usr/sbin/plymouthd --mode=reboot --attach-to-session
ExecStartPost=-/usr/bin/plymouth show-splash
ExecStart=/usr/sbin/plymouthd --mode=boot --pid-file=/var/run/plymouth/pid --attach-to-session
ExecStartPost=-/usr/bin/plymouth show-splash
ExecStart=-/usr/bin/plymouth update-root-fs --new-root-dir=/sysroot   <<---

我们可以考虑的另一个例子是,在switch_root时,systemd 简单地调用plymouth-switch-root.service,后者又运行带有更新的root文件系统的plymouthd二进制文件作为sysroot.,换句话说,你可以和switch_root一起说plymouth将其根目录从 initramfs 更改为实际的根文件系统。更进一步,您可以看到 systemd 启动plymouth服务的方式与 systemd 在引导序列结束时向plymouthd发送quit消息的方式相同。同时,您可能注意到 systemd 在重启或关机时也会调用plymouth。这其实没什么大不了的,因为它只是用适当的模式调用同一个plymouthd

Sysinit.target

所以,我们已经到了sysinit.target阶段。图 [7-64 显示了到目前为止我们已经介绍过的引导顺序。

img/493794_1_En_7_Fig64_HTML.jpg

图 7-64

到目前为止介绍的引导顺序

因为这是一个target单元,它的工作是持有或启动一堆其他单元(服务、套接字等)。).单位列表将在其wants目录中提供。正如您所看到的,可用的单元文件只是到原始服务单元文件的符号链接。

#ls -l usr/lib/systemd/system/sysinit.target.wants/

total 0
kmod-static-nodes.service -> ../kmod-static-nodes.service
plymouth-start.service -> ../plymouth-start.service
systemd-ask-password-console.path -> ../systemd-ask-password-console.path
systemd-journald.service -> ../systemd-journald.service
systemd-modules-load.service -> ../systemd-modules-load.service
systemd-sysctl.service -> ../systemd-sysctl.service
systemd-tmpfiles-setup-dev.service -> ../systemd-tmpfiles-setup-dev.service
systemd-tmpfiles-setup.service -> ../systemd-tmpfiles-setup.service
systemd-udevd.service -> ../systemd-udevd.service
systemd-udev-trigger.service -> ../systemd-udev-trigger.service

大多数服务在我们到达sysinit.target.之前就已经启动了,例如systemd-udevd.servicesystemd-udev-trigger.service(在pre-trigger服务之后)已经启动了,我们已经看到systemd -udevd.service将执行/usr/lib/systemd/systemd-udevd二进制,而systemd-udev-trigger服务将执行udevadm二进制。那我们为什么要用sysinit.target重新启动这些服务呢?我们没有。sysinit.target将仅启动尚未启动的服务,并且将忽略对已经启动的服务采取的任何操作。让我们看看这些服务单元文件的用途。

kmod-static-nodes systemd 单元文件用static-nodes开关执行kmod二进制。我们已经在第五章看到了lsmodinsmodmodinfomodprobedepmod等。,是指向kmod二进制文件的符号链接。

#lsinitrd | grep -i kmod

lrwxrwxrwx   1 root  root  11 Jul 25 03:54 usr/sbin/depmod -> ../bin/kmod
lrwxrwxrwx   1 root  root  11 Jul 25 03:54 usr/sbin/insmod -> ../bin/kmod
lrwxrwxrwx   1 root  root  11 Jul 25 03:54 usr/sbin/lsmod -> ../bin/kmod
lrwxrwxrwx   1 root  root  11 Jul 25 03:54 usr/sbin/modinfo -> ../bin/kmod
lrwxrwxrwx   1 root  root  11 Jul 25 03:54 usr/sbin/modprobe -> ../bin/kmod
lrwxrwxrwx   1 root  root  11 Jul 25 03:54 usr/sbin/rmmod -> ../bin/kmod

# cat usr/lib/systemd/system/kmod-static-nodes.service | grep -v '#'
[Unit]
Description=Create list of static device nodes for the current kernel
DefaultDependencies=no
Before=sysinit.target systemd-tmpfiles-setup-dev.service
ConditionCapability=CAP_SYS_MODULE
ConditionFileNotEmpty=/lib/modules/%v/modules.devname

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/kmod static-nodes --format=tmpfiles --output=/run/tmpfiles.d/static-nodes.conf

使用static-nodes开关,systemd 只是收集系统中存在的所有静态节点(设备)。为什么在动态节点处理(udev)的时代我们还需要静态节点?有一些像fuseALSA这样的模块需要一些存在于/dev中的设备文件,或者他们可以创建它们。但是这可能是危险的,因为设备文件是由kerneludev生成的。因此,为了避免模块创建设备文件,systemd 将通过kmod-static-nodes.service.创建类似/dev/fuse/dev/snd/seq的静态节点。以下是由kmod-static-nodes.service在 Fedora 系统上创建的静态节点:

# kmod static-nodes
Module: fuse
      Device node: /dev/fuse
            Type: character device
            Major: 10
            Minor: 229
Module: btrfs
      Device node: /dev/btrfs-control
            Type: character device
            Major: 10
            Minor: 234
Module: loop
      Device node: /dev/loop-control
            Type: character device
            Major: 10
            Minor: 237
Module: tun
      Device node: /dev/net/tun
            Type: character device
            Major: 10
            Minor: 200
Module: ppp_generic
      Device node: /dev/ppp
            Type: character device
            Major: 108
            Minor: 0
Module: uinput
      Device node: /dev/uinput

            Type: character device
            Major: 10
            Minor: 223
Module: uhid
      Device node: /dev/uhid
            Type: character device
            Major: 10
            Minor: 239
Module: vfio
      Device node: /dev/vfio/vfio
            Type: character device
            Major: 10
            Minor: 196
Module: hci_vhci
      Device node: /dev/vhci
            Type: character device
            Major: 10
            Minor: 137
Module: vhost_net
      Device node: /dev/vhost-net
            Type: character device
            Major: 10
            Minor: 238
Module: vhost_vsock
      Device node: /dev/vhost-vsock
            Type: character device
            Major: 10
            Minor: 241
Module: snd_timer
      Device node: /dev/snd/timer
            Type: character device
            Major: 116
            Minor: 33
Module: snd_seq
      Device node: /dev/snd/seq
            Type: character device
            Major: 116
            Minor: 1
Module: cuse
      Device node: /dev/cuse
            Type: character device
            Major: 10
            Minor: 203

接下来我们有plymouth服务,已经开始了;然后我们有systemd-ask-password-console.path,这是一个.path的单位档案。

# cat usr/lib/systemd/system/systemd-ask-password-console.path | grep -v '#'

[Unit]
Description=Dispatch Password Requests to Console Directory Watch
Documentation=man:systemd-ask-password-console.service(8)
DefaultDependencies=no
Conflicts=shutdown.target emergency.service
After=plymouth-start.service
Before=paths.target shutdown.target cryptsetup.target
ConditionPathExists=!/run/plymouth/pid

[Path]
DirectoryNotEmpty=/run/systemd/ask-password
MakeDirectory=yes

.path单元文件用于基于路径的激活,但是因为我们没有用 LUKS 加密我们的根磁盘,所以我们没有接受用户密码的实际服务文件。如果我们配置了 LUKS,我们就会有/usr/lib/systemd/system/systemd-ask-password-plymouth.service服务单元文件,如下所示:

# cat usr/lib/systemd/system/systemd-ask-password-plymouth.service
[Unit]
Description=Forward Password Requests to Plymouth
Documentation=http://www.freedesktop.org/wiki/Software/systemd/PasswordAgents
DefaultDependencies=no
Conflicts=shutdown.target
After=plymouth-start.service
Before=shutdown.target
ConditionKernelCommandLine=!plymouth.enable=0
ConditionVirtualization=!container
ConditionPathExists=/run/plymouth/pid

[Service]
ExecStart=/usr/bin/systemd-tty-ask-password-agent --watch --plymouth

如您所见,这是在执行systemd-tty-ask-password-agent二进制文件,它将要求输入带有plymouth而不是 TTY 的密码。接下来,服务单元文件是systemd-journald.service,它将为我们启动journald守护进程。在此之前,所有的消息都是用journald套接字记录的,systemd 将这个套接字作为引导序列的第一个服务启动。journald插座大小为 8 MB。如果套接字用完了缓冲区,那么服务将被阻塞,直到套接字变得可用。8 MB 的缓冲空间对于生产系统来说绰绰有余。

#vim usr/lib/systemd/system/sysinit.target.wants/systemd-journald.service
[Unit]
Description=Journal Service
Documentation=man:systemd-journald.service(8) man:journald.conf(5)
DefaultDependencies=no
Requires=systemd-journald.socket
After=systemd-journald.socket systemd-journald-dev-log.socket systemd-journald-audit.socket syslog.socket
Before=sysinit.target

[Service]
OOMScoreAdjust=-250
CapabilityBoundingSet=CAP_SYS_ADMIN CAP_DAC_OVERRIDE CAP_SYS_PTRACE CAP_SYSLOG CAP_AUDIT_CONTROL CAP_AUDIT_READ CAP_CHOWN CAP_DAC_READ_SEARCH CAP_FOWNER CAP_SETUID CAP_SETGID CAP_MAC_OVERRIDE
DeviceAllow=char-* rw
ExecStart=/usr/lib/systemd/systemd-journald
FileDescriptorStoreMax=4224
IPAddressDeny=any
LockPersonality=yes
MemoryDenyWriteExecute=yes
Restart=always
RestartSec=0
RestrictAddressFamilies=AF_UNIX AF_NETLINK

RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
Sockets=systemd-journald.socket systemd-journald-dev-log.socket systemd-journald-audit.socket
StandardOutput=null
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM
SystemCallFilter=@system-service
Type=notify
WatchdogSec=3min

LimitNOFILE=524288

接下来,如果你想让 systemd 静态加载某个特定的模块,那么你可以从我们的下一个服务得到一些帮助,这个服务就是systemd-modules-load.service

# cat usr/lib/systemd/system/systemd-modules-load.service | grep -v '#'

[Unit]
Description=Load Kernel Modules
Documentation=man:systemd-modules-load.service(8) man:modules-load.d(5)
DefaultDependencies=no
Conflicts=shutdown.target
Before=sysinit.target shutdown.target
ConditionCapability=CAP_SYS_MODULE
ConditionDirectoryNotEmpty=|/lib/modules-load.d
ConditionDirectoryNotEmpty=|/usr/lib/modules-load.d
ConditionDirectoryNotEmpty=|/usr/local/lib/modules-load.d
ConditionDirectoryNotEmpty=|/etc/modules-load.d
ConditionDirectoryNotEmpty=|/run/modules-load.d
ConditionKernelCommandLine=|modules-load
ConditionKernelCommandLine=|rd.modules-load

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/lib/systemd/systemd-modules-load
TimeoutSec=90s

服务执行/usr/lib/systemd/systemd-modules-load .二进制理解两个命令行参数。

  • module_load:这是一个内核命令行参数。

  • rd.module_load:这是一个 dracut 命令行参数。

如果您传递一个 dracut 命令行参数,那么systemd-modules-load将统计地将模块加载到内存中,但是为此,模块必须存在于 initramfs 中。如果它不在 initramfs 中,那么首先必须将其拉入 initramfs。在生成 initramfs 时,dracut 从这里读取<module-name>.conf文件:

/etc/modules-load.d/*.conf
/run/modules-load.d/*.conf
/usr/lib/modules-load.d/*.conf

您需要创建*.conf文件,并且需要在其中提到模块名,这是您想要添加到 initramfs 中的。

例如,这里我们创建了一个新的 initramfs 映像,其中没有vfio模块:

# dracut new.img
# lsinitrd | grep -i vfio
  <no_output>

为了在 initramfs 中以统计方式提取模块,我们在这里创建了vfio.conf文件:

# cat /usr/lib/modules-load.d/vfio.conf
  vfio

在这里,我们重建了 initramfs:

# dracut new.img -f
# lsinitrd new.img | grep -i vfio

Jul 25 03:54 usr/lib/modules/5.3.16-300.fc31.x86_64/kernel/drivers/vfio
Jul 25 03:54 usr/lib/modules/5.3.16-300.fc31.x86_64/kernel/drivers/vfio/vfio.ko.xz
Jul 25 03:54 usr/lib/modules-load.d/vfio.conf

如您所见,该模块已经被拉入 initramfs 中,一旦服务systemd-modules-load.service启动,它就会被加载到内存中。

以统计方式加载模块并不是一个好主意。如今,模块在必要或要求时被动态加载到内存中,而静态模块总是被加载到内存中,而不管需要或要求如何。

不要和/etc/modprobe.d目录混淆。它的用途是将选项传递给模块。这里有一个例子:

#cat /etc/modprobe.d/lockd.conf
     options lockd nlm_timeout=10

nlm_timeour=10是传递给lockd模块的选项。记住,/etc/modprobe.d中的.conf文件必须是一个模块名。通过同一个 conf 文件,您可以为模块名设置一个别名。这里有一个例子:

"alias my-mod really_long_modulename"

接下来,systemd 会在systemd-sysctl.service的帮助下设置sysctl内核参数。

# cat usr/lib/systemd/system/systemd-sysctl.service | grep -v '#'

[Unit]
Description=Apply Kernel Variables
Documentation=man:systemd-sysctl.service(8) man:sysctl.d(5)
DefaultDependencies=no
Conflicts=shutdown.target
After=systemd-modules-load.service
Before=sysinit.target shutdown.target
ConditionPathIsReadWrite=/proc/sys/net/

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/lib/systemd/systemd-sysctl
TimeoutSec=90s

systemd-sysctl.service将启动/usr/lib/systemd/systemd-sysctl二进制文件,它将通过从三个不同的位置读取*.conf文件来设置内核调优参数。

/etc/sysctl.d/*.conf
     /run/sysctl.d/*.conf
     /usr/lib/sysctl.d/*.conf

这里有一个例子:

# sysctl -a | grep -i swappiness
      vm.swappiness = 60

默认的swappiness内核参数值设置为 60。如果您想将其更改为 10,并且它必须在重启后保持不变,那么在/etc/sysctl.d/99-sysctl.conf中添加它。

#cat /etc/sysctl.d/99-sysctl.conf

     vm.swappiness = 10

您可以使用以下命令重新加载和设置sysctl参数:

# sysctl -p
vm.swappiness = 10

要在 initramfs 中进行这些更改,您需要重新生成 initramfs。在引导时,systemd-sysctl.service将从99-sysctl.conf文件中读取swappiness值,并将它设置在 initramfs 环境中。

systemd 为其顺利执行创建了许多临时文件。设置好sysctl参数后,它执行下一个服务systemd-tmpfiles-setup-dev.service,这个服务将执行/usr/bin/systemd-tmpfiles --prefix=/dev --create --boot二进制文件。这将根据以下规则创建dev与文件系统相关的临时文件:

/etc/tmpfiles.d/*.conf
/run/tmpfiles.d/*.conf
/usr/lib/tmpfiles.d/*.conf

sysinit.target之后,systemd 将通过sockets.target验证是否创建了所需的套接字。

# ls usr/lib/systemd/system/sockets.target.wants/ -l
total 0
32 Jan  3 18:05 systemd-journald-audit.socket -> ../systemd-journald-audit.socket
34 Jan  3 18:05 systemd-journald-dev-log.socket -> ../systemd-journald-dev-log.socket
26 Jan  3 18:05 systemd-journald.socket -> ../systemd-journald.socket
31 Jan  3 18:05 systemd-udevd-control.socket -> ../systemd-udevd-control.socket
30 Jan  3 18:05 systemd-udevd-kernel.socket -> ../systemd-udevd-kernel.socket

因此,我们的引导过程已经完成了到sysinit.target.的序列,参见图 7-65 所示的流程图。

img/493794_1_En_7_Fig65_HTML.jpg

图 7-65

到目前为止介绍的引导顺序

“无法启动”问题 8 (sysctl.conf)

问题:重启后,内核死机,系统无法启动。控制台上显示的内容如下:

[    4.596220] Mem-Info:
[    4.597455] active_anon:566 inactive_anon:1 isolated_anon:0
[    4.597455]  active_file:0 inactive_file:0 isolated_file:0
[    4.597455]  unevictable:19700 dirty:0 writeback:0 unstable:0
[    4.597455]  slab_reclaimable:2978 slab_unreclaimable:3180
[    4.597455]  mapped:2270 shmem:22 pagetables:42 bounce:0
[    4.597455]  free:23562 free_pcp:1982 free_cma:0
[    4.611930] Node 0 active_anon:2264kB inactive_anon:4kB active_file:0kB inactive_file:0kB unevictable:78800kB isolated(anon):0kB isolated(file):0kB mapped:9080kB dirty:0kB writeback:0kB shmem:88kB shmem_thp: 0kB shmem_pmdmapped: 0kB anon_thp: 0kB writeback_tmp:0kB unstable:0kB all_unreclaimable? yes
[    4.621748] Node 0 DMA free:15900kB min:216kB low:268kB high:320kB active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:0kB writepending:0kB present:15992kB managed:15908kB mlocked:0kB kernel_stack:0kB pagetables:0kB bounce:0kB free_pcp:0kB local_pcp:0kB free_cma:0kB
[    4.632561] lowmem_reserve[]: 0 1938 4764 4764 4764
[    4.634609] Node 0 DMA32 free:38516kB min:27404kB low:34252kB high:41100kB active_anon:0kB inactive_anon:0kB active_file:0kB inactive_file:0kB unevictable:0kB writepending:0kB present:2080628kB managed:2015092kB mlocked:0kB kernel_stack:0kB pagetables:0kB bounce:0kB free_pcp:2304kB local_pcp:0kB free_cma:0kB
[    4.645636] lowmem_reserve[]: 0 0 2826 2826 2826
[    4.647886] Node 0 Normal free:39832kB min:39956kB low:49944kB high:59932kB active_anon:2264kB inactive_anon:4kB active_file:0kB inactive_file:0kB unevictable:78800kB writepending:0kB present:3022848kB managed:2901924kB mlocked:0kB kernel_stack:1776kB pagetables:168kB bounce:0kB free_pcp:5624kB local_pcp:1444kB free_cma:0kB
[    4.659458] lowmem_reserve[]: 0 0 0 0 0
[    4.661319] Node 0 DMA: 1*4kB (U) 1*8kB (U) 1*16kB (U) 0*32kB 2*64kB (U) 1*128kB (U) 1*256kB (U) 0*512kB 1*1024kB (U) 1*2048kB (M) 3*4096kB (M) = 15900kB
[    4.666730] Node 0 DMA32: 1*4kB (M) 0*8kB 1*16kB (M) 1*32kB (M) 1*64kB (M) 0*128kB 0*256kB 1*512kB (M) 3*1024kB (M) 1*2048kB (M) 8*4096kB (M) = 38516kB
[    4.673247] Node 0 Normal: 69*4kB (UME) 16*8kB (M) 10*16kB (UME) 7*32kB (ME) 5*64kB (E) 1*128kB (E) 1*256kB (U) 9*512kB (ME) 9*1024kB (UME) 2*2048kB (ME) 5*4096kB (M) = 39892kB
[    4.680399] Node 0 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=1048576kB
[    4.683930] Node 0 hugepages_total=2303 hugepages_free=2303 hugepages_surp=0 hugepages_size=2048kB
[    4.687749] 19722 total pagecache pages
[    4.689841] 0 pages in swap cache
[    4.691580] Swap cache stats: add 0, delete 0, find 0/0
[    4.694275] Free swap  = 0kB
[    4.696039] Total swap = 0kB
[    4.697617] 1279867 pages RAM
[    4.699229] 0 pages HighMem/MovableOnly
[    4.700862] 46636 pages reserved
[    4.703868] 0 pages cma reserved
[    4.705589] 0 pages hwpoisoned
[    4.707435] Tasks state (memory values in pages):
[    4.709532] [  pid  ]   uid  tgid total_vm      rss pgtables_bytes swapents oom_score_adj name
[    4.713849] [    341]     0   341     5118     1178    77824        0         -1000 (md-udevd)
[    4.717805] Out of memory and no killable processes...
[    4.719861] Kernel panic - not syncing: System is deadlocked on memory
[    4.721926] CPU: 3 PID: 1 Comm: systemd Not tainted 5.3.7-301.fc31.x86_64 #1
[    4.724343] Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.12.0-2.fc30 04/01/2014
[    4.727959] Call Trace:
[    4.729204]  dump_stack+0x5c/0x80
[    4.730707]  panic+0x101/0x2d7
[    4.747357]  out_of_memory.cold+0x2f/0x88
[    4.749172]  __alloc_pages_slowpath+0xb09/0xe00
[    4.750890]  __alloc_pages_nodemask+0x2ee/0x340
[    4.752452]  alloc_slab_page+0x19f/0x320
[    4.753982]  new_slab+0x44f/0x4d0
[    4.755317]  ? alloc_slab_page+0x194/0x320
[    4.757016]  ___slab_alloc+0x507/0x6a0
[    4.758768]  ? copy_verifier_state+0x1f7/0x270
[    4.760591]  ? ___slab_alloc+0x507/0x6a0
[    4.763266]  __slab_alloc+0x1c/0x30
[    4.764846]  kmem_cache_alloc_trace+0x1ee/0x220
[    4.766418]  ? copy_verifier_state+0x1f7/0x270
[    4.768120]  copy_verifier_state+0x1f7/0x270
[    4.769604]  ? kmem_cache_alloc_trace+0x162/0x220
[    4.771098]  ? push_stack+0x35/0xe0
[    4.772367]  push_stack+0x66/0xe0
[    4.774010]  check_cond_jmp_op+0x1fe/0xe60
[    4.775644]  ? _cond_resched+0x15/0x30
[    4.777524]  ? _cond_resched+0x15/0x30
[    4.779315]  ? kmem_cache_alloc_trace+0x162/0x220
[    4.780916]  ? copy_verifier_state+0x1f7/0x270
[    4.782357]  ? copy_verifier_state+0x16f/0x270
[    4.783785]  do_check+0x1c06/0x24e0
[    4.785218]  bpf_check+0x1aec/0x24d4
[    4.786613]  ? _cond_resched+0x15/0x30
[    4.788073]  ? kmem_cache_alloc_trace+0x162/0x220
[    4.789672]  ? selinux_bpf_prog_alloc+0x1f/0x60
[    4.791564]  bpf_prog_load+0x3a3/0x670
[    4.794915]  ? seq_vprintf+0x30/0x50
[    4.797085]  ? seq_printf+0x53/0x70
[    4.799013]  __do_sys_bpf+0x7e5/0x17d0
[    4.800909]  ? __fput+0x168/0x250
[    4.802352]  do_syscall_64+0x5f/0x1a0
[    4.803826]  entry_SYSCALL_64_after_hwframe+0x44/0xa9
[    4.805587] RIP: 0033:0x7f471557915d
[    4.807638] Code: 00 c3 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 8b 0d fb 5c 0c 00 f7 d8 64 89 01 48
[    4.814732] RSP: 002b:00007fffd36da028 EFLAGS: 00000246 ORIG_RAX: 0000000000000141
[    4.818390] RAX: ffffffffffffffda RBX: 000055fb6ad3add0 RCX: 00007f471557915d
[    4.820448] RDX: 0000000000000070 RSI: 00007fffd36da030 RDI: 0000000000000005
[    4.822536] RBP: 0000000000000002 R08: 0070756f7267632f R09: 000001130000000f
[    4.826605] R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
[    4.829312] R13: 0000000000000006 R14: 000055fb6ad3add0 R15: 00007fffd36da1e0
[    4.831792] Kernel Offset: 0x26000000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbfffffff)
[    4.835316] ---[ end Kernel panic - not syncing: System is deadlocked on memory ]---

所以,这是一个“内核恐慌”问题。我们需要首先隔离问题,因为内核崩溃可能是由成千上万种情况引起的。如果您查看突出显示的内核崩溃消息,很明显,由于系统内存不足,已经调用了“OOM-killer”。内核试图从缓存中释放内存,甚至试图使用交换空间,但最终放弃了,内核慌了。

所以,我们已经隔离了这个问题。我们需要专注于谁在吞噬记忆。当系统有巨大的内存压力时,操作系统内存不足(OOM)机制将被调用。

在三种情况下,OOM-killer 会在引导过程中被调用:

  • 系统安装的物理内存非常低。

  • 设置了错误的内核调整参数。

  • 一些模块有内存泄漏。

这个系统有 4.9 GB 的物理内存,不算大,但是对于 Linux 内核完成引导序列来说肯定绰绰有余。

一些模块可能有内存泄漏,但是识别这一点将是一项困难的任务。因此,我们将首先验证是否有任何与内存相关的内核调优参数设置不正确。

img/493794_1_En_7_Fig67_HTML.jpg

图 7-67

禁用 hugepage 设置

img/493794_1_En_7_Fig66_HTML.jpg

图 7-66

内核命令行参数

  1. 为此,我们将把自己放在 initramfs 中。在图 7-66 中,我们已经将rd.break作为内核命令行参数进行了传递。

  2. 我们将在读写模式下重新挂载sysroot,并验证sysctl参数。

    switch_root:/# cat /proc/sys/vm/nr_hugepages
                   2400
    
    
  3. 问题是错误地保留了大量页面。我们将根据图 7-67 禁用该设置。

重新启动后,系统能够成功引导。让我们试着理解哪里出了问题。这个系统有 4.9 GB 的内存,早期没有保留大页面。

# cat /proc/meminfo | grep -e MemTotal -e HugePages_Total

MemTotal:        4932916 kB
HugePages_Total:       0

# cat /proc/sys/vm/nr_hugepages
0

普通页面的大小是 4 KB,而大页面的大小是 2 MB,是普通页面的 512 倍。大型网页有它自己的优点,但同时也有它自己的缺点。

  • 一个大页面不能被换出。

  • 内核不使用大页面。

  • 只有支持大页面的应用程序才能使用大页面。

有人错误地设置了 2400 个页面,并重新构建了 initramfs。

# echo "vm.nr_hugepages = 2400" >> /etc/sysctl.conf

     # sysctl -p
           vm.nr_hugepages = 2400

     # dracut /boot/new.img
     # reboot

因此,2,400 个 hugepages = 4.9 GB,这是所有安装的主内存,由于总内存被保留在 hugepages 中,内核无法使用它。因此,在引导时,当 systemd 到达阶段sysinit.target并执行systemd-sysctl.service时,服务从 initramfs 中读取sysctl.conf文件,并保留 4.9 GB 的 hugepages,这是内核无法使用的。所以内核本身就内存不足,系统就慌了。

基本目标

所以,我们到了basic.target。正如我们所知,目标是为了同步或分组单元。basic.target是后期引导服务的同步点。

# cat usr/lib/systemd/system/basic.target | grep -v '#'
[Unit]
Description=Basic System
Documentation=man:systemd.special(7)
Requires=sysinit.target
Wants=sockets.target timers.target paths.target slices.target
After=sysinit.target sockets.target paths.target slices.target tmp.mount

RequiresMountsFor=/var /var/tmp
Wants=tmp.mount

因此,当所有早期服务的单元文件requireswantsafter阶段成功启动时,basic.target将会成功。事实上,几乎所有的军种都在其单位档案中添加了After=basic.target

dracut-预安装服务

systemd 将在 initramfs 中挂载用户的根文件系统之前执行dracut-pre-mount.service服务。因为它是一个 dracut 服务,所以只有当用户传递了rd.break=pre-mount dracut 命令行参数时,它才会执行。图 7-68 显示我们已经将rd.break=pre-mount作为内核命令行参数传递。

img/493794_1_En_7_Fig68_HTML.jpg

图 7-68

内核命令行参数

正如你在图 7-69 中看到的,它把我们放在了紧急 shell 中,并且用户的根文件系统没有挂载在sysroot.是的,我说它把我们放在了紧急 shell 中,但是你会惊讶地看到,紧急 shell 只不过是 systemd 提供的一个简单的 bash shell,但是在引导还没有完成的时候。为了更好地理解紧急 shell,我们将暂停我们的引导序列一会儿,并在第八章中讨论 initramfs 的调试 shell。我们将在第九章继续暂停的 systemd 引导序列。

img/493794_1_En_7_Fig69_HTML.jpg

图 7-69

预安装钩

八、调试 Shell

到目前为止,我们知道 initramfs 内置了 bash,并且我们不时地通过rd.break钩子来使用它。本章的目的是理解 systemd 如何在 initramfs 中为我们提供一个 shell。必须遵循的步骤是什么,如何更有效地使用它?但是在此之前,让我们回顾一下到目前为止我们所学到的关于 initramfs 的调试和紧急 shells 的知识。

贝壳

rd.break
           drop to a shell at the end

rd.break将我们放入 initramfs 中,我们可以通过它探索 initramfs 环境。这个 initramfs 环境也被称为紧急模式。在正常情况下,当 initramfs 无法挂载用户的根文件系统时,我们会在紧急模式下掉线。请记住,在将用户的根文件系统挂载到/sysroot下之后,但在对其执行switch_root之前,不带任何参数地传递rd.break会将我们放到 initramfs。你总能在/run/initramfs/rdsosreport.txt文件中找到详细的日志。图 8-1 显示了来自rdsosreport.txt的日志。

img/493794_1_En_8_Fig1_HTML.jpg

图 8-1

rdsosreport.txt 运行时日志

在日志消息中,您可以清楚地看到它就在执行pivot_root之前被丢弃。pivot_rootswitch_root将在第九章讨论,而chroot将在第十章讨论。一旦退出紧急 shell,systemd 将继续暂停的引导序列,并最终提供登录屏幕。

然后我们讨论了如何使用紧急 shells 来修复一些“无法启动”的问题。例如,initramfs 与用户的根文件系统一样好。因此,它确实有lvmraid和与文件系统相关的二进制文件,我们可以用它们来查找、组装、诊断和修复丢失的用户根文件系统。然后我们讨论了如何将它安装在/sysroot下,并探索它的内容,例如修复grub.cfg的错误条目。

同样,rd.break也为我们提供了不同的选项来打破不同阶段的引导顺序。

  • cmdline:这个钩子获取内核命令行参数。

  • pre-udev:这打破了udev处理程序之前的引导顺序。

  • pre-trigger:可以用udevadm控件设置udev环境变量,也可以用udevadm控件设置--property=KEY=value类参数或控制udev的进一步执行。

  • pre-mount:这在/sysroot挂载用户的根文件系统之前中断了引导序列。

  • mount:这打破了在/sysroot挂载根文件系统后的引导顺序。

  • pre-pivot:这在切换到实际的根文件系统之前中断了引导序列。

现在让我们看看 systemd 是如何在这些不同的阶段为我们提供 shells 的。

systemd 如何让我们进入紧急状态?

让我们考虑一个pre-mount钩子的例子。来自 initramfs 的 systemd 从dracut-cmdline.service收集rd.break=pre-mount命令行参数,并从 initramfs 位置/usr/lib/systemd/system.运行 systemd 服务dracut-pre-mount.service,该服务将在运行initrd-root-fs.targetsysroot.mountsystemd-fsck-root.service之前运行。

# cat usr/lib/systemd/system/dracut-pre-mount.service | grep -v #'

[Unit]
Description=dracut pre-mount hook
Documentation=man:dracut-pre-mount.service(8)
DefaultDependencies=no
Before=initrd-root-fs.target sysroot.mount systemd-fsck-root.service
After=dracut-initqueue.service cryptsetup.target
ConditionPathExists=/usr/lib/initrd-release
ConditionDirectoryNotEmpty=|/lib/dracut/hooks/pre-mount
ConditionKernelCommandLine=|rd.break=pre-mount
Conflicts=shutdown.target emergency.target

[Service]
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
Type=oneshot
ExecStart=-/bin/dracut-pre-mount
StandardInput=null
StandardOutput=syslog
StandardError=syslog+console
KillMode=process
RemainAfterExit=yes

KillSignal=SIGHUP

如您所见,它只是从 initramfs 执行了/bin/dracut-pre-mount脚本。

# vim bin/dracut-pre-mount
  1 #!/usr/bin/sh
  2
  3 export DRACUT_SYSTEMD=1
  4 if [ -f /dracut-state.sh ]; then
  5     . /dracut-state.sh 2>/dev/null
  6 fi
  7 type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh
  8
  9 source_conf /etc/conf.d
 10
 11 make_trace_mem "hook pre-mount" '1:shortmem' '2+:mem' '3+:slab' '4+:komem'
 12 # pre pivot scripts are sourced just before we doing cleanup and switch over
 13 # to the new root.
 14 getarg 'rd.break=pre-mount' 'rdbreak=pre-mount' && emergency_shell -n pre-mount "Break pre-mount"
 15 source_hook pre-mount
 16
 17 export -p > /dracut-state.sh
 18
 19 exit 0

/bin/dracut-pre-mount脚本中,最重要的一行如下:

getarg rd.break=pre-mount' rdbreak=pre-mount
     && emergency_shell -n pre-mount "Break pre-mount"

我们已经讨论过了getarg函数,它用于检查什么参数被传递给了rd.break=。如果已经通过了rd.break=pre-mount,那么只调用emergency-shell()函数。该函数在/usr/lib/dracut-lib.sh中定义,并将pre-mount作为字符串参数传递给它。-n代表以下内容:

  • [ -n STRING ] or [ STRING ]:如果STRING的长度不为零,则为真

emergency_shell函数接受_rdshell_name变量的值作为pre-mount.

if [ "$1" = "-n" ]; then
      _rdshell_name=$2

这里,-n被认为是第一个自变量($1),而pre-mount是第二个自变量($2)。所以,_rdshell_name的值变成了pre-mount

#vim /usr/lib/dracut-lib.sh
1123 emergency_shell()
1124 {
1125     local _ctty
1126     set +e
1127     local _rdshell_name="dracut" action="Boot" hook="emergency"
1128     local _emergency_action
1129
1130     if [ "$1" = "-n" ]; then
1131         _rdshell_name=$2
1132         shift 2
1133     elif [ "$1" = "--shutdown" ]; then
1134         _rdshell_name=$2; action="Shutdown"; hook="shutdown-emergency"
1135         if type plymouth >/dev/null 2>&1; then
1136             plymouth --hide-splash
1137         elif [ -x /oldroot/bin/plymouth ]; then
1138             /oldroot/bin/plymouth --hide-splash
1139         fi
1140         shift 2
1141     fi
1142
1143     echo ; echo
1144     warn "$*"
1145     echo
1146
1147     _emergency_action=$(getarg rd.emergency)
1148     [ -z "$_emergency_action" ] \
1149         && [ -e /run/initramfs/.die ] \
1150         && _emergency_action=halt
1151
1152     if getargbool 1 rd.shell -d -y rdshell || getarg rd.break -d rdbreak; then
1153         _emergency_shell $_rdshell_name
1154     else
1155         source_hook "$hook"
1156         warn "$action has failed. To debug this issue add \"rd.shell rd.debug\" to the kernel command line."
1157         [ -z "$_emergency_action" ] && _emergency_action=halt
1158     fi
1159
1160     case "$_emergency_action" in
1161         reboot)
1162             reboot || exit 1;;
1163         poweroff)
1164             poweroff || exit 1;;
1165         halt)
1166             halt || exit 1;;
1167     esac
1168 }

然后,在最后,它从同一个文件中调用另一个_emergency_shell函数(注意函数名前的下划线)。如您所见,_rdshell_name_emergency_shell函数的参数。

_emergency_shell $_rdshell_name

_emergency_shell()函数内部,我们可以看到_name得到参数,也就是pre-mount

local _name="$1"

#vim usr/lib/dracut-lib.sh
1081 _emergency_shell()
1082 {
1083     local _name="$1"
1084     if [ -n "$DRACUT_SYSTEMD" ]; then
1085         > /.console_lock
1086         echo "PS1=\"$_name:\\\${PWD}# \"" >/etc/profile
1087         systemctl start dracut-emergency.service
1088         rm -f -- /etc/profile
1089         rm -f -- /.console_lock
1090     else
1091         debug_off
1092         source_hook "$hook"
1093         echo
1094         /sbin/rdsosreport
1095         echo 'You might want to save "/run/initramfs/rdsosreport.txt" to a USB stick or /boot'
1096         echo 'after mounting them and attach it to a bug report.'
1097         if ! RD_DEBUG= getargbool 0 rd.debug -d -y rdinitdebug -d -y rdnetdebug; then
1098             echo
1099             echo 'To get more debug information in the report,'
1100             echo 'reboot with "rd.debug" added to the kernel command line.'
1101         fi
1102         echo
1103         echo 'Dropping to debug shell.'
1104         echo
1105         export PS1="$_name:\${PWD}# "
1106         [ -e /.profile ] || >/.profile
1107
1108         _ctty="$(RD_DEBUG= getarg rd.ctty=)" && _ctty="/dev/${_ctty##*/}"
1109         if [ -z "$_ctty" ]; then
1110             _ctty=console
1111             while [ -f /sys/class/tty/$_ctty/active ]; do
1112                 _ctty=$(cat /sys/class/tty/$_ctty/active)
1113                 _ctty=${_ctty##* } # last one in the list
1114             done
1115             _ctty=/dev/$_ctty
1116         fi
1117         [ -c "$_ctty" ] || _ctty=/dev/tty1
1118         case "$(/usr/bin/setsid --help 2>&1)" in *--ctty*) CTTY="--ctty";; esac
1119         setsid $CTTY /bin/sh -i -l 0<>$_ctty 1<>$_ctty 2<>$_ctty
1120     fi

相同的pre-mount字符串已被传递给PS1。让我们先看看PS1到底是什么。

PS1称为一个变量。当用户成功登录时,bash 会显示出来。这里有一个例子:

[root@fedora home]#
  |  |   |    |
[username]@[host][CWD][# since it is a root user]

bash 接受的理想条目是PS1='\u:\w\$'

  • 这是用户名。

  • 这是工作目录。

  • **\(** =如果 UID 为 0,则`#`;否则`\)'`。

所以,在我们的例子中,当我们得到一个紧急 shell 时,PS1将被 shell 打印如下:

'pre-mount#'

接下来在源代码中,您可以看到PS1变量的新值也被添加到了/etc/profile.中,原因是 bash 每次在将 shell 呈现给用户之前都会读取这个文件。最后,我们简单地启动了dracut-emergency服务。

systemctl start dracut-emergency.service

以下是 initramfs 的usr/lib/systemd/system/中的dracut-emergency.service文件:

# cat usr/lib/systemd/system/dracut-emergency.service | grep -v #'

[Unit]
Description=Dracut Emergency Shell
DefaultDependencies=no
After=systemd-vconsole-setup.service
Wants=systemd-vconsole-setup.service
Conflicts=shutdown.target emergency.target

[Service]
Environment=HOME=/
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
WorkingDirectory=/
ExecStart=-/bin/dracut-emergency
ExecStopPost=-/bin/rm -f -- /.console_lock
Type=oneshot
StandardInput=tty-force
StandardOutput=inherit
StandardError=inherit
KillMode=process
IgnoreSIGPIPE=no
TasksMax=infinity

KillSignal=SIGHUP

服务只是简单地执行/bin/dracut-emergency。这个脚本首先停止plymouth服务。

type plymouth >/dev/null 2>&1 && plymouth quit

这会将hook变量的值存储为emergency,并使用emergency参数调用source_hook函数。

export _rdshell_name="dracut" action="Boot" hook="emergency"
source_hook "$hook"

# vim bin/dracut-emergency
     1 #!/usr/bin/sh
  2
  3 export DRACUT_SYSTEMD=1
  4 if [ -f /dracut-state.sh ]; then
  5     . /dracut-state.sh 2>/dev/null
  6 fi
  7 type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh
  8
  9 source_conf /etc/conf.d
 10
 11 type plymouth >/dev/null 2>&1 && plymouth quit
 12
 13 export _rdshell_name="dracut" action="Boot" hook="emergency"
 14 _emergency_action=$(getarg rd.emergency)
 15
 16 if getargbool 1 rd.shell -d -y rdshell || getarg rd.break -d rdbreak; then
 17     FSTXT="/run/dracut/fsck/fsck_help_$fstype.txt"
 18     source_hook "$hook"
 19     echo
 20     rdsosreport
 21     echo
 22     echo
 23     echo Entering emergency mode. Exit the shell to continue.'
 24     echo Type "journalctl" to view system logs.'
 25     echo You might want to save "/run/initramfs/rdsosreport.txt" to a USB stick or /boot'
 26     echo after mounting them and attach it to a bug report.'
 27     echo
 28     echo
 29     [ -f "$FSTXT" ] && cat "$FSTXT"
 30     [ -f /etc/profile ] && . /etc/profile
 31     [ -z "$PS1" ] && export PS1="$_name:\${PWD}# "
 32     exec sh -i -l
 33 else
 34     export hook="shutdown-emergency"
 35     warn "$action has failed. To debug this issue add \"rd.shell rd.debug\" to the kernel command line."
 36     source_hook "$hook"
 37     [ -z "$_emergency_action" ] && _emergency_action=halt
 38 fi
 39
 40 /bin/rm -f -- /.console_lock
 41
 42 case "$_emergency_action" in
 43     reboot)
 44         reboot || exit 1;;
 45     poweroff)
 46         poweroff || exit 1;;
 47     halt)
 48         halt || exit 1;;
 49 esac
 50
 51 exit 0

usr/lib/dracut-lib.sh中再次定义了source_hook功能。

source_hook() {
    local _dir
    _dir=$1; shift
    source_all "/lib/dracut/hooks/$_dir" "$@"
}

_dir变量已经捕获了钩子名称,即emergency。所有的钩子都只是一堆脚本,从 initramfs 的/lib/dracut/hooks/目录中存储和执行。

# tree usr/lib/dracut/hooks/
usr/lib/dracut/hooks/
├── cleanup
├── cmdline
│   ├── 30-parse-lvm.sh
│   ├── 91-dhcp-root.sh
│   └── 99-nm-config.sh
├── emergency
│   └── 50-plymouth-emergency.sh
├── initqueue
│   ├── finished
│   ├── online
│   ├── settled
│   │   └── 99-nm-run.sh
│   └── timeout
│       └── 99-rootfallback.sh
├── mount
├── netroot
├── pre-mount
├── pre-pivot
│   └── 85-write-ifcfg.sh
├── pre-shutdown
├── pre-trigger
├── pre-udev
│   └── 50-ifname-genrules.sh
├── shutdown
│   └── 25-dm-shutdown.sh
└── shutdown-emergency

对于紧急钩子,它正在执行usr/lib/dracut/hooks/emergency/50-plymouth-emergency.sh,这正在停止plymouth服务。

#!/usr/bin/sh
plymouth --hide-splash 2>/dev/null || :

一旦emergency挂钩被执行并且plymouth被停止,它将返回到bin/dracut-emergency并打印以下横幅:

echo Entering emergency mode. Exit the shell to continue.'
echo Type "journalctl" to view system logs.'
echo You might want to save "/run/initramfs/rdsosreport.txt" to a USB stick or /boot'
echo after mounting them and attach it to a bug report.'

因此,rd.break=hook_name用户通过了什么并不重要。systemd 将执行emergency钩子,一旦横幅被打印出来,它将获取我们已经添加了PS1=_rdshell_name / PS1=hook_name/etc/profile目录,然后我们就可以简单地运行 bash shell 了。

exec sh -i –l

当 shell 开始运行时,它会读取/etc/profile,并找到PS1=hook_name变量。在这里,hook_name就是pre-mount。这就是为什么pre-mount作为 bash 的提示名被印了出来。请参考图 8-2 所示的流程图,以便更好地理解这一点。

img/493794_1_En_8_Fig2_HTML.jpg

图 8-2

流程图

如果用户向rd.break传递任何其他参数,例如initqueue,那么它将被馈入PS1_rdshell_name和钩子变量。稍后,bash 将通过紧急服务被调用。Bash 将从/etc/profile文件中读取PS1值,并在提示中显示initqueue名称。

结论是,相同的 bash shell 会以不同的提示名称(cmdlinepre-mountswitch_rootpre-udevemergency等)提供给用户。)但是在 initramfs 的不同引导阶段。

cmdline:/# pre-udev:/#
pre-trigger:/# initqueue:/#
pre-mount:/# pre-pivot:/#
switch_root:/#

与此类似,rescue.target将由 systemd 执行。

救援服务和紧急服务

救援服务在 systemd 世界中也被称为单用户模式。因此,如果用户请求以单用户模式引导,那么 systemd 实际上会在rescue.service阶段将用户放在紧急 shell 中。图 8-3 显示了到目前为止的引导顺序。

img/493794_1_En_8_Fig3_HTML.jpg

图 8-3

引导序列的流程图

你可以通过rescue.target或者通过runlevel1.target或者emergency.servicesystemd.unit以单用户模式引导。如图 8-4 所示,这次我们将使用 Ubuntu 来探索引导阶段。

img/493794_1_En_8_Fig4_HTML.jpg

图 8-4

内核命令行参数

这会让我们陷入紧急状态。单用户模式、救援服务和紧急服务都启动dracut-emergency二进制。这是我们在 dracut 的紧急挂钩中发布的相同二进制文件。

# cat usr/lib/systemd/system/emergency.service | grep -v ' #'

[Unit]
Description=Emergency Shell
DefaultDependencies=no
After=systemd-vconsole-setup.service
Wants=systemd-vconsole-setup.service
Conflicts=shutdown.target
Before=shutdown.target

[Service]
Environment=HOME=/
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
WorkingDirectory=/
ExecStart=/bin/dracut-emergency
ExecStopPost=-/usr/bin/systemctl --fail --no-block default
Type=idle
StandardInput=tty-force
StandardOutput=inherit
StandardError=inherit
KillMode=process
IgnoreSIGPIPE=no
TasksMax=infinity

KillSignal=SIGHUP

# cat usr/lib/systemd/system/rescue.service | grep -v ' #'

[Unit]
Description=Emergency Shell
DefaultDependencies=no
After=systemd-vconsole-setup.service
Wants=systemd-vconsole-setup.service
Conflicts=shutdown.target
Before=shutdown.target

[Service]
Environment=HOME=/
Environment=DRACUT_SYSTEMD=1

Environment=NEWROOT=/sysroot
WorkingDirectory=/
ExecStart=/bin/dracut-emergency
ExecStopPost=-/usr/bin/systemctl --fail --no-block default
Type=idle
StandardInput=tty-force
StandardOutput=inherit
StandardError=inherit
KillMode=process
IgnoreSIGPIPE=no
TasksMax=infinity

KillSignal=SIGHUP

众所周知,dracut-emergency脚本执行一个 bash shell。

# vim bin/dracut-emergency
  1 #!/usr/bin/sh
  2
  3 export DRACUT_SYSTEMD=1
  4 if [ -f /dracut-state.sh ]; then
  5     . /dracut-state.sh 2>/dev/null
  6 fi
  7 type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh
  8
  9 source_conf /etc/conf.d
 10
 11 type plymouth >/dev/null 2>&1 && plymouth quit
 12
 13 export _rdshell_name="dracut" action="Boot" hook="emergency"
 14 _emergency_action=$(getarg rd.emergency)
 15
 16 if getargbool 1 rd.shell -d -y rdshell || getarg rd.break -d rdbreak; then
 17     FSTXT="/run/dracut/fsck/fsck_help_$fstype.txt"
 18     source_hook "$hook"
 19     echo
 20     rdsosreport
 21     echo
 22     echo
 23     echo 'Entering emergency mode. Exit the shell to continue.'
 24     echo 'Type "journalctl" to view system logs.'
 25     echo 'You might want to save "/run/initramfs/rdsosreport.txt" to a USB stick or /boot'
 26     echo 'after mounting them and attach it to a bug report.'
 27     echo
 28     echo
 29     [ -f "$FSTXT" ] && cat "$FSTXT"
 30     [ -f /etc/profile ] && . /etc/profile
 31     [ -z "$PS1" ] && export PS1="$_name:\${PWD}# "
 32     exec sh -i -l
 33 else
 34     export hook="shutdown-emergency"

 35     warn "$action has failed. To debug this issue add \"rd.shell rd.debug\" to the kernel command line."
 36     source_hook "$hook"
 37     [ -z "$_emergency_action" ] && _emergency_action=halt
 38 fi
 39
 40 /bin/rm -f -- /.console_lock
 41
 42 case "$_emergency_action" in
 43     reboot)
 44         reboot || exit 1;;
 45     poweroff)
 46         poweroff || exit 1;;
 47     halt)
 48         halt || exit 1;;
 49 esac
 50
 51 exit 0

如图 8-5 所示,sysroot还没有挂载,因为我们还没有到达启动的挂载阶段。

img/493794_1_En_8_Fig5_HTML.jpg

图 8-5

应急 Shell

我希望您现在理解了 systemd 如何在不同的引导阶段向用户呈现紧急 shell。在下一章中,我们将继续暂停的 systemd 的引导序列。

九、系统:第二部分

到目前为止,我们已经到达了服务dracut.pre-mount.service,其中用户的根文件系统还没有挂载到 initramfs 中。systemd 的下一个引导阶段将在sysroot上挂载根文件系统。

sysroot.mount

systemd 接受mount dracut 命令行参数,这将把我们放到一个mount紧急 shell 中。如图 9-1 所示,我们已经传递了rd.break=mount内核命令行参数。

img/493794_1_En_9_Fig1_HTML.jpg

图 9-1

内核命令行参数

正如您在图 9-2 中看到的,sysroot已经以只读模式挂载到用户的根文件系统中。

img/493794_1_En_9_Fig2_HTML.jpg

图 9-2

该安装钩

dracut.mount钩子(usr/lib/systemd/system/dracut-mount.service)将从 initramfs 运行/bin/dracut-mount脚本,它将完成挂载部分。

#vim usr/lib/systemd/system/dracut-mount.service

如您所见,这是从 initramfs 执行dracut-mount脚本,并导出带有sysroot值的NEWROOT变量。

Environment=NEWROOT=/sysroot
ExecStart=-/bin/dracut-mount

[Unit]
Description=dracut mount hook
Documentation=man:dracut-mount.service(8)
After=initrd-root-fs.target initrd-parse-etc.service
After=dracut-initqueue.service dracut-pre-mount.service
ConditionPathExists=/usr/lib/initrd-release
ConditionDirectoryNotEmpty=|/lib/dracut/hooks/mount
ConditionKernelCommandLine=|rd.break=mount
DefaultDependencies=no
Conflicts=shutdown.target emergency.target

[Service]
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
Type=oneshot
ExecStart=-/bin/dracut-mount
StandardInput=null
StandardOutput=syslog
StandardError=syslog+console
KillMode=process
RemainAfterExit=yes

KillSignal=SIGHUP
#vim bin/dracut-mount
  1 #!/usr/bin/sh
  2 export DRACUT_SYSTEMD=1
  3 if [ -f /dracut-state.sh ]; then
  4     . /dracut-state.sh 2>/dev/null
  5 fi
  6 type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh
  7
  8 source_conf /etc/conf.d
  9
 10 make_trace_mem "hook mount" '1:shortmem' '2+:mem' '3+:slab'
 11
 12 getarg 'rd.break=mount' -d 'rdbreak=mount' && emergency_shell -n mount "Break mount"
 13 # mount scripts actually try to mount the root filesystem, and may
 14 # be sourced any number of times. As soon as one suceeds, no more are sourced.
 15 i=0
 16 while :; do

 17     if ismounted "$NEWROOT"; then
 18         usable_root "$NEWROOT" && break;
 19         umount "$NEWROOT"
 20     fi
 21     for f in $hookdir/mount/*.sh; do
 22         [ -f "$f" ] && . "$f"
 23         if ismounted "$NEWROOT"; then
 24             usable_root "$NEWROOT" && break;
 25             warn "$NEWROOT has no proper rootfs layout, ignoring and removing offending mount hook"
 26             umount "$NEWROOT"
 27             rm -f -- "$f"
 28         fi
 29     done
 30
 31     i=$(($i+1))
 32     [ $i -gt 20 ] && emergency_shell "Can't mount root filesystem"
 33 done
 34
 35 export -p > /dracut-state.sh
 36
 37 exit 0

我们在第八章中看到了它是如何让我们陷入紧急状态的,以及它的相关功能。由于我们在 initramfs 中挂载用户的根文件系统后停止了引导序列,正如你在图 9-3 中看到的,已经执行了systemd-fstab-generator,并且已经创建了-mount单元文件。

img/493794_1_En_9_Fig3_HTML.jpg

图 9-3

systemd-fstab-生成器行为

请记住,在sysroot.mount中添加的用户根文件系统名是从/proc/cmdline文件中提取的。sysroot.mount明确提到必须安装什么以及安装在哪里。

initrd.target

正如我们多次说过的,引导序列的最终目的是向用户提供用户的根文件系统,在这样做的同时,systemd 实现的主要阶段如下:

  1. 找到用户的根文件系统。

  2. 挂载用户的根文件系统(我们已经到了引导阶段)。

  3. 找到其他必要的文件系统并挂载它们(usrvarnfscifs等)。).

  4. 切换到挂载的用户的根文件系统。

  5. 启动用户空间守护进程。

  6. 启动multi-user.targetgraphical.target(这超出了本书的范围)。

如您所见,到目前为止,我们已经到了第 2 步,在 initramfs 中挂载用户的根文件系统。我们都知道 systemd 有.targetstarget不过是一堆单元文件。只有当.target的所有单元文件都成功启动后,它才能成功启动。

systemd 世界里有很多目标,比如basic.targetmulti-user.targetgraphical.targetdefault.targetsysinit.target等等。initramfs的最终目的是实现initrd.target。一旦initrd.target成功启动,那么 systemd 就会switch_root进入其中。所以,首先,让我们看看initrd.target以及它在引导序列中的位置。请参考图 9-4 所示的流程图。

img/493794_1_En_9_Fig4_HTML.jpg

图 9-4

引导序列

当你在 initramfs 之外(也就是说在switch_root之后),systemd 的default.target会是multi-user.target或者graphical.target,而在 initramfs 之内(也就是说在switch_root之前)basic.target之后,systemd 的default.target会是initrd.target。所以,在成功完成sysinit.targetbasic.target之后,systemd 的主要任务就是实现initrd.target。为了到达那里,systemd 将使用sysroot.mount阶段来读取由systemd-fstab-generator创建的挂载单元文件。服务dracut-mount.service将把用户的根文件系统挂载到/sysroot,然后 systemd 将执行服务initrd-parse-etc.service.,它将解析/sysroot/etc/fstab文件,并为usr或任何其他设置了x-initrd.mount选项的挂载点创建挂载单元文件。这就是initrd-parse-etc.service的工作原理:

# cat usr/lib/systemd/system/initrd-parse-etc.service | grep -v '#'

[Unit]
Description=Reload Configuration from the Real Root
DefaultDependencies=no
Requires=initrd-root-fs.target
After=initrd-root-fs.target
OnFailure=emergency.target
OnFailureJobMode=replace-irreversibly
ConditionPathExists=/etc/initrd-release

[Service]
Type=oneshot
ExecStartPre=-/usr/bin/systemctl daemon-reload
ExecStart=-/usr/bin/systemctl --no-block start initrd-fs.target
ExecStart=/usr/bin/systemctl --no-block start initrd-cleanup.service

基本上,服务是通过一个daemon-reload开关来执行systemctl的。这将重新加载 systemd 管理器配置。这将重新运行所有生成器,重新加载所有单元文件,并重新创建整个依赖关系树。当守护进程被重新加载时,systemd 代表用户配置监听的所有套接字将保持可访问。将重新执行的 systemd 生成器如下:

# ls usr/lib/systemd/system-generators/ -l
     total 92
     -rwxr-xr-x. 1 root root  3750 Jan 10 19:18 dracut-rootfs-generator
     -rwxr-xr-x. 1 root root 45640 Dec 21 12:19 systemd-fstab-generator
     -rwxr-xr-x. 1 root root 37032 Dec 21 12:19 systemd-gpt-auto-generator

如您所见,它将执行systemd-fstab-generator,读取/sysroot/etc/fstab条目,并为usr和设置了x-initrd.mount选项的设备创建挂载单元文件。总之,systemd-fstab-generator已经执行了两次。

因此,当您进入挂载 shell ( rd.break=mount)时,您实际上中断了目标initrd.target之后的引导序列。该目标仅运行以下服务:

# ls usr/lib/systemd/system/initrd.target.wants/

     dracut-cmdline-ask.service  dracut-mount.service      dracut-pre-trigger.service
     dracut-cmdline.service      dracut-pre-mount.service  dracut-pre-udev.service
     dracut-initqueue.service    dracut-pre-pivot.service

请参考图 9-5 以更好地理解这一点。

img/493794_1_En_9_Fig5_HTML.jpg

图 9-5

initrd.target 的整体执行

开关根/枢轴根

现在我们已经到了 systemd 引导的最后阶段,也就是switch_root。systemd 将根文件系统从 initramfs ( /)切换到用户的根文件系统(/sysroot)。systemd 通过采取以下步骤来实现这一点:

  1. 挂载新的根文件系统(/sysroot)

  2. 将它转换成根文件系统(/)

  3. 删除对旧的(initramfs)根文件系统的所有访问

  4. 卸载 initramfs 文件系统并取消分配 ramfs 文件系统

本章将讨论三个要点。

  • switch_root:我们会用老的init方式来解释。

  • pivot_root:我们将以systemd的方式来解释这一点。

  • 我们将在第十章中解释这一点。

在基于 init 的系统上切换到新的根文件系统

基于init的系统使用switch_root切换到新的根文件系统(sysroot)。switch_root的用途在它的手册页上有很好的解释,如下所示:

#man switch_root
NAME
       switch_root - switch to another filesystem as the root of the mount tree

SYNOPSIS
       switch_root [-hV]

       switch_root newroot init [arg...]

DESCRIPTION
       switch_root moves already mounted /proc, /dev, /sys and /run to newroot and makes newroot the new root filesystem and starts init process.

       WARNING: switch_root removes recursively all files and directories on the current root filesystem.

OPTIONS
       -h, --help
              Display help text and exit.

       -V, --version
              Display version information and exit.

RETURN VALUE
       switch_root returns 0 on success and 1 on failure.

NOTES
       switch_root will fail to function

 if newroot is not the root of a mount. If you want to switch root into a directory that does not meet this requirement then you can first use a bind-mounting trick to turn any directory into a mount point:

              mount --bind $DIR $DIR

因此,它切换到一个新的根文件系统(sysroot),并与根文件系统一起移动旧的根文件系统的虚拟文件系统(procdevsys等)。)到新根。switch_root最好的特性是在挂载新的根文件系统后,它自己启动init进程。切换到新的根文件系统发生在 dracut 的源代码中。在写这本书的时候,dracut 的最新版本是 049。switch_root功能在dracut-049/modules.d/99base/init.sh文件中定义。

387 unset PS4
388
389 CAPSH=$(command -v capsh)
390 SWITCH_ROOT=$(command -v switch_root)
391 PATH=$OLDPATH
392 export PATH
393
394 if [ -f /etc/capsdrop ]; then
395     . /etc/capsdrop
396     info "Calling $INIT with capabilities $CAPS_INIT_DROP dropped."
397     unset RD_DEBUG
398     exec $CAPSH --drop="$CAPS_INIT_DROP" -- \
399         -c "exec switch_root \"$NEWROOT\" \"$INIT\" $initargs" || \
400     {
401         warn "Command:"
402         warn capsh --drop=$CAPS_INIT_DROP -- -c exec switch_root "$NEWROOT" "$INIT" $initargs
403         warn "failed."
404         emergency_shell
405     }
406 else
407     unset RD_DEBUG
408     exec $SWITCH_ROOT "$NEWROOT" "$INIT" $initargs || {
409         warn "Something went very badly wrong in the initramfs.  Please "
410         warn "file a bug against dracut."
411         emergency_shell
412     }
413 fi

在前面的代码中,您可以看到exec switch_root已经被调用,就像在switch_root的手册页上描述的那样。NEWROOTINIT的定义变量值如下:

NEWROOT = "/sysroot"
INIT   = 'init' or  'sbin/init'

仅供参考,这些天的init文件是一个symlinksystemd

# ls -l sbin/init
lrwxrwxrwx. 1 root root 22 Dec 21 12:19 sbin/init -> ../lib/systemd/systemd

为了成功地switch_root虚拟文件系统,必须首先挂载它们。这将通过dracut-049/modules.d/99base/init.sh来实现。以下是将要遵循的步骤:

  1. 挂载proc文件系统。

  2. 挂载sys文件系统。

  3. devtmpfs挂载/dev目录。

  4. 手动创建stdinstdoutstderrptsshm设备文件。

  5. 制作包含 tmpfs 的/run挂载点。(/run挂载点在基于init的系统上不可用。)

#vim dracut-049/modules.d/99base/init.sh

 11 NEWROOT="/sysroot"
 12 [ -d $NEWROOT ] || mkdir -p -m 0755 $NEWROOT
 13
 14 OLDPATH=$PATH
 15 PATH=/usr/sbin:/usr/bin:/sbin:/bin
 16 export PATH
 17
 18 # mount some important things
 19 [ ! -d /proc/self ] && \
 20     mount -t proc -o nosuid,noexec,nodev proc /proc >/dev/null
 21
 22 if [ "$?" != "0" ]; then
 23     echo "Cannot mount proc on /proc! Compile the kernel with CONFIG_PROC_FS!"
 24     exit 1
 25 fi
 26
 27 [ ! -d /sys/kernel ] && \
 28     mount -t sysfs -o nosuid,noexec,nodev sysfs /sys >/dev/null

 29
 30 if [ "$?" != "0" ]; then
 31     echo "Cannot mount sysfs on /sys! Compile the kernel with CONFIG_SYSFS!"
 32     exit 1
 33 fi
 34
 35 RD_DEBUG=""
 36 . /lib/dracut-lib.sh
 37
 38 setdebug
 39
 40 if ! ismounted /dev; then
 41     mount -t devtmpfs -o mode=0755,noexec,nosuid,strictatime devtmpfs /dev >/dev/null
 42 fi
 43
 44 if ! ismounted /dev; then
 45     echo "Cannot mount devtmpfs on /dev! Compile the kernel with CONFIG_DEVTMPFS!"
 46     exit 1
 47 fi
 48
 49 # prepare the /dev directory
 50 [ ! -h /dev/fd ] && ln -s /proc/self/fd /dev/fd >/dev/null 2>&1
 51 [ ! -h /dev/stdin ] && ln -s /proc/self/fd/0 /dev/stdin >/dev/null 2>&1
 52 [ ! -h /dev/stdout ] && ln -s /proc/self/fd/1 /dev/stdout >/dev/null 2>&1
 53 [ ! -h /dev/stderr ] && ln -s /proc/self/fd/2 /dev/stderr >/dev/null 2>&1
 54
 55 if ! ismounted /dev/pts; then
 56     mkdir -m 0755 /dev/pts
 57     mount -t devpts -o gid=5,mode=620,noexec,nosuid devpts /dev/pts >/dev/null
 58 fi
 59
 60 if ! ismounted /dev/shm; then
 61     mkdir -m 0755 /dev/shm
 62     mount -t tmpfs -o mode=1777,noexec,nosuid,nodev,strictatime tmpfs /dev/shm >/dev/null

 63 fi
 64
 65 if ! ismounted /run; then
 66     mkdir -m 0755 /newrun
 67     if ! str_starts "$(readlink -f /bin/sh)" "/run/"; then
 68         mount -t tmpfs -o mode=0755,noexec,nosuid,nodev,strictatime tmpfs /newrun >/dev/null
 69     else
 70         # the initramfs binaries are located in /run, so don't mount it with noexec
 71         mount -t tmpfs -o mode=0755,nosuid,nodev,strictatime tmpfs /newrun >/dev/null
 72     fi
 73     cp -a /run/* /newrun >/dev/null 2>&1
 74     mount --move /newrun /run
 75     rm -fr -- /newrun
 76 fi

在基于 systemd 的系统上切换到新的根文件系统

这些步骤几乎类似于我们讨论的基于init的系统。对于systemd来说,唯一的区别就是用 C 代码做的二进制。因此,很明显,切换根将发生在 systemd 的 C 源代码中,如下所示:

src/shared/switch-root.c:

首先,考虑以下情况:

new_root = sysroot
old_root = /

这将移动已经在 initramfs 的根文件系统中填充的虚拟文件系统;然后,path_equal函数检查new_root路径是否可用。

if (path_equal(new_root, "/"))
      return 0;

稍后,它调用一个pivot_root系统调用(init使用switch_root)并将根从/(initramfs 根文件系统)更改为sysroot(用户的根文件系统)。

pivot_root(new_root, resolved_old_root_after) >= 0)

在我们进一步讨论之前,我们需要了解pivot_root是什么,它做什么。

注意,根据 pivot_root 的实现,调用者的 root 和 cwd 可能会也可能不会改变。下面是调用 pivot_root 的顺序,无论哪种情况都适用,假设 pivot_root 和 chroot 都在当前路径:

CD new _ root

pivot _ root。

exec ch root。命令

注意,chroot 必须在旧根下和新根下可用,因为 pivot_root 可能隐式更改了 shell 的根目录,也可能没有。

注意,exec chroot 改变正在运行的可执行文件,如果以后要卸载旧的根目录,这是必须的。还要注意,标准输入、输出和错误可能仍然指向旧的根文件系统上的设备,使其保持忙碌。当调用 chroot 时,可以很容易地更改它们(见下文;请注意,无论 pivot_root 是否更改了 shell 的根,都没有使用前导斜杠)。

# man pivot_root
NAME
       pivot_root - change the root filesystem

SYNOPSIS
       pivot_root new_root put_old

DESCRIPTION
       pivot_root moves the root file system of the current process to the directory put_old and makes new_root the new root file system. Since pivot_root(8) simply calls pivot_root(2), we refer to the man page of the latter for further details:

pivot_root将当前进程(systemd)的根文件系统(initramfs 根文件系统)更改为新的根文件系统(sysroot),同时将正在运行的可执行文件(systemd from initramfs)更改为新的可执行文件(systemd from user ' s root file system)。

pivot_root之后,它分离 initramfs ( src/shared/switch-root.c)的旧根设备。

# vim src/shared/switch-root.c

96         /* We first try a pivot_root() so that we can umount the old root dir. In many cases (i.e. where rootfs is /),
 97          * that's not possible however, and hence we simply overmount root */
 98         if (pivot_root(new_root, resolved_old_root_after) >= 0) {
 99
100                 /* Immediately get rid of the old root, if detach_oldroot is set.
101                  * Since we are running off it we need to do this lazily. */
102                 if (unmount_old_root) {
103                         r = umount_recursive(old_root_after, MNT_DETACH);
104                         if (r < 0)
105                                 log_warning_errno(r, "Failed to unmount old root directory tree, ignoring: %m");
106                 }
107
108         } else if (mount(new_root, "/", NULL, MS_MOVE, NULL) < 0)
109                 return log_error_errno(errno, "Failed to move %s to /: %m", new_root);
110

在成功的pivot_root之后,这是当前状态:

  • sysroot已经变成了根(/)。

  • 当前工作目录变成了根目录(/)。

  • 这样 bash 会将其根目录从旧的根(initramfs)更改为新的(用户的)根文件系统。chroot将在下一章讨论。

最后删除old_root设备(rm -rf)。

110
111         if (chroot(".") < 0)
112                 return log_error_errno(errno, "Failed to change root: %m");
113
114         if (chdir("/") < 0)
115                 return log_error_errno(errno, "Failed to change directory: %m");
116
117         if (old_root_fd >= 0) {
118                 struct stat rb;
119
120                 if (fstat(old_root_fd, &rb) < 0)
121                         log_warning_errno(errno, "Failed to stat old root directory, leaving: %m");
122                 else
123                         (void) rm_rf_children(TAKE_FD(old_root_fd), 0, &rb); /* takes possession of the dir fd, even on failure */
124         }

为了更好地理解,我强烈建议阅读这里显示的整个src/shared/switch-root.c源代码:

  1 /* SPDX-License-Identifier: LGPL-2.1+ */
  2
  3 #include <errno.h>
  4 #include <fcntl.h>
  5 #include <limits.h>
  6 #include <stdbool.h>
  7 #include <sys/mount.h>
  8 #include <sys/stat.h>
  9 #include <unistd.h>
 10
 11 #include "base-filesystem.h"
 12 #include "fd-util.h"
 13 #include "fs-util.h"
 14 #include "log.h"
 15 #include "missing_syscall.h"
 16 #include "mkdir.h"
 17 #include "mount-util.h"
 18 #include "mountpoint-util.h"
 19 #include "path-util.h"
 20 #include "rm-rf.h"
 21 #include "stdio-util.h"
 22 #include "string-util.h"
 23 #include "strv.h"
 24 #include "switch-root.h"
 25 #include "user-util.h"
 26 #include "util.h"
 27
 28 int switch_root(const char *new_root,

 29                 const char *old_root_after, /* path below the new root, where to place the old root after the transition */
 30                 bool unmount_old_root,
 31                 unsigned long mount_flags) {  /* MS_MOVE or MS_BIND */
 32
 33         _cleanup_free_ char *resolved_old_root_after = NULL;
 34         _cleanup_close_ int old_root_fd = -1;
 35         bool old_root_remove;
 36         const char *i;
 37         int r;
 38
 39         assert(new_root);
 40         assert(old_root_after);
 41
 42         if (path_equal(new_root, "/"))
 43                 return 0;
 44
 45         /* Check if we shall remove the contents of the old root */
 46         old_root_remove = in_initrd();
 47         if (old_root_remove) {
 48                 old_root_fd = open("/", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_NOCTTY|O_DIRECTORY);
 49                 if (old_root_fd < 0)
 50                         return log_error_errno(errno, "Failed to open root directory: %m");
 51         }
 52
 53         /* Determine where we shall place the old root after the transition */
 54         r = chase_symlinks(old_root_after, new_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved_old_root_after, NULL);
 55         if (r < 0)
 56                 return log_error_errno(r, "Failed to resolve %s/%s: %m", new_root, old_root_after);
 57         if (r == 0) /* Doesn't exist yet. Let's create it */
 58                 (void) mkdir_p_label(resolved_old_root_after, 0755);
 59
 60         /* Work-around for kernel design: the kernel refuses MS_MOVE if any file systems are mounted MS_SHARED. Hence

 61          * remount them MS_PRIVATE here as a work-around.
 62          *
 63          * https://bugzilla.redhat.com/show_bug.cgi?id=847418 */
 64         if (mount(NULL, "/", NULL, MS_REC|MS_PRIVATE, NULL) < 0)
 65                 return log_error_errno(errno, "Failed to set \"/\" mount propagation to private: %m");
 66
 67         FOREACH_STRING(i, "/sys", "/dev", "/run", "/proc") {
 68                 _cleanup_free_ char *chased = NULL;
 69
 70                 r = chase_symlinks(i, new_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &chased, NULL);
 71                 if (r < 0)
 72                         return log_error_errno(r, "Failed to resolve %s/%s: %m", new_root, i);
 73                 if (r > 0) {
 74                         /* Already exists. Let's see if it is a mount point already. */
 75                         r = path_is_mount_point(chased, NULL, 0);
 76                         if (r < 0)
 77                                 return log_error_errno(r, "Failed to determine whether %s is a mount point: %m", chased);
 78                         if (r > 0) /* If it is already mounted, then do nothing */
 79                                 continue;
 80                 } else
 81                          /* Doesn't exist yet? */
 82                         (void) mkdir_p_label(chased, 0755);
 83
 84                 if (mount(i, chased, NULL, mount_flags, NULL) < 0)
 85                         return log_error_errno(errno, "Failed to mount %s to %s: %m", i, chased);
 86         }
 87
 88         /* Do not fail if base_filesystem_create() fails. Not all switch roots are like base_filesystem_create() wants
 89          * them to look like. They might even boot, if they are RO and don't have the FS layout. Just ignore the error
 90          * and switch_root() nevertheless. */
 91         (void) base_filesystem_create(new_root, UID_INVALID, GID_INVALID);
 92
 93         if (chdir(new_root) < 0)
 94                 return log_error_errno(errno, "Failed to change directory to %s: %m", new_root);
 95
 96         /* We first try a pivot_root() so that we can umount the old root dir. In many cases (i.e. where rootfs is /),
 97          * that's not possible however, and hence we simply overmount root */
 98         if (pivot_root(new_root, resolved_old_root_after) >= 0) {
 99
100                 /* Immediately get rid of the old root, if detach_oldroot is set.
101                  * Since we are running off it we need to do this lazily. */
102                 if (unmount_old_root) {
103                         r = umount_recursive(old_root_after, MNT_DETACH);
104                         if (r < 0)
105                                 log_warning_errno(r, "Failed to unmount old root directory tree, ignoring: %m");
106                 }

107
108         } else if (mount(new_root, "/", NULL, MS_MOVE, NULL) < 0)
109                 return log_error_errno(errno, "Failed to move %s to /: %m", new_root);
110
111         if (chroot(".") < 0)
112                 return log_error_errno(errno, "Failed to change root: %m");
113
114         if (chdir("/") < 0)
115                 return log_error_errno(errno, "Failed to change directory: %m");
116
117         if (old_root_fd >= 0) {
118                 struct stat rb;
119
120                 if (fstat(old_root_fd, &rb) < 0)
121                         log_warning_errno(errno, "Failed to stat old root directory, leaving: %m");
122                 else
123                         (void) rm_rf_children(TAKE_FD(old_root_fd), 0, &rb); /* takes possession of the dir fd, even on failure */
124         }
125
126         return 0;
127 }

这里,我们已经成功地切换到用户的根文件系统,并离开了 initramfs 环境。现在,PID 为 1 的用户根文件系统中的 systemd 将开始运行,并负责引导过程的其余部分,如下所示:

  • systemd 将启动httpdmysqlpostfixnetwork services等用户空间服务。

  • 最终,目标将是达到default.target。我们之前讨论过,在switch_root之前,systemd 的目标default.target会是initrd.target,在switch_root之后,不是multi-user.target就是graphical.target

但是从 initramfs(根文件系统)开始的现有的systemd进程会发生什么呢?是在switch_root还是pivot_root之后被干掉?新的systemd进程是从用户的根文件系统开始的吗?

答案很简单。

  1. initramfs 的 systemd 创建一个管道。

  2. forks 系统。

  3. 原 PID 1 chroot变为/systemd并执行/sysroot/usr/lib/systemd/systemd

  4. 分叉的 systemd 通过管道将其状态序列化为 PID 1 并退出。

  5. PID 1 反序列化来自管道的数据,并继续使用/(以前的/sysroot)中的新配置。

我希望您喜欢 initramfs 中的 systemd 之旅。正如我们前面提到的,systemd 引导序列的其余部分将发生在 initramfs 之外,与我们到目前为止所讨论的差不多。

GUI 如何启动超出了本书的范围。在我们的下一章,我们将讨论现场 ISO 图像和救援模式。

6# 十、救援模式和实时图像

在这最后一章,我们将涵盖救援模式和现场图像。在我们的救援模式讨论中,我们将涵盖救援 initramfs,以及一些“无法启动”的问题。实时映像讨论包括 Squashfs、rootfs.img和实时映像的引导顺序。

救援模式

在救援模式下有两种启动方式。

img/493794_1_En_10_Fig2_HTML.jpg

图 10-2

来自实时图像的救援模式条目

  • Through the built-in GRUB menuentry. Refer to Figure 10-1.

    img/493794_1_En_10_Fig1_HTML.jpg

    图 10-1

    GRUB 中的救援模式条目

  • 通过实时 ISO 图像。参见图 10-2 。

顾名思义,这种模式旨在拯救陷入“无法启动”问题的系统。想象一下这样一种情况,系统无法挂载根文件系统,您会收到这样一条永无止境的通用消息:

dracut-initqueue: warning dracut-initqueue timeout - starting timeout scripts’。

假设您只安装了一个内核,如下所示:

<snip>
.
.
[  OK  ] Started Show Plymouth Boot Screen.
[  OK  ] Started Forward Password R...s to Plymouth Directory Watch.
[  OK  ] Reached target Paths.
[  OK  ] Reached target Basic System.
[  145.832487] dracut-initqueue[437]: Warning: dracut-initqueue timeout - starting timeout scripts
[  146.541525] dracut-initqueue[437]: Warning: dracut-initqueue timeout - starting timeout scripts
[  147.130873] dracut-initqueue[437]: Warning: dracut-initqueue timeout - starting timeout scripts
[  147.703069] dracut-initqueue[437]: Warning: dracut-initqueue timeout - starting timeout scripts
[  148.267123] dracut-initqueue[437]: Warning: dracut-initqueue timeout - starting timeout scripts
[  148.852865] dracut-initqueue[437]: Warning: dracut-initqueue timeout - starting timeout scripts
[  149.430171] dracut-initqueue[437]: Warning: dracut-initqueue timeout - starting timeout scripts
.
.
</snip>

由于这个系统只有一个内核(不能启动),如果没有环境,你将如何解决“不能启动”的问题?救援模式就是为了这个唯一的目的而创建的。让我们首先选择默认的救援模式,它预装在 Linux 中,可以从 GRUB 菜单中选择。请参见图 10-3 。

img/493794_1_En_10_Fig3_HTML.jpg

图 10-3

GRUB 屏幕

救援模式将正常启动,如图 10-4 所示,如果一切正常,它将向用户显示其根文件系统。

img/493794_1_En_10_Fig4_HTML.jpg

图 10-4

在救援模式下挂载的根文件系统

但是我想到了一个问题:当正常的内核无法启动时,为什么同一个系统能够在救援模式下启动呢?

这是因为当您安装 Fedora 或任何 Linux 发行版时,Linux 的安装程序 Anaconda 会在/boot中安装两个内核。

# ls -lh /boot/
total 164M
-rw-r--r--. 1 root root 209K Oct 22 01:03 config-5.3.7-301.fc31.x86_64
drwx------. 4 root root 4.0K Oct 24 04:44 efi
-rw-r--r--. 1 root root 181K Aug  2  2019 elf-memtest86+-5.01
drwxr-xr-x. 2 root root 4.0K Oct 24 04:42 extlinux
drwx------. 5 root root 4.0K Mar 28 13:37 grub2
-rw-------. 1 root root  80M Dec  9 10:18 initramfs-0-rescue-2058a9f13f9e489dba29c477a8ae2493.img
-rw-------. 1 root root  32M Dec  9 10:19 initramfs-5.3.7-301.fc31.x86_64.img
drwxr-xr-x. 3 root root 4.0K Dec  9 10:18 loader
drwx------. 2 root root  16K Dec  9 10:12 lost+found
-rw-r--r--. 1 root root 179K Aug  2  2019 memtest86+-5.01
-rw-------. 1 root root  30M Jan  6 09:37 new.img
-rw-------. 1 root root 4.3M Oct 22 01:03 System.map-5.3.7-301.fc31.x86_64
-rwxr-xr-x. 1 root root 8.9M Dec  9 10:18 vmlinuz-0-rescue-2058a9f13f9e489dba29c477a8ae2493
-rwxr-xr-x. 1 root root 8.9M Oct 22 01:04 vmlinuz-5.3.7-301.fc31.x86_64

如您所见,vmlinuz-5.3.7-301.fc31.x86_64是一个普通内核,而vmlinuz-0-rescue-19a08a3e86c24b459999fbac68e42c05是一个救援内核,它是一个独立的内核,有自己的 initramfs 文件,名为initramfs-0-rescue-19a08a3e86c24b459999fbac68e42c05.img

假设你安装了一个由nvidia提供的新软件包(.rpm.deb),里面有新的图形驱动程序。由于图形驱动程序必须添加到 initramfs 中,nvidia包重新构建了原来的内核 initramfs ( initramfs-5.3.7-301.fc31.x86_64.img)。因此,原来的内核有新添加的图形驱动程序,但是 rescue initramfs 没有添加该驱动程序。当用户尝试引导时,由于安装的图形驱动程序与连接的图形卡不兼容,系统无法使用原始内核(vmlinuz-5.3.7-301.fc31.x86_64)进行引导,但同时,由于不兼容的驱动程序不在 rescue initramfs 中,系统将使用 rescue 模式成功引导。救援模式内核将具有与普通内核相同的命令行参数,因此安装的救援内核知道用户根文件系统的名称。

图 10-5 显示了普通内核的命令行参数。

img/493794_1_En_10_Fig5_HTML.jpg

图 10-5

普通内核的命令行参数

图 10-6 显示了救援内核的命令行参数。

img/493794_1_En_10_Fig6_HTML.jpg

图 10-6

救援内核的命令行参数

救援模式初始化

救援模式 initramfs ( initramfs-0-rescue-2058a9f13f9e489dba29c477a8ae2493.img)的大小比原始内核的 initramfs ( initramfs-5.3.7-301.fc31.x86_64.img)大得多。

# ls -lh /boot/
total 164M
-rw-r--r--. 1 root root 209K Oct 22 01:03 config-5.3.7-301.fc31.x86_64
drwx------. 4 root root 4.0K Oct 24 04:44 efi
-rw-r--r--. 1 root root 181K Aug  2  2019 elf-memtest86+-5.01
drwxr-xr-x. 2 root root 4.0K Oct 24 04:42 extlinux
drwx------. 5 root root 4.0K Mar 28 13:37 grub2
-rw-------. 1 root root  80M Dec  9 10:18 initramfs-0-rescue-2058a9f13f9e489dba29c477a8ae2493.img
-rw-------. 1 root root  32M Dec  9 10:19 initramfs-5.3.7-301.fc31.x86_64.img
drwxr-xr-x. 3 root root 4.0K Dec  9 10:18 loader
drwx------. 2 root root  16K Dec  9 10:12 lost+found
-rw-r--r--. 1 root root 179K Aug  2  2019 memtest86+-5.01
-rw-------. 1 root root  30M Jan  6 09:37 new.img
-rw-------. 1 root root 4.3M Oct 22 01:03 System.map-5.3.7-301.fc31.x86_64
-rwxr-xr-x. 1 root root 8.9M Dec  9 10:18 vmlinuz-0-rescue-2058a9f13f9e489dba29c477a8ae2493
-rwxr-xr-x. 1 root root 8.9M Oct 22 01:04 vmlinuz-5.3.7-301.fc31.x86_64

这是为什么?这是因为救援 initramfs 不像普通内核的 initramfs 那样是特定于主机的。rescue initramfs 是一个通用的 initramfs,它是通过考虑用户可以在其上创建根文件系统的所有可能的设备而准备的。让我们比较一下这两个 initramfs 系统。

# tree
.
├── normal_kernel
│   └── initramfs-5.3.7-301.fc31.x86_64.img
└── rescue_kernel
    └── initramfs-0-rescue-2058a9f13f9e489dba29c477a8ae2493.img

2 directories, 2 files

我们将把它们提取到各自的目录中。

#/usr/lib/dracut/skipcpio
     initramfs-5.3.7-301.fc31.x86_64.img | gunzip -c | cpio -idv

#/usr/lib/dracut/skipcpio
     initramfs-0-rescue-2058a9f13f9e489dba29c477a8ae2493.img | gunzip -c | cpio -idv

我们将从提取的 initramfs 中创建文件列表。

# tree normal_kernel/ > normal.txt
# tree rescue_kernel/ > rescue.txt

以下是两个 initramfs 系统之间的差异。与正常的 initramfs 相比,rescue initramfs 系统几乎多了 2,189 个文件。此外,几乎 719 额外的模块已被添加到救援 initramfs。

# diff -yt rescue.txt normal.txt  | grep '<' | wc -l
     2186
# diff -yt rescue.txt normal.txt  | grep '<' | grep -i '.ko'  | wc -l
     719

<skip>
.
.
│   │   ├── lspci                                               <
│   │   ├── mdadm                                               <
│   │   ├── mdmon                                               <
│   │   ├── mdraid-cleanup                                      <
│   │   ├── mdraid_start                                        <
│   │   ├── mount.cifs                                          <
│   │   ├── mount.nfs                                           <
│   │   ├── mount.nfs4 -> mount.nfs                             <
│   │   ├── mpathpersist                                        <
│   │   ├── multipath                                           <
│   │   ├── multipathd                                          <
│   │   ├── nfsroot                                             <
│   │   ├── partx                                               <
│   │   ├── pdata_tools                                         <
│   │   ├── ping -> ../bin/ping                                 <
│   │   ├── ping6 -> ../bin/ping                                <
│   │   ├── rpcbind -> ../bin/rpcbind                           <
│   │   ├── rpc.idmapd                                          <
│   │   ├── rpcinfo -> ../bin/rpcinfo                           <
│   │   ├── rpc.statd                                           <
│   │   ├── setpci                                              <
│   │   ├── showmount                                           <
│   │   ├── thin_check -> pdata_tools                           <
│   │   ├── thin_dump -> pdata_tools                            <
│   │   ├── thin_repair -> pdata_tools                          <
│   │   ├── thin_restore -> pdata_tools                         <
│   │   ├── xfs_db                                              <
│   │   ├── xfs_metadump                                        <
│   │   └── xfs_repair                                          <
    ├── lib                                                     <
    │   ├── iscsi                                               <
    │   ├── lldpad                                              <
    │   ├── nfs                                                 <
    │   │   ├── rpc_pipefs                                      <
    │   │   └── statd                                           <
    │   │       └── sm                                          <
</skip>

rescue initramfs 将拥有用户可以在其上创建根文件系统的设备的几乎所有模块和支持的文件,而普通 initramfs 将是特定于主机的。它将只包含用户在其上创建了根文件系统的设备的那些模块和支持的文件。如果您想自己创建一个 rescue initramfs,那么您可以在基于 Fedora 的系统上安装一个dracut-config-generic包。这个包只提供了一个文件,并且它具有关闭特定于主机的 initramfs 生成的配置。

# rpm -ql dracut-config-generic
     /usr/lib/dracut/dracut.conf.d/02-generic-image.conf

# cat /usr/lib/dracut/dracut.conf.d/02-generic-image.conf
     hostonly="no"

如您所见,该文件将限制 dracut 创建特定于主机的 initramfs。

“无法启动”问题 9 (chroot)

问题:普通内核和救援内核都无法启动。图 10-7 显示了正常的内核紧急信息。

img/493794_1_En_10_Fig7_HTML.jpg

图 10-7

内核紧急消息

抛出的内核紧急消息抱怨内核无法挂载根文件系统。我们前面看到,每当内核无法挂载用户的根文件系统时,它就会抛出dracut-initqueue超时消息。

'dracut-initqueue: warning dracut-initqueue timeout - starting timeout scripts'

然而,这一次,恐慌信息是不同的。因此,看起来这个问题与用户的根文件系统无关。另一个线索是它提到了 VFS 文件系统;VFS 代表“虚拟文件系统”,因此这表示紧急消息无法从 initramfs 挂载根文件系统。基于这些线索,我想我们已经隔离了这个问题,我们应该把注意力集中在两个内核的 initramfs 上。

如图 10-8 所示,救援模式内核紧急信息也是类似的。

img/493794_1_En_10_Fig8_HTML.jpg

图 10-8

救援模式内核紧急消息

解决方法:以下是解决问题的步骤:

  1. Since the installed rescue kernel is also panicking, we need to use the live image of Fedora or of any Linux distribution to boot. As shown in Figure 10-9 and Figure 10-10, we are using a live image of Fedora.

    img/493794_1_En_10_Fig10_HTML.jpg

    图 10-10

    使用实时映像启动

    img/493794_1_En_10_Fig9_HTML.jpg

    图 10-9

    实时图像欢迎屏幕

  2. 系统已在救援模式下启动。实时映像引导序列将在本章的“实时映像”一节中讨论。先成为sudo用户吧。

$ sudo su
We trust you have received the usual lecture from your local system administrator. It usually boils down to these three things:
#1) Respect the privacy of others.
#2) Think before you type.
#3) With great power comes great responsibility.
[root@localhost-live liveuser] #

  1. 我们在这里看到的根目录来自一个实时图像。因为实时映像内核不知道用户根文件系统的名称,所以它不能像救援内核一样挂载它。

    [root@localhost-live liveuser]# ls /
         bin boot dev etc home lib lib64 lost+found media mnt
         opt proc root run sbin srv sys tmp usr var
    
    
  2. 让我们来看看正常内核和救援内核的 initramfs 有什么问题。为此,我们需要首先挂载用户的根文件系统。

    # vgscan -v
      Found volume group "fedora_localhost-live" using metadata type lvm2
    
    # lvscan -v
      ACTIVE      '/dev/fedora_localhost-live/swap' [2.20 GiB] inherit
      ACTIVE      '/dev/fedora_localhost-live/root' [18.79 GiB] inherit
    
    # pvscan -v
      PV /dev/sda2  VG fedora_localhost-live  lvm2 [<21.00 GiB / 0  free]
      Total: 1 [<21.00 GiB] / in use: 1 [<21.00 GiB] / in no VG: 0 [0 ]
    
    

如您所见,该系统有一个基于 LVM 的用户根文件系统。物理卷在 sda 设备上。接下来,我们将在一个临时目录中挂载用户的根文件系统。

  1. 让我们检查 initramfs 文件的状态。

    # ls temp_root/boot/ -l
         total 0
    
    
# mkdir temp_root
# mount /dev/fedora_localhost-live/root temp_root/
# ls temp_root/
     bin   dev  home  lib64  media  opt   root  sbin  sys
     tmp usr boot  etc  lib   lost+found  mnt    proc  run
     srv   @System.solv user_root_fs.txt  var

用户根文件系统的引导目录为空。这是因为在这个系统上,引导是一个单独的分区。

# mount /dev/sda1 temp_root/boot/
#ls temp_root/boot/
Config-5.3.7-301.fc31.x86_64  efi elf-memtest86+-5.01
extlinux grub2 loader lost+found
Memtest86+-5.01 System.map-5.3.7-301.fc31.x86_64
vmlinuz-0-rescue-19a08a3e86c24b459999fbac68e42c05
vmlinuz-5.3.7-301.fc31.x86_64

令人惊讶的是,正如您所看到的,在用户的根文件系统上没有可用的 initramfs 文件,这就是两个内核都死机的原因。

因此,问题已经确定,我们需要重新生成 initramfs。要制作新的 initramfs,我们需要使用dracut命令,但是有一些问题。

  • 无论我们执行哪个二进制文件或命令,该二进制文件都将来自实时映像根文件系统。例如,dracut命令将从/usr/bin/dracut运行,而用户根文件系统的二进制文件在temp_root/usr/bin/dracut中。

  • 要运行任何二进制文件,它需要像libc.so这样的支持库,这些库将再次从一个活动映像的根文件系统中使用。这意味着我们现在使用的整个环境来自实时图像,这可能会产生严重的问题。例如,我们可以安装任何包,它将被安装在实时映像的根文件系统中,而不是用户的根文件系统中。

简而言之,我们需要将当前的根文件系统(/)从动态映像根文件系统更改为用户的根文件系统(temp_root)。chroot是我们为此需要使用的命令。

  1. 顾名思义,它会将 bash 的根从当前根更改为新根。只有当虚拟文件系统已经装载在新的根上时,chroot才会成功。

    root@localhost-live liveuser]# ls /
         bin  boot  dev  etc  home  lib  lib64  lost+found  media  mnt
         opt  proc  root  run  sbin  srv  sys  tmp  usr  var
    
    

我们当前的根是实时镜像根文件系统。在chroot之前,我们将挂载procdevdevptssysrun虚拟文件系统。

  1. 我们都被设置为进入用户的根文件系统。
# mount -v --bind /dev/ temp_root/dev
mount: /dev bound on /home/liveuser/temp_root/dev.

# mount -vt devpts devpts temp_root/dev/pts -o gid=5,mode=620
mount: devpts mounted on /home/liveuser/temp_root/dev/pts.

# mount -vt proc proc temp_root/proc
mount: proc mounted on /home/liveuser/temp_root/proc.

# mount -vt sysfs sysfs temp_root/sys
mount: sysfs mounted on /home/liveuser/temp_root/sys.

# mount -vt tmpfs tmpfs temp_root/run
mount: tmpfs mounted on /home/liveuser/temp_root/run.

# chroot temp_root/# ls
     bin   dev  home  lib64       media  opt   root  sbin  sys   tmp
     usr boot  etc  lib   lost+found  mnt    proc  run   srv
     @System.solv  user_root_fs.txt  var

所以,temp_root现在成了 bash 的根文件系统。如果退出这个 shell,bash 会将其根目录从用户的根文件系统更改为动态镜像根文件系统。所以,只要我们在同一个 shell 实例中,我们的根目录就是temp_root。现在,无论我们执行什么命令或二进制文件,它都将在用户的根文件系统环境中运行。因此,现在在这个环境中执行进程是完全安全的。

  1. 要解决这个“无法启动”的问题,我们需要重新生成 initramfs。

    root@localhost-live /]# ls /lib/modules
    5.3.7-301.fc31.x86_64
    
    [root@localhost-live /]# cd /boot/
    
    [root@localhost-live boot]# rpm -qa | grep -i 'kernel-5' kernel-5.3.7-301.fc31.x86_64
    
    [root@localhost-live boot]# dracut initramfs-5.3.7-301.fc31.x86_64.img 5.3.7-301.fc31.x86_64
    
    
  2. 如果你想重新生成救援内核 initramfs,那么你需要安装一个dracut-config-generic包。

  3. 重新启动后,系统能够启动,并且“无法启动”问题已得到修复。

企业 Linux 发行版的拯救模式

在一些 Linux 发行版(如 CentOS)中,rescue image 方法有点不同。Linux 的企业版将试图自己找到用户的根文件系统。让我们来看看实际情况。图 10-11 和图 10-12 显示了 CentOS 的救援模式选择程序。

img/493794_1_En_10_Fig12_HTML.jpg

图 10-12

救援模式选择

img/493794_1_En_10_Fig11_HTML.jpg

图 10-11

CentOS 欢迎屏幕

它将启动,如图 10-13 所示,它将在屏幕上显示一些消息。

img/493794_1_En_10_Fig13_HTML.jpg

图 10-13

信息丰富的消息

如果我们选择选项 1,continue,那么救援模式将搜索磁盘,并将自己找到根文件系统。一旦用户的根文件系统被识别,它将把它挂载到/mnt/sysimage目录下。请参见图 10-14 。

img/493794_1_En_10_Fig14_HTML.jpg

图 10-14

根文件系统安装在/mnt/sysimage 下

如您所见,它已经在/mnt/sysimage中挂载了用户的根文件系统;我们只需要chroot进入其中。但是美妙之处在于我们不需要预先挂载虚拟文件系统。这是因为,正如你在图 10-15 中看到的,CentOS 中使用的chroot二进制文件已经被定制,它将自己挂载虚拟文件系统。

img/493794_1_En_10_Fig15_HTML.jpg

图 10-15

根目录

如果我们选择了选项 2,Read-Only Mount,那么救援脚本会以只读模式挂载用户的根文件系统,而不是在/mnt/sysimage中。如果我们选择了第三个选项Skip,救援系统就不会试图自己找到并安装用户的根文件系统;它只会给我们提供一个 Shell。

但是,当 CentOS ISO 的 rescue 内核没有用户的根文件系统名称时,它是如何找到根文件系统的呢?

在这里,Anaconda 没有办法找出用户的根文件系统名。Anaconda 将挂载连接到系统的每一个磁盘,并检查/etc/fstab是否存在。如果找到了/etc/fstab,那么它将从中获取用户的根文件系统名。如果您的系统连接了大量磁盘,那么 Anaconda 很可能需要很长时间来挂载用户的根文件系统。在这种情况下,最好手动挂载用户的根文件系统。查找用户根文件系统的源代码存在于 Anaconda 的源 tarball 中,如下所示:

#vim pyanaconda/storage/root.py

 91 def _find_existing_installations(devicetree):
 92     """Find existing GNU/Linux installations on devices from the device tree.
 93
 94     :param devicetree: a device tree to find existing installations in
 95     :return: roots of all found installations
 96     """
 97     if not os.path.exists(conf.target.physical_root):
 98         blivet_util.makedirs(conf.target.physical_root)
 99
100     sysroot = conf.target.physical_root
101     roots = []
102     direct_devices = (dev for dev in devicetree.devices if dev.direct)
103     for device in direct_devices:
104         if not device.format.linux_native or not device.format.mountable or \
105            not device.controllable or not device.format.exists:
106             continue
107
108         try:
109             device.setup()
110         except Exception:  # pylint: disable=broad-except
111             log_exception_info(log.warning, "setup of %s failed", [device.name])
112             continue
113
114         options = device.format.options + ",ro"
115         try:
116             device.format.mount(options=options, mountpoint=sysroot)
117         except Exception:  # pylint: disable=broad-except
118             log_exception_info(log.warning, "mount of %s as %s failed", [device.name, device.format.type])
119             blivet_util.umount(mountpoint=sysroot)
120             continue
121
122         if not os.access(sysroot + "/etc/fstab", os.R_OK):
123             blivet_util.umount(mountpoint=sysroot)
124             device.teardown()
125             continue
126
127         try:
128             (architecture, product, version) = get_release_string(chroot=sysroot)
129         except ValueError:
130             name = _("Linux on %s") % device.name
131         else:
132             # I'd like to make this finer grained, but it'd be very difficult
133             # to translate.
134             if not product or not version or not architecture:
135                 name = _("Unknown Linux")
136             elif "linux" in product.lower():
137                 name = _("%(product)s %(version)s for %(arch)s") % \
138                     {"product": product, "version": version, "arch": architecture}
139             else:
140                 name = _("%(product)s Linux %(version)s for %(arch)s") % \
141                     {"product": product, "version": version, "arch": architecture}
142
143         (mounts, swaps) = _parse_fstab(devicetree, chroot=sysroot)
144         blivet_util.umount(mountpoint=sysroot)
145         if not mounts and not swaps:
146             # empty /etc/fstab. weird, but I've seen it happen.
147             continue
148         roots.append(Root(mounts=mounts, swaps=swaps, name=name))
149

实时图像

实时图像是 Linux 系统最好的特性之一。如果我们只是坚持正常的硬盘引导部分,这本书就不会完整。让我们来看看一个 Linux 的现场图像是如何启动的。首先让我们挂载 ISO 映像,看看它包含了什么。

# mkdir live_image
# mount /dev/cdrom live_image/
mount: /home/yogesh/live_image: WARNING: device write-protected, mounted read-only.

# tree live_image/
live_image/
├── EFI
│   └── BOOT
│       ├── BOOT.conf
│       ├── BOOTIA32.EFI
│       ├── BOOTX64.EFI
│       ├── fonts
│       │   └── unicode.pf2
│       ├── grub.cfg
│       ├── grubia32.efi
│       ├── grubx64.efi
│       ├── mmia32.efi
│       └── mmx64.efi
├── images
│   ├── efiboot.img
│   ├── macboot.img
│   └── pxeboot
│       ├── initrd.img
│       └── vmlinuz
├── isolinux
│   ├── boot.cat
│   ├── boot.msg
│   ├── grub.conf
│   ├── initrd.img
│   ├── isolinux.bin
│   ├── isolinux.cfg
│   ├── ldlinux.c32
│   ├── libcom32.c32
│   ├── libutil.c32
│   ├── memtest
│   ├── splash.png
│   ├── vesamenu.c32
│   └── vmlinuz
└── LiveOS
    └── squashfs.img

实时图像分为四个目录:EFIimagesisolinuxLiveOS

  • EFI:

    我们在讨论 bootloader 的时候已经讨论过这个目录。UEFI 固件将跳转到该目录并运行grubx64.efi文件。grubx64.efi文件将读取grub.cfg文件,并将从isolinux目录中提取initrd.imgvmlinuz文件。

  • 图像:

    这将主要在我们通过 PXE 引导时使用。网络引导超出了本书的范围。

  • 等 linux:

    如果 UEFI 以 BIOS 方式启动,那么它将从这里读取grub.conf文件。该目录主要用于存储initrdvmlinuz文件。换句话说,这个目录是普通根文件系统的/boot

  • 层:

    这就是奇迹发生的地方。这个目录有一个名为squashfs.img的文件。一旦你安装了它,你会在里面找到rootfs.img

# mkdir live_image_extract_1
# mount live_image/LiveOS/squashfs.img  live_image_extract_1/

# ls live_image_extract_1/
     LiveOS
# ls live_image_extract_1/LiveOS/
     rootfs.img

# mkdir live_image_extract_2
# mount live_image_extract_1/LiveOS/rootfs.img live_image_extract_2/

# ls live_image_extract_2/
     bin  boot  dev  etc  home  lib  lib64  lost+found  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

壁球比赛

Squashfs 是一个小型的压缩只读文件系统。这个文件系统通常用于嵌入式系统,其中存储的每个字节都很宝贵。Squashfs 为我们提供了比 tarball 归档更多的灵活性和性能。Squashfs 在其中存储了一个动态 Fedora 的根文件系统(rootfs.img),它将以只读方式挂载。

# mount | grep -i rootfs
/home/yogesh/live_image_extract_1/LiveOS/rootfs.img on /home/yogesh/live_image_extract_2 type ext4 (ro,relatime,seclabel)

您可以使用squashfs-tool提供的mksquashfs命令来制作 Squashfs 图像/档案。

rootfs.img

rootfs.img是一个 ext4 文件系统,其中有一个典型的根文件系统。有些发行版为实时图像创建一个访客用户或一个名为live的用户,但是在 Fedora 中是根用户做所有的事情。

# file live_image_extract_1/LiveOS/rootfs.img
live_image_extract_1/LiveOS/rootfs.img: Linux rev 1.0 ext4 filesystem data, UUID=849bdfdc-c8a9-4fed-a727-de52e24d981f, volume name "Anaconda" (extents) (64bit) (large files) (huge files)

实时映像的引导序列

顺序如下:

  1. 固件会调用引导程序(grubx64.efi)。它将读取grub.cfg文件,并从isolinux目录中复制vmlinuzinitrd文件。

  2. 内核将在特定的位置提取自身,并将在任何可用的位置提取 initramfs。

  3. 从 initramfs 启动的 systemd 会在/dev/mapper/live-rwrootfs.img文件提取到设备映射器目标设备,将它挂载到根(/)文件系统,并将switch_root文件放入其中。

  4. 一旦根文件系统可用,您可以将它视为安装在 CD、DVD 或.iso文件中的正常操作。

此外,很明显,与特定于主机的 initramfs 相比,实时映像 initramfs 的大小要大得多。

posted @ 2024-08-02 19:34  绝不原创的飞龙  阅读(0)  评论(0编辑  收藏  举报