从睡眠恢复后损坏的应用 ── Windows 电源事件机制与扛得住恢复的业务应用

· · Windows, 电源管理, Windows 开发, 业务应用, 设备控制, 故障排查, Win32 API

「合上笔记本,第二天早上打开,业务应用满是错误。」「设备监控应用只在午饭后才丢数据。」「向 Excel 导出的常驻工具有时会因连接错误停住。」——这些工单指向同一个嫌疑对象。睡眠

台式机为主流的年代里,业务应用写在「电脑一直开着」这一未言明的假设上。今天的主战场是笔记本,默认空闲几分钟就会睡眠。在具备 Modern Standby 能力的机器上,睡眠本身的语义也已不同于传统模型。面向在 Windows 上编写业务应用和设备控制软件的开发者,本文根据一手资料整理:睡眠前后操作系统会通知应用什么、什么会坏掉,以及如何写出扛得住恢复的应用。

1. 先说结论

  • 睡眠是应用「无权拒绝」的事件。事前会用 WM_POWERBROADCAST(PBT_APMSUSPEND)通知,但宽限期大约 2 秒;紧急挂起时连通知都不会来。12
  • 从挂起恢复时会到达 PBT_APMRESUMEAUTOMATIC;用户发起的恢复还会再到达 PBT_APMRESUMESUSPEND。重连等必要工作原则上放在前者。不过 Modern Standby 进出低功耗空闲并不总是与这些通知对齐,因此把通知当作辅助。34
  • 按「TCP 连接、串口和设备句柄跨不过恢复」来设计。在恢复通知或通信错误时重建它们的重连逻辑,才是主戏。
  • 留意定时器和对时间的处理。周期性工作在睡眠期间会停,恢复后立刻怎么触发因定时器 API 和运行时而异。「经过时间突然跳一大截」也会发生,安全做法是在恢复时重建日程。
  • 对不想被睡眠打断的区间,明确抑制睡眠。使用 SetThreadExecutionState(ES_SYSTEM_REQUIRED)或电源请求(PowerSetRequest),工作结束后务必清除。45
  • 在 Modern Standby 机器上,系统在睡眠期间仍会间歇运行,但桌面应用会被暂停。不能抱着「我们的应用睡眠时还应继续跑」的期待。6
  • 标准调查工具是 powercfg(/requests、/lastwake、/sleepstudy)以及事件日志中的 Kernel-Power。

2. 睡眠前后会发生什么 ── 电源事件的流程

操作系统把电源状态变化作为 WM_POWERBROADCAST 消息广播给每个应用。2 涉及睡眠与恢复的主要事件有三个。

事件 含义
PBT_APMSUSPEND 即将进入睡眠(准备的最后机会)
PBT_APMRESUMEAUTOMATIC 已恢复(恢复时总会到达)
PBT_APMRESUMESUSPEND 由用户操作引起的恢复(这一项是有条件的)

PBT_APMSUSPEND 是睡眠前一刻的通知,此时可以关闭文件、保存状态来做准备。但有两个条件。第一,允许处理的时间大约是每个应用 2 秒,超时后系统不会等待。1 第二,电池电量危急之类的紧急挂起立即睡眠,没有提前通知2「必须在睡眠前做完」的设计不成立。把通知当作「赶得上就做」的机会,把主工作放在恢复一侧。

恢复一侧分两段。从挂起转换恢复时会到达 PBT_APMRESUMEAUTOMATIC。在此之上,若机器因电源按钮或按键等用户操作而恢复(或之后检测到用户在场),还会跟上 PBT_APMRESUMESUSPEND。反过来,因网络远程唤醒或维护而发生的无人值守恢复,则只送达 PBT_APMRESUMEAUTOMATIC。3 这两段本身就是拆分工作的提示——在 PBT_APMRESUMEAUTOMATIC 上做重建连接等机械恢复,在 PBT_APMRESUMESUSPEND 上做屏幕更新或重新登录提示等面向用户的动作

