☰
Linux内核休眠机制详解:内存快照、进程冻结与恢复路径
2026/10/7 17:22:17 网站建设 项目流程

开始之前,先聊聊我为什么要写 hibernation

如果你调试过笔记本电脑合盖、写过/sys/power/state里的disk,或者被 systemd 的hibernate.target折腾过,那你已经在跟 Linux 内核的 hibernation 子系统打交道了。但说真的,当你往那个文件里写入一个disk,内核到底在背后干了多少事,大多数人其实是没概念的。

这不是一篇教科书式的源码逐行注释,我更想从一个“内核工程师面对一个真实问题”的角度,把 hibernation 的整个过程拆开揉碎:它为什么设计成现在这个样子、核心的几个阶段分别解决什么问题、哪些环节最容易出错、出错了该怎么下手查。掌握这条主线之后,你写驱动、调功耗、修 resume 失败的问题,都会有完全不一样的思路。

这篇文章更适合两类人:一类是正在看kernel/power/目录源码、需要一条主线把hibernate.c、snapshot.c、swap.c、process.c串起来的学习者;另一类是嵌入式或服务器方向、需要在真实产品里调通休眠/恢复功能、被各种诡异问题折磨过的实践者。看完你应该能画出完整的调用链,也能在实际排查时快速定位到具体模块。

1. 整体设计:hibernation 到底在解决什么问题

1.1 一个“把运行状态搬到磁盘”的朴素需求

hibernation 的原始需求其实很朴素:我在跑一堆服务,开着几十个终端,内存里有几百 GB 的数据和进程状态,现在我要把机器彻底断电,但下次开机时想回到当前这个状态。注意,这里的关键不是“关闭”而是“恢复”,不是把数据保存成文件,而是把整个运行中的内存镜像变成一个可持久化的快照。

这个需求落到工程上就变成了三个子问题:怎么稳定地拿到一份一致的内存镜像、怎么把镜像可靠地写到持久存储、怎么在下次开机时把镜像重新装载回内存并恢复运行。

听起来好像不复杂,但仔细想就会发现问题不少。内存里除了用户态进程,还有内核自己的状态、设备寄存器的值、CPU 上下文、中断状态。更麻烦的是,在保存镜像的过程中,内存里的数据还在不断变化,进程还在调度、DMA 还在搬运数据、CPU 缓存还在回写。你需要先把整个系统“定格”在一个一致的、不会继续变化的状态,才能开始保存。这就是 hibernation 整个设计的第一条主线:一致性优先,一切其他目标都得给它让路。

1.2 与 suspend(挂起到内存)的分工和关联

很多初学者会把 suspend(STR,S3)和 hibernation(STD,S4)混在一起。它们确实共享了相当多的基础设施,比如进程冻结、设备电源管理回调,但本质区别在于:suspend 期间内存还在供电,镜像不需要落盘,恢复时直接切回内存即可;hibernation 则要经历完全断电,必须把镜像搬到磁盘上,下次引导时再装回来。

从功耗设计的角度看,这两者互为补充:suspend 恢复快但仍有静态功耗,hibernation 几乎零功耗但恢复慢。现代笔记本上的 s2idle、suspend-to-idle、hibernate-after-suspend 这类组合策略,本质上就是在两者之间做权衡。理解 hibernation 的过程,也有助于理解为什么内核要反复强调“冻结进程”和“设备挂起”这两个公共环节——它们是整个功耗子系统共用的一套基础设施,不是只为休眠服务的。

1.3 关键代码路径的分布

看源码的时候,建议把这条主线先刻在脑子里:

  • kernel/power/hibernate.c:整个 hibernation 的状态机,入口函数hibernate(),恢复入口software_resume();
  • kernel/power/snapshot.c:内存快照的生成和恢复,页面位图、镜像元数据、内存页分配;
  • kernel/power/swap.c:镜像数据写到交换分区/交换文件的 I/O 逻辑;
  • kernel/power/process.c:冻结/解冻用户态进程和内核线程;
  • kernel/power/device.c:设备的电源管理回调,这里是dpm_suspend()/dpm_resume()的所在地;
  • kernel/power/main.c:sysfs 接口层,/sys/power/state和/sys/power/disk。

