资讯动态

Linux系统休眠与唤醒:状态选型到驱动调试的完整指南

发布时间:2026/10/8 23:23:30 来源:尧图企业网站定制
Linux 内核功耗子系统这个系列写到这一篇终于要聊一个大家日常最“有感知”的环节system sleep也就是整机休眠唤醒。说实话很多人一开始做低功耗接触最多的就是让屏幕灭了、CPU idle但那只算“运行时动态功耗管理”真正要让整台机器睡过去再被一个 RTC 闹钟、一个按键或者一个网络唤醒包叫起来整套流程的复杂度和坑都不在一个量级。这篇文章就围绕 system sleep 的 suspend/resume 完整链路来讲分为状态选型、内核框架、驱动对接、实操调参与问题排查几个部分。适合正在给板子适配休眠唤醒、被设备 resume 卡死折磨过或者想把 /sys/power/state 背后逻辑彻底吃透的工程师。1. 先把“睡眠深度”说清楚freeze、standby、mem、disk 各自干了什么1.1 四个睡眠状态并不是“省电程度递增”这么简单很多人第一次看/sys/power/state会看到类似freeze standby [mem] disk四个字段然后下意识觉得 mem 一定比 standby 更深、更省电。这个理解不准确尤其在 ACPI 平台上状态的意义取决于硬件实现和固件策略。先给一个粗粒度对照表入口内核代表ACPI 对应内存供电主要延迟适用的典型场景freezesuspend-to-idle (s2idle)S0 低功耗空闲 / S0ix保持最快毫秒级现代 x86 笔记本、手机、嵌入式standbyPM_SUSPEND_STANDBYS1保持较快平台支持且愿意做浅睡眠时mem可配PM_SUSPEND_MEMS3deep或 s2idle保持中等大多数 PC、服务器、工控机diskhibernateS4可断电最慢秒级需要完全断电保命的场景这里最容易混淆的是mem。ACPI S3 是真正的 suspend-to-RAMCPU 和大部分外设断电内存进入自刷新恢复时需要重新上电并跳回唤醒向量。但是内核在mem下面还做了一个可配置项叫mem_sleep它可以是s2idle、shallow或deep。也就是说你写echo mem /sys/power/state时实际上进入的可能是 s2idle 而不是 S3具体取决于cat /sys/power/mem_sleep显示哪个中括号。所以与其把 sleep 状态想成“一排档位”不如拆成两个维度一个维度是“内存是否保持供电”另一个维度是“CPU 是否真正停止取指”。freeze 和 s2idle 保持内存、保持 CPU 可被普通中断唤醒standby 和 deep 则把大部分电源轨切掉唤醒路径依赖固件和特殊唤醒源。理解了这个再去排查“为什么我睡了等于没睡”“为什么唤醒变慢”会顺手很多。1.2 为什么现代平台上“s2idle 反而是常态”我最早在 ARM 嵌入式平台接触的是标准的 suspend-to-RAMPMIC 切掉 CPU 电源域DDR 自刷新唤醒源只有 RTC、按键和几个 wakeup 中断。但后来到了 x86 平台上才发现现代很多笔记本宁可选择 s2idle 也不走 S3原因主要有三个。第一ACPI S3 需要平台固件完整实现卧室里的唤醒逻辑包括重新培训内存、恢复 PCIe 链路、处理显卡状态等这中间的固件 bug 非常多很多笔记本厂商干脆默认关闭 S3。第二新硬件追求“始终连接”体验比如后台收邮件、语音唤醒需要 CPU 保持浅睡眠、随时响应s2idle 正好符合这个需求。第三在 Linux 上s2idle 的设备 suspend/resume 路径是完整走内核设备模型的驱动出问题的概率相对可控而 deep S3 一旦挂掉多半要怀疑固件。实践里我一般这样选型如果产品是便携设备追求快速唤醒且外设不多优先调 s2idle如果产品是真的要进低功耗模式省电比如电池供电的工控机、待机几个月不丢数据才考虑 S3/hibernate。至于“能不能两种都支持”答案是能内核允许你在/sys/power/mem_sleep里切换测试阶段建议都跑一遍。1.3 hibernate 完全不是 suspend 的“更深处”很多人把 hibernate 理解成 suspend-to-RAM 的加强版其实它是另一条链路把内存镜像写进 swap然后系统直接断电或软关机。恢复时引导加载器加载内核内核再从 swap 里读回镜像恢复进用户态。这条路径里“休眠唤醒”最关键的两个点是内存镜像的完整性和 swap 空间的规划。内核在做 hibernate 时会通过hibernate_preallocate_memory预留内存然后把要写盘的用户态页面压缩如果开了压缩后写出为了腾出连续内存甚至需要把一些进程先冻结、把页面回收掉。所以你会看到hibernate经常被吐槽“没 swap 寸步难行”“第一次慢得很”。它和省电深度关系不大本质是“把 RAM 的状态持久化”。另外还有一个 hybrid sleep 的概念先把镜像写盘再进 S3/S2idle这样万一睡眠过程中完全断电下次开机能从镜像恢复没断电则直接快速唤醒。这功能在部分发行版里由 systemd 的systemctl hybrid-sleep触发内核层面没有单独的状态名称它内部会先走 hibernate 的镜像保存流程再走 suspend。2. 从 echo mem 到整机睡过去内核 suspend/resume 的完整调用链2.1 用户态的“开关”最终落到哪几个函数我们平时调试最常用的动作就是echo mem /sys/power/state。这个写操作在kernel/power/main.c里对应state_store()它会解析字符串匹配到PM_SUSPEND_MEM或PM_SUSPEND_STANDBY然后调用pm_suspend()。pm_suspend()的核心是enter_state()在那里做了三件事检查当前状态是否允许进入睡眠如果有正在进行的 suspend 流程会直接返回-EBUSY。调用suspend_prepare()这个阶段会广播PM_SUSPEND_PREPARE通知然后冻结所有用户进程和允许冻结的内核线程为“快照一致性”做准备。进入suspend_devices_and_enter()这是最核心的设备挂起和平台睡眠入口。hibernate 那边则是另一条路径入口是hibernate()它要先执行create_image()写镜像然后才进power_down()。我建议调试时先分清自己走的到底是 suspend 还是 hibernate别拿着 hibernate 的 log 去套 suspend 的时序会看得一头雾水。2.2 设备 suspend 为什么分这么多阶段设备挂起不是一口气调完的而是分成了prepare - suspend - late - noirq以及对应的 resume 反向。这样分的原因非常实际睡眠越深系统能提供的服务越少设备必须按照“还能用多少资源”的顺序逐步退出。拿一个带中断、依赖时钟的 PCIe 网卡举例。prepare阶段可以做耗时操作比如通知用户态、停止 DMAsuspend阶段还可以拿互斥锁、调用可能睡眠的函数到late阶段就最好别做重活noirq阶段中断已经禁用这时的回调必须非常克制只做寄存器保存和最低限度的硬件操作。反过来resume 时先执行noirq恢复中断控制器状态、确保 CPU 能接收中断再early再resume最后complete这样设备每多恢复一分能力后续设备就能多依赖一分。内核通过dpm_list管理所有设备的挂起顺序基本思想是“后注册的先 suspend先注册的先 resume”。这跟设备驱动模型里 probe 的先后顺序强相关所以有时候你会发现改一下 probe 顺序整个 suspend/resume 的稳定性就变了。我见过最典型的案例是 i2c 触摸屏和它的 regulator如果 regulator 先被 suspend 了触摸屏晚一点 suspend 时操作硬件就报错。调整dpm_list顺序或者保证依赖关系问题就消失了。2.3 真正“睡过去”的一瞬间syscore 和平台固件设备全部挂起后还轮不到系统立刻断电。内核要先调用syscore_suspend()把那些最底层的系统核心设备也保存起来包括中断控制器、时钟源、时间子系统等。什么是 syscore你可以理解成“比设备模型再低一级的骨架”普通设备还能通过 dev_pm_ops 接管但中断控制器、clocksource 这类一旦断电就全乱了必须由syscore_ops统一处理。它的挂起顺序也是注册顺序不过数量少一般来说很快。在 x86 上syscore_suspend之后会走到arch_suspend()保存 CPU 状态然后调用acpi_suspend_enter()写入 ACPI 寄存器触发平台进入 S3。到这一步CPU 的指令流才真正停住。对 ARM 平台而言对应的是cpu_suspend和平台封装的suspend_ops这些实现最终会把 CPU 送进 WFI 或者更低功耗模式。因此你在 dmesg 里看到一条“PM: suspend entry (deep)”之后系统就“消失”了。观察是否真的睡过去最简单方式是看唤醒后的 log如果“PM: suspend exit”和“PM: suspend entry”之间有个明显时间差说明这段时间确实断电或者 CPU 停了如果时间差是 0那大概率只是走了 s2idle并没有真正“睡死”。2.4 唤醒路径谁叫醒了 CPU谁先醒唤醒过程从硬件信号开始。以 RTC 唤醒为例RTC 中断被配置成唤醒源它在系统睡眠时仍然供电到点产生一个硬件事件平台固件或中断控制器把 CPU 从睡眠状态拉起然后进入内核的唤醒路径。代码层面suspend 时suspend_enter()在 platform 的enter之后会配置好wakeup相关的中断真正的唤醒触发点通常是在 s2idle 路径CPU 在 idle loop 里等待中断普通中断可以直接唤醒然后dpm_resume_noirq等恢复流程开始。在 S3/deep 路径固件恢复处理器执行到唤醒向量内核先调用syscore_resume再dpm_resume_noirq把中断控制器恢复后才允许设备中断进来。所以“谁唤醒”这个问题看似是硬件说了算实际上内核在 suspend 阶段就已经决定了哪些中断保持使能。驱动里常看到enable_irq_wake(irq)或dev_pm_set_wake_irq()就是在告诉中断子系统“这只中断在睡眠时要保持唤醒能力”。反过来如果没调这些接口即使硬件产生了中断中断控制器也可能在睡眠时把它丢掉了表现为“设备明明有事件但系统醒不过来”。3. 实操让设备按预期睡过去、按预期醒过来3.1 五分钟内摸清当前平台的睡眠能力拿到一台设备我建议先做下面这组操作把平台能力摸清楚cat /sys/power/state cat /sys/power/mem_sleep cat /sys/power/disk cat /sys/power/wakeup_countmem_sleep里如果有s2idle deep两项说明 mem 状态可以有两条路。想强制使用 S3就echo deep /sys/power/mem_sleep echo mem /sys/power/state想切回浅睡眠则echo s2idle /sys/power/mem_sleep。注意mem_sleep的设置会直接影响后续每次echo mem的行为。另外很多发行版用户习惯用 systemd 而不是直接 echo。注意systemctl suspend默认会走suspend.target具体进入什么模式还是由/sys/power/mem_sleep决定。测试阶段我建议绕开 systemd直接操作 sysfs因为 systemd 会做各种锁和 inhibitor 检查出问题时很难判断是内核还是用户态拦住了。3.2 驱动要正确支持 sleepdev_pm_ops 该填哪些回调自己写的驱动如果想在那条设备 suspend 链里被正确调用就要在struct dev_pm_ops里实现回调。开发时偷懒只填suspend和resume也能跑但强烈建议按需求把整套回调想清楚回调用途能睡觉吗中断可用prepare通知用户态、做准备工作可以可以suspend停止 DMA、保存状态可以可以suspend_late关闭时钟、关联外设依赖尽量别可以suspend_noirq最后一次保存、关中断关键路径不可以不行resume_noirq恢复中断关键资源不可以视平台resume_early恢复时钟、使能依赖尽量别可以resume恢复运行、重新启 DMA可以可以complete通知用户态、清理可以可以写这些回调的时候要时刻记着一个原则noirq 阶段千万别调用msleep、mutex_lock、kmalloc这类可能睡眠的操作。很多“休眠唤醒概率性崩溃”就是 driver 在 noirq 里拿锁结果睡眠流程还没恢复调度器死锁之后触发 watchdog。如果设备承担了唤醒源的任务比如一个带 WOL 的网卡、一个 USB 触摸屏还要额外做两件事在 suspend 时调用enable_irq_wake(irq)在 resume 时disable_irq_wake(irq)如果用的是dev_pm_set_wake_irq()则这套开关由 PM 核心统一管理。很多人漏了disable_irq_wake会导致设备虽然 resume 了但唤醒中断一直保持使能之后每次有无关中断都可能把系统从浅睡眠中拉起来。3.3 sleep 前后如何判断“睡没睡对”我调试时最喜欢看两个东西dmesg 和/sys/kernel/debug/wakeup_sources。一个完整成功的 suspend/resume 在 dmesg 里看起来大概是这样PM: suspend entry (deep) PM: suspend exit如果你看到PM: suspend entry (s2idle)说明实际进了浅睡眠如果看到类似PM: Some devices failed to suspend, or early wake event detected的警告说明有设备或者唤醒事件打断了流程。suspend_stats也值得看cat /sys/kernel/debug/suspend_stats里面会记录failed_suspend、failed_resume的次数和最后失败设备的名字排查“间歇性休眠失败”时非常管用。当设备 suspend 失败时文件里会有last_failed_devresume 失败时会有last_failed_resume_dev。没有 debugfs 挂载的话先mount -t debugfs none /sys/kernel/debug。4. 排查实录这些年遇到的休眠唤醒经典问题4.1 一睡不起或者一醒就重启这是最吓人的问题。我遇到的情况有两种一种是系统进入睡眠后彻底黑屏无论按什么都无响应另一种是唤醒瞬间看 log 发现有 panic紧接着 watchdog 拉起了系统看上去像重启。先说“彻底睡死”。如果是 S3/deep先怀疑平台固件再怀疑唤醒源配置。DDR 自刷新失败、PCIe 链路掉电后没有正确重建都可能导致“物理上醒不来”。这种问题能做的排查手段有限在 kernel cmdline 里加no_console_suspend保持串口输出或者接逻辑分析仪看电源时序。如果是 s2idle睡死大多和中断有关某个外设中断在睡眠时没有被屏蔽CPU 进入 idle 后不断被中断唤醒又立刻睡回去最终在某次唤醒路径里崩掉。这类问题用dmesg抓最后几十行往往能看到是哪个中断或驱动的调用栈。“一醒就重启”则多半是内核 panic被 watchdog 复位了。常见元凶是设备 resume 时访问了还没就绪的硬件或者 GPIO/clock 状态没有正确恢复。这时候把panic10加进内核 cmdline让 panic 后停一会而不是立刻重启再抓 console 的栈。如果栈顶指向某个 driver 的 resume 回调那就顺着它查。4.2 系统总是在不该醒的时候醒频繁被莫名唤醒的现象在笔记本和车机上尤其常见。典型场景是合盖睡眠后过几分钟屏幕亮了、电流上去了系统根本没睡多久。第一步永远先查唤醒源cat /sys/kernel/debug/wakeup_sources这里的每一行代表一个wakeup_sourcelast_time能告诉我最近一次激活时间。如果某个设备显示刚刚触发过而你的用户操作并没有碰它那它就是“真凶”。比如 USB 控制器默认开启了 wakeup鼠标轻微抖动就会中断网卡开了 WOL局域网内有广播包也能唤醒。处理方式很简单对该设备关闭 wakeup 能力。cat /sys/devices/pci0000:00/0000:00:14.0/power/wakeup echo disabled /sys/devices/pci0000:00/0000:00:14.0/power/wakeup如果设备在 sysfs 里看不到就回到驱动层用device_init_wakeup(dev, false)或干脆不调用使能 wakeup 的接口。记住wakeup和wakeup_count不是一回事前者是“允许唤醒”后者是配合PM_WAKEUP接口做的防误唤醒握手机制不要混。4.3 suspend/resume 慢得离谱怎么定位瓶颈唤醒要 5 秒、10 秒用户绝对受不了。这多半是某个设备在 resume 时长时间等待或者是平台固件恢复本身慢。用 pm-graph 的sleepgraph.py是业界最顺手的办法它能画出一整条 suspend/resume 的设备时序图每类设备耗时一目了然。执行方式大致是sudo sleepgraph.py -m mem -d 3 -g /tmp/suspend.html-m mem表示以 mem 模式睡眠-d是重复次数-g输出 html 报告。打开报告后看“device resume time ranking”。我优化过的两个常见 Top 大户是 USB 控制器和 NVMeUSB 控制器如果有设备处于未连接状态resume 时枚举超时会拖慢很多NVMe 的 APST 和链路恢复有时会固定等几百毫秒。对不太重要的设备可以尝试在驱动里把suspend阶段直接给dev-power.async_suspend true让设备之间并行挂起/恢复。但要注意如果你的设备之间本来就有依赖并行反而容易引入竞态测试要加量。4.4 RTC 定时唤醒的正确打开方式调试时最常用到的定时唤醒工具是rtcwake。它的用法是设置 RTC 闹钟然后立刻让系统进入指定睡眠到点自动唤醒。sudo rtcwake -m mem -s 30上面这条命令会进入 mem 睡眠 30 秒。如果你只是想设闹钟而不立即睡觉可以用-m nosudo rtcwake -m no -s 300模式支持mem、disk、freeze、off等。off其实很有用等于先设 RTC 闹钟再软关机适合做“定时开机”测试。需要注意rtcwake成功的前提是 RTC 驱动正确注册了wakealarm功能否则会报read RTC alarm failed。嵌入式平台上我还踩过一个坑RTC 的备份电池没接掉电后时间不对导致闹钟触发时间完全错乱。这类问题往往不是内核代码错而是硬件没给 RTC 域供电排查时可以看看/sys/class/rtc/rtc0/wakealarm写进去的值是否被正确解析。5. 驱动合入前建议把这几条经验当成门槛做功耗尤其是休眠唤醒我吃过不少亏最后沉淀成几条固定动作每次合代码前都会自查一遍。第一凡是新增了dev_pm_ops的驱动先用echo freeze /sys/power/state跑一遍、再echo mem跑一遍、再echo disk至少跑一次。很多驱动在浅睡眠下没问题但 S3 后中断控制器整体复位漏掉某个.suspend_noirq就会出怪问题。第二suspend_stats里如果有任何一次失败哪怕只失败一次都要追出原因再提交。休眠唤醒是低概率问题放大器1000 次里出现 1 次崩溃用户大概率会复现。第三调试信息里保留no_console_suspend和ignore_loglevel但合入正式配置前应该去掉这些参数因为它们本身会影响功耗和唤醒时序也会掩盖一些只有“真黑屏”才能暴露的问题。第四团队里有人在改设备 probe 顺序、改 regulator 或者改中断控制器驱动时建议顺手把全系统的 suspend/resume 回归跑一遍。这算是我个人经验里最容易被忽视的一条因为改动看起来跟功耗毫无关系但 dpm_list 顺序极其敏感很多“改了 A 坏了 B”的诡异休眠问题都出在这里。休眠唤醒这件事代码看起来就那么几个回调真正难的是把它放到真实的硬件环境里反复磨。建议拿到新板子后先把这套流程完整跑通并记录下来后面的问题排查会轻松很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