Linux 的休眠
Contents
先区分一下两个名词:睡眠(sleep)和休眠(hibernate)。
- 睡眠:将工作镜像写入内存(RAM),以便快速恢复。内存读写很快,所以睡眠的特点就是“睡得快”和“醒得快”。对于笔记本来说,合上盖子就睡了,打开盖子你的工作区间即刻就能恢复,很是方便。但是睡眠有一个缺点,就是要给内存供电,一旦断电,你的镜像数据就会丢失,工作区间将不复存在。当然这来自于内存的固有特点,建议百度 RAM。
- 休眠:将工作镜像写入硬盘(disk,ROM),这样你也可以恢复工作区间。只是睡下去和醒过来的时间比内存慢不少。但是,它有一个好处就是断电了也不会丢失数据。当你再次开机,系统就会从硬盘里面读取镜像,恢复你的工作区间。
作为一个不求甚解的小白,我用 linux 这么些年,一直都只用过睡眠,每天晚上合上笔记本的盖子,第二天早上打开,工作区间即刻恢复,其实也是非常方便的,再也不用忍受关机开机的痛苦。这样一夜下来,大概要耗费 7-8% 的电量,还可以接受不是=。= 但是一旦你很长时间没用电脑,比如说放长假回家了,好久没碰电脑,那么笔记本的电池是会耗尽的,此时你的工作区间就丢了。(当然,这样的情况并不多见)
其实我以前也是鼓捣过 linux 休眠的,大概 3-4 年前,刚接触 linux 那会儿,在网上一通乱搜,一顿瞎试,未果。现在想来,失败的原因一是当时太菜,而是当时那个电脑太老旧了。据我所知,GPT 分区下搞休眠的坑是比较多的。现在的电脑大都是 EFI 分区,更加简单易用。
总体来说,休眠还是值得折腾的,因为支持断电!而且现在普遍使用固态硬盘,休眠和恢复的速度也并不是很慢。还有一个很重要的原因,笔记本电池的寿命很短,我的本子买了 3 年了,现在电池容量已经缩水 2/3 了!
好了,闲话少叙,进入正题。
确保 swap 分区足够大
拟使用 swap 分区作为写入镜像的目标分区。
一般建议 swap 分区为本机内存的一半,不过我认为有条件的还是将 swap 分区设置的略大于内存。此处,由于睡眠是将镜像写到内存,要确保 swap 分区能够容得下这个镜像,就必须将 swap 分区设置的大于内存。这并不是说 swap 小于内存就无法休眠了 1,具体还是要看工作区间的镜像大小了。我现在的 swap 就只有本机内存的一半,但还是休眠成功了。
查看fstab
|
|
系统启动时会读取该文件,按照其中的描述挂载对应的分区。默认生成的fstab
中,swap 分区的类型是swap
,将它改为none
.
以下命令均可以查看分区信息:
|
|
添加恢复分区的内核参数
|
|
可以看到,内核的启动参数中resume=/dev/nvme0n1p7
这一项就指定了从该分区恢复,而该分区正是 swap 分区。
那么如何修改内核的命令行参数呢?找到你所使用的 boot manager(启动引导)程序,更改相应的配置。我使用的是 rEFInd,需要做的更改为:
# file: /boot/refind_linux.conf
"Boot with standard options" "ro root=/dev/nvme0n1p5 rw resume=/dev/nvme0n1p7"
"Boot to single-user mode" "ro root=/dev/nvme0n1p5 single"
直接在第一行最后的参数列表里加上rw resume=/dev/nvme0n1p7
即可。Ubuntu 默认使用 grub 作为引导,这个网上教程更为详尽,此处就不再复制粘贴了。
重新生成启动镜像
作完更改之后,使用
|
|
重新生成启动镜像,使更改生效,最后重启系统。
重新进入系统之后,
|
|
如果参数列表里有resume=/dev/nvme0n1p7
则说明设置成功。你可以打开一个程序,然后
|
|
令系统休眠,然后再按下电源开关,系统会自动恢复之前的工作环境。
今天先这样,写的不够详细,改日再完善吧~
Re: hibernation
万万没想到,今日(2022-05-04 00:14),我又为了休眠的事儿排查了两天之久。
自文章写完之后,一年多来,休眠一直工作的很好。直到这次五一,我准备解决一下之前一直悬而未决的屏幕撕裂问题。在此过程中,我尝试了启用笔记本的独显 NVIDIA Corporation GP108M [GeForce MX150], 也为此做了很多工作,甚至从 Nvidia 官网下载了驱动进行安装,就是这个过程,安装报错了,然后我打算放弃,执行了卸载,卸载也报错了。最后实现的效果,确实 X 和 picom 都运行在独显上了,但是进入 X 之后,屏幕一片漆黑。彼时夜已深,我就打算放弃了。直接电脑休眠,而我去睡觉了。
第二天打开电脑,才发现大事不妙。直接变成开机了,之前的工作状态并未还原。思来想去这期间干了什么呢?尝试安装独显驱动,解决屏幕撕裂,archlinux-keyring 损坏并重置,进行了系统全量更新(内核升级到 5.17.5-arch1-1), 很难排查到底是什么原因导致的。只能打开 journal 细细排查可能的原因,
|
|
第一个疑似的原因就是这个,但经过一番搜索,他也仅仅是个内核的 bug2,并不能证明他和休眠失败有直接关系。
接着我直接找到这段时间的系统日志,一行一行的查看,凡是有疑似的都搜索之,未果。期间我发现,之前正常休眠恢复的日志序列大概是下面这个样子:
|
|
可以看到,日志从 4 月 28 日 23:59:06 一下子跳到 4 月 29 日 19:48:53,也就是说我在 28 日晚上发起了休眠,在 29 日晚上再度打开电脑,成功恢复了工作区。
而在休眠失败后日志是下面这样:
|
|
同样在时间跳跃节点看,此前的日志看起来是休眠成功了,此后竟然直接走了 boot 流程!因此我猜测肯定是恢复(resume)过程出了问题,休眠(hibernation)是正常工作的。
我尝试过将内涵参数中的resume=/dev/xxx
改为resume=UUID=xxx-xxxx-xx
,也还是不行。最让人无奈的是,到现在没有发现问题所在,一直在摸黑尝试。
后来看到两篇文章(其实就是 Ref2 中提及的):
开始对休眠过程进行更具针对性的 debug,推荐从第一篇文章开始。经过一番 debug,发现我的休眠功能确实没啥问题,恢复功能也没问题,即第一篇文章提到的:
That test can be used to check if failures to resume from hibernation are related to bad interactions with the platform firmware. That is, if the above works every time, but resume from actual hibernation does not work or is unreliable, the platform firmware may be responsible for the failures.
但即便知道了可能是硬件问题,我也看不出来是哪里的问题啊(太菜了 orz)。无奈之下尝试第二篇的 debug 方法,在鼓捣了一对内核参数之后,日志确实更详尽了,但其中暴露出的问题,google 都没有搜索结果。我哪看得懂啊?最后带着快要放弃的心情,再次翻开了 ArchWiki(再次高呼,ArchWiki YYDS!)上的一篇文章(Ref7), 其中提到:
The kernel parameters will only take effect after rebooting. To be able to hibernate right away, obtain the volume’s major and minor device numbers from lsblk and echo them in format
*major*:*minor*
to/sys/power/resume
. If using a swap file, additionally echo the resume offset to/sys/power/resume_offset
.[2]
我就试探性的照做了,
|
|
以及 此处 提及
- When an initramfs with the
base
hook is used, which is the default, theresume
hook is required in/etc/mkinitcpio.conf
. Whether by label or by UUID, the swap partition is referred to with a udev device node, so theresume
hook must go after theudev
hook. This example was made starting from the default hook configuration:
HOOKS=(base udev autodetect keyboard modconf block filesystems **resume** fsck)
Remember to regenerate the initramfs for these changes to take effect.
- When an initramfs with the
systemd
hook is used, a resume mechanism is already provided, and no further hooks need to be added.
总结一下:就是往/sys/power/resume
里写入正确的数值,以及在/etc/mkinitcpio.conf
里加上resume
hook,重新mkinitcpio -P
,然后休眠恢复就成功了!
小尝试
经过尝试,把/etc/mkinitcpio.conf
中 HOOKS 中的 resume 去掉,再mkinitcpio -P
,再次休眠后就无法恢复,直接走 boot 流程了。并且启动后/sys/power/resume
的值丢失了(恢复默认):
|
|
而将 HOOKS 中的 resume 加上之后,再mkinitcpio -P
生效之,重启后
|
|
有值了,而且休眠之后可以成功恢复。
看起来就是这里的原因了,恢复的时候由于找不到 swap 分区导致 fallback 到 boot 流程,而 resume hook 就是起到告诉 kernel swap 分区的标识,因此才能成功恢复。但有一个问题,之前我没有动过这些,也能休眠并恢复成功。从 ArchWiki 上的描述 来看,HOOKS 中使用了systemd
的,不需要加resume
;使用了base
的,需要加resume
。看来是某些操作改了我的/etc/mkinitcpio.conf
?
Bonus: 使用 sleep hook 在休眠时上锁
此前使用休眠的场景是这样的:terminal 里面敲systemctl hibernate
,等待休眠成功,合上盖子,time flies,打开盖子,启动电源,恢复工作区。这个过程没有涉及到用户验证,所以如果此间别人拿了你的电脑,自然能一窥你的裙底风光。所以,合理的做法应该是休眠时顺便锁个屏。
其实 ArchWiki 也有提到34,利用systemd
管理的 service 可以做到。具体说来,可以创建如下文件:
|
|
形如xxx@.service
的文件称为 template service,它可以带一个参数拼接成一个 instantiated service 文件,比如xxx@username.service
,具体可参考man 5 systemd.service
. 上述我创建了一个suspend@.service
,然后我们启用(enable on boot)一个 instantiated service
|
|
接着 reload 一下使其立刻生效,
|
|
然后调用systemctl hibernate
看看效果,果然在挂起、恢复之后,出现了锁屏界面5。
热心观众可能发现,上述 service 对systemctl suspend
同样生效,其原因是 suspend 和 hibernate 同样都在sleep.target
之后,而我们的 service 定义了Before=sleep.target
,说明suspend@yychi.service
要在sleep.target
之前执行。因此无论是 sleep 还是 hibernate 都能用上。印证如下:
|
|
Re2: hibernation
今天(2024-01-27),又出现了休眠问题,好在有了之前的经历,2H 就定位出来了。
休眠失败,首先看 journal:
1月 27 19:32:12 MiBook-Air systemd[1]: Started User suspend actions.
1月 27 19:32:12 MiBook-Air systemd[1]: Reached target Sleep.
1月 27 19:32:12 MiBook-Air systemd[1]: Starting System Hibernate...
1月 27 19:32:12 MiBook-Air systemd-sleep[11138]: Performing sleep operation 'hibernate'...
1月 27 19:32:12 MiBook-Air kernel: PM: hibernation: hibernation entry
1月 27 19:32:32 MiBook-Air kernel: Filesystems sync: 0.022 seconds
1月 27 19:32:32 MiBook-Air kernel: Freezing user space processes
1月 27 19:32:32 MiBook-Air kernel: Freezing user space processes completed (elapsed 0.003 seconds)
1月 27 19:32:32 MiBook-Air kernel: OOM killer disabled.
1月 27 19:32:32 MiBook-Air kernel: PM: hibernation: Marking nosave pages: [mem 0x00000000-0x00000fff]
1月 27 19:32:32 MiBook-Air kernel: PM: hibernation: Marking nosave pages: [mem 0x00058000-0x00058fff]
1月 27 19:32:32 MiBook-Air kernel: PM: hibernation: Marking nosave pages: [mem 0x0009e000-0x000fffff]
1月 27 19:32:32 MiBook-Air kernel: PM: hibernation: Marking nosave pages: [mem 0x71c54000-0x71c54fff]
1月 27 19:32:32 MiBook-Air kernel: PM: hibernation: Marking nosave pages: [mem 0x71c80000-0x71c80fff]
1月 27 19:32:32 MiBook-Air kernel: PM: hibernation: Marking nosave pages: [mem 0x725d9000-0x725d9fff]
1月 27 19:32:32 MiBook-Air kernel: PM: hibernation: Marking nosave pages: [mem 0x725e9000-0x725e9fff]
1月 27 19:32:32 MiBook-Air kernel: PM: hibernation: Marking nosave pages: [mem 0x7312f000-0x73130fff]
1月 27 19:32:32 MiBook-Air kernel: PM: hibernation: Marking nosave pages: [mem 0x75388000-0x75c87fff]
1月 27 19:32:32 MiBook-Air kernel: PM: hibernation: Marking nosave pages: [mem 0x7bff2000-0x7bff7fff]
1月 27 19:32:32 MiBook-Air kernel: PM: hibernation: Marking nosave pages: [mem 0x8be9e000-0x8cffdfff]
1月 27 19:32:32 MiBook-Air kernel: PM: hibernation: Marking nosave pages: [mem 0x8cfff000-0xffffffff]
1月 27 19:32:32 MiBook-Air kernel: PM: hibernation: Basic memory bitmaps created
1月 27 19:32:32 MiBook-Air kernel: PM: hibernation: Preallocating image memory
1月 27 19:32:32 MiBook-Air kernel: PM: hibernation: Allocated 791290 pages for snapshot
1月 27 19:32:32 MiBook-Air kernel: PM: hibernation: Allocated 3165160 kbytes in 1.56 seconds (2028.94 MB/s)
1月 27 19:32:32 MiBook-Air kernel: Freezing remaining freezable tasks
1月 27 19:32:32 MiBook-Air kernel: Freezing remaining freezable tasks completed (elapsed 0.001 seconds)
1月 27 19:32:32 MiBook-Air kernel: printk: Suspending console(s) (use no_console_suspend to debug)
1月 27 19:32:32 MiBook-Air kernel: ata1.00: Entering standby power mode
1月 27 19:32:32 MiBook-Air kernel: nouveau 0000:01:00.0: DRM: failed to idle channel 1 [DRM]
1月 27 19:32:32 MiBook-Air kernel: nouveau 0000:01:00.0: PM: pci_pm_freeze(): nouveau_pmops_freeze+0x0/0x20 [nouveau] returns -16
1月 27 19:32:32 MiBook-Air kernel: nouveau 0000:01:00.0: PM: dpm_run_callback(): pci_pm_freeze+0x0/0xc0 returns -16
1月 27 19:32:32 MiBook-Air kernel: nouveau 0000:01:00.0: PM: failed to freeze async: error -16
1月 27 19:32:32 MiBook-Air kernel: usb usb1: root hub lost power or was reset
1月 27 19:32:32 MiBook-Air kernel: usb usb2: root hub lost power or was reset
1月 27 19:32:32 MiBook-Air kernel: nvme nvme0: 4/0/0 default/read/poll queues
1月 27 19:32:32 MiBook-Air kernel: usb 1-1: reset full-speed USB device number 2 using xhci_hcd
...
1月 27 19:32:32 MiBook-Air systemd-sleep[11138]: Failed to put system to sleep. System resumed again: Device or resource busy
1月 27 19:32:32 MiBook-Air kernel: PM: hibernation: hibernation exit
...
1月 27 19:32:32 MiBook-Air systemd[1]: systemd-hibernate.service: Main process exited, code=exited, status=1/FAILURE
1月 27 19:32:32 MiBook-Air systemd[1]: systemd-hibernate.service: Failed with result 'exit-code'.
1月 27 19:32:32 MiBook-Air systemd[1]: Failed to start System Hibernate.
1月 27 19:32:32 MiBook-Air systemd[1]: Dependency failed for System Hibernation.
1月 27 19:32:32 MiBook-Air systemd[1]: hibernate.target: Job hibernate.target/start failed with result 'dependency'.
1月 27 19:32:32 MiBook-Air systemd[1]: systemd-hibernate.service: Consumed 1.750s CPU time.
1月 27 19:32:32 MiBook-Air systemd[1]: Stopped target Sleep.
1月 27 19:32:32 MiBook-Air systemd-logind[379]: Operation 'sleep' finished.
可以看到,日志里清晰可见说了休眠失败,resume 到休眠前的状态。而往上溯源可以看到,失败原因是
1月 27 19:32:32 MiBook-Air kernel: nouveau 0000:01:00.0: DRM: failed to idle channel 1 [DRM]
1月 27 19:32:32 MiBook-Air kernel: nouveau 0000:01:00.0: PM: pci_pm_freeze(): nouveau_pmops_freeze+0x0/0x20 [nouveau] returns -16
1月 27 19:32:32 MiBook-Air kernel: nouveau 0000:01:00.0: PM: dpm_run_callback(): pci_pm_freeze+0x0/0xc0 returns -16
1月 27 19:32:32 MiBook-Air kernel: nouveau 0000:01:00.0: PM: failed to freeze async: error -16
根据这个线索搜索,最后发现直接 block nouveau 即可6,即禁用 nvidia 的显卡。因为我的笔记本有两块显卡,一个 intel 的核显,一个 nvidia 的集显。在 linux 环境下为这集显我没少踩坑。
yychi@~> lspci -k | grep -A 2 -E "(VGA|3D)"
00:02.0 VGA compatible controller: Intel Corporation HD Graphics 620 (rev 02)
Subsystem: Xiaomi HD Graphics 620
Kernel driver in use: i915
--
01:00.0 3D controller: NVIDIA Corporation GP108M [GeForce MX150] (rev a1)
Subsystem: Xiaomi GP108M [GeForce MX150]
Kernel modules: nouveau
解决方法:直接加个文件/etc/modprobe.d/nouveau_blacklist.conf
,重启后即可正常休眠。
ychi@~> cat /etc/modprobe.d/nouveau_blacklist.conf
# block nouveau, otherwise hibernation will not work.
# yychi, 2024.1
blacklist nouveau
关于休眠,暂时探索至此…
References
- Is Hybrid Sleep the same in Linux as in Windows?
- How can I hibernate on Ubuntu 16.04?
- How do I use pm-suspend-hybrid by default instead of pm-suspend?
- Kernel parameters
- Error resume: no device specified for hibernation
- Hibernation: Resume Can’t Find Swap
- Power management/Suspend and hibernate - ArchWiki
ACPI BIOS Error (bug): Could not resolve symbol [\_PR.PR00._CPC] ↩︎
此处我用的是 slock,X 下一个非常简单轻巧的锁屏工具。简单到什么程度呢?它连配置文件都没有,想要自定义,必须改
config.h
然后重新编译! ↩︎参考帖子:[Solved]Issues with mesa. ↩︎