后面我拆解过程的时候会反复引用这几个文件。你可以照着这个表去源码里定位,会比直接瞎翻高效得多。

2. 触发入口和主状态机的流转

2.1 从用户态写入 /sys/power/state 说起

先看最基础的触发路径。用户在终端执行echo disk > /sys/power/state,这个操作最终会落到kernel/power/main.c的state_store()里。

// kernel/power/main.c static ssize_t state_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t n) { suspend_state_t state = PM_SUSPEND_MAX; int error; if (state == PM_SUSPEND_MAX) { error = hibernate(); } return error ? error : n; }

hibernate()是总入口,它做的事情可以概括成一句话:让所有活动停下来,生成内存镜像,写盘,然后按指定方式断电。我们直接看它的骨架,这样后面拆解细节时你始终知道自己在流程中的什么位置。

// kernel/power/hibernate.c,经过简化的逻辑 int hibernate(void) { struct snapshot_data snapshot; int error; pr_info("hibernation entered\n"); // 1. 全局准备,阻塞 PM 事件 pm_prepare(); // 2. 冻结用户态进程和内核线程 error = freeze_processes(); // 3. 生成内存快照,这一步只建立镜像,不落盘 error = hibernation_snapshot(&snapshot, platform_hibernation_available() ? platform_begin : HIBERNATION_SHUTDOWN); // 4. 把镜像写入交换区/交换文件 error = swsusp_write(&snapshot, &resume_file); // 5. 进入低功耗平台状态,或关机/重启 power_down(snapshot); // 6. 如果走到了这里,说明休眠被中止/失败,要解冻并恢复 thaw_processes(); pm_restore(); return error; }

这个骨架虽然砍掉了大量错误处理和条件分支,但主线非常清楚:准备、冻结、快照、写盘、断电。每一步都有一大堆讲究,下面一节一节说。

2.2 hibernation 的几种模式:shutdown / reboot / platform / emergency

看到power_down()这个名字,你可能会以为休眠最后一定是机器断电。其实内核在这一步有四种选择,由/sys/power/disk控制,对应的内核枚举是HIBERNATION_SHUTDOWN、HIBERNATION_REBOOT、HIBERNATION_PLATFORM和HIBERNATION_EMERGENCY。

在绝大多数 x86 笔记本上,HIBERNATION_PLATFORM模式会调用 ACPI S4 进入真正的挂起到磁盘状态,这时平台的电源管理代码会接管,芯片组只保留必要的唤醒逻辑。而嵌入式设备或服务器上,更常见的是SHUTDOWN模式,也就是写完镜像后直接关机,下次上电引导时再由引导流程加载镜像。REBOOT模式则不真正断电,而是马上重启,通常用于测试或者平台不信任 S4 的场合。

理解这四种模式对调试非常重要。有些平台 S4 支持不完善,休眠后无法唤醒,这时把模式切到 shutdown 反而能稳定工作;反之,有些平台要求必须是 platform 模式才能让 BIOS 在恢复后正确初始化设备。这个我会在后面的常见问题里再展开。

2.3 用户态是怎么过来的:pm_suspend 和 autosleep 的关系

除了直接 echo disk,实际上还有一条路径是通过autosleep进入。比如 systemd 的Sleep=配置、桌面环境的合盖动作,通常会走pm-suspend或systemd suspend这类用户态工具,最后由内核的autosleep机制或/sys/power/wakeup_count控制进入。这条路径最终还是会落入hibernate()或者enter_state()。

