不知道同学们有没有遇到过,在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
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
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