协议:CC BY-NC-SA 4.0
八、调试 Shell
到目前为止,我们知道 initramfs 内置了 bash,并且我们不时地通过rd.break
钩子来使用它。本章的目的是理解 systemd 如何在 initramfs 中为我们提供一个 shell。必须遵循的步骤是什么,如何更有效地使用它?但是在此之前,让我们回顾一下到目前为止我们所学到的关于 initramfs 的调试和紧急 shells 的知识。
贝壳
rd.break
drop to a shell at the end
rd.break
将我们放入 initramfs 中,我们可以通过它探索 initramfs 环境。这个 initramfs 环境也被称为紧急模式。在正常情况下,当 initramfs 无法挂载用户的根文件系统时,我们会在紧急模式下掉线。请记住,在将用户的根文件系统挂载到/sysroot
下之后,但在对其执行switch_root
之前,不带任何参数地传递rd.break
会将我们放到 initramfs。你总能在/run/initramfs/rdsosreport.txt
文件中找到详细的日志。图 8-1 显示了来自rdsosreport.txt
的日志。
图 8-1
rdsosreport.txt 运行时日志
在日志消息中,您可以清楚地看到它就在执行pivot_root
之前被丢弃。pivot_root
和switch_root
将在第九章讨论,而chroot
将在第十章讨论。一旦退出紧急 shell,systemd 将继续暂停的引导序列,并最终提供登录屏幕。
然后我们讨论了如何使用紧急 shells 来修复一些“无法启动”的问题。例如,initramfs 与用户的根文件系统一样好。因此,它确实有lvm
、raid
和与文件系统相关的二进制文件,我们可以用它们来查找、组装、诊断和修复丢失的用户根文件系统。然后我们讨论了如何将它安装在/sysroot
下,并探索它的内容,例如修复grub.cfg
的错误条目。
同样,rd.break
也为我们提供了不同的选项来打破不同阶段的引导顺序。
-
cmdline
:这个钩子获取内核命令行参数。 -
pre-udev
:这打破了udev
处理程序之前的引导顺序。 -
pre-trigger
:可以用udevadm
控件设置udev
环境变量,也可以用udevadm
控件设置--property=KEY=value
类参数或控制udev
的进一步执行。 -
pre-mount
:这在/sysroot
挂载用户的根文件系统之前中断了引导序列。 -
mount
:这打破了在/sysroot
挂载根文件系统后的引导顺序。 -
pre-pivot
:这在切换到实际的根文件系统之前中断了引导序列。
现在让我们看看 systemd 是如何在这些不同的阶段为我们提供 shells 的。
systemd 如何让我们进入紧急状态?
让我们考虑一个pre-mount
钩子的例子。来自 initramfs 的 systemd 从dracut-cmdline.service
收集rd.break=pre-mount
命令行参数,并从 initramfs 位置/usr/lib/systemd/system.
运行 systemd 服务dracut-pre-mount.service
,该服务将在运行initrd-root-fs.target
、sysroot.mount
和systemd-fsck-root.service
之前运行。
# cat usr/lib/systemd/system/dracut-pre-mount.service | grep -v #'
[Unit]
Description=dracut pre-mount hook
Documentation=man:dracut-pre-mount.service(8)
DefaultDependencies=no
Before=initrd-root-fs.target sysroot.mount systemd-fsck-root.service
After=dracut-initqueue.service cryptsetup.target
ConditionPathExists=/usr/lib/initrd-release
ConditionDirectoryNotEmpty=|/lib/dracut/hooks/pre-mount
ConditionKernelCommandLine=|rd.break=pre-mount
Conflicts=shutdown.target emergency.target
[Service]
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
Type=oneshot
ExecStart=-/bin/dracut-pre-mount
StandardInput=null
StandardOutput=syslog
StandardError=syslog+console
KillMode=process
RemainAfterExit=yes
KillSignal=SIGHUP
如您所见,它只是从 initramfs 执行了/bin/dracut-pre-mount
脚本。
# vim bin/dracut-pre-mount
1 #!/usr/bin/sh
2
3 export DRACUT_SYSTEMD=1
4 if [ -f /dracut-state.sh ]; then
5 . /dracut-state.sh 2>/dev/null
6 fi
7 type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh
8
9 source_conf /etc/conf.d
10
11 make_trace_mem "hook pre-mount" '1:shortmem' '2+:mem' '3+:slab' '4+:komem'
12 # pre pivot scripts are sourced just before we doing cleanup and switch over
13 # to the new root.
14 getarg 'rd.break=pre-mount' 'rdbreak=pre-mount' && emergency_shell -n pre-mount "Break pre-mount"
15 source_hook pre-mount
16
17 export -p > /dracut-state.sh
18
19 exit 0
在/bin/dracut-pre-mount
脚本中,最重要的一行如下:
getarg rd.break=pre-mount' rdbreak=pre-mount
&& emergency_shell -n pre-mount "Break pre-mount"
我们已经讨论过了getarg
函数,它用于检查什么参数被传递给了rd.break=
。如果已经通过了rd.break=pre-mount
,那么只调用emergency-shell()
函数。该函数在/usr/lib/dracut-lib.sh
中定义,并将pre-mount
作为字符串参数传递给它。-n
代表以下内容:
[ -n STRING ] or [ STRING ]
:如果STRING
的长度不为零,则为真
emergency_shell
函数接受_rdshell_name
变量的值作为pre-mount.
if [ "$1" = "-n" ]; then
_rdshell_name=$2
这里,-n
被认为是第一个自变量($1
),而pre-mount
是第二个自变量($2
)。所以,_rdshell_name
的值变成了pre-mount
。
#vim /usr/lib/dracut-lib.sh
1123 emergency_shell()
1124 {
1125 local _ctty
1126 set +e
1127 local _rdshell_name="dracut" action="Boot" hook="emergency"
1128 local _emergency_action
1129
1130 if [ "$1" = "-n" ]; then
1131 _rdshell_name=$2
1132 shift 2
1133 elif [ "$1" = "--shutdown" ]; then
1134 _rdshell_name=$2; action="Shutdown"; hook="shutdown-emergency"
1135 if type plymouth >/dev/null 2>&1; then
1136 plymouth --hide-splash
1137 elif [ -x /oldroot/bin/plymouth ]; then
1138 /oldroot/bin/plymouth --hide-splash
1139 fi
1140 shift 2
1141 fi
1142
1143 echo ; echo
1144 warn "$*"
1145 echo
1146
1147 _emergency_action=$(getarg rd.emergency)
1148 [ -z "$_emergency_action" ] \
1149 && [ -e /run/initramfs/.die ] \
1150 && _emergency_action=halt
1151
1152 if getargbool 1 rd.shell -d -y rdshell || getarg rd.break -d rdbreak; then
1153 _emergency_shell $_rdshell_name
1154 else
1155 source_hook "$hook"
1156 warn "$action has failed. To debug this issue add \"rd.shell rd.debug\" to the kernel command line."
1157 [ -z "$_emergency_action" ] && _emergency_action=halt
1158 fi
1159
1160 case "$_emergency_action" in
1161 reboot)
1162 reboot || exit 1;;
1163 poweroff)
1164 poweroff || exit 1;;
1165 halt)
1166 halt || exit 1;;
1167 esac
1168 }
然后,在最后,它从同一个文件中调用另一个_emergency_shell
函数(注意函数名前的下划线)。如您所见,_rdshell_name
是_emergency_shell
函数的参数。
_emergency_shell $_rdshell_name
在_emergency_shell()
函数内部,我们可以看到_name
得到参数,也就是pre-mount
。
local _name="$1"
#vim usr/lib/dracut-lib.sh
1081 _emergency_shell()
1082 {
1083 local _name="$1"
1084 if [ -n "$DRACUT_SYSTEMD" ]; then
1085 > /.console_lock
1086 echo "PS1=\"$_name:\\\${PWD}# \"" >/etc/profile
1087 systemctl start dracut-emergency.service
1088 rm -f -- /etc/profile
1089 rm -f -- /.console_lock
1090 else
1091 debug_off
1092 source_hook "$hook"
1093 echo
1094 /sbin/rdsosreport
1095 echo 'You might want to save "/run/initramfs/rdsosreport.txt" to a USB stick or /boot'
1096 echo 'after mounting them and attach it to a bug report.'
1097 if ! RD_DEBUG= getargbool 0 rd.debug -d -y rdinitdebug -d -y rdnetdebug; then
1098 echo
1099 echo 'To get more debug information in the report,'
1100 echo 'reboot with "rd.debug" added to the kernel command line.'
1101 fi
1102 echo
1103 echo 'Dropping to debug shell.'
1104 echo
1105 export PS1="$_name:\${PWD}# "
1106 [ -e /.profile ] || >/.profile
1107
1108 _ctty="$(RD_DEBUG= getarg rd.ctty=)" && _ctty="/dev/${_ctty##*/}"
1109 if [ -z "$_ctty" ]; then
1110 _ctty=console
1111 while [ -f /sys/class/tty/$_ctty/active ]; do
1112 _ctty=$(cat /sys/class/tty/$_ctty/active)
1113 _ctty=${_ctty##* } # last one in the list
1114 done
1115 _ctty=/dev/$_ctty
1116 fi
1117 [ -c "$_ctty" ] || _ctty=/dev/tty1
1118 case "$(/usr/bin/setsid --help 2>&1)" in *--ctty*) CTTY="--ctty";; esac
1119 setsid $CTTY /bin/sh -i -l 0<>$_ctty 1<>$_ctty 2<>$_ctty
1120 fi
相同的pre-mount
字符串已被传递给PS1
。让我们先看看PS1
到底是什么。
PS1
称为一个伪变量。当用户成功登录时,bash 会显示出来。这里有一个例子:
[root@fedora home]#
| | | |
[username]@[host][CWD][# since it is a root user]
bash 接受的理想条目是PS1='\u:\w\$'
。
-
这是用户名。
-
这是工作目录。
-
**KaTeX parse error: Expected 'EOF', got '#' at position 18: … =如果 UID 为 0,则`#̲`;否则`'`。
所以,在我们的例子中,当我们得到一个紧急 shell 时,PS1
将被 shell 打印如下:
'pre-mount#'
接下来在源代码中,您可以看到PS1
变量的新值也被添加到了/etc/profile.
中,原因是 bash 每次在将 shell 呈现给用户之前都会读取这个文件。最后,我们简单地启动了dracut-emergency
服务。
systemctl start dracut-emergency.service
以下是 initramfs 的usr/lib/systemd/system/
中的dracut-emergency.service
文件:
# cat usr/lib/systemd/system/dracut-emergency.service | grep -v #'
[Unit]
Description=Dracut Emergency Shell
DefaultDependencies=no
After=systemd-vconsole-setup.service
Wants=systemd-vconsole-setup.service
Conflicts=shutdown.target emergency.target
[Service]
Environment=HOME=/
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
WorkingDirectory=/
ExecStart=-/bin/dracut-emergency
ExecStopPost=-/bin/rm -f -- /.console_lock
Type=oneshot
StandardInput=tty-force
StandardOutput=inherit
StandardError=inherit
KillMode=process
IgnoreSIGPIPE=no
TasksMax=infinity
KillSignal=SIGHUP
服务只是简单地执行/bin/dracut-emergency
。这个脚本首先停止plymouth
服务。
type plymouth >/dev/null 2>&1 && plymouth quit
这会将hook
变量的值存储为emergency
,并使用emergency
参数调用source_hook
函数。
export _rdshell_name="dracut" action="Boot" hook="emergency"
source_hook "$hook"
# vim bin/dracut-emergency
1 #!/usr/bin/sh
2
3 export DRACUT_SYSTEMD=1
4 if [ -f /dracut-state.sh ]; then
5 . /dracut-state.sh 2>/dev/null
6 fi
7 type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh
8
9 source_conf /etc/conf.d
10
11 type plymouth >/dev/null 2>&1 && plymouth quit
12
13 export _rdshell_name="dracut" action="Boot" hook="emergency"
14 _emergency_action=$(getarg rd.emergency)
15
16 if getargbool 1 rd.shell -d -y rdshell || getarg rd.break -d rdbreak; then
17 FSTXT="/run/dracut/fsck/fsck_help_$fstype.txt"
18 source_hook "$hook"
19 echo
20 rdsosreport
21 echo
22 echo
23 echo Entering emergency mode. Exit the shell to continue.'
24 echo Type "journalctl" to view system logs.'
25 echo You might want to save "/run/initramfs/rdsosreport.txt" to a USB stick or /boot'
26 echo after mounting them and attach it to a bug report.'
27 echo
28 echo
29 [ -f "$FSTXT" ] && cat "$FSTXT"
30 [ -f /etc/profile ] && . /etc/profile
31 [ -z "$PS1" ] && export PS1="$_name:\${PWD}# "
32 exec sh -i -l
33 else
34 export hook="shutdown-emergency"
35 warn "$action has failed. To debug this issue add \"rd.shell rd.debug\" to the kernel command line."
36 source_hook "$hook"
37 [ -z "$_emergency_action" ] && _emergency_action=halt
38 fi
39
40 /bin/rm -f -- /.console_lock
41
42 case "$_emergency_action" in
43 reboot)
44 reboot || exit 1;;
45 poweroff)
46 poweroff || exit 1;;
47 halt)
48 halt || exit 1;;
49 esac
50
51 exit 0
在usr/lib/dracut-lib.sh
中再次定义了source_hook
功能。
source_hook() {
local _dir
_dir=$1; shift
source_all "/lib/dracut/hooks/$_dir" "$@"
}
_dir
变量已经捕获了钩子名称,即emergency
。所有的钩子都只是一堆脚本,从 initramfs 的/lib/dracut/hooks/
目录中存储和执行。
# tree usr/lib/dracut/hooks/
usr/lib/dracut/hooks/
├── cleanup
├── cmdline
│ ├── 30-parse-lvm.sh
│ ├── 91-dhcp-root.sh
│ └── 99-nm-config.sh
├── emergency
│ └── 50-plymouth-emergency.sh
├── initqueue
│ ├── finished
│ ├── online
│ ├── settled
│ │ └── 99-nm-run.sh
│ └── timeout
│ └── 99-rootfallback.sh
├── mount
├── netroot
├── pre-mount
├── pre-pivot
│ └── 85-write-ifcfg.sh
├── pre-shutdown
├── pre-trigger
├── pre-udev
│ └── 50-ifname-genrules.sh
├── shutdown
│ └── 25-dm-shutdown.sh
└── shutdown-emergency
对于紧急钩子,它正在执行usr/lib/dracut/hooks/emergency/50-plymouth-emergency.sh
,这正在停止plymouth
服务。
#!/usr/bin/sh
plymouth --hide-splash 2>/dev/null || :
一旦emergency
挂钩被执行并且plymouth
被停止,它将返回到bin/dracut-emergency
并打印以下横幅:
echo Entering emergency mode. Exit the shell to continue.'
echo Type "journalctl" to view system logs.'
echo You might want to save "/run/initramfs/rdsosreport.txt" to a USB stick or /boot'
echo after mounting them and attach it to a bug report.'
因此,rd.break=hook_name
用户通过了什么并不重要。systemd 将执行emergency
钩子,一旦横幅被打印出来,它将获取我们已经添加了PS1=_rdshell_name
/ PS1=hook_name
的/etc/profile
目录,然后我们就可以简单地运行 bash shell 了。
exec sh -i –l
当 shell 开始运行时,它会读取/etc/profile
,并找到PS1=hook_name
变量。在这里,hook_name
就是pre-mount
。这就是为什么pre-mount
作为 bash 的提示名被印了出来。请参考图 8-2 所示的流程图,以便更好地理解这一点。
图 8-2
流程图
如果用户向rd.break
传递任何其他参数,例如initqueue
,那么它将被馈入PS1
、_rdshell_name
和钩子变量。稍后,bash 将通过紧急服务被调用。Bash 将从/etc/profile
文件中读取PS1
值,并在提示中显示initqueue
名称。
结论是,相同的 bash shell 会以不同的提示名称(cmdline
、pre-mount
、switch_root
、pre-udev
、emergency
等)提供给用户。)但是在 initramfs 的不同引导阶段。
cmdline:/# pre-udev:/#
pre-trigger:/# initqueue:/#
pre-mount:/# pre-pivot:/#
switch_root:/#
与此类似,rescue.target
将由 systemd 执行。
救援服务和紧急服务
救援服务在 systemd 世界中也被称为单用户模式。因此,如果用户请求以单用户模式引导,那么 systemd 实际上会在rescue.service
阶段将用户放在紧急 shell 中。图 8-3 显示了到目前为止的引导顺序。
图 8-3
引导序列的流程图
你可以通过rescue.target
或者通过runlevel1.target
或者emergency.service
到systemd.unit
以单用户模式引导。如图 8-4 所示,这次我们将使用 Ubuntu 来探索引导阶段。
图 8-4
内核命令行参数
这会让我们陷入紧急状态。单用户模式、救援服务和紧急服务都启动dracut-emergency
二进制。这是我们在 dracut 的紧急挂钩中发布的相同二进制文件。
# cat usr/lib/systemd/system/emergency.service | grep -v ' #'
[Unit]
Description=Emergency Shell
DefaultDependencies=no
After=systemd-vconsole-setup.service
Wants=systemd-vconsole-setup.service
Conflicts=shutdown.target
Before=shutdown.target
[Service]
Environment=HOME=/
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
WorkingDirectory=/
ExecStart=/bin/dracut-emergency
ExecStopPost=-/usr/bin/systemctl --fail --no-block default
Type=idle
StandardInput=tty-force
StandardOutput=inherit
StandardError=inherit
KillMode=process
IgnoreSIGPIPE=no
TasksMax=infinity
KillSignal=SIGHUP
# cat usr/lib/systemd/system/rescue.service | grep -v ' #'
[Unit]
Description=Emergency Shell
DefaultDependencies=no
After=systemd-vconsole-setup.service
Wants=systemd-vconsole-setup.service
Conflicts=shutdown.target
Before=shutdown.target
[Service]
Environment=HOME=/
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
WorkingDirectory=/
ExecStart=/bin/dracut-emergency
ExecStopPost=-/usr/bin/systemctl --fail --no-block default
Type=idle
StandardInput=tty-force
StandardOutput=inherit
StandardError=inherit
KillMode=process
IgnoreSIGPIPE=no
TasksMax=infinity
KillSignal=SIGHUP
众所周知,dracut-emergency
脚本执行一个 bash shell。
# vim bin/dracut-emergency
1 #!/usr/bin/sh
2
3 export DRACUT_SYSTEMD=1
4 if [ -f /dracut-state.sh ]; then
5 . /dracut-state.sh 2>/dev/null
6 fi
7 type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh
8
9 source_conf /etc/conf.d
10
11 type plymouth >/dev/null 2>&1 && plymouth quit
12
13 export _rdshell_name="dracut" action="Boot" hook="emergency"
14 _emergency_action=$(getarg rd.emergency)
15
16 if getargbool 1 rd.shell -d -y rdshell || getarg rd.break -d rdbreak; then
17 FSTXT="/run/dracut/fsck/fsck_help_$fstype.txt"
18 source_hook "$hook"
19 echo
20 rdsosreport
21 echo
22 echo
23 echo 'Entering emergency mode. Exit the shell to continue.'
24 echo 'Type "journalctl" to view system logs.'
25 echo 'You might want to save "/run/initramfs/rdsosreport.txt" to a USB stick or /boot'
26 echo 'after mounting them and attach it to a bug report.'
27 echo
28 echo
29 [ -f "$FSTXT" ] && cat "$FSTXT"
30 [ -f /etc/profile ] && . /etc/profile
31 [ -z "$PS1" ] && export PS1="$_name:\${PWD}# "
32 exec sh -i -l
33 else
34 export hook="shutdown-emergency"
35 warn "$action has failed. To debug this issue add \"rd.shell rd.debug\" to the kernel command line."
36 source_hook "$hook"
37 [ -z "$_emergency_action" ] && _emergency_action=halt
38 fi
39
40 /bin/rm -f -- /.console_lock
41
42 case "$_emergency_action" in
43 reboot)
44 reboot || exit 1;;
45 poweroff)
46 poweroff || exit 1;;
47 halt)
48 halt || exit 1;;
49 esac
50
51 exit 0
如图 8-5 所示,sysroot
还没有挂载,因为我们还没有到达启动的挂载阶段。
图 8-5
应急 Shell
我希望您现在理解了 systemd 如何在不同的引导阶段向用户呈现紧急 shell。在下一章中,我们将继续暂停的 systemd 的引导序列。
九、系统:第二部分
到目前为止,我们已经到达了服务dracut.pre-mount.service
,其中用户的根文件系统还没有挂载到 initramfs 中。systemd 的下一个引导阶段将在sysroot
上挂载根文件系统。
sysroot.mount
systemd 接受mount
dracut 命令行参数,这将把我们放到一个mount
紧急 shell 中。如图 9-1 所示,我们已经传递了rd.break=mount
内核命令行参数。
图 9-1
内核命令行参数
正如您在图 9-2 中看到的,sysroot
已经以只读模式挂载到用户的根文件系统中。
图 9-2
该安装钩
dracut.mount
钩子(usr/lib/systemd/system/dracut-mount.service
)将从 initramfs 运行/bin/dracut-mount
脚本,它将完成挂载部分。
#vim usr/lib/systemd/system/dracut-mount.service
如您所见,这是从 initramfs 执行dracut-mount
脚本,并导出带有sysroot
值的NEWROOT
变量。
Environment=NEWROOT=/sysroot
ExecStart=-/bin/dracut-mount
[Unit]
Description=dracut mount hook
Documentation=man:dracut-mount.service(8)
After=initrd-root-fs.target initrd-parse-etc.service
After=dracut-initqueue.service dracut-pre-mount.service
ConditionPathExists=/usr/lib/initrd-release
ConditionDirectoryNotEmpty=|/lib/dracut/hooks/mount
ConditionKernelCommandLine=|rd.break=mount
DefaultDependencies=no
Conflicts=shutdown.target emergency.target
[Service]
Environment=DRACUT_SYSTEMD=1
Environment=NEWROOT=/sysroot
Type=oneshot
ExecStart=-/bin/dracut-mount
StandardInput=null
StandardOutput=syslog
StandardError=syslog+console
KillMode=process
RemainAfterExit=yes
KillSignal=SIGHUP
#vim bin/dracut-mount
1 #!/usr/bin/sh
2 export DRACUT_SYSTEMD=1
3 if [ -f /dracut-state.sh ]; then
4 . /dracut-state.sh 2>/dev/null
5 fi
6 type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh
7
8 source_conf /etc/conf.d
9
10 make_trace_mem "hook mount" '1:shortmem' '2+:mem' '3+:slab'
11
12 getarg 'rd.break=mount' -d 'rdbreak=mount' && emergency_shell -n mount "Break mount"
13 # mount scripts actually try to mount the root filesystem, and may
14 # be sourced any number of times. As soon as one suceeds, no more are sourced.
15 i=0
16 while :; do
17 if ismounted "$NEWROOT"; then
18 usable_root "$NEWROOT" && break;
19 umount "$NEWROOT"
20 fi
21 for f in $hookdir/mount/*.sh; do
22 [ -f "$f" ] && . "$f"
23 if ismounted "$NEWROOT"; then
24 usable_root "$NEWROOT" && break;
25 warn "$NEWROOT has no proper rootfs layout, ignoring and removing offending mount hook"
26 umount "$NEWROOT"
27 rm -f -- "$f"
28 fi
29 done
30
31 i=$(($i+1))
32 [ $i -gt 20 ] && emergency_shell "Can't mount root filesystem"
33 done
34
35 export -p > /dracut-state.sh
36
37 exit 0
我们在第八章中看到了它是如何让我们陷入紧急状态的,以及它的相关功能。由于我们在 initramfs 中挂载用户的根文件系统后停止了引导序列,正如你在图 9-3 中看到的,已经执行了systemd-fstab-generator
,并且已经创建了-mount
单元文件。
图 9-3
systemd-fstab-生成器行为
请记住,在sysroot.mount
中添加的用户根文件系统名是从/proc/cmdline
文件中提取的。sysroot.mount
明确提到必须安装什么以及安装在哪里。
initrd.target
正如我们多次说过的,引导序列的最终目的是向用户提供用户的根文件系统,在这样做的同时,systemd 实现的主要阶段如下:
-
找到用户的根文件系统。
-
挂载用户的根文件系统(我们已经到了引导阶段)。
-
找到其他必要的文件系统并挂载它们(
usr
、var
、nfs
、cifs
等)。). -
切换到挂载的用户的根文件系统。
-
启动用户空间守护进程。
-
启动
multi-user.target
或graphical.target
(这超出了本书的范围)。
如您所见,到目前为止,我们已经到了第 2 步,在 initramfs 中挂载用户的根文件系统。我们都知道 systemd 有.targets
,target
不过是一堆单元文件。只有当.target
的所有单元文件都成功启动后,它才能成功启动。
systemd 世界里有很多目标,比如basic.target
、multi-user.target
、graphical.target
、default.target
、sysinit.target
等等。initramfs
的最终目的是实现initrd.target
。一旦initrd.target
成功启动,那么 systemd 就会switch_root
进入其中。所以,首先,让我们看看initrd.target
以及它在引导序列中的位置。请参考图 9-4 所示的流程图。
图 9-4
引导序列
当你在 initramfs 之外(也就是说在switch_root
之后),systemd 的default.target
会是multi-user.target
或者graphical.target
,而在 initramfs 之内(也就是说在switch_root
之前)basic.target
之后,systemd 的default.target
会是initrd.target
。所以,在成功完成sysinit.target
和basic.target
之后,systemd 的主要任务就是实现initrd.target
。为了到达那里,systemd 将使用sysroot.mount
阶段来读取由systemd-fstab-generator
创建的挂载单元文件。服务dracut-mount.service
将把用户的根文件系统挂载到/sysroot
,然后 systemd 将执行服务initrd-parse-etc.service.
,它将解析/sysroot/etc/fstab
文件,并为usr
或任何其他设置了x-initrd.mount
选项的挂载点创建挂载单元文件。这就是initrd-parse-etc.service
的工作原理:
# cat usr/lib/systemd/system/initrd-parse-etc.service | grep -v '#'
[Unit]
Description=Reload Configuration from the Real Root
DefaultDependencies=no
Requires=initrd-root-fs.target
After=initrd-root-fs.target
OnFailure=emergency.target
OnFailureJobMode=replace-irreversibly
ConditionPathExists=/etc/initrd-release
[Service]
Type=oneshot
ExecStartPre=-/usr/bin/systemctl daemon-reload
ExecStart=-/usr/bin/systemctl --no-block start initrd-fs.target
ExecStart=/usr/bin/systemctl --no-block start initrd-cleanup.service
基本上,服务是通过一个daemon-reload
开关来执行systemctl
的。这将重新加载 systemd 管理器配置。这将重新运行所有生成器,重新加载所有单元文件,并重新创建整个依赖关系树。当守护进程被重新加载时,systemd 代表用户配置监听的所有套接字将保持可访问。将重新执行的 systemd 生成器如下:
# ls usr/lib/systemd/system-generators/ -l
total 92
-rwxr-xr-x. 1 root root 3750 Jan 10 19:18 dracut-rootfs-generator
-rwxr-xr-x. 1 root root 45640 Dec 21 12:19 systemd-fstab-generator
-rwxr-xr-x. 1 root root 37032 Dec 21 12:19 systemd-gpt-auto-generator
如您所见,它将执行systemd-fstab-generator
,读取/sysroot/etc/fstab
条目,并为usr
和设置了x-initrd.mount
选项的设备创建挂载单元文件。总之,systemd-fstab-generator
已经执行了两次。
因此,当您进入挂载 shell ( rd.break=mount
)时,您实际上中断了目标initrd.target
之后的引导序列。该目标仅运行以下服务:
# ls usr/lib/systemd/system/initrd.target.wants/
dracut-cmdline-ask.service dracut-mount.service dracut-pre-trigger.service
dracut-cmdline.service dracut-pre-mount.service dracut-pre-udev.service
dracut-initqueue.service dracut-pre-pivot.service
请参考图 9-5 以更好地理解这一点。
图 9-5
initrd.target 的整体执行
开关根/枢轴根
现在我们已经到了 systemd 引导的最后阶段,也就是switch_root
。systemd 将根文件系统从 initramfs ( /
)切换到用户的根文件系统(/sysroot
)。systemd 通过采取以下步骤来实现这一点:
-
挂载新的根文件系统(
/sysroot
) -
将它转换成根文件系统(
/
) -
删除对旧的(initramfs)根文件系统的所有访问
-
卸载 initramfs 文件系统并取消分配 ramfs 文件系统
本章将讨论三个要点。
-
switch_root
:我们会用老的init
方式来解释。 -
pivot_root
:我们将以systemd
的方式来解释这一点。 -
我们将在第十章中解释这一点。
在基于 init 的系统上切换到新的根文件系统
基于init
的系统使用switch_root
切换到新的根文件系统(sysroot
)。switch_root
的用途在它的手册页上有很好的解释,如下所示:
#man switch_root
NAME
switch_root - switch to another filesystem as the root of the mount tree
SYNOPSIS
switch_root [-hV]
switch_root newroot init [arg...]
DESCRIPTION
switch_root moves already mounted /proc, /dev, /sys and /run to newroot and makes newroot the new root filesystem and starts init process.
WARNING: switch_root removes recursively all files and directories on the current root filesystem.
OPTIONS
-h, --help
Display help text and exit.
-V, --version
Display version information and exit.
RETURN VALUE
switch_root returns 0 on success and 1 on failure.
NOTES
switch_root will fail to function
if newroot is not the root of a mount. If you want to switch root into a directory that does not meet this requirement then you can first use a bind-mounting trick to turn any directory into a mount point:
mount --bind $DIR $DIR
因此,它切换到一个新的根文件系统(sysroot
),并与根文件系统一起移动旧的根文件系统的虚拟文件系统(proc
、dev
、sys
等)。)到新根。switch_root
最好的特性是在挂载新的根文件系统后,它自己启动init
进程。切换到新的根文件系统发生在 dracut 的源代码中。在写这本书的时候,dracut 的最新版本是 049。switch_root
功能在dracut-049/modules.d/99base/init.sh
文件中定义。
387 unset PS4
388
389 CAPSH=$(command -v capsh)
390 SWITCH_ROOT=$(command -v switch_root)
391 PATH=$OLDPATH
392 export PATH
393
394 if [ -f /etc/capsdrop ]; then
395 . /etc/capsdrop
396 info "Calling $INIT with capabilities $CAPS_INIT_DROP dropped."
397 unset RD_DEBUG
398 exec $CAPSH --drop="$CAPS_INIT_DROP" -- \
399 -c "exec switch_root \"$NEWROOT\" \"$INIT\" $initargs" || \
400 {
401 warn "Command:"
402 warn capsh --drop=$CAPS_INIT_DROP -- -c exec switch_root "$NEWROOT" "$INIT" $initargs
403 warn "failed."
404 emergency_shell
405 }
406 else
407 unset RD_DEBUG
408 exec $SWITCH_ROOT "$NEWROOT" "$INIT" $initargs || {
409 warn "Something went very badly wrong in the initramfs. Please "
410 warn "file a bug against dracut."
411 emergency_shell
412 }
413 fi
在前面的代码中,您可以看到exec switch_root
已经被调用,就像在switch_root
的手册页上描述的那样。NEWROOT
和INIT
的定义变量值如下:
NEWROOT = "/sysroot"
INIT = 'init' or 'sbin/init'
仅供参考,这些天的init
文件是一个symlink
到systemd
。
# ls -l sbin/init
lrwxrwxrwx. 1 root root 22 Dec 21 12:19 sbin/init -> ../lib/systemd/systemd
为了成功地switch_root
虚拟文件系统,必须首先挂载它们。这将通过dracut-049/modules.d/99base/init.sh
来实现。以下是将要遵循的步骤:
-
挂载
proc
文件系统。 -
挂载
sys
文件系统。 -
用
devtmpfs
挂载/dev
目录。 -
手动创建
stdin
、stdout
、stderr
、pts
和shm
设备文件。 -
制作包含 tmpfs 的
/run
挂载点。(/run
挂载点在基于init
的系统上不可用。)
#vim dracut-049/modules.d/99base/init.sh
11 NEWROOT="/sysroot"
12 [ -d $NEWROOT ] || mkdir -p -m 0755 $NEWROOT
13
14 OLDPATH=$PATH
15 PATH=/usr/sbin:/usr/bin:/sbin:/bin
16 export PATH
17
18 # mount some important things
19 [ ! -d /proc/self ] && \
20 mount -t proc -o nosuid,noexec,nodev proc /proc >/dev/null
21
22 if [ "$?" != "0" ]; then
23 echo "Cannot mount proc on /proc! Compile the kernel with CONFIG_PROC_FS!"
24 exit 1
25 fi
26
27 [ ! -d /sys/kernel ] && \
28 mount -t sysfs -o nosuid,noexec,nodev sysfs /sys >/dev/null
29
30 if [ "$?" != "0" ]; then
31 echo "Cannot mount sysfs on /sys! Compile the kernel with CONFIG_SYSFS!"
32 exit 1
33 fi
34
35 RD_DEBUG=""
36 . /lib/dracut-lib.sh
37
38 setdebug
39
40 if ! ismounted /dev; then
41 mount -t devtmpfs -o mode=0755,noexec,nosuid,strictatime devtmpfs /dev >/dev/null
42 fi
43
44 if ! ismounted /dev; then
45 echo "Cannot mount devtmpfs on /dev! Compile the kernel with CONFIG_DEVTMPFS!"
46 exit 1
47 fi
48
49 # prepare the /dev directory
50 [ ! -h /dev/fd ] && ln -s /proc/self/fd /dev/fd >/dev/null 2>&1
51 [ ! -h /dev/stdin ] && ln -s /proc/self/fd/0 /dev/stdin >/dev/null 2>&1
52 [ ! -h /dev/stdout ] && ln -s /proc/self/fd/1 /dev/stdout >/dev/null 2>&1
53 [ ! -h /dev/stderr ] && ln -s /proc/self/fd/2 /dev/stderr >/dev/null 2>&1
54
55 if ! ismounted /dev/pts; then
56 mkdir -m 0755 /dev/pts
57 mount -t devpts -o gid=5,mode=620,noexec,nosuid devpts /dev/pts >/dev/null
58 fi
59
60 if ! ismounted /dev/shm; then
61 mkdir -m 0755 /dev/shm
62 mount -t tmpfs -o mode=1777,noexec,nosuid,nodev,strictatime tmpfs /dev/shm >/dev/null
63 fi
64
65 if ! ismounted /run; then
66 mkdir -m 0755 /newrun
67 if ! str_starts "$(readlink -f /bin/sh)" "/run/"; then
68 mount -t tmpfs -o mode=0755,noexec,nosuid,nodev,strictatime tmpfs /newrun >/dev/null
69 else
70 # the initramfs binaries are located in /run, so don't mount it with noexec
71 mount -t tmpfs -o mode=0755,nosuid,nodev,strictatime tmpfs /newrun >/dev/null
72 fi
73 cp -a /run/* /newrun >/dev/null 2>&1
74 mount --move /newrun /run
75 rm -fr -- /newrun
76 fi
在基于 systemd 的系统上切换到新的根文件系统
这些步骤几乎类似于我们讨论的基于init
的系统。对于systemd
来说,唯一的区别就是用 C 代码做的二进制。因此,很明显,切换根将发生在 systemd 的 C 源代码中,如下所示:
src/shared/switch-root.c:
首先,考虑以下情况:
new_root = sysroot
old_root = /
这将移动已经在 initramfs 的根文件系统中填充的虚拟文件系统;然后,path_equal
函数检查new_root
路径是否可用。
if (path_equal(new_root, "/"))
return 0;
稍后,它调用一个pivot_root
系统调用(init
使用switch_root
)并将根从/
(initramfs 根文件系统)更改为sysroot
(用户的根文件系统)。
pivot_root(new_root, resolved_old_root_after) >= 0)
在我们进一步讨论之前,我们需要了解pivot_root
是什么,它做什么。
注意,根据 pivot_root 的实现,调用者的 root 和 cwd 可能会也可能不会改变。下面是调用 pivot_root 的顺序,无论哪种情况都适用,假设 pivot_root 和 chroot 都在当前路径:
CD new _ root
pivot _ root。
exec ch root。命令
注意,chroot 必须在旧根下和新根下可用,因为 pivot_root 可能隐式更改了 shell 的根目录,也可能没有。
注意,exec chroot 改变正在运行的可执行文件,如果以后要卸载旧的根目录,这是必须的。还要注意,标准输入、输出和错误可能仍然指向旧的根文件系统上的设备,使其保持忙碌。当调用 chroot 时,可以很容易地更改它们(见下文;请注意,无论 pivot_root 是否更改了 shell 的根,都没有使用前导斜杠)。
# man pivot_root
NAME
pivot_root - change the root filesystem
SYNOPSIS
pivot_root new_root put_old
DESCRIPTION
pivot_root moves the root file system of the current process to the directory put_old and makes new_root the new root file system. Since pivot_root(8) simply calls pivot_root(2), we refer to the man page of the latter for further details:
pivot_root
将当前进程(systemd)的根文件系统(initramfs 根文件系统)更改为新的根文件系统(sysroot
),同时将正在运行的可执行文件(systemd from initramfs)更改为新的可执行文件(systemd from user ’ s root file system)。
在pivot_root
之后,它分离 initramfs ( src/shared/switch-root.c
)的旧根设备。
# vim src/shared/switch-root.c
96 /* We first try a pivot_root() so that we can umount the old root dir. In many cases (i.e. where rootfs is /),
97 * that's not possible however, and hence we simply overmount root */
98 if (pivot_root(new_root, resolved_old_root_after) >= 0) {
99
100 /* Immediately get rid of the old root, if detach_oldroot is set.
101 * Since we are running off it we need to do this lazily. */
102 if (unmount_old_root) {
103 r = umount_recursive(old_root_after, MNT_DETACH);
104 if (r < 0)
105 log_warning_errno(r, "Failed to unmount old root directory tree, ignoring: %m");
106 }
107
108 } else if (mount(new_root, "/", NULL, MS_MOVE, NULL) < 0)
109 return log_error_errno(errno, "Failed to move %s to /: %m", new_root);
110
在成功的pivot_root
之后,这是当前状态:
-
sysroot
已经变成了根(/
)。 -
当前工作目录变成了根目录(
/
)。 -
这样 bash 会将其根目录从旧的根(initramfs)更改为新的(用户的)根文件系统。
chroot
将在下一章讨论。
最后删除old_root
设备(rm -rf
)。
110
111 if (chroot(".") < 0)
112 return log_error_errno(errno, "Failed to change root: %m");
113
114 if (chdir("/") < 0)
115 return log_error_errno(errno, "Failed to change directory: %m");
116
117 if (old_root_fd >= 0) {
118 struct stat rb;
119
120 if (fstat(old_root_fd, &rb) < 0)
121 log_warning_errno(errno, "Failed to stat old root directory, leaving: %m");
122 else
123 (void) rm_rf_children(TAKE_FD(old_root_fd), 0, &rb); /* takes possession of the dir fd, even on failure */
124 }
为了更好地理解,我强烈建议阅读这里显示的整个src/shared/switch-root.c
源代码:
1 /* SPDX-License-Identifier: LGPL-2.1+ */
2
3 #include <errno.h>
4 #include <fcntl.h>
5 #include <limits.h>
6 #include <stdbool.h>
7 #include <sys/mount.h>
8 #include <sys/stat.h>
9 #include <unistd.h>
10
11 #include "base-filesystem.h"
12 #include "fd-util.h"
13 #include "fs-util.h"
14 #include "log.h"
15 #include "missing_syscall.h"
16 #include "mkdir.h"
17 #include "mount-util.h"
18 #include "mountpoint-util.h"
19 #include "path-util.h"
20 #include "rm-rf.h"
21 #include "stdio-util.h"
22 #include "string-util.h"
23 #include "strv.h"
24 #include "switch-root.h"
25 #include "user-util.h"
26 #include "util.h"
27
28 int switch_root(const char *new_root,
29 const char *old_root_after, /* path below the new root, where to place the old root after the transition */
30 bool unmount_old_root,
31 unsigned long mount_flags) { /* MS_MOVE or MS_BIND */
32
33 _cleanup_free_ char *resolved_old_root_after = NULL;
34 _cleanup_close_ int old_root_fd = -1;
35 bool old_root_remove;
36 const char *i;
37 int r;
38
39 assert(new_root);
40 assert(old_root_after);
41
42 if (path_equal(new_root, "/"))
43 return 0;
44
45 /* Check if we shall remove the contents of the old root */
46 old_root_remove = in_initrd();
47 if (old_root_remove) {
48 old_root_fd = open("/", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_NOCTTY|O_DIRECTORY);
49 if (old_root_fd < 0)
50 return log_error_errno(errno, "Failed to open root directory: %m");
51 }
52
53 /* Determine where we shall place the old root after the transition */
54 r = chase_symlinks(old_root_after, new_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved_old_root_after, NULL);
55 if (r < 0)
56 return log_error_errno(r, "Failed to resolve %s/%s: %m", new_root, old_root_after);
57 if (r == 0) /* Doesn't exist yet. Let's create it */
58 (void) mkdir_p_label(resolved_old_root_after, 0755);
59
60 /* Work-around for kernel design: the kernel refuses MS_MOVE if any file systems are mounted MS_SHARED. Hence
61 * remount them MS_PRIVATE here as a work-around.
62 *
63 * https://bugzilla.redhat.com/show_bug.cgi?id=847418 */
64 if (mount(NULL, "/", NULL, MS_REC|MS_PRIVATE, NULL) < 0)
65 return log_error_errno(errno, "Failed to set \"/\" mount propagation to private: %m");
66
67 FOREACH_STRING(i, "/sys", "/dev", "/run", "/proc") {
68 _cleanup_free_ char *chased = NULL;
69
70 r = chase_symlinks(i, new_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &chased, NULL);
71 if (r < 0)
72 return log_error_errno(r, "Failed to resolve %s/%s: %m", new_root, i);
73 if (r > 0) {
74 /* Already exists. Let's see if it is a mount point already. */
75 r = path_is_mount_point(chased, NULL, 0);
76 if (r < 0)
77 return log_error_errno(r, "Failed to determine whether %s is a mount point: %m", chased);
78 if (r > 0) /* If it is already mounted, then do nothing */
79 continue;
80 } else
81 /* Doesn't exist yet? */
82 (void) mkdir_p_label(chased, 0755);
83
84 if (mount(i, chased, NULL, mount_flags, NULL) < 0)
85 return log_error_errno(errno, "Failed to mount %s to %s: %m", i, chased);
86 }
87
88 /* Do not fail if base_filesystem_create() fails. Not all switch roots are like base_filesystem_create() wants
89 * them to look like. They might even boot, if they are RO and don't have the FS layout. Just ignore the error
90 * and switch_root() nevertheless. */
91 (void) base_filesystem_create(new_root, UID_INVALID, GID_INVALID);
92
93 if (chdir(new_root) < 0)
94 return log_error_errno(errno, "Failed to change directory to %s: %m", new_root);
95
96 /* We first try a pivot_root() so that we can umount the old root dir. In many cases (i.e. where rootfs is /),
97 * that's not possible however, and hence we simply overmount root */
98 if (pivot_root(new_root, resolved_old_root_after) >= 0) {
99
100 /* Immediately get rid of the old root, if detach_oldroot is set.
101 * Since we are running off it we need to do this lazily. */
102 if (unmount_old_root) {
103 r = umount_recursive(old_root_after, MNT_DETACH);
104 if (r < 0)
105 log_warning_errno(r, "Failed to unmount old root directory tree, ignoring: %m");
106 }
107
108 } else if (mount(new_root, "/", NULL, MS_MOVE, NULL) < 0)
109 return log_error_errno(errno, "Failed to move %s to /: %m", new_root);
110
111 if (chroot(".") < 0)
112 return log_error_errno(errno, "Failed to change root: %m");
113
114 if (chdir("/") < 0)
115 return log_error_errno(errno, "Failed to change directory: %m");
116
117 if (old_root_fd >= 0) {
118 struct stat rb;
119
120 if (fstat(old_root_fd, &rb) < 0)
121 log_warning_errno(errno, "Failed to stat old root directory, leaving: %m");
122 else
123 (void) rm_rf_children(TAKE_FD(old_root_fd), 0, &rb); /* takes possession of the dir fd, even on failure */
124 }
125
126 return 0;
127 }
这里,我们已经成功地切换到用户的根文件系统,并离开了 initramfs 环境。现在,PID 为 1 的用户根文件系统中的 systemd 将开始运行,并负责引导过程的其余部分,如下所示:
-
systemd 将启动
httpd
、mysql
、postfix
、network services
等用户空间服务。 -
最终,目标将是达到
default.target
。我们之前讨论过,在switch_root
之前,systemd 的目标default.target
会是initrd.target
,在switch_root
之后,不是multi-user.target
就是graphical.target
。
但是从 initramfs(根文件系统)开始的现有的systemd
进程会发生什么呢?是在switch_root
还是pivot_root
之后被干掉?新的systemd
进程是从用户的根文件系统开始的吗?
答案很简单。
-
initramfs 的 systemd 创建一个管道。
-
forks 系统。
-
原 PID 1
chroot
变为/systemd
并执行/sysroot/usr/lib/systemd/systemd
。 -
分叉的 systemd 通过管道将其状态序列化为 PID 1 并退出。
-
PID 1 反序列化来自管道的数据,并继续使用
/
(以前的/sysroot
)中的新配置。
我希望您喜欢 initramfs 中的 systemd 之旅。正如我们前面提到的,systemd 引导序列的其余部分将发生在 initramfs 之外,与我们到目前为止所讨论的差不多。
GUI 如何启动超出了本书的范围。在我们的下一章,我们将讨论现场 ISO 图像和救援模式。
6# 十、救援模式和实时图像
在这最后一章,我们将涵盖救援模式和现场图像。在我们的救援模式讨论中,我们将涵盖救援 initramfs,以及一些“无法启动”的问题。实时映像讨论包括 Squashfs、rootfs.img
和实时映像的引导顺序。
救援模式
在救援模式下有两种启动方式。
图 10-2
来自实时图像的救援模式条目
-
Through the built-in GRUB menuentry. Refer to Figure 10-1.
图 10-1
GRUB 中的救援模式条目
-
通过实时 ISO 图像。参见图 10-2 。
顾名思义,这种模式旨在拯救陷入“无法启动”问题的系统。想象一下这样一种情况,系统无法挂载根文件系统,您会收到这样一条永无止境的通用消息:
dracut-initqueue: warning dracut-initqueue timeout - starting timeout scripts
’。
假设您只安装了一个内核,如下所示:
<snip>
.
.
[ OK ] Started Show Plymouth Boot Screen.
[ OK ] Started Forward Password R...s to Plymouth Directory Watch.
[ OK ] Reached target Paths.
[ OK ] Reached target Basic System.
[ 145.832487] dracut-initqueue[437]: Warning: dracut-initqueue timeout - starting timeout scripts
[ 146.541525] dracut-initqueue[437]: Warning: dracut-initqueue timeout - starting timeout scripts
[ 147.130873] dracut-initqueue[437]: Warning: dracut-initqueue timeout - starting timeout scripts
[ 147.703069] dracut-initqueue[437]: Warning: dracut-initqueue timeout - starting timeout scripts
[ 148.267123] dracut-initqueue[437]: Warning: dracut-initqueue timeout - starting timeout scripts
[ 148.852865] dracut-initqueue[437]: Warning: dracut-initqueue timeout - starting timeout scripts
[ 149.430171] dracut-initqueue[437]: Warning: dracut-initqueue timeout - starting timeout scripts
.
.
</snip>
由于这个系统只有一个内核(不能启动),如果没有环境,你将如何解决“不能启动”的问题?救援模式就是为了这个唯一的目的而创建的。让我们首先选择默认的救援模式,它预装在 Linux 中,可以从 GRUB 菜单中选择。请参见图 10-3 。
图 10-3
GRUB 屏幕
救援模式将正常启动,如图 10-4 所示,如果一切正常,它将向用户显示其根文件系统。
图 10-4
在救援模式下挂载的根文件系统
但是我想到了一个问题:当正常的内核无法启动时,为什么同一个系统能够在救援模式下启动呢?
这是因为当您安装 Fedora 或任何 Linux 发行版时,Linux 的安装程序 Anaconda 会在/boot
中安装两个内核。
# ls -lh /boot/
total 164M
-rw-r--r--. 1 root root 209K Oct 22 01:03 config-5.3.7-301.fc31.x86_64
drwx------. 4 root root 4.0K Oct 24 04:44 efi
-rw-r--r--. 1 root root 181K Aug 2 2019 elf-memtest86+-5.01
drwxr-xr-x. 2 root root 4.0K Oct 24 04:42 extlinux
drwx------. 5 root root 4.0K Mar 28 13:37 grub2
-rw-------. 1 root root 80M Dec 9 10:18 initramfs-0-rescue-2058a9f13f9e489dba29c477a8ae2493.img
-rw-------. 1 root root 32M Dec 9 10:19 initramfs-5.3.7-301.fc31.x86_64.img
drwxr-xr-x. 3 root root 4.0K Dec 9 10:18 loader
drwx------. 2 root root 16K Dec 9 10:12 lost+found
-rw-r--r--. 1 root root 179K Aug 2 2019 memtest86+-5.01
-rw-------. 1 root root 30M Jan 6 09:37 new.img
-rw-------. 1 root root 4.3M Oct 22 01:03 System.map-5.3.7-301.fc31.x86_64
-rwxr-xr-x. 1 root root 8.9M Dec 9 10:18 vmlinuz-0-rescue-2058a9f13f9e489dba29c477a8ae2493
-rwxr-xr-x. 1 root root 8.9M Oct 22 01:04 vmlinuz-5.3.7-301.fc31.x86_64
如您所见,vmlinuz-5.3.7-301.fc31.x86_64
是一个普通内核,而vmlinuz-0-rescue-19a08a3e86c24b459999fbac68e42c05
是一个救援内核,它是一个独立的内核,有自己的 initramfs 文件,名为initramfs-0-rescue-19a08a3e86c24b459999fbac68e42c05.img
。
假设你安装了一个由nvidia
提供的新软件包(.rpm
或.deb
),里面有新的图形驱动程序。由于图形驱动程序必须添加到 initramfs 中,nvidia
包重新构建了原来的内核 initramfs ( initramfs-5.3.7-301.fc31.x86_64.img
)。因此,原来的内核有新添加的图形驱动程序,但是 rescue initramfs 没有添加该驱动程序。当用户尝试引导时,由于安装的图形驱动程序与连接的图形卡不兼容,系统无法使用原始内核(vmlinuz-5.3.7-301.fc31.x86_64
)进行引导,但同时,由于不兼容的驱动程序不在 rescue initramfs 中,系统将使用 rescue 模式成功引导。救援模式内核将具有与普通内核相同的命令行参数,因此安装的救援内核知道用户根文件系统的名称。
图 10-5 显示了普通内核的命令行参数。
图 10-5
普通内核的命令行参数
图 10-6 显示了救援内核的命令行参数。
图 10-6
救援内核的命令行参数
救援模式初始化
救援模式 initramfs ( initramfs-0-rescue-2058a9f13f9e489dba29c477a8ae2493.img
)的大小比原始内核的 initramfs ( initramfs-5.3.7-301.fc31.x86_64.img
)大得多。
# ls -lh /boot/
total 164M
-rw-r--r--. 1 root root 209K Oct 22 01:03 config-5.3.7-301.fc31.x86_64
drwx------. 4 root root 4.0K Oct 24 04:44 efi
-rw-r--r--. 1 root root 181K Aug 2 2019 elf-memtest86+-5.01
drwxr-xr-x. 2 root root 4.0K Oct 24 04:42 extlinux
drwx------. 5 root root 4.0K Mar 28 13:37 grub2
-rw-------. 1 root root 80M Dec 9 10:18 initramfs-0-rescue-2058a9f13f9e489dba29c477a8ae2493.img
-rw-------. 1 root root 32M Dec 9 10:19 initramfs-5.3.7-301.fc31.x86_64.img
drwxr-xr-x. 3 root root 4.0K Dec 9 10:18 loader
drwx------. 2 root root 16K Dec 9 10:12 lost+found
-rw-r--r--. 1 root root 179K Aug 2 2019 memtest86+-5.01
-rw-------. 1 root root 30M Jan 6 09:37 new.img
-rw-------. 1 root root 4.3M Oct 22 01:03 System.map-5.3.7-301.fc31.x86_64
-rwxr-xr-x. 1 root root 8.9M Dec 9 10:18 vmlinuz-0-rescue-2058a9f13f9e489dba29c477a8ae2493
-rwxr-xr-x. 1 root root 8.9M Oct 22 01:04 vmlinuz-5.3.7-301.fc31.x86_64
这是为什么?这是因为救援 initramfs 不像普通内核的 initramfs 那样是特定于主机的。rescue initramfs 是一个通用的 initramfs,它是通过考虑用户可以在其上创建根文件系统的所有可能的设备而准备的。让我们比较一下这两个 initramfs 系统。
# tree
.
├── normal_kernel
│ └── initramfs-5.3.7-301.fc31.x86_64.img
└── rescue_kernel
└── initramfs-0-rescue-2058a9f13f9e489dba29c477a8ae2493.img
2 directories, 2 files
我们将把它们提取到各自的目录中。
#/usr/lib/dracut/skipcpio
initramfs-5.3.7-301.fc31.x86_64.img | gunzip -c | cpio -idv
#/usr/lib/dracut/skipcpio
initramfs-0-rescue-2058a9f13f9e489dba29c477a8ae2493.img | gunzip -c | cpio -idv
我们将从提取的 initramfs 中创建文件列表。
# tree normal_kernel/ > normal.txt
# tree rescue_kernel/ > rescue.txt
以下是两个 initramfs 系统之间的差异。与正常的 initramfs 相比,rescue initramfs 系统几乎多了 2,189 个文件。此外,几乎 719 额外的模块已被添加到救援 initramfs。
# diff -yt rescue.txt normal.txt | grep '<' | wc -l
2186
# diff -yt rescue.txt normal.txt | grep '<' | grep -i '.ko' | wc -l
719
<skip>
.
.
│ │ ├── lspci <
│ │ ├── mdadm <
│ │ ├── mdmon <
│ │ ├── mdraid-cleanup <
│ │ ├── mdraid_start <
│ │ ├── mount.cifs <
│ │ ├── mount.nfs <
│ │ ├── mount.nfs4 -> mount.nfs <
│ │ ├── mpathpersist <
│ │ ├── multipath <
│ │ ├── multipathd <
│ │ ├── nfsroot <
│ │ ├── partx <
│ │ ├── pdata_tools <
│ │ ├── ping -> ../bin/ping <
│ │ ├── ping6 -> ../bin/ping <
│ │ ├── rpcbind -> ../bin/rpcbind <
│ │ ├── rpc.idmapd <
│ │ ├── rpcinfo -> ../bin/rpcinfo <
│ │ ├── rpc.statd <
│ │ ├── setpci <
│ │ ├── showmount <
│ │ ├── thin_check -> pdata_tools <
│ │ ├── thin_dump -> pdata_tools <
│ │ ├── thin_repair -> pdata_tools <
│ │ ├── thin_restore -> pdata_tools <
│ │ ├── xfs_db <
│ │ ├── xfs_metadump <
│ │ └── xfs_repair <
├── lib <
│ ├── iscsi <
│ ├── lldpad <
│ ├── nfs <
│ │ ├── rpc_pipefs <
│ │ └── statd <
│ │ └── sm <
</skip>
rescue initramfs 将拥有用户可以在其上创建根文件系统的设备的几乎所有模块和支持的文件,而普通 initramfs 将是特定于主机的。它将只包含用户在其上创建了根文件系统的设备的那些模块和支持的文件。如果您想自己创建一个 rescue initramfs,那么您可以在基于 Fedora 的系统上安装一个dracut-config-generic
包。这个包只提供了一个文件,并且它具有关闭特定于主机的 initramfs 生成的配置。
# rpm -ql dracut-config-generic
/usr/lib/dracut/dracut.conf.d/02-generic-image.conf
# cat /usr/lib/dracut/dracut.conf.d/02-generic-image.conf
hostonly="no"
如您所见,该文件将限制 dracut 创建特定于主机的 initramfs。
“无法启动”问题 9 (chroot)
**问题:**普通内核和救援内核都无法启动。图 10-7 显示了正常的内核紧急信息。
图 10-7
内核紧急消息
抛出的内核紧急消息抱怨内核无法挂载根文件系统。我们前面看到,每当内核无法挂载用户的根文件系统时,它就会抛出dracut-initqueue
超时消息。
'dracut-initqueue: warning dracut-initqueue timeout - starting timeout scripts'
然而,这一次,恐慌信息是不同的。因此,看起来这个问题与用户的根文件系统无关。另一个线索是它提到了 VFS 文件系统;VFS 代表“虚拟文件系统”,因此这表示紧急消息无法从 initramfs 挂载根文件系统。基于这些线索,我想我们已经隔离了这个问题,我们应该把注意力集中在两个内核的 initramfs 上。
如图 10-8 所示,救援模式内核紧急信息也是类似的。
图 10-8
救援模式内核紧急消息
**解决方法:**以下是解决问题的步骤:
-
Since the installed rescue kernel is also panicking, we need to use the live image of Fedora or of any Linux distribution to boot. As shown in Figure 10-9 and Figure 10-10, we are using a live image of Fedora.
图 10-10
使用实时映像启动
图 10-9
实时图像欢迎屏幕
-
系统已在救援模式下启动。实时映像引导序列将在本章的“实时映像”一节中讨论。先成为
sudo
用户吧。
$ sudo su
We trust you have received the usual lecture from your local system administrator. It usually boils down to these three things:
#1) Respect the privacy of others.
#2) Think before you type.
#3) With great power comes great responsibility.
[root@localhost-live liveuser] #
-
我们在这里看到的根目录来自一个实时图像。因为实时映像内核不知道用户根文件系统的名称,所以它不能像救援内核一样挂载它。
[root@localhost-live liveuser]# ls / bin boot dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var
-
让我们来看看正常内核和救援内核的 initramfs 有什么问题。为此,我们需要首先挂载用户的根文件系统。
# vgscan -v Found volume group "fedora_localhost-live" using metadata type lvm2 # lvscan -v ACTIVE '/dev/fedora_localhost-live/swap' [2.20 GiB] inherit ACTIVE '/dev/fedora_localhost-live/root' [18.79 GiB] inherit # pvscan -v PV /dev/sda2 VG fedora_localhost-live lvm2 [<21.00 GiB / 0 free] Total: 1 [<21.00 GiB] / in use: 1 [<21.00 GiB] / in no VG: 0 [0 ]
如您所见,该系统有一个基于 LVM 的用户根文件系统。物理卷在 sda 设备上。接下来,我们将在一个临时目录中挂载用户的根文件系统。
-
让我们检查 initramfs 文件的状态。
# ls temp_root/boot/ -l total 0
# mkdir temp_root
# mount /dev/fedora_localhost-live/root temp_root/
# ls temp_root/
bin dev home lib64 media opt root sbin sys
tmp usr boot etc lib lost+found mnt proc run
srv @System.solv user_root_fs.txt var
用户根文件系统的引导目录为空。这是因为在这个系统上,引导是一个单独的分区。
# mount /dev/sda1 temp_root/boot/
#ls temp_root/boot/
Config-5.3.7-301.fc31.x86_64 efi elf-memtest86+-5.01
extlinux grub2 loader lost+found
Memtest86+-5.01 System.map-5.3.7-301.fc31.x86_64
vmlinuz-0-rescue-19a08a3e86c24b459999fbac68e42c05
vmlinuz-5.3.7-301.fc31.x86_64
令人惊讶的是,正如您所看到的,在用户的根文件系统上没有可用的 initramfs 文件,这就是两个内核都死机的原因。
因此,问题已经确定,我们需要重新生成 initramfs。要制作新的 initramfs,我们需要使用dracut
命令,但是有一些问题。
-
无论我们执行哪个二进制文件或命令,该二进制文件都将来自实时映像根文件系统。例如,
dracut
命令将从/usr/bin/dracut
运行,而用户根文件系统的二进制文件在temp_root/usr/bin/dracut
中。 -
要运行任何二进制文件,它需要像
libc.so
这样的支持库,这些库将再次从一个活动映像的根文件系统中使用。这意味着我们现在使用的整个环境来自实时图像,这可能会产生严重的问题。例如,我们可以安装任何包,它将被安装在实时映像的根文件系统中,而不是用户的根文件系统中。
简而言之,我们需要将当前的根文件系统(/
)从动态映像根文件系统更改为用户的根文件系统(temp_root
)。chroot
是我们为此需要使用的命令。
-
顾名思义,它会将 bash 的根从当前根更改为新根。只有当虚拟文件系统已经装载在新的根上时,
chroot
才会成功。root@localhost-live liveuser]# ls / bin boot dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var
我们当前的根是实时镜像根文件系统。在chroot
之前,我们将挂载proc
、dev
、devpts
、sys
和run
虚拟文件系统。
- 我们都被设置为进入用户的根文件系统。
# mount -v --bind /dev/ temp_root/dev
mount: /dev bound on /home/liveuser/temp_root/dev.
# mount -vt devpts devpts temp_root/dev/pts -o gid=5,mode=620
mount: devpts mounted on /home/liveuser/temp_root/dev/pts.
# mount -vt proc proc temp_root/proc
mount: proc mounted on /home/liveuser/temp_root/proc.
# mount -vt sysfs sysfs temp_root/sys
mount: sysfs mounted on /home/liveuser/temp_root/sys.
# mount -vt tmpfs tmpfs temp_root/run
mount: tmpfs mounted on /home/liveuser/temp_root/run.
# chroot temp_root/# ls
bin dev home lib64 media opt root sbin sys tmp
usr boot etc lib lost+found mnt proc run srv
@System.solv user_root_fs.txt var
所以,temp_root
现在成了 bash 的根文件系统。如果退出这个 shell,bash 会将其根目录从用户的根文件系统更改为动态镜像根文件系统。所以,只要我们在同一个 shell 实例中,我们的根目录就是temp_root
。现在,无论我们执行什么命令或二进制文件,它都将在用户的根文件系统环境中运行。因此,现在在这个环境中执行进程是完全安全的。
-
要解决这个“无法启动”的问题,我们需要重新生成 initramfs。
root@localhost-live /]# ls /lib/modules 5.3.7-301.fc31.x86_64 [root@localhost-live /]# cd /boot/ [root@localhost-live boot]# rpm -qa | grep -i 'kernel-5' kernel-5.3.7-301.fc31.x86_64 [root@localhost-live boot]# dracut initramfs-5.3.7-301.fc31.x86_64.img 5.3.7-301.fc31.x86_64
-
如果你想重新生成救援内核 initramfs,那么你需要安装一个
dracut-config-generic
包。 -
重新启动后,系统能够启动,并且“无法启动”问题已得到修复。
企业 Linux 发行版的拯救模式
在一些 Linux 发行版(如 CentOS)中,rescue image 方法有点不同。Linux 的企业版将试图自己找到用户的根文件系统。让我们来看看实际情况。图 10-11 和图 10-12 显示了 CentOS 的救援模式选择程序。
图 10-12
救援模式选择
图 10-11
CentOS 欢迎屏幕
它将启动,如图 10-13 所示,它将在屏幕上显示一些消息。
图 10-13
信息丰富的消息
如果我们选择选项 1,continue
,那么救援模式将搜索磁盘,并将自己找到根文件系统。一旦用户的根文件系统被识别,它将把它挂载到/mnt/sysimage
目录下。请参见图 10-14 。
图 10-14
根文件系统安装在/mnt/sysimage 下
如您所见,它已经在/mnt/sysimage
中挂载了用户的根文件系统;我们只需要chroot
进入其中。但是美妙之处在于我们不需要预先挂载虚拟文件系统。这是因为,正如你在图 10-15 中看到的,CentOS 中使用的chroot
二进制文件已经被定制,它将自己挂载虚拟文件系统。
图 10-15
根目录
如果我们选择了选项 2,Read-Only Mount
,那么救援脚本会以只读模式挂载用户的根文件系统,而不是在/mnt/sysimage
中。如果我们选择了第三个选项Skip
,救援系统就不会试图自己找到并安装用户的根文件系统;它只会给我们提供一个 Shell。
但是,当 CentOS ISO 的 rescue 内核没有用户的根文件系统名称时,它是如何找到根文件系统的呢?
在这里,Anaconda 没有办法找出用户的根文件系统名。Anaconda 将挂载连接到系统的每一个磁盘,并检查/etc/fstab
是否存在。如果找到了/etc/fstab
,那么它将从中获取用户的根文件系统名。如果您的系统连接了大量磁盘,那么 Anaconda 很可能需要很长时间来挂载用户的根文件系统。在这种情况下,最好手动挂载用户的根文件系统。查找用户根文件系统的源代码存在于 Anaconda 的源 tarball 中,如下所示:
#vim pyanaconda/storage/root.py
91 def _find_existing_installations(devicetree):
92 """Find existing GNU/Linux installations on devices from the device tree.
93
94 :param devicetree: a device tree to find existing installations in
95 :return: roots of all found installations
96 """
97 if not os.path.exists(conf.target.physical_root):
98 blivet_util.makedirs(conf.target.physical_root)
99
100 sysroot = conf.target.physical_root
101 roots = []
102 direct_devices = (dev for dev in devicetree.devices if dev.direct)
103 for device in direct_devices:
104 if not device.format.linux_native or not device.format.mountable or \
105 not device.controllable or not device.format.exists:
106 continue
107
108 try:
109 device.setup()
110 except Exception: # pylint: disable=broad-except
111 log_exception_info(log.warning, "setup of %s failed", [device.name])
112 continue
113
114 options = device.format.options + ",ro"
115 try:
116 device.format.mount(options=options, mountpoint=sysroot)
117 except Exception: # pylint: disable=broad-except
118 log_exception_info(log.warning, "mount of %s as %s failed", [device.name, device.format.type])
119 blivet_util.umount(mountpoint=sysroot)
120 continue
121
122 if not os.access(sysroot + "/etc/fstab", os.R_OK):
123 blivet_util.umount(mountpoint=sysroot)
124 device.teardown()
125 continue
126
127 try:
128 (architecture, product, version) = get_release_string(chroot=sysroot)
129 except ValueError:
130 name = _("Linux on %s") % device.name
131 else:
132 # I'd like to make this finer grained, but it'd be very difficult
133 # to translate.
134 if not product or not version or not architecture:
135 name = _("Unknown Linux")
136 elif "linux" in product.lower():
137 name = _("%(product)s %(version)s for %(arch)s") % \
138 {"product": product, "version": version, "arch": architecture}
139 else:
140 name = _("%(product)s Linux %(version)s for %(arch)s") % \
141 {"product": product, "version": version, "arch": architecture}
142
143 (mounts, swaps) = _parse_fstab(devicetree, chroot=sysroot)
144 blivet_util.umount(mountpoint=sysroot)
145 if not mounts and not swaps:
146 # empty /etc/fstab. weird, but I've seen it happen.
147 continue
148 roots.append(Root(mounts=mounts, swaps=swaps, name=name))
149
实时图像
实时图像是 Linux 系统最好的特性之一。如果我们只是坚持正常的硬盘引导部分,这本书就不会完整。让我们来看看一个 Linux 的现场图像是如何启动的。首先让我们挂载 ISO 映像,看看它包含了什么。
# mkdir live_image
# mount /dev/cdrom live_image/
mount: /home/yogesh/live_image: WARNING: device write-protected, mounted read-only.
# tree live_image/
live_image/
├── EFI
│ └── BOOT
│ ├── BOOT.conf
│ ├── BOOTIA32.EFI
│ ├── BOOTX64.EFI
│ ├── fonts
│ │ └── unicode.pf2
│ ├── grub.cfg
│ ├── grubia32.efi
│ ├── grubx64.efi
│ ├── mmia32.efi
│ └── mmx64.efi
├── images
│ ├── efiboot.img
│ ├── macboot.img
│ └── pxeboot
│ ├── initrd.img
│ └── vmlinuz
├── isolinux
│ ├── boot.cat
│ ├── boot.msg
│ ├── grub.conf
│ ├── initrd.img
│ ├── isolinux.bin
│ ├── isolinux.cfg
│ ├── ldlinux.c32
│ ├── libcom32.c32
│ ├── libutil.c32
│ ├── memtest
│ ├── splash.png
│ ├── vesamenu.c32
│ └── vmlinuz
└── LiveOS
└── squashfs.img
实时图像分为四个目录:EFI
、images
、isolinux
和LiveOS
。
-
EFI:
我们在讨论 bootloader 的时候已经讨论过这个目录。UEFI 固件将跳转到该目录并运行
grubx64.efi
文件。grubx64.efi
文件将读取grub.cfg
文件,并将从isolinux
目录中提取initrd.img
和vmlinuz
文件。 -
图像:
这将主要在我们通过 PXE 引导时使用。网络引导超出了本书的范围。
-
等 linux:
如果 UEFI 以 BIOS 方式启动,那么它将从这里读取
grub.conf
文件。该目录主要用于存储initrd
和vmlinuz
文件。换句话说,这个目录是普通根文件系统的/boot
。 -
层:
这就是奇迹发生的地方。这个目录有一个名为
squashfs.img
的文件。一旦你安装了它,你会在里面找到rootfs.img
。
# mkdir live_image_extract_1
# mount live_image/LiveOS/squashfs.img live_image_extract_1/
# ls live_image_extract_1/
LiveOS
# ls live_image_extract_1/LiveOS/
rootfs.img
# mkdir live_image_extract_2
# mount live_image_extract_1/LiveOS/rootfs.img live_image_extract_2/
# ls live_image_extract_2/
bin boot dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var
壁球比赛
Squashfs 是一个小型的压缩只读文件系统。这个文件系统通常用于嵌入式系统,其中存储的每个字节都很宝贵。Squashfs 为我们提供了比 tarball 归档更多的灵活性和性能。Squashfs 在其中存储了一个动态 Fedora 的根文件系统(rootfs.img
),它将以只读方式挂载。
# mount | grep -i rootfs
/home/yogesh/live_image_extract_1/LiveOS/rootfs.img on /home/yogesh/live_image_extract_2 type ext4 (ro,relatime,seclabel)
您可以使用squashfs-tool
提供的mksquashfs
命令来制作 Squashfs 图像/档案。
rootfs.img
rootfs.img
是一个 ext4 文件系统,其中有一个典型的根文件系统。有些发行版为实时图像创建一个访客用户或一个名为live
的用户,但是在 Fedora 中是根用户做所有的事情。
# file live_image_extract_1/LiveOS/rootfs.img
live_image_extract_1/LiveOS/rootfs.img: Linux rev 1.0 ext4 filesystem data, UUID=849bdfdc-c8a9-4fed-a727-de52e24d981f, volume name "Anaconda" (extents) (64bit) (large files) (huge files)
实时映像的引导序列
顺序如下:
-
固件会调用引导程序(
grubx64.efi
)。它将读取grub.cfg
文件,并从isolinux
目录中复制vmlinuz
和initrd
文件。 -
内核将在特定的位置提取自身,并将在任何可用的位置提取 initramfs。
-
从 initramfs 启动的 systemd 会在
/dev/mapper/live-rw
将rootfs.img
文件提取到设备映射器目标设备,将它挂载到根(/
)文件系统,并将switch_root
文件放入其中。 -
一旦根文件系统可用,您可以将它视为安装在 CD、DVD 或
.iso
文件中的正常操作。
此外,很明显,与特定于主机的 initramfs 相比,实时映像 initramfs 的大小要大得多。