有一个细节值得注意:hibernation 在触发之前会检查是否有正在进行的 suspend 流程或者wakeup_count不一致的情况,避免出现两个电源状态转换打架。历史上就出过 suspend 和 hibernate 同时触发的竞态问题,后来内核引入了 PM 睡眠锁(sleep lock)和pm_pr_prepare()等机制来保证互斥。这个设计思路在嵌入式上尤其重要:如果你的系统同时接了电源按键、RTC 唤醒、外部 GPIO 唤醒多条路径,这些锁和标志位就是保证状态机不乱的基石。

3. 核心过程逐段拆解:冻结、快照、写盘、断电

3.1 进程冻结:给内存镜像一个“稳定基准”

进程冻结是 hibernation 里第一个真正有技术含量的环节,也是很多初学者最容易忽略的。为什么要冻结进程?因为我们要保存内存镜像,如果进程还在运行,它的栈、堆、映射关系随时在变,你不可能拿到一张自洽的内存图。

内核在这里做的事情可以概括为:向所有用户态进程和可冻结的内核线程发送冻结信号,让它们停在一个明确定义的位置上,不再参与调度。用户态进程好办,睡在TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE上就行;麻烦的是内核线程,比如kworker、kswapd,它们要是不配合,镜像一致性根本没法保证。

由此引出/sys/power/pm_test这个调试利器:它可以把休眠流程截断在不同的阶段,比如只执行到进程冻结完成就返回,不生成镜像也不写盘。我调试过的很多问题,其实第一步就是靠这个参数缩小范围——如果冻结阶段就卡死,那就不用怀疑 swap 或者恢复流程了。

3.2 内存镜像的生成逻辑:不是 memcpy 那么简单

到了hibernation_snapshot(),内核要做的事情是“生成一份当前内存状态的快照”。注意,这里不能用memcpy把全部物理内存复制到另一个地方——一来内存没有那么大空间,二来复制过程中内存还在变化。所以内核采用了一套更精巧的思路:先用页面位图记录哪些物理页需要保存,然后在后续写盘阶段迭代地把这些页的内容读出来。

具体来说,整个快照建立过程会经历几个子阶段:先冻结进程、暂停设备到PMSG_FREEZE状态,接着保存 CPU 寄存器和中断状态,然后才开始枚举内存页。因为设备已经暂停、进程已经冻结,此刻内存处于高度静止状态,快照的“一致性窗口”才真正建立起来。

有个细节很有意思:写盘时并不是把所有物理页都写出去。内核会跳过空闲页、保留页、以及镜像元数据自己占用的页。页面描述符结构会记录每个页的原始物理页框号(PFN),恢复时按这个编号把内容填回原位置。如果某些页不可能被再次使用,内核甚至会在恢复时直接放弃它们。这就是为什么休眠镜像的大小通常明显小于实际内存占用——它保存的是“真实在用”的部分,而不是全部 DIMM 空间。

3.3 交换空间的准备和镜像写入

镜像生成完毕后,接下来就是swsusp_write()。这函数要干的事情分三块:在交换区写一个头部结构标识这是休眠镜像、把镜像数据按块写入交换空间、记录镜像在交换空间的偏移位置,供下次恢复时定位。

// kernel/power/swap.c 的逻辑框架 static int swsusp_write(struct snapshot_handle *handle, struct resume_device *resume_file) { // 1. 在交换空间头部写入 HIBERNATION_SIG 魔术标记 // 2. 创建交换页位图,避免覆盖正在使用的 swap 页 // 3. 逐页写出镜像数据,可启用压缩(LZO/LZ4) // 4. 记录镜像的起始块号和偏移量 }

这里要特别提醒一个知识点:如果你用的是交换文件而不是交换分区,那么除了resume=参数,还必须在 GRUB 或者内核命令行里指定resume_offset=。这个偏移量表示交换文件在文件系统里的物理块偏移,恢复时内核需要它来定位镜像头。很多人休眠失败都是栽在只配了resume没配resume_offset,或者文件系统碎片化导致偏移变化。

