有一句话叫“言多必失”,用在写代码上也基本合适。因为,代码多了也容易有闪失,出BUG(臭虫,纰漏之别名)。因此,在今天这样的“软件大生产”时代里,总有抓不完的BUG。
上周四一早从长沙回到上海后,我便开始抓BUG。这个BUG困扰格蠹小伙伴多日,他们想了很多办法都没有能找到根本原因。
卡在INITRAMFS
BUG发生在幽兰代码本启动过程中,可以在u-boot命令行通过bootyl nvme命令稳定重现。故障的症状非常明显,Linux系统启动失败,启动徽标消失,但桌面没有显示出来,屏幕一片黑色,没有内容,仿佛发射中的火箭突然解体,戛然而止。
通过串口输出,可以看到系统完成了内核空间的初始化,在初始化用户空间时发生错误:
/init: line 73: wait-for-root: not found
/init: line 932: logsave: not found
The root filesystem on /dev/nvme0n1p2 requires a manual fsck
看起来是错误信息来自著名的init进程。
打印上述错误后,系统停泊在(initramfs)的命令环境中。
[10644.891525] debugfs: Directory 'fdab0000.npu-rknpu' with parent 'vdd_npu_s0' already present!
[10644.894932] RKNPU fdab0000.npu: failed to find power_model node
[10644.894944] RKNPU fdab0000.npu: RKNPU: failed to initialize power model
[10644.894950] RKNPU fdab0000.npu: RKNPU: failed to get dynamic-coefficient
/init: line 73: wait-for-root: not found
/init: line 932: logsave: not found
The root filesystem on /dev/nvme0n1p2 requires a manual fsck
BusyBox v1.36.1 (Ubuntu 1:1.36.1-3ubuntu1) built-in shell (ash)
Enter 'help' for a list of built-in commands.
(initramfs)
(initramfs)
(initramfs)
(initramfs)
INITRAMFS为何物?
软件系统的启动过程有点像创业,复杂,冗长,涉及面广,有很多不确定性,也有很大的概率失败。一个创业公司需要一点点长点,经历很多阶段,由小变大,由弱变强。系统的启动也是这样,要经历很多个阶段,逐步改善运行条件,一开始没有DDR,更别谈什么显示。
以Linux系统为例,一般要经过bootrom,bootloader(u-boot/UEFI),kernel,initramfs(initrd)等阶段,才能进入rootfs,做完整的用户空间初始化,显示出可以操作的桌面。
有了这个铺垫后,我尝试解释一下initramfs是什么。它是启动过程中的一个环节,它的前一棒是内核,后一棒是桌面。它的使命是为初始化豪华的用户空间做准备。它要做哪些准备呢?
以幽兰为例,它做的任务包括:
- 初始化主机名
# mdadm needs hostname to be set. This has to be done before the udev rules are called!
if [ -f "/etc/hostname" ]; then
/bin/hostname -F /etc/hostname >/dev/null 2>&1
fi
- 加载驱动程序,以便可以访问更多的硬件设备;
- 解析内核命令行,判断本次启动是否是从休眠唤醒等;
- 挂接基于内存的文件系统tmpfs到/run
- 挂接根文件系统(rootfs)
- 运行根文件系统/scripts目录下的初始化脚本
- 挂接/sys和/proc假文件系统
- 寻找接班人,也就是rootfs上定义的init程序,如果找不到则终止,
validate_init() {
run-init -n "${rootmnt}" "${1}"
}
# Check init is really there
if ! validate_init "$init"; then
echo "Target filesystem doesn't have requested ${init}."
init=
for inittest in /sbin/init /etc/init /bin/init /bin/sh; do
if validate_init "${inittest}"; then
init="$inittest"
break
fi
done
fi
- 如果找到则通过exec run_init命令掸位给新的init
# 来自幽兰initramfs中的init脚本
# Chain to real filesystem
# shellcheck disable=SC2086,SC2094
exec run-init ${drop_caps} "${rootmnt}" "${init}" "$@" <"${rootmnt}/dev/console" >"${rootmnt}/dev/console" 2>&1
echo "Something went badly wrong in the initramfs."
panic "Please file a bug on initramfs-tools."
在幽兰上,新的init程序就是根文件系统中的systemd,完整路径为/usr/lib/systemd/systemd。为了能让initramfs找到它,它又有个链接文件:/sbin/init
geduer@ulan:/sbin$ ls -l init
lrwxrwxrwx 1 root root 20 Oct 26 2023 init -> /lib/systemd/systemd
简单来说,INITRAMFS是用户空间的开路先锋。它进一步改善运行环境,挂接真实的文件系统,执行关键的初始化动作,为运行根文件系统上的系统进程做好各种准备工作。
从进程的角度来看,内核会启动INITRAMFS中的一个关键程序,通常称为init,它可能是脚本形式,也可以是编译好的init程序。在initramfs的命令行中,执行ps命令可以看到它的身影。
1 0 2652 S {init} /bin/sh /init splash earlyprintk verbose nokaslr
在幽兰的initramfs中,使用的是init脚本,它的完整内容可以在幽兰的wiki中找到:https://www.nanocode.cn/wiki/docs/youlan/init_sh
从实现的角度来看,INITRAMFS是一个特殊的镜像文件,这个镜像文件里放着“开路”用的各种装备。打个比方,它像是一辆特制的修路车,里面装着各种工具和应急材料,这些工具和材料一方面是要满足正常的施工需要,目标是把路修通;另一方面还要考虑万一修路时出现事故,那么还要进行故障诊断和修复,进入一个基本的命令行环境。现在正调试的问题就是后一种情况,内核把执行权移交给init脚本后,init脚本遇到意外,显示了几行错误信息后就进入诊断命令行不动了......
(未完待续)
(写文章很辛苦,恳请各位读者点击“在看”,欢迎转发)
*************************************************
正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生
扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物
也欢迎关注格友公众号