睡眠与恢复的通知流程睡眠前一刻到达 PBT_APMSUSPEND,宽限约 2 秒;恢复时总会到达 PBT_APMRESUMEAUTOMATIC,仅用户发起的恢复才会再跟上 PBT_APMRESUMESUSPEND应用OS应用OS睡眠(代码不运行)PBT_APMSUSPEND(约 2 秒宽限)保存状态并关闭连接PBT_APMRESUMEAUTOMATIC(恢复时到达)重连并恢复状态PBT_APMRESUMESUSPEND(仅用户发起的恢复)屏幕更新等面向用户的工作

图 1: 通知只是「事前一句话,恢复后再一两句」。恢复的主角是恢复一侧的工作。

普通睡眠与紧急挂起的差异普通睡眠会在事前送达 PBT_APMSUSPEND 并约有 2 秒准备,但电量危急之类的紧急挂起没有提前通知就停下,因此依赖提前通知的设计不成立普通睡眠PBT_APMSUSPEND(约 2 秒宽限)准备,然后停下紧急挂起(电量危急)没有提前通知就停下假设通知会来的设计不成立

图 2: 紧急挂起没有任何预警。因此准备是「赶得上就赚到」,主工作放在恢复一侧。

注意,WM_POWERBROADCAST 并不区分低功耗状态的种类(睡眠还是休眠)。4 对应用正确的抽象是把它当作一类事件:「停了,又回来了」。无窗口的服务和控制台应用可以用回调形式(DEVICE_NOTIFY_CALLBACK)的 RegisterSuspendResumeNotification 接收同样的通知。7

把工作拆到两段恢复通知上把重连等机械恢复放在恢复时到达的 PBT_APMRESUMEAUTOMATIC 上;把屏幕更新或重新登录提示等面向用户的工作放在仅用户发起恢复才到达的 PBT_APMRESUMESUSPEND 上PBT_APMRESUMEAUTOMATIC(恢复时)机械恢复PBT_APMRESUMESUSPEND(用户发起的恢复)面向用户的工作重连并重新打开句柄屏幕更新和重新登录提示

图 3: 无人值守恢复不会到达后者,把必要恢复放在后者上就会漏掉。

3. Modern Standby ── 「睡眠」的含义已经变了

还要纳入的一个现代事实是 Modern Standby。传统 S3 睡眠是「把系统整体停下来」的简单模型;Modern Standby 机器上的睡眠是类似智能手机的模型:屏幕熄灭后,系统仍间歇地继续运行

对业务应用重要的是:桌面应用会在进入睡眠的第一阶段被 Desktop Activity Moderator(DAM)暂停6 系统本身仍会不时运行以维持网络并接收通知,但受益于该机制的是参与其中的组件——普通桌面应用代码不会跑。因此从开发者角度看,Modern Standby 与 S3 的结论相同——按睡眠期间你的代码不运行来设计。

传统睡眠与 Modern Standby 的差异传统 S3 睡眠停下整个系统,而 Modern Standby 下屏幕熄灭后系统仍间歇运行。桌面应用在两种情况下都会被 DAM 暂停,因此应用代码不运行传统 S3 睡眠:整个系统停下应用代码不运行Modern Standby:系统间歇运行桌面应用被 DAM 暂停

图 4: 模型变了,但对桌面应用结论相同:「睡眠期间不能运行」。

另一点要小心的是,通知有多不可靠。在 Modern Standby 下,进出低功耗空闲并不与传统挂起转换对齐,连接可能已经断了却从未收到通知。把恢复通知当作辅助,把由错误检测触发的重连(第 5 章)放在主恢复路径上。

还有一层差异是行为的「滑溜」感。进入深度睡眠是分阶段的,断开和停止的时机不像 S3 那样干脆。用户也很难看出「只是屏幕灭了」和「已经睡了」的区别,因此接到症状时需要确认「有没有合盖」以及「空闲了几分钟」。

4. 什么会坏 ── 经典症状