关于压缩,内核在 5.x 之后的版本里集成了 LZO 和 LZ4 支持。压缩能显著缩短写盘时间,因为实际写的数据量变小了,但代价是恢复时要先解压,CPU 占用会上升。实测下来,如果存储设备是普通 SATA SSD,LZO 压缩确实有正收益;如果是 NVMe,带宽很充裕,压缩收益就小一些,但也不至于有负优化。

3.4 设备挂起和断电:平台开始接管

写盘完成之后,系统要做最后的清理:把设备逐个挂起,把 CPU 转向低功耗状态,然后调用平台特定的代码进入休眠。

设备挂起走的是dpm_suspend()路径。注意这里有两个阶段,先是PMSG_FREEZE(冻结阶段),这个阶段在快照生成前就执行过;写盘完成后还有一个PMSG_SUSPEND/PMSG_HIBERNATE阶段,用来把设备真正置于关闭或低功耗状态。之所以分两步,是为了在快照一致性上做权衡:冻结阶段要保证内存不变,所以只做最小处理;真正断电前再彻底关闭设备。

PCI 设备、网络设备、块设备驱动在休眠里的表现差异巨大,这是实战中问题最多的区域。举个典型例子:某些网卡驱动在 resume 后回不到工作状态,表现为网络不通或者固件崩溃。这类问题往往不是你系统配置错了,而是设备驱动没有实现完整、正确的恢复回调。排查思路我放到第 5 节讲。

最后一步,power_down()按前面说的模式选择执行。以 ACPI 平台为例,acpi_sleep_enter()会往 ACPI 寄存器写入 S4 状态值,之后整个平台的电源控制就交给硬件了,CPU 停在那里,直到按下电源键或 RTC 唤醒信号到来。

3.5 恢复路径:重新点亮系统的反向流程

恢复路径和休眠路径是相反的,但这里有一个非常关键的区别:休眠是“从一个运行中的系统制作镜像”,而恢复是“从崩溃或冷启动状态读取镜像”。所以内核在引导过程里就得先判断:这次启动是不是一次休眠恢复?

判断依据是引导参数resume=或自动探测到的交换分区。在software_resume()中,内核会读取交换区头部的魔术标记,如果匹配就认为存在有效镜像,接着做三件事:分配足够的内存承载镜像、把镜像数据从磁盘读入、跳转到hibernation_restore()重建原内存布局。

// kernel/power/hibernate.c,简化恢复流程 static int software_resume(void) { // 1. 根据 resume= 参数定位交换设备 // 2. 校验镜像签名和校验和 // 3. swsusp_read() 读入镜像 // 4. hibernate_restore() 恢复页表和寄存器 }

这里有个感受很深的细节:恢复阶段内存分配是“完全不同的世界”。因为此时内核还没有完全初始化,内存管理器的很多路径不可用,所以 swsusp 有一套特殊的预分配机制,在读取镜像之前先预留出足够的内存页。如果系统内存太小,或者内核对镜像所需的内存估算不对,就会出现“镜像无法装载”的致命错误。

4. 需要深入理解的几个实现细节

4.1 为什么需要 hibernation 平台支持:ACPI S4 的底层逻辑

前面反复提到 platform 模式,这里展开说一下。ACPI 定义了 S1、S2、S3、S4、S5 几种睡眠状态,S4 就是“挂起到磁盘”。但 S4 和 S5(软关机)在 ACPI 的硬件实现上非常接近——区别只在于 S4 状态里有一个SLP_TYP字段被设置为特定值,系统在恢复时 BIOS 能识别出这是从休眠恢复,从而走特殊初始化路径。

Linux 的hibernation_platform_enter()做的事情,就是与 ACPI 固件协商,把系统放到真正的 S4。这中间涉及 FADT 表、_S4对象、唤醒向量配置等一堆 ACPI 细节。实际操作中,我建议调试初始化就先检查dmesg里的 ACPI S4 相关信息,确认固件确实声明了 S4 支持。如果 FADT 里根本没有 S4,那你的“休眠”大概率会退化成关机或重启。

