Linux 启动实用指南(三)

原文:Hands-on Booting

协议:CC BY-NC-SA 4.0

六、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>

**解决方法:**以下是解决问题的步骤:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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模块。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 显示了屏幕上可见的内容。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-4

内核命令行参数

但是yogesh在实际的根文件系统上不可用;因此,如图 6-5 所示,将无法启动。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-5

应急 Shell

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-7

普利茅斯银幕

  • 几乎每个 Linux 发行版都在引导时显示动画,以使引导过程更加激动人心,但是分析引导序列所需的重要控制台消息将隐藏在动画后面。要停止动画并在屏幕上查看详细的控制台消息,请移除rhgbquite参数。

  • rhgbquite通过后,如图 6-7 所示,将显示plymouth动画。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 中。用户的根文件系统将以只读模式装载。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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.

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 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模块。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-12

内核命令行参数

virtio相关的驱动程序很重要。虚拟磁盘和虚拟机管理程序网络就是通过这个驱动程序向来宾操作系统公开的。由于我们将它们列入黑名单,客户操作系统将停止启动。您可以在图 6-13 中看到“无法启动”控制台信息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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脚本模型,会发生这样的情况:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-1

初始模型

  1. syslog会先开始。

  2. 当它完全准备好时,dbus服务将被启动。

  3. dbus之后,avahi将被启动。

  4. 最后会启动bluetooth服务。见图 7-1 。

bluetoothavahi互不依赖,但是bluetooth要等到avahi启动。类似 Ubuntu 的发行版使用upstart而不是init,这在一定程度上改善了引导时间。在upstart中,互不依赖的服务将并行启动,意味着avahibluetooth将一起启动。请参考图 7-2 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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.

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 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 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 都会立即返回。)”

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-5

启用网络管理器-等待-在线-服务后的图

  1. 启用NetworkManager-wait-online-service后,问题已解决,但开机时间略有减少。如图 7-5 所示,大部分开机时间已经被NetworkManager-wait-online-service吃光了,这是意料之中的。

systemd 提供了另一个工具bootchart,它基本上是一个守护进程,通过它可以对 Linux 引导过程进行性能分析。它将在启动时收集数据并制作图表。你可以认为bootchartsystemd-analyze剧情的高级版本。要使用这个工具,如图 7-6 所示,您需要将systemd-bootchart二进制文件的完整路径传递给init内核命令行参数。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-6

内核命令行参数

在成功的引导过程后,如图 7-7 所示,工具将在/run/log/bootchart*.创建一个详细的图形图像。一旦图像生成,systemd-bootchart将控制权移交给 systemd,systemd 将继续引导过程。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-7

引导图表

既然我们现在了解了 systemd 的基础知识,我们可以继续我们暂停的引导序列。到目前为止,我们已经到了内核已经提取 RAM 中的 initramfs 并从中启动 systemd 二进制文件的阶段。一旦 systemd 进程启动,它将遵循常规的引导顺序。

initramfs 中的系统流

systemd 将从 initramfs 启动,并遵循图 7-8 所示的引导顺序。Harald Hoyer(他创建了 dracut initramfs,并且是 systemd 的主要开发人员)创建了这个流程图,它也可以在 systemd 手册页中找到。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 显示了该情况。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-10

条件

如果用户已经通过了rd.break=cmdline,那么脚本调用名为emergency_shell.的函数顾名思义,它会提供调试 shell,如果调试 shell 已经成功启动,那么它调用另一个名为source_hook的函数,并向其传递cmdline参数。谁写这个代码给用户提供调试 Shell,谁就是天才程序员!

我们在这个阶段不会讨论紧急 shell 函数,因为我们需要先了解 systemd。因此,我们将在第八章中更详细地讨论它。

图 7-11 显示了dracut-cmdline.service单元工作的流程图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-11

dracut-cmdline.service 流程图

更进一步,用户的根文件系统名称可能只是/dev/sda5,但是同一个 sda5 设备可能通过uuidpartuuidlabel被引用。最后,sda5 的每隔一个引用都要到达/dev/sda5;因此,内核在/dev/disk/下为所有这些不同的设备名准备了符号链接文件。请参见图 7-12 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-13

内核命令行参数

内核将提取 initramfs,systemd 进程将启动,systemd 将初始化journald套接字,正如你在图 7-14 中看到的,systemd 将把我们放到一个命令行 shell 中,因为我们告诉 systemd 在执行dracut-cmdline钩子之前中断(钩子)引导序列。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-14