TCP 连接已经死了。睡眠期间,对端、NAT 和防火墙把你的沉默当作超时并丢弃连接。更糟的是,这边的套接字并不知道出错,所以只有在恢复后发送或接收时才会失败。更糟的情况是,接收等待根本不会出错(这就是需要保活的原因)。数据库连接和 WebSocket 也是同样的形状。

串口和 USB 设备句柄失效。USB 连接的设备在恢复时可能看起来像「拔掉又插上」了一次,原先打开的句柄开始返回错误。这就是设备控制应用「只在午饭后才出通信错误」的典型模式。重连设计也写在串口通信文章里。

时间的连续性断了。「每 10 秒轮询一次」这类由定时器驱动的工作在睡眠期间不会触发。恢复后立刻怎么触发(到期的工作立刻触发一次、直到下一周期什么都不发生,等等)因所用定时器 API 和运行时而异,因此不要把错过的节拍交给隐式行为——安全做法是在恢复通知上重建日程。另外,经过时间计算(与上一时间戳的差)会突然变成「8 小时的量」,平均值计算或超时判断就会坏。「每天凌晨 2 点跑」这类定时工作,若那时 PC 在睡,就根本不会跑(需要的话用任务计划程序的从睡眠唤醒功能唤醒)。

时间连续性断裂的三种形态周期性工作在睡眠期间停下且恢复后的触发因 API 而异,因此在恢复时重建日程;与上一时间戳的差在恢复后会变得巨大,因此要加防护;定时工作在机器睡着时不会跑,因此考虑任务计划程序的从睡眠唤醒周期性工作:停下重建日程经过时间:爆炸防护异常差值定时:从未跑过从睡眠唤醒

图 5: 按「时间会跳」来写定时器和时间处理。三种形态各有一类对策。

跨越睡眠会坏掉的三件事跨越睡眠时,TCP 连接已被对端超时丢弃,USB 设备的句柄作为重连而失效,基于经过时间的工作观察到巨大的时间跳跃。分别用重连、重新打开和差值防护来恢复睡眠区间TCP:对端已丢弃USB 还是经过时间?USB:句柄无效经过时间:一次跳跃检测 + 重连重新打开设备防护异常差值

图 6: 会坏的东西分成三族——「连接」、「句柄」和「时间连续性」——各自有固定的恢复类型。

对共享资源的重新认证。网络驱动器和 VPN 在恢复后往往需要重新建立,恢复后立刻有几秒到几十秒的「启动低谷」,访问会失败。更稳妥的是不要在恢复后立刻一次性重试一切,而是稍等再分阶段重试。

5. 做出扛得住恢复的应用

原则只有一条。假设「连接和句柄跨不过睡眠」,把应用结构设计成总能恢复。

检测恢复并恢复。当顶层窗口的 WM_POWERBROADCAST 收到 PBT_APMRESUMEAUTOMATIC 时,丢弃持有的连接并重建。要点是不要只依赖恢复通知。漏掉的通知、以及通知到达之前发生的通信都是真实情况,因此始终配上「检测到通信错误就重连」的路径,把恢复通知当作只是更早启动那条路径的触发器。

// C#: funnel both the resume notification and communication errors into the same reconnect path
protected override void WndProc(ref Message m)
{
    const int WM_POWERBROADCAST = 0x0218;
    const int PBT_APMRESUMEAUTOMATIC = 0x0012;
    if (m.Msg == WM_POWERBROADCAST && (int)m.WParam == PBT_APMRESUMEAUTOMATIC)
    {
        _connectionManager.RequestReconnect();   // idempotent reconnect request
    }
    base.WndProc(ref m);
}

让重连工作本身幂等(无论调用多少次都安全),失败时用指数退避重试,稳态下用保活尽早发现死连接——把这三者作为一套,就能不仅扛住从睡眠恢复,也能扛住短暂的网络掉线和设备重启。

扛得住恢复的重连设计恢复通知、通信错误和保活失败都汇入同一条幂等重连工作,失败时用指数退避重试恢复通知(PBT_APMRESUMEAUTOMATIC)幂等重连工作通信错误检测保活失败成功?回到正常运行指数退避后重试