4.2 镜像校验和页面属性:保证恢复可信

你可能好奇,写盘过程怎么知道数据没写坏?内核在镜像元数据里加了一个校验字段,恢复时会检查整个镜像的 CRC 或者页面属性一致性。如果数据损坏,内核会拒绝恢复,直接进入正常启动流程。这比带着半坏的镜像硬恢复要安全得多——宁可丢弃镜像,也不能让系统在一个未知状态下运行。

另外还有一个“已分配页面是否覆盖系统关键区域”的检查。如果镜像尝试把某个页恢复到内核自身正在使用的地址,那就会直接拒绝,防止出现自我破坏。这一块是 swsusp 代码里相对晦涩的部分,但想深入读源码时你会发现这些都是设计上的安全底线。

4.3 内存压缩技术对 hibernation 的影响

现代内核里,内存压缩(memory compaction)和迁移(migration)在正常情况下会改变物理页的位置。那 hibernation 时怎么办?答案是:镜像保存的是页框号(PFN)和内容的对应关系,恢复时按 PFN 放回去,所以物理页是否连续并不重要。但有一点需要注意:如果 swap 设备上同时存在正常的匿名页交换活动和 hibernation 镜像,两者可能互相踩踏。swsusp_write()在写入镜像前会记录哪些 swap 页正在被使用,尽量避开,但如果你 swap 太小,镜像写不下,内核只能报错。

4.4 设备驱动视角:驱动应该怎么注册断电/恢复回调

对写驱动的工程师来说,hibernation 最直接的要求是:你需要正确实现dev_pm_ops里的freeze、thaw、poweroff、restore回调。这四个回调分别对应休眠流程的不同阶段。

回调休眠流程中的时机推荐行为
freeze快照生成前停止 DMA、保存硬件状态、让设备静默
thaw镜像生成失败/中止后恢复freeze前的状态
poweroff镜像写入完成、真正断电前彻底关闭设备电源或进入最低功耗态
restore恢复镜像后重新初始化硬件、恢复 DMA

我见过很多驱动只实现了 suspend/resume 而漏了 hibernation 专用的freeze/thaw/poweroff/restore,导致休眠恢复正常但设备工作异常。虽然内核在某些情况下会回退到 suspend/resume 回调,但那是兜底行为,不要依赖。

5. 实战排查:常见的 hibernation 问题与定位思路

5.1 现象:“休眠后无法唤醒,屏幕一直黑着”

这是笔记本上最常见的故障。排查时先分清是恢复流程没启动,还是恢复后显卡/背光驱动出问题。第一判断方法:按电源键后,听风扇和硬盘的声音。如果听到磁盘转动和明显的系统启动声,说明硬件已经从 S4 醒来,问题大概率在显卡或显示驱动;如果什么反应都没有,说明平台根本没从 S4 状态唤醒,重点查 ACPI 唤醒配置、电源按键的 GPE(General Purpose Event)。

5.2 现象:“resume 时提示 image not found”

这说明内核在启动时没能在交换空间找到有效的休眠镜像。常见原因有三个:一是resume=参数指向的设备不对;二是用了交换文件但没配resume_offset;三是休眠后交换空间被清理过,比如某些发行版的mkinitcpio或dracut钩子在恢复前重置了 swap。第三个原因最隐蔽,因为你有印象“刚才是正常休眠的”,但实际镜像头已经被破坏了。

5.3 现象:“hibernate 直接返回错误,不进入休眠”

这种问题可以在内核日志里找到直接线索。常见是PM: Some devices failed to suspend或者PM: Cannot find swap partition。前者是某个设备驱动的 suspend 回调返回了错误码,这时内核会启动回滚流程,把已经暂停的设备重新恢复,然后让你回到用户态。后者就简单了,检查swapon是否启用、resume=是否正确。