命令行挂钩

目前,我们在 initramfs 内部,由于dracut-cmdline.service还没有启动,systemd 还没有从/proc/cmdline收集到内核命令行参数,如rootrsflagsfstype,我们已经在systemd-journal.socket.之后暂停(dracut hooked)了 systemd 的引导序列。请参见图 7-15 以便更好地理解。另外,/dev/disk下的符号链接还没有被 dracut 创建。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-15

命令行挂钩

因为 systemd 还没有收集用户根文件系统的名称,所以毫无疑问,您不会发现用户的根文件系统挂载在 initramfs 中。sysroot是 initramfs 中的一个目录,systemd 在这里挂载用户的根文件系统。参见图 7-16 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-16

sysroot 目录

但是如果我们不向rd.break传递任何参数或者简单地退出当前的 cmdline shell,我们将被丢弃到switch_root shell。switch_root shell 是 initramfs 中 systemd 引导序列的最后阶段。在图 7-17 中,你可以看到我们正在没有任何争论的情况下通过rd.break

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-17

rd.break 内核命令行参数

如图 7-18 所示,在switch_root shell 中,由于dracut-cmdline.service已经执行,你会发现内核命令行参数已经被 systemd 收集。此外,用户的根文件系统已经挂载在sysroot下的 initramfs 中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-18

开关根挂钩

如果我们从这个阶段退出,switch_root ( pivot_root)将由 systemd 执行,它将离开 initramfs 环境。稍后 systemd 将执行剩余的引导程序,如图 7-19 所示,最终我们将得到桌面。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-19

Fedora 的登录屏幕

回到我们到目前为止的引导顺序,我们已经到达了预udev阶段。对此可参考图 7-20 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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作为内核命令行参数进行了传递。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-21

传递 udev 之前的内核命令行参数

为了理解pre-udev钩子,你可以简单地列出/dev的内容,在图 7-22 中你会注意到没有名为 sda 的设备文件。sda 是我们的硬盘,在那里我们有我们的根文件系统。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 中看到引导序列。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-23

到目前为止介绍的引导顺序

dracut-预触发服务

如果用户传递了rd.break=pre-trigger dracut 命令行参数,systemd 的 initramfs 引导序列将被中断(挂钩)。你可以在图 7-24 中看到,我们已经将pre-trigger作为参数传递给了rd.break内核命令行参数。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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下还不可用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-25

预触发钩

这是因为udevadm触发器还没有被执行。服务dracut-pre-trigger.service只执行重新加载udev规则.udevadm control --reload,,如图 7-26 所示,服务systemd-udev.service已经启动,但systemd-udev-trigger服务尚未启动。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-26

预触发钩

systemd-udev-trigger.service

图 7-27 显示了我们已经到达的引导阶段。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-27

到目前为止的引导顺序

正如我们已经看到的,在 pre-?? 中,/dev没有被填充,因为systemd-udevd.service本身没有启动。与pre-trigger一样:/dev没有被填充,但是udevd服务已经开始。通过使用udevd守护进程提供的环境,udevd服务将创建一个环境来启动/运行各种udev工具,如udevadm.,如图 7-28 所示,在pre-trigger中,我们将能够执行udevadm,这是我们在pre-udev shell 中无法使用的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-28

预触发钩

正如您在pre-trigger开关内部看到的,sda 设备尚未创建。但是由于我们已经准备好了一个udevadm环境,我们可以通过它发现设备。如图 7-29 所示,我们将首先挂载内核配置文件系统。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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钩子中不会成功。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-30

前 udev hook 中的 udevadm

这就是dracut-pre-trigger.servicepre-trigger挂钩的重要性。

图 7-31 中给出的流程图将帮助您理解迄今为止 systemd 在 initramfs 中采取的步骤。读完第八章后,流程图会更容易理解。我强烈推荐在看完第八章后重温这一章。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-31

流程图

本地文件系统目标

如图 7-32 所示,我们已经到了启动的local-fs-target阶段。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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二进制文件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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.

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    图 7-33

    系统 d-fstab-generato0072

  3. 它创建了一个sysroot.mount单元文件。顾名思义,创建它是为了挂载用户的根文件系统。读取/proc/cmdline.已创建单元文件,参见图 7-34 查看sysroot.mount文件内容。

根文件系统将从 sda5(通过使用 UUID)挂载到sysroot目录。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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命令行参数传递给内核。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-36