图 7: 把重连集中到一条幂等路径,从恢复通知、错误检测或保活进入同一条路。

重新审视对时间的处理。对使用「自上次以来的经过时间」的工作,加上检测到异常大差值就作废该区间的防护(不要折进平均值,不要当作超时)。跨恢复测量经过时间,需要区分睡眠期间仍前进的时钟(墙上时钟)和实际花在工作上的时间。

对不想被睡眠打断的区间,明确抑制睡眠。在数据迁移、与设备持续通信等绝不能被睡掉的工作期间,可以用 SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED) 让系统保持清醒(若还想保持屏幕亮着,加上 ES_DISPLAY_REQUIRED)。45 更规矩的方法是电源请求 API(PowerCreateRequest + PowerSetRequest),可以附上原因字符串,然后 powercfg /requests 就会显示「谁在挡、为什么」。8 注意,经 SetThreadExecutionState 的抑制是按线程的,要从设置它的同一线程清除。对 async/await 这类会换线程的工作,用由句柄管理的电源请求一侧。有几点要注意。第一,这些抑制的是空闲自动睡眠。挡不住合盖或从开始菜单选择睡眠这类显式用户操作,因此即便开着抑制也不能跳过本章的重连设计。第二,在 Modern Standby 机器的电池供电下,睡眠超时经过一段时间后这些电源请求也会被切断。不能中断的工作必须由交流电源或运维来保证。8 第三,工作结束后务必清除。漏清会变成新 bug:「这台电脑不知为何不肯睡」。

抑制睡眠的两种手段无论用方便的 SetThreadExecutionState,还是能附上原因字符串并可通过 powercfg 让管理员看见的电源请求 API,工作结束时都务必清除绝不能被睡掉的工作区间SetThreadExecutionState电源请求(PowerSetRequest)方便——只要标志带原因——在 powercfg 中可见工作结束时务必清除

图 8: 对两种手段,「做完就清」都是绝对条件。能让原因可见的电源请求对运维更友好。

服务和无窗口应用RegisterSuspendResumeNotification(DEVICE_NOTIFY_CALLBACK)接收回调通知。7 若持续运行是真正的需求,根解决方案是重新审视把工作常驻在会睡眠的客户端 PC 上的设计,把它移到服务器一侧或不睡眠运维的机器上。

6. 调查 ── powercfg 与事件日志

电源相关调查,操作系统自带的工具就很好用。

  • 不肯睡powercfg /requests 列出发出电源请求的进程和驱动。「应用忘了清除 SetThreadExecutionState」也会出现在这里。
  • 自己醒来powercfg /lastwake 显示最近一次唤醒原因,powercfg /waketimers 显示当前预约唤醒机器的定时器。
  • Modern Standby 质量powercfg /sleepstudy 生成每个睡眠区间的功耗与活动报告。9
  • 确认时间线:事件日志(系统)中的 Kernel-Power 源保留进入睡眠和恢复的记录。与应用日志对照,就能客观确认「错误前是否刚有一次恢复」。
电源故障症状到调查命令的对应对不肯睡的症状,用 powercfg /requests 找出谁持有电源请求;对自己醒来的症状,用 /lastwake 和 /waketimers 找出唤醒原因;对时间线,用事件日志中的 Kernel-Power不肯睡powercfg /requests自己醒来powercfg /lastwake 和 /waketimers想确认时间线事件日志中的 Kernel-Power遗忘的睡眠抑制也会出现

图 9: 症状按三族对应到调查命令。先确认「刚才睡了没有」,再拆分。

处理工单时,先问「电脑刚才是不是睡着了(有没有合盖)」,隔离速度会快很多。