调试这类问题一定要先学会看内核日志,推荐在 GRUB 里临时去掉quiet和splash,让内核日志直接打到终端。休眠期间设备初始化本来就比正常启动脆弱,日志里往往能直接看到是哪个驱动卡的。

5.4 如何缩小排查范围:利用 pm_test 分层定位

/sys/power/pm_test这个参数绝对是个隐藏神器。它可以设置为none、core、platform、devices、freezer几个级别,作用是让休眠流程执行到对应阶段后主动退回,不生成镜像也不真正断电。

比如设成devices,系统会执行设备挂起到PMSG_FREEZE状态然后退回来。如果这一步就失败,说明问题在设备驱动;如果设备这步过了但platform级别失败,说明是 ACPI/平台相关。用这个参数做二分定位,能省下大把瞎猜的时间。

5.5 若干能直接提升成功率的配置建议

根据我经历过的实际部署,给你几条实用的配置经验。

  • 优先使用独立的交换分区而不是交换文件。虽然文件也支持,但 offset 计算和文件系统状态相关的坑太多。
  • swap 分区大小最好等于或略大于内存大小。如果内存是 16 GB,swap 至少 16 GB,因为镜像通常略小于实际内存用量,但你要留余量。
  • 在嵌入式设备上,如果你对丢状态不敏感,宁愿配置 HIBERNATION_SHUTDOWN 模式,也不要依赖平台的 S4 支持。S4 的 bug 在固件层面极难追。
  • 如果用了 initramfs,确认resume钩子确实被触发生效。很多发行版的 initramfs 生成工具默认不把 resume 模块编进去,恢复时直接没加载镜像。

6. 一次完整的休眠调试实录(示例)

我最后用一个实际案例来收尾这个主题。之前调试过一台 x86 工业整机,现象很典型:执行systemctl hibernate后屏幕熄灭,电源指示灯也灭了,但再按电源键开机,系统直接冷启动,完全没有恢复到休眠前的桌面。

一开始我怀疑是 GRUB 的 resume 参数没填,一查cat /proc/cmdline,里面确实没有resume=。常规做法是在/etc/default/grub里加上resume=/dev/sda2,重新生成 GRUB 配置。但这次加上参数重启后,机器还是冷启动。

继续查dmesg | grep -i resume,发现software_resume模块根本没执行——原因是 initramfs 里没有resume钩子。折腾了一点时间排查。这个发行版用的工具在生成 initramfs 时默认不包含 resume 支持,需要在/etc/initramfs-tools/modules或相应的 hook 目录里把resume加进去,重新生成 initramfs 才生效。

解决了启动恢复这一步之后,又冒出来个新问题:系统能恢复,但网卡 link 不上。看了一下日志,网卡驱动的核心固件在恢复后没有重新加载。原因是这个设备的驱动没有实现poweroff/restore回调,内核默认走了 generic 的 suspend/resume,而该网卡的硬件状态在 S4 后已经全部丢失。最终的修复是在驱动里补上了restore回调,重新做一遍固件下载和 PHY 初始化。

整个案例走下来,恰好覆盖了三个不同层面的问题:内核参数、initramfs 阶段、设备驱动。所以你看,搞 hibernation 不是只读内核源码就能搞定的事情,它是一条贯穿着 Bootloader、initramfs、内核 PM 核心、ACPI、设备驱动的长链路。掌握了这条链路,你才能在问题出现时迅速判断“我该去看哪一层的日志”。

如果你正要开始优化自己设备的休眠功能,我建议先别急着改配置,而是找一台干净环境,打开串口输出,把/sys/power/pm_test从freezer一路放到none,完整跑一遍流程,把每一步的内核日志都过一遍。这个过程比看十篇文档都管用,因为你亲眼看到的状态转换、设备回调、镜像写入路径,远比纸面上的流程图来得可靠。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询