协议: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
的配置文件。
-
突然,您决定将您的根文件系统从普通的本地磁盘转移到 SAN(我绝不会建议在生产系统上进行这样的改变),SAN 通过多路径设备连接。
-
要获得多路径设备的整个环境,您需要在这里添加多路径 dracut 模块,以便将多路径的整个环境拉入 initramfs。
-
几天后,您在同一系统上添加了新的 NIC 卡,NIC 卡供应商已经为其提供了驱动程序。驱动程序只不过是一个
.ko
文件(内核对象)。要在 initramfs 中添加这个模块,您必须选择添加kernel module
选项。这将只添加网卡的驱动程序,而不是整个环境。
但是,如果您想在 initramfs 中添加某个特定的文件,它既不是内核模块,也不是 dracut 模块,该怎么办呢?dracut 提供了dracut.conf
的install_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 闪屏
-
错误信息开始时会说它无法到达交换设备,然后该过程超时。
[TIME] Timed out waiting for device /dev/mapper/fedora_localhost--live-swap
这是一条非常重要的信息,因为它告诉您这个系统的文件系统有问题。
-
交换设备基于 HDD,并且交换文件系统已经在其上创建。现在交换设备本身丢失了。因此,要么底层磁盘本身不可访问,要么交换文件系统已损坏。了解了这一点,我们现在可以只关注存储方面。隔离问题很重要,因为“无法启动”问题有数千种可能导致系统停止启动的情况。
-
我们要么以救援模式启动,要么使用相同发行版和版本的实时映像。这是一个 Fedora 31 系统,如图 6-1 所示,我会使用 GRUB 的 rescue 选项。
-
一旦我们引导进入救援模式,我们将挂载用户的根文件系统并
chroot
到其中。为什么救援模式能够启动,而普通内核却不能在同一系统上启动?这是一个有效的问题,答案将在第十章中给出。 -
因为我们能够在救援内核中挂载根文件系统,但是不能在普通内核中挂载根文件系统,这意味着 initramfs 映像有问题。也许是一些模块是必要的处理硬盘丢失。我们来验证一下这个理论。
-
这是一个虚拟化系统,这意味着它有一个虚拟磁盘。这个从
/dev
目录就能看出来。 -
为了处理虚拟磁盘,我们需要在 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
模块不见了。
-
由于
virtio_blk
缺失,显然内核无法检测和访问vda
磁盘,用户在这里拥有根文件系统和交换文件系统。 -
要解决这个问题,我们需要在 initramfs 中添加缺失的
virtio_blk
模块。
图 6-2
Fedora 的登录屏幕
-
我们将使用我们的
new.img
initramfs 进行引导。如何借助 GRUB 命令提示符手动引导系统已经在“无法引导”问题 1 中讨论过了。 -
在添加了丢失的
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
控制台消息
**解决方法:**以下是解决问题的步骤:
-
这很容易理解和解决。
-
错误消息是不言自明的;initramfs 文件本身缺失。
-
要么是 initramfs 本身丢失,要么是因为
/boot/loader/entries/*
文件中有一个错误的条目。在这种情况下,initramfs 本身是缺失的。 -
因此,我们需要以救援模式引导,并挂载用户的根文件系统。
-
要么重新安装内核的
rpm
包,让包的postscripts
部分重新生成丢失的 initramfs,并相应地更新 BLS 条目。 -
或者您可以在
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 发行版都在引导时显示动画,以使引导过程更加激动人心,但是分析引导序列所需的重要控制台消息将隐藏在动画后面。要停止动画并在屏幕上查看详细的控制台消息,请移除
rhgb
和quite
参数。 -
当
rhgb
和quite
通过后,如图 6-7 所示,将显示plymouth
动画。
图 6-8
控制台消息
-
当
rhgb
和quite
被移除时,如图 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
和/usr
的fsck
。如果您将/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 并将命令行参数如
root
和ro
传递给 systemd,systemd 将从 initramfs 开始。 -
systemd 将找到实际的根文件系统。
-
一旦根文件系统(设备)被识别,systemd 将对其执行
fsck
。 -
如果
fsck
成功,那么 systemd 将把根文件系统作为ro
(根据传递的内核命令行参数)装入 initramfs 本身。它将以只读方式安装在 initramfs 的/sysroot
目录中。 -
如图 6-9 所示,内核已经提取了 initramfs 并从中启动 systemd(我已经去掉了
rhgb
和quite
参数)。
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 undersysroot
. 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_balloon
、virtio_net
、virtio_console
、virtio_blk
和virtio_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
模块是多个其他模块的依赖,如btrtl
、btintel
、btbcm
、bnep
、btusb
、rfcomm
、rfkill
。因此,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.pre
和rd.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。级别越高意味着调试输出越多:
- 这将在屏幕上打印所有与内存子系统相关的信息,如
meminfo
和slabinfo
文件内容。
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
接受的参数:
七、系统:第一部分
以下是我们目前所知的引导顺序:
-
引导加载程序在内存中加载内核和 initramfs。
-
内核将被加载到特定的位置(特定于架构的位置),而 initramfs 将被加载到任何可用的位置。
-
内核在
vmlinuz
文件头的帮助下提取自己。 -
内核在主内存(
init/initramfs.c
)中提取 initramfs,并将其作为临时根文件系统(/
)安装在主内存中。 -
内核从临时根文件系统启动(
init/main.c
)systemd 作为第一个进程,使用 PID-1。 -
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 |
-
它用
journalctl
维护日志。 -
它广泛使用 cgroups 版本 1 和 2。
-
它减少了启动时间。
-
它管理单元。是 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 单元文件相关的httpd
或mysql
是没有意义的。我们来试着了解一下 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
服务才会启动。Before
与After
相反Wants
、After
、Before
和Requires
都是相互独立工作的。通常将Wants
和After
一起使用。
Conflicts=
这可用于列出与当前单位冲突的单位。启动此设备可能会停止列出的冲突设备。
OnFailure=
当任何给定单元达到故障状态时,单元将启动。
- 【服务】:
ExecStart=/usr/sbin/sshd
启动一个sshd
服务单元只是启动在ExecStart.
之后提到的二进制
- 【安装】:
systemd 不使用单元文件的Install
部分。而是由systemctl
enable
或disable
命令使用。它将被systemctl
用来创建或破坏符号链接。
systemd 如何减少启动时间?
systemd 的创造者 Lennart Poettering 在他的博客 http://0pointer.de/blog/projects/systemd.html
中给出了 systemd 如何减少启动时间的经典例子。如果你真的想深入 systemd 世界,这个博客是最好的资源之一。
有四个守护进程:syslog
、dbus
、avahi
和bluetooth
。
每个守护进程都需要记录消息。因此,syslog
是其他所有守护进程的要求。avahi
需要syslog
和dbus
来运行。bluetooth
需要dbus
和syslog
但不需要avahi
运行。使用Sysv/init
脚本模型,会发生这样的情况:
图 7-1
初始模型
-
syslog
会先开始。 -
当它完全准备好时,
dbus
服务将被启动。 -
在
dbus
之后,avahi
将被启动。 -
最后会启动
bluetooth
服务。见图 7-1 。
bluetooth
和avahi
互不依赖,但是bluetooth
要等到avahi
启动。类似 Ubuntu 的发行版使用upstart
而不是init
,这在一定程度上改善了引导时间。在upstart
中,互不依赖的服务将并行启动,意味着avahi
和bluetooth
将一起启动。请参考图 7-2 。
图 7-2
暴发户模式
在systemd,
中,所有的服务都是借助sockets
.
同时启动的下面是一个例子:
-
systemd 将为
syslog
(已经被替换为journald
)创建一个套接字。 -
套接字
/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 用来创建套接字文件。
-
对于
dbus
,套接字是在/run/dbus/system_bus_socket.
创建运行的,dbus
需要journald
才能运行,但由于系统仍在引导,journald
/syslog
尚未完全启动,dbus
会将其消息记录到journald
的套接字/dev/log
中,每当journald
服务完全就绪时,它就会从套接字中获取消息。 -
It’s the same for the
bluetooth
service ; it needs thedbus
service to be running to start. So, systemd will create a/run/dbus/system_bus_socket
socket before thedbus
service starts. Thebluetooth
service will not wait fordbus
to start. You can refer to Figure 7-3 for a better understanding.图 7-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-analyse
的plot
工具,它将生成图表并提供更多关于启动时间的细节。生成的绘图图像如图 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
服务无法启动。
以下是解决此问题的步骤:
-
我们需要首先隔离问题。当 GRUB 出现在屏幕上时,删除
rhgb quiet
内核命令行参数。 -
详细日志显示系统能够启动,但是
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.
-
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
可能无法联系其服务器。
- 为了解决这样的问题,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
启用网络管理器-等待-在线-服务后的图
- 启用
NetworkManager-wait-online-service
后,问题已解决,但开机时间略有减少。如图 7-5 所示,大部分开机时间已经被NetworkManager-wait-online-service
吃光了,这是意料之中的。
systemd 提供了另一个工具bootchart
,它基本上是一个守护进程,通过它可以对 Linux 引导过程进行性能分析。它将在启动时收集数据并制作图表。你可以认为bootchart
是systemd-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 环境,并通过启动httpd
、mysql
等用户空间服务继续引导过程。如果用户以图形模式启动系统,它还会绘制一个桌面/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
收集内核命令行参数,如root
、rflags
和fstype
变量。这也被称为 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 =用户的根文件系统标志(
ro
或rw
) -
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 }
以下是到目前为止的引导顺序:
-
引导装载程序从用户那里收集内核命令行参数,并将它们存储在自己的配置文件中(
grub.cfg
)。 -
它通过填充内核头将这些命令行参数传递给内核。
-
内核提取自身并复制内核头中的内核命令行参数。
-
内核提取内存中的 initramfs,并将其用作临时根文件系统。
-
在相同的过程中,内核准备虚拟文件系统,如
proc
、sys
、dev
、devpts
、shm
等。 -
内核将命令行参数存储在
/proc/cmdline
文件中。 -
systemd 通过读取
/proc/cmdline
文件收集内核命令行参数,并将它们存储在root
、rootfs
和fstype
变量中。
我们可以通过使用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 设备可能通过uuid
、partuuid
或label
被引用。最后,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
收集到内核命令行参数,如root
、rsflags
和fstype
,我们已经在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
它会把我们扔在一个前贝壳上。注意after
、before
和wants
变量。执行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
装置的完整顺序如下:
-
如果用户在启动时将蓝牙设备连接到系统,则内核或内核中编译的驱动程序或稍后插入的模块将检测蓝牙设备并将其对象注册到
/sys
。 -
稍后,内核将在
/dev
挂载点创建一个设备文件。设备文件创建完成后,内核会发送一个uevent
给udevd
。 -
udevd
将从 initramfs 引用lib/udev/rules.d/99-systemd.rules
并将调用 systemd。根据标签,systemd 应该处理剩下的部分。 -
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
请注意服务单元文件的After
、Before
和wants
部分。如果这个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-trigger
Shell。第八章介绍了提供应急 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.service
或pre-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 文件
-
首先我已经删除了
/tmp
目录的内容。这是因为fstab
生成器在/tmp.
中生成挂载单元文件 -
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
-
它创建了一个
sysroot.mount
单元文件。顾名思义,创建它是为了挂载用户的根文件系统。读取/proc/cmdline.
已创建单元文件,参见图 7-34 查看sysroot.mount
文件内容。
根文件系统将从 sda5(通过使用 UUID)挂载到sysroot
目录。
图 7-35
systemd-fsck-root.service 文件内容
- 检查
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
文件。相反,它的工作是在/tmp
或run/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.c
中add_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");
当前系统只有一个根分区。为了帮助您更好地理解这一点,这里我准备了一个测试系统,它有root
、boot
、usr
、var
和opt
作为独立的文件系统:
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-pivot
Shell 中(我们还没有讨论过)。图 7-36 显示我们已经将rd.break=pre-pivot
命令行参数传递给内核。
图 7-36
内核命令行参数
如图 7-37 所示,在pre-pivot
钩子中,root
文件系统将与usr
文件系统一起被挂载,因为pre-pivot
钩子在sysroot
上挂载用户的根文件系统后停止了引导序列。但是opt
、var
和boot
不会被安装。
图 7-37
预枢转钩
即使运行systemd-fstab-generator
,你也会发现只创建了usr
和root
挂载单元文件。在图 7-38 中可以看到systemd-fstab-generator
输出。
图 7-38
预旋转挂钩中的 systemd-fstab-generator
这证明了在 initramfs 环境中,只有root
和usr
会被挂载。其余的挂载点将在 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-generator
在local-fs.target
期间运行时,它只为 root 创建挂载单元文件;然后它继续引导序列并在sysroot
上挂载根文件系统。一旦挂载了根文件系统,它就从/etc/sysroot/etc/fstab
中读取usr
条目,并创建一个usr.mount
单元文件,最后挂载它。让我们交叉验证这种理解:
-
放下
pre-pivot
挂钩。 -
从安装的
/sysroot.
中删除/etc/fstab
-
运行
systemd-fstab-generator.
-
参见图 7-42 。
因为root
文件系统名称将由dracut-cmdline
从proc/cmdline, systemd-fstab-generator
中取出,将成为sysroot.mount
。但是由于sysroot
中缺少fstab
文件,它会将usr
视为一个不可用的独立分区,并且它会跳过创建usr.mount
单元文件,即使usr
是一个独立的挂载点。
图 7-42
systemd-fstab-生成器行为
如果您希望在/sysroot
中有类似于opt
和var
的独立挂载点,或者您希望它们在 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-.target
为root
和usr
创建挂载点,而swap.target
为交换设备创建挂载单元文件。一旦根文件系统挂载文件准备好了,就根据它挂载sysroot
。systemd-fstab-generator
将读取fstab
,如果交换设备条目存在,它将生成swap.mount
单元文件。这意味着只有在切换到用户的根文件系统(switch_root
到sysroot
)之后,才会创建swap.mount
文件。在此阶段不会创建swap.mount
。
dracut-initqueue .服务
该服务创建实际的root
、swap
和usr
设备。我们用一个例子来理解这个。
通过pre-udev
钩子,我们看到了类似 sda 的设备是不可用的。因为udevd
服务本身还没有启动,所以两个udevadm
命令都不起作用。参见图 7-46 。
图 7-46
预 udev 挂钩的工作原理
使用pre-trigger
钩子,sda 设备没有创建,但是udevd
服务已经启动;因此,如图 7-47 和图 7-48 所示,您可以使用类似udevadm
的工具在/dev
下创建sda
器件,但不会在其上创建类似lvm
或raid
的器件。这种设备也称为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 中传递的root
和rd.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
设备。
-
让我们首先通过回忆执行的步骤来隔离问题。原
root lv
名如下: -
root volume group
名称已被更改。# vgrename fedora_localhost-live root_vg The volume group Fedora_localhost-live was successfully renamed to root_vg.
-
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
,那么你可以这样做:
-
从
gnome-look.org
下载plymouth-theme
,也可以使用以下: -
将下载的主题解压到以下位置:
/usr/share/plymouth/themes/
# dnf install plymouth-theme*
- 当
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.service
和systemd-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
二进制。我们已经在第五章看到了lsmod
、insmod
、modinfo
、modprobe
、depmod
等。,是指向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
)的时代我们还需要静态节点?有一些像fuse
或ALSA
这样的模块需要一些存在于/dev
中的设备文件,或者他们可以创建它们。但是这可能是危险的,因为设备文件是由kernel
或udev
生成的。因此,为了避免模块创建设备文件,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
内核命令行参数
-
为此,我们将把自己放在 initramfs 中。在图 7-66 中,我们已经将
rd.break
作为内核命令行参数进行了传递。 -
我们将在读写模式下重新挂载
sysroot
,并验证sysctl
参数。switch_root:/# cat /proc/sys/vm/nr_hugepages 2400
-
问题是错误地保留了大量页面。我们将根据图 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
因此,当所有早期服务的单元文件requires
、wants
和after
阶段成功启动时,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
预安装钩