7. 小结

  • 睡眠无法拒绝。提前通知(PBT_APMSUSPEND)是约 2 秒宽限的尽力而为,紧急时不会来。把主设计放在恢复一侧。
  • 恢复通知是 PBT_APMRESUMEAUTOMATIC(从挂起恢复时)+ PBT_APMRESUMESUSPEND(用户操作时)。为通知不到达的情况,把由错误驱动的重连放在主路径上。
  • 假设连接和句柄跨不过恢复,实现幂等重连 + 指数退避 + 保活这三件套。
  • 给基于经过时间的工作加上「异常差值」防护。按睡眠期间不运行来设计定时工作。
  • 对绝不能被睡掉的区间,用 SetThreadExecutionState 或电源请求明确抑制睡眠,做完务必清除。
  • 调查用 powercfg(/requests、/lastwake、/sleepstudy)和 Kernel-Power 事件日志。处理工单时先问「刚才睡了没有」。

从应用的角度看,睡眠是「时间毫无预警地跳跃、与周围的连接被切断,然后又回来」的事件。是把它当作日常生活的一部分织进设计,而不是当作异常状况——这才是笔记本时代业务应用稳定性的分水岭。

相关文章

相关咨询领域

小村软件有限公司承接「从睡眠恢复后通信中断」「午饭后与设备的连接掉线」这类缺陷的根因调查、为既有应用补上重连逻辑与电源事件处理,以及对假定笔记本运行的业务应用和设备控制软件的设计评审。

参考链接

  1. Microsoft Learn, PBT_APMSUSPEND event. 关于这是计算机即将进入挂起状态前到达的事件;关于应用被期望完成保存数据所需的工作;以及关于系统允许大约 2 秒来处理此通知,继续超过该时间的应用可能被中断。  2

  2. Microsoft Learn, System Power Management Events. 关于系统提前广播睡眠等运行模式变化;关于 PBT_APMSUSPEND 在空闲睡眠前通知以便关闭文件并保存数据;关于紧急挂起(电量危急等)不给提前通知;关于处理此消息每个应用最多允许 2 秒、超时后被切断;以及关于恢复时每个应用都会收到通知。  2 3

  3. Microsoft Learn, PBT_APMRESUMESUSPEND event. 关于在用户发起的恢复或之后检测到用户输入时,它在 PBT_APMRESUMEAUTOMATIC 之后发送;关于远程唤醒等外部原因的恢复只发送 PBT_APMRESUMEAUTOMATIC;以及关于应用被期望重新打开睡眠时关闭的文件并为用户输入做准备。  2

  4. Microsoft Learn, WM_POWERBROADCAST message. 关于恢复时总会发送 PBT_APMRESUMEAUTOMATIC,由用户输入引起的恢复还会发送 PBT_APMRESUMESUSPEND;关于此消息不区分低功耗状态的种类;关于电源状态转换的细节记录在系统事件日志中;以及关于调用 SetThreadExecutionState 以防止系统进入低功耗状态。  2 3 4

  5. Microsoft Learn, SetThreadExecutionState function (winbase.h). 关于 ES_SYSTEM_REQUIRED 和 ES_DISPLAY_REQUIRED 能够抑制系统的空闲睡眠和显示器关闭;以及关于用 ES_CONTINUOUS 声明持续抑制,完成后单独调用 ES_CONTINUOUS 来清除。  2

  6. Microsoft Learn, Prepare software for modern standby. 关于 Desktop Activity Moderator(DAM)在进入 Modern Standby 转换的第一阶段暂停桌面应用;以及关于系统随后分阶段进入低功耗阶段和韧性阶段,只有被允许的组件间歇运行。  2

  7. Microsoft Learn, RegisterSuspendResumeNotification function (winuser.h). 关于这是注册以接收挂起/恢复通知的 API,以及关于指定 DEVICE_NOTIFY_CALLBACK 后,无窗口应用或服务除了向窗口句柄投递消息外,还可通过回调接收通知。  2

  8. Microsoft Learn, PowerSetRequest function (winbase.h). 关于能在用 PowerCreateRequest 创建的电源请求对象上设置系统或显示器保持清醒等请求类型;关于能附上诊断用原因字符串;以及关于未完成的电源请求可用 powercfg /requests 枚举。  2

  9. Microsoft Learn, Modern standby SleepStudy. 关于 powercfg /sleepstudy 生成的报告可以按每个 Modern Standby 区间检查功耗、活动以及唤醒原因(电源按钮、用户输入、唤醒定时器等)。 

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