内核命令行参数

如图 7-37 所示,在pre-pivot钩子中,root文件系统将与usr文件系统一起被挂载,因为pre-pivot钩子在sysroot上挂载用户的根文件系统后停止了引导序列。但是optvarboot不会被安装。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-37

预枢转钩

即使运行systemd-fstab-generator,你也会发现只创建了usrroot挂载单元文件。在图 7-38 中可以看到systemd-fstab-generator输出。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-38

预旋转挂钩中的 systemd-fstab-generator

这证明了在 initramfs 环境中,只有rootusr会被挂载。其余的挂载点将在 initramfs 之后或者切换到 root 之后挂载。因为var文件系统还没有安装,所以journalctl日志将从/run文件系统中维护,正如我们所知,这是一个临时文件系统。这清楚地表明,在 initramfs 环境中,您不能访问 journald 的永久日志,它们位于/var/log。请参考图 7-39 、 7-40 和 7-41 更好地理解这一点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-41

预透视挂钩中的日志行为

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-40

journalctl 提供的日志来自/run

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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是一个独立的挂载点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 中的流程图,以帮助您更好地理解这一点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-44

流程图

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-43

systemd-fstab-generator 的工作原理

交换目标

如图 7-45 所示,我们已经到了启动的swap.target阶段。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-46

预 udev 挂钩的工作原理

使用pre-trigger钩子,sda 设备没有创建,但是udevd服务已经启动;因此,如图 7-47 和图 7-48 所示,您可以使用类似udevadm的工具在/dev下创建sda器件,但不会在其上创建类似lvmraid的器件。这种设备也称为dm(设备映射器)设备。因此,如果根在lvm上,pre-trigger服务将不能为根创建设备文件,因此像/dev/fedora_localhost-live/这样的设备将不会被创建。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-48

sda 装置已在预触发挂钩下创建

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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内核命令行参数。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-49

内核命令行参数

如图 7-50 所示,lvm_scan命令被写入其中一个文件。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-50

initqueue 挂钩

因此,这里我们有两个选择:或者我们可以只执行/bin/dracut-initqueue或者如图 7-51 所示,我们可以从pre-trigger钩子或者从initqueue钩子执行lvm_scan命令。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 显示了屏幕上可见的内容。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 是如何让我们进入紧急状态的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-53

应急 Shell

如图 7-54 所示,我们将扫描当前可用的 LVs,并将挂载root vg以验证其内容。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-54

激活 LVs

如您所见,root_vg(重命名为vg)可用,我们也可以激活它。这显然意味着 LVM 元数据没有损坏,LVM 设备没有任何完整性问题。如图 7-55 所示,我们将root_vg挂载到一个临时目录,并从应急 shell 本身交叉验证其fstab条目。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-55

挂载根文件系统

vg是完整的,fstab条目是正确的,我们能够挂载根vg。那还缺什么?

缺少的部分是 GRUB 中没有调整内核命令行参数。见图 7-56 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-56

内核命令行参数

为了启动,我们需要中断 GRUB 闪屏,并需要更改图 7-57 中所示的内核命令行参数。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-57

旧的内核命令行参数

新的见图 7-58 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-59

引导序列

如图 7-60 所示,plymouth将在整个启动过程中处于活动状态。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-60

普利茅斯

plymouth是一个在启动时显示动画的工具。例如,在 Fedora 中,它不显示如图 7-61 所示的控制台消息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-61

当普利茅斯不可用时

plymouth向您展示如图 7-62 所示的动画。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 显示了到目前为止我们已经介绍过的引导顺序。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 所示的流程图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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 内核完成引导序列来说肯定绰绰有余。

一些模块可能有内存泄漏,但是识别这一点将是一项困难的任务。因此,我们将首先验证是否有任何与内存相关的内核调优参数设置不正确。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-67

禁用 hugepage 设置

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 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作为内核命令行参数传递。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-68

内核命令行参数

正如你在图 7-69 中看到的,它把我们放在了紧急 shell 中,并且用户的根文件系统没有挂载在sysroot.是的,我说它把我们放在了紧急 shell 中,但是你会惊讶地看到,紧急 shell 只不过是 systemd 提供的一个简单的 bash shell,但是在引导还没有完成的时候。为了更好地理解紧急 shell,我们将暂停我们的引导序列一会儿,并在第八章中讨论 initramfs 的调试 shell。我们将在第九章继续暂停的 systemd 引导序列。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-69

预安装钩

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值