[知其然不知其所以然-6] 为什么initrd尝试从休眠中恢复失败了?

不知道同学们有没有遇到过,在linux下尝试从休眠恢复失败的问题(我指的是hibernation,不是suspend-to-memory)。

我最近就遇到了一些,经过总结,我发现主要是两类问题,一个是内核配置问题,一个是initrd问题,本文主要讨论后一种。

首先我们提出一个知识,对于Linux发行版来说,用户有两次机会可以从休眠状态恢复,

第一次是内核启动流程的恢复,第二次是如果内核启动恢复失败,就尝试让发行版的initrd文件系统来手工恢复。

第一次失败的原因,大多数是因为内核配置错误,如把SATA驱动配置成m模块编译,这样就带来了一个问题,内核在尝试恢复时,

一般在late_initcall,这个时候udev还没起来,这样就导致swap分区设备节点没有创建,内核找不到swap分区从而退出。这个问题

其实在内核文档里已经提到了,要求用户把SATA驱动做成built-in来解决。不过,这个功能也太不友好了,用户都不知道是什么情况

就失败了,每次只打印Hibernate failed,也不说是什么原因。

如果第一次内核恢复失败,那么就轮到发行版的initrd来救火。不过,第二次initrd也可能会失败,只不过失败的概率相当小。

我们来看看initrd为什么会失败。本文主要给出一个排查的过程,而不是直接给出结论,因为排查的过程比起结论要重要百倍。

首先,我们从initrd正常恢复的流程开始分析,我们注意到initrd每次恢复时,内核都会打印:

PM: Starting manual resume from disk

在initrd恢复失败时,看不到这句打印。于是我们找到内核对应的这句话,加一句打印当前进程名的代码,看看是哪个进程在负责做resume

的操作。结果显示,这个进程名字叫resume,pid号是300多。经过网上一些资料的查询,我们确认这个resume进程的来源,是ubuntu使用

的initrd源代码klibc里的resume.c。这个resume.c的代码很简单,就是把上层调用resume程序时传递的swap分区名字,通过echo的方式写入

内核的/sys/power/resume,从而达到手工复位的目的。为了分析这个resume的流程,在不清楚系统加载流程的情况下,我们首先得找到resume相关的文件。

于是我搞了个这个步骤,在根目录下直接搜索resume相关的文件:

root@chenyu-Broadwell-Client-platform:/# find . -name "resume"
./usr/share/initramfs-tools/scripts/local-premount/resume
./usr/lib/klibc/bin/resume
./home/chenyu/initrd_test/scripts/local-premount/resume
./home/chenyu/initrd_test/bin/resume
./home/chenyu/initrd_test/conf/conf.d/resume
./etc/initramfs-tools/conf.d/resume
./sys/power/resume
幸好文件不多,我们可以看到,/usr/lib/klibc/bin下面就是执行resume的主程序。另外剩下的一些基本都是脚本。经过艰苦的脚本语法分析,我们

最终得到resume流程的沙盒:

1. 系统启动时,加载

/usr/share/initramfs-tools/init
在该脚本内,依次执行:

2.

for conf in conf/conf.d/*; do
        [ -f ${conf} ] && . ${conf}
done
设置一些环境变量,其中跟我们resume相关的是

./etc/initramfs-tools/conf.d/resume

RESUME=UUID=e8737fc0-83d2-4174-bf8e-86398a37c427
也就是设置了RESUME的环境变量路径,这其实就是sda3


3. 解析cmdline参数

# Parse command line options
for x in $(cat /proc/cmdline); do
case $x in
 resume=*)
                RESUME="${x#resume=}"
如果启动参数里有resume=xx,那么就替换RESUME为cmdline的版本。

上面赋值语句的意思是,如果启动参数里有resume=/.dev/sda3,那么就把该字符串

里,'resume='字符串以及之前的字符去掉后,赋值给RESUME,也就是

赋值后RESUME=/dev/sda3,而不是RESUME=resume=/dev/sda3


4.设置需要传递给klibc/resume程序的参数

resume=${RESUME:-}
上面这句话是一个二值判断,类似C语言里的?:表达式,

就是说,如果RESUME有值,就resume=RESUME,否则resume=NULL。

:和-是关键字。


5. 尝试加载systemd-udev

run_scripts /scripts/init-top
就是执行scripts/init-top下的所有脚本,其中有一个udev脚本:

/lib/systemd/systemd-udevd --daemon --resolve-names=never

6.尝试执行resume

mountroot

这个脚本就是启动resume程序,尝试从休眠恢复:

case $resume in

UUID=*)
        resume="/dev/disk/by-uuid/${resume#UUID=}"
        ;;
/bin/resume ${resume}
resume变量在init已经赋值,因此这里会直接传递给klibc的resume程序。

这里resume会被赋值为/dev/disk/by-uuid/e8737fc0-83d2-4174-bf8e-86398a37c427,

这个其实就是一个指向/dev/sda3的符号链接:

ls -l /dev/disk/by-uuid/e8737fc0-83d2-4174-bf8e-86398a37c427
lrwxrwxrwx 1 root root 10  7月 21 14:52 /dev/disk/by-uuid/e8737fc0-83d2-4174-bf8e-86398a37c427 -> ../../sda3

7. klibc resume程序echo字符串来手动恢复休眠

powerfd = open("/sys/power/resume", O_WRONLY)
		
	snprintf(device_string, sizeof device_string,
		       "%u:%u:%llu",
		       major(resume_device), minor(resume_device),
		       resume_offset);
	
	write(powerfd, device_string, len) 
		

最终恢复完成。




好,问题来了,为什么initrd有时候会恢复失败呢? 还是跟udev有关。

原因就在于,第5步与第6步没有做同步,第5步开启的systemd-udev和第6步的执行是很可能并发执行的,如果udev跑的稍微

慢一点,那么第6步就可能找不到/dev/sda3设备节点,从而resume失败。


问题根源,磁盘驱动的加载时机与系统尝试从休眠恢复的时间没有做好同步。解决方案,待定。


顺带说一下,为了initrd的调试,可以尝试修改initrd文件后,再打包,用如下命令可以完成initrd文件的解压和压缩。

Cgz文件内容的查看与解压

解压:zcat**.cgz |cpio -di

压缩:find *| cpio -o -H newc | gzip -n -9 > ../**.cgz


你可能感兴趣的:(linux,Hibernate,initrd)