ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux hibernation 休眠唤醒全链路解析:swap、dev_pm_ops 与内核功耗子系统

Linux hibernation 休眠唤醒全链路解析:swap、dev_pm_ops 与内核功耗子系统 1. 从一次休眠唤醒失败说起hibernation 到底在做什么很多人第一次接触 Linux 的 hibernation休眠都是被一个很具体的需求逼出来的笔记本合盖之后希望整机断电第二天打开还能回到原来的工作状态内存里的东西一个不少。这个需求和 suspend挂起最大的区别在于——suspend 只是把大部分硬件关掉内存还在供电维持数据而 hibernation 是把内存里的全部内容写到磁盘上然后整机彻底断电唤醒时再从磁盘读回来。换句话说hibernation 是用磁盘换电的方案代价是慢收益是断电也不丢状态。我在实际项目里遇到过最典型的一个故障设备执行echo disk /sys/power/state之后屏幕黑了但电源指示灯还亮着风扇还在转等了几分钟没有任何反应只能长按电源键强制关机。重启之后dmesg里能看到一堆PM: Cannot find swap device和PM: Cannot get swap writer的报错。这个现象背后其实牵扯到 hibernation 整条链路的几个关键环节swap 空间够不够、swap 设备能不能被内核正确识别、dev_pm_ops的回调有没有按顺序执行、镜像写入过程中有没有设备提前掉线。这篇文章就把这条链路从头到尾梳理一遍重点讲清楚每个阶段内核到底做了什么、哪些地方最容易出问题、出问题之后怎么定位。需要先说明的是hibernation 属于 Linux 内核功耗子系统Power Management里最复杂的一块它同时涉及内存管理、块设备、设备模型、ACPI 等多个子系统。本文不会去逐行读源码而是以一个从业者在调试 hibernation 问题时需要掌握的完整心智模型为目标把流程、关键数据结构、常见坑点讲透。如果你正在做嵌入式 Linux 的电源管理、或者在调试一台机器的休眠唤醒问题这篇内容应该能帮你少走不少弯路。关键词里提到的swap、dev_pm_ops、内核功耗子系统是理解整条链路的三个抓手swap 决定了镜像往哪写、dev_pm_ops决定了设备怎么被冻结和恢复、功耗子系统则提供了sysfs这套用户态接口。下面按执行顺序逐个展开。2. hibernation 的四个阶段freeze、snapshot、write、power down2.1 用户态触发路径与 sysfs 接口触发 hibernation 最直接的方式是往/sys/power/state写diskecho disk /sys/power/state这个写操作会进入内核的state_store()最终调用hibernate()。但在这之前用户态通常还要做几件事确认/sys/power/disk里的模式platform、shutdown、reboot、suspend等确认/sys/power/image_size的限制以及最关键的——确保有可用的 swap。/sys/power/disk这个文件决定了 snapshot 写完之后系统怎么进入低功耗。platform表示交给固件ACPI 的 S4shutdown表示走正常的关机流程reboot表示重启。大多数发行版默认是platform因为唤醒速度最快。但有些机器固件的 S4 实现有问题改成shutdown反而更稳代价是唤醒要走一次完整的开机自检。提示调试阶段建议先把/sys/power/disk设成shutdown排除固件 S4 路径的干扰确认 snapshot 和 restore 本身没问题之后再切回platform。2.2 阶段一冻结用户进程与内核线程hibernation 的第一步和 suspend 一样是 freeze。内核会遍历所有用户态进程把它们逐个冻结freeze_processes()然后冻结可冻结的内核线程freeze_kernel_threads()。冻结的本质是给进程发一个假信号让它们停在安全点上不再持有任何会阻碍 snapshot 的锁。这一步最容易出问题的地方是某个内核线程没有正确实现冻结回调或者某个驱动在 workqueue 里提交了无法被冻结的任务。表现就是echo disk之后卡在Freezing user space processes ...这一行不动等很久之后打印Freezing of tasks failed after 20.00 seconds。遇到这种情况/sys/power/pm_freeze_timeout可以调大超时但根本解法还是找到那个不配合的线程。2.3 阶段二生成内存快照snapshot冻结完成之后内核调用hibernate_preallocate_memory()预分配内存然后进入create_image()。这里的核心动作是把当前内存里的所有页除了标记为nosave的页复制到一个快照里。注意这个快照不是立刻写到磁盘而是先在内存里组织好因为写磁盘的过程中内核自己还要用内存。快照的大小直接决定了需要多大的 swap。理论上需要保存的页数乘以页大小就是镜像的原始大小但实际写入时会做压缩LZO 或 LZ4所以最终占用的 swap 通常比原始内存小。/sys/power/image_size就是用来限制镜像大小的默认值大约是内存的 2/3。如果你把 swap 分得比内存还大那基本不会因为空间不足失败如果 swap 比内存小很多就要靠压缩率来赌了。2.4 阶段三把镜像写入 swap这是整个流程里最重的一步。内核通过 swap writer 把快照数据写到 swap 分区或 swap 文件里。写入过程中所有非 boot 设备会被逐步关闭通过dev_pm_ops的freeze/poweroff回调只留下写 swap 需要的那个块设备。这里有个关键点写 swap 的设备本身不能被关掉。如果 swap 在 SATA 盘上那 SATA 控制器和这块盘必须保持供电直到写完。内核通过dpm_suspend()和dpm_poweroff()两个阶段来控制设备关闭的顺序dev_pm_ops里的回调就是在这两个阶段被调用的。2.5 阶段四进入低功耗与唤醒恢复镜像写完内核执行power_down()根据/sys/power/disk的设置走platform或shutdown。唤醒时bootloader 或固件把控制权交回内核内核检测到 swap 里有有效的 hibernation 镜像就进入 restore 流程重新初始化必要的设备把镜像读回内存然后解冻进程恢复执行。整个 restore 过程对用户来说是透明的——你看到的还是休眠前的桌面但底层其实经历了一次完整的设备重新初始化和内存重建。这也是为什么 hibernation 对驱动的要求比 suspend 更高suspend 只是把设备挂起再恢复而 hibernation 是设备被彻底断电后重新走一遍 probe 流程部分设备或者 restore 回调。3. swap 空间hibernation 的硬门槛与常见误判3.1 swap 分区和 swap 文件的差异swap 可以是独立分区也可以是文件。两者在 hibernation 场景下的行为有细微差别。swap 分区的好处是内核可以直接通过块设备偏移访问不需要经过文件系统层写入路径更短、更可靠。swap 文件则需要内核能定位到文件在磁盘上的物理块这在有日志、有 CoW 的文件系统比如 btrfs上会变得复杂。我个人的经验是如果这台机器的主要用途就是跑 hibernation优先用 swap 分区。如果只能用 swap 文件ext4 上相对稳妥btrfs 上要特别小心因为 btrfs 的 CoW 特性可能导致文件块在写入过程中被重新分配内核拿到的物理块映射就失效了。3.2 到底需要多大的 swap这是被问得最多的问题。一个常见的经验法则是 swap 至少等于内存大小但这个说法过于粗糙。更准确的做法是看/sys/power/image_size的当前值和实际内存使用量。场景建议 swap 大小说明内存 8G日常占用 3G4G 以上压缩后镜像通常 1.5G~2.5G内存 16G日常占用 10G12G 以上高占用场景压缩收益有限内存 32G跑虚拟机建议 32G虚拟机内存页压缩率低嵌入式 512M512M~768M留出压缩和元数据余量判断标准其实很简单/sys/power/image_size默认是内存的 2/3如果你的 swap 小于这个值内核会尝试压缩到 swap 能放下为止压不下就失败。所以最保险的做法是 swap 不小于image_size。3.3 swap 没被识别那些让人抓狂的报错回到开头那个故障。PM: Cannot find swap device这个报错通常意味着内核在 resume 或者 hibernate 时找不到之前用的 swap。原因可能有几种swap 分区在/etc/fstab里用的是 UUID但内核命令行resume参数用的是设备路径两者对不上swap 文件所在的文件系统在 resume 时还没挂载内核无法解析文件位置使用了 LVM 或加密卷resume 时这些层还没初始化。对于 swap 分区正确的做法是在内核命令行里加resume/dev/sdaX或者resumeUUIDxxx并且确保这个参数和实际 swap 一致。对于 swap 文件还需要额外指定resume_offset这个偏移量可以用filefrag -v或者swap-offset工具算出来。注意resume参数写错不会导致系统起不来但会导致唤醒时找不到镜像系统会当成一次冷启动你休眠前的工作全部丢失。这个坑非常隐蔽因为冷启动看起来正常只是数据没了。4. dev_pm_ops设备在休眠链路里的角色分工4.1 dev_pm_ops 的回调集合struct dev_pm_ops是设备驱动向功耗子系统注册回调的入口里面和 hibernation 相关的回调主要有这几组freeze/thaw用于 snapshot 之前的冻结和 snapshot 之后的解冻poweroff/restore用于写镜像之前的关设备和唤醒之后的恢复suspend/resumesuspend 路径用hibernation 的某些阶段也会复用prepare/complete在 freeze 之前和 thaw 之后调用用于做准备工作。这几组回调的执行顺序是有严格规定的。prepare先于freezefreeze先于poweroff。唤醒时顺序反过来restore先于thawthaw先于complete。理解这个顺序是排查某个设备在休眠后状态不对类问题的关键。4.2 为什么有些设备必须实现 restore 而不是 resume这是 hibernation 和 suspend 最大的差异点之一。suspend 时设备只是进入低功耗状态寄存器的值可能还保留着resume回调只需要把设备唤醒即可。但 hibernation 时设备被彻底断电寄存器全部丢失唤醒后设备相当于重新上电restore回调需要把设备重新初始化到休眠前的状态。如果一个驱动只实现了resume没实现restore在 hibernation 唤醒后设备可能处于未初始化状态表现就是网卡不工作、声卡没声音、触摸板失灵。这类问题的排查方法是在dmesg里找唤醒后设备 probe 相关的日志看它走的是哪条路径。4.3 设备关闭顺序与依赖关系dpm_poweroff()会按照设备树的层级从叶子往根关闭dpm_restore()则从根往叶子恢复。这个顺序保证了父设备比如 PCI 控制器在子设备比如挂在它下面的网卡之后关闭、之前恢复。如果驱动之间的依赖关系没有通过设备树正确表达就可能出现父设备已经关了子设备还在访问它的情况表现是写镜像过程中卡死或者报 I/O 错误。这类问题往往需要看dpm_list的实际顺序可以通过打开CONFIG_PM_DEBUG和CONFIG_PM_ADVANCED_DEBUG来打印详细的设备顺序。5. 一次完整的排查链路从卡死到定位5.1 现象记录与初步判断假设你遇到的现象是echo disk /sys/power/state之后终端输出停在PM: Syncing filesystems ... done.然后就没有然后了。这时候不要急着重启先看能不能通过串口或者 SSH 拿到更多信息。如果是本地终端试试AltSysRq组合键SysRq的w可以打印当前所有任务栈SysRq的t可以打印所有线程栈。拿到任务栈之后重点看有没有任务卡在hibernate()相关的调用链上比如create_image、swap_writepage、dpm_poweroff。这一步的目的是判断卡在哪个阶段。5.2 分阶段验证如果卡在 freeze 阶段问题多半在进程冻结。检查/sys/power/pm_freeze_timeout调大之后重试看是否能过。如果还是不行用echo 1 /sys/power/pm_debug_messages打开详细日志看是哪个任务没冻结。如果卡在 snapshot 阶段通常是内存不足或者有页无法被保存。检查dmesg里有没有PM: Not enough free memory之类的报错。这时候可以尝试减小/sys/power/image_size让内核少保存一些页。如果卡在写 swap 阶段问题多半在块设备或者 swap 本身。检查dmesg里有没有 I/O 错误确认 swap 设备在写镜像期间保持可用。5.3 用 ftrace 追踪关键函数对于更隐蔽的问题ftrace 是利器。可以这样追踪 hibernation 相关的函数调用cd /sys/kernel/debug/tracing echo function current_tracer echo hibernate set_ftrace_filter echo 1 tracing_on echo disk /sys/power/state # 唤醒后 cat trace这样能看到hibernate路径下所有函数的调用顺序和耗时快速定位到卡在哪个函数。如果hibernate本身没被追踪到说明问题发生在更早的阶段比如state_store或者freeze_processes。5.4 常见报错与对应处理报错信息可能原因处理方向Cannot find swap deviceresume 参数错误或 swap 未激活检查内核命令行和 fstabNot enough free memoryswap 太小或内存占用过高扩大 swap 或减小 image_sizeFreezing of tasks failed某进程/线程无法冻结查任务栈定位PM: Image not found镜像写入失败或 resume 参数错检查写入日志和 resume 配置Restarting tasks ... done后设备异常驱动 restore 回调缺失检查驱动 dev_pm_ops6. 嵌入式场景下的特殊考量6.1 内存受限时的策略嵌入式设备内存通常很小512M 甚至 256M 很常见。这种场景下 hibernation 的挑战在于swap 空间可能比内存还小压缩率成为关键。LZ4 比 LZO 快但压缩率略低如果 CPU 性能不是瓶颈选 LZO 能省更多空间。另一个策略是减少需要保存的页。内核提供了nosave内存区域的概念一些明确不需要保存的内存比如 DMA 缓冲区、固件保留区可以标记为nosave这样镜像会小很多。具体做法是在设备树或者内核启动参数里配置。6.2 唤醒源配置嵌入式设备唤醒通常靠特定的中断源比如按键、RTC、USB 插入。这些唤醒源需要在设备树里正确配置并且对应的驱动要支持wakeup能力。如果唤醒源没配好设备休眠后就叫不醒只能断电重启。配置唤醒源的通用方法是往/sys/devices/.../power/wakeup写enabled但嵌入式场景下更推荐在设备树里静态配置避免依赖用户态脚本。6.3 与 suspend 的取舍不是所有场景都需要 hibernation。如果设备只是短时间不用suspend 更快、对驱动要求更低。只有当需要长时间断电、或者电池电量极低需要彻底断电保数据时hibernation 才有价值。在嵌入式项目里我通常会把两者都配上让用户态根据电量和使用场景动态选择。7. 几个我踩过的坑和对应的经验第一个坑是 swap 文件放在 btrfs 上。当时图省事直接在 btrfs 根分区建了个 swap 文件swapon正常suspend 也正常但 hibernation 写入到一半就报 I/O 错误。后来查资料才知道 btrfs 的 CoW 会导致文件块在写入过程中被重新分配内核预取的物理块映射失效。换成 ext4 的独立分区之后问题消失。这个坑的教训是hibernation 对 swap 的底层稳定性要求比普通 swap 高得多。第二个坑是resume参数用了设备路径但系统里磁盘顺序会变。有一次机器上插了 U 盘重启后/dev/sda变成了 U 盘原来的系统盘变成/dev/sdb结果resume/dev/sda2指向了错误的位置唤醒时找不到镜像直接冷启动。后来统一改成resumeUUIDxxx问题再没出现过。UUID 不随设备顺序变化这是它比设备路径可靠的根本原因。第三个坑是某个 USB 控制器的驱动没实现restore回调。现象是 hibernation 唤醒后 USB 键盘鼠标全部失灵但系统本身是活的能通过网络 SSH 进去。查驱动代码发现它只实现了resume而 hibernation 唤醒走的是restore路径回调为空导致控制器没被重新初始化。补上restore回调之后问题解决。这个案例说明调试 hibernation 问题时不能只看设备有没有被挂起还要看唤醒后有没有被正确恢复。第四个坑和image_size有关。有一次在一台 16G 内存的机器上swap 只分了 8G日常内存占用 6G 左右按理说够用。但那次刚好开了几个虚拟机内存占用冲到 12Ghibernation 直接失败。后来把image_size从默认的 2/3 调小到 8G让内核在 snapshot 阶段就主动丢弃一些可回收页虽然丢了一些缓存但至少能成功休眠。这个取舍在内存紧张时很实用。8. 调试工具与配置清单把常用的调试手段和配置项整理成一张表方便排查时对照工具/配置路径或命令用途power state/sys/power/state触发 suspend/hibernatedisk mode/sys/power/disk选择 platform/shutdownimage size/sys/power/image_size限制镜像大小freeze timeout/sys/power/pm_freeze_timeout调整冻结超时pm debug/sys/power/pm_debug_messages打开详细日志ftrace/sys/kernel/debug/tracing追踪函数调用SysRqAltSysRqw/t打印任务栈resume 参数内核命令行resume指定 swap 位置swap 状态swapon --show、/proc/swaps确认 swap 可用配置层面建议在/etc/default/grub里显式写上resumeUUIDxxx然后update-grub生效。对于 swap 文件还要加上resume_offset。这两个参数是 hibernation 能否正常唤醒的决定性因素比任何驱动配置都重要。最后分享一个我常用的验证方法配置好之后先手动执行一次systemctl hibernate等机器完全断电后重新开机看能否回到休眠前的状态。如果能再测试合盖休眠、定时休眠等场景。每次改动 swap 或内核参数之后都要重新验证一遍因为这类配置的错误往往不会立刻暴露而是在某次唤醒时才突然出现。
返回列表