常见问题

汇总了咨询这一主题时常见的问题。

应用能提前得知睡眠并拒绝吗?
在当前 Windows 上可以收到通知,但不能拒绝。即将睡眠时,WM_POWERBROADCAST 消息会送达 PBT_APMSUSPEND 事件,此时可以关闭文件、保存状态来做准备,但允许处理的时间大约是每个应用 2 秒,超时后系统不会等待。电池电量危急之类的紧急挂起,连提前通知都不会来。因此「必须在睡眠前做完」的设计不成立;需要无论在哪一刻被切断,都能在恢复时恢复的设计。对于一段真正不想被睡眠打断的工作,用 SetThreadExecutionState 或电源请求(PowerSetRequest)明确抑制睡眠。
如何检测机器已经恢复?
如果应用有窗口,处理 WM_POWERBROADCAST。从挂起恢复时会到达 PBT_APMRESUMEAUTOMATIC;若恢复由用户操作(电源按钮或按键)引起,随后还会到达 PBT_APMRESUMESUSPEND。无人值守的恢复若马上再次睡眠,则只送达 PBT_APMRESUMEAUTOMATIC,因此基本拆分是:把重连等必要工作放在 PBT_APMRESUMEAUTOMATIC 一侧,把屏幕更新等面向用户的工作放在 PBT_APMRESUMESUSPEND 一侧。无窗口的服务和控制台应用可以用 RegisterSuspendResumeNotification 配合 DEVICE_NOTIFY_CALLBACK,通过回调接收同样的通知。
睡眠期间能让应用继续运行吗?
原则上不能。睡眠期间 CPU 执行本身会停止(在 Modern Standby 机器上,桌面应用会被 Desktop Activity Moderator 暂停),应用代码不会跑。有两种选择。一种是只在工作进行期间抑制睡眠。用 SetThreadExecutionState 指定 ES_SYSTEM_REQUIRED,或用 PowerCreateRequest/PowerSetRequest 发出电源请求,可以在该区间抑制空闲自动睡眠(可用 powercfg /requests 确认)。这仍挡不住用户合盖之类的显式睡眠,因此即便开着抑制,也仍需为恢复做好准备。另一种是接受睡眠,设计成「恢复后再补上」。夜间批处理这类定时工作,也可以用任务计划程序的「唤醒计算机以运行此任务」来唤醒 PC。真正需要持续运行的工作,应放到服务器或不睡眠的服务配置上。
为什么 TCP 连接和串口在恢复后就不能用了?
因为网卡和 USB 设备在睡眠期间也会进入低功耗状态。TCP 连接已被对端或 NAT、防火墙超时丢弃,恢复后的收发会返回错误(往往要到出错才发现)。USB 转串口适配器之类,有时会在恢复时被当作设备拔出再插入,原先打开的句柄失效。对两者正确的假设都是「句柄和连接跨不过恢复」,正确做法是在恢复通知或通信错误时重建连接的重连逻辑。周期性保活,失败时用指数退避重试,是成熟模式。
如何调查意外睡眠或意外恢复?
powercfg 命令是第一工具。在「不肯睡」方向,powercfg /requests 列出哪些进程和驱动发出了阻塞睡眠的电源请求。在「自己醒来」方向,powercfg /lastwake 显示最近一次唤醒原因,powercfg /waketimers 显示当前预约唤醒机器的定时器。在 Modern Standby 机器上,powercfg /sleepstudy 会生成睡眠期间的功耗与活动报告。睡眠和恢复历史也记录在事件日志(系统日志中的 Kernel-Power 源)里,因此可以在时间线上确认「何时睡了、何时因何醒来」。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表