从应用看到的 Windows 关机 ── 正确扛住退出通知、重启与断电
· Go Komura · Windows, 关机, Windows 开发, Windows 服务, 设备 PC, 数据完整性, 长时间运行, UPS
「夜间 Windows Update 重启时,设备 PC 上的测量应用写到一半就停了,早上测量档已经坏了。」「有人从共用 PC 注销,抱怨未保存的编辑不见了。」── 对长时间运行的 Windows 应用来说,这两种咨询是经典款。
两个现场的共通点,是把关机当成「不该发生的异常事件」。实际上,从 Windows Update 自动重启、用户注销、UPS 发起的关机,到毫无预告的断电,从应用外面切断执行的事件迟早会来。你挡不住它来。挡得住的是「来的时候把数据弄丢」。
幸好,无论 GUI 应用、控制台应用还是服务,Windows 都有在关机前通知应用的机制。本文以中小企业信息人员与 Windows 应用开发者(尤其是设备 PC 与长时间运行应用)为对象,依 2026 年 8 月当下的 Microsoft Learn 一次信息,整理怎么接这些通知、怎么把收尾设计成「几秒内做完」、重启后怎么自动恢复,以及完全没有通知的断电该怎么准备。
1. 先讲结论
- 把关机设计成「迟早会来的正常事件」。收到通知后能用的时间原则上只有大约 5 秒,当场手忙脚乱存一切的设计会崩。前提是平常就频繁自动保存,让「关机时必须存的差额」保持很小。1
- 从 Windows 8 起的客户端 OS,在开启快速启动时(多数支持休眠的 PC 默认如此),「关机」是混合关机,核心只是在休眠。真正完整重设的只有「重启」。这才是「关机没好、重启就好了」的真正原因。2
- GUI 应用应对 WM_QUERYENDSESSION 立刻回 TRUE,收尾放在 WM_ENDSESSION。原则上不可回 FALSE(拒绝)。1
- 只有真的有不可中断的操作时,才用 ShutdownBlockReasonCreate 显示原因。即便如此,用户和 OS 仍可强制继续,因此「我们挡得住」的设计不成立。34
- 控制台应用用 SetConsoleCtrlHandler 接收通知。宽限期更短──关闭控制台默认 5 秒。还有一个陷阱:载入了 gdi32.dll 或 user32.dll 的处理程序,有些事件不会送到。56
- 依赖 .NET AppDomain.ProcessExit 的收尾,从 .NET 10 起,在处理程序「从外面被结束」的路径上不会跑。从 Main 正常返回时仍会跑,但运行时不再为关闭控制台与关机这类结束信号提供默认处理,那些路径的收尾必须改到符合应用模型的通知。7
- Windows 服务可用 SERVICE_ACCEPT_PRESHUTDOWN,比 SERVICE_ACCEPT_SHUTDOWN(约 20 秒宽限)更早收到、且宽限期可设置。不过 PRESHUTDOWN 默认超时从 Windows 10 Creators Update 起缩成 10 秒,无论哪条路都不要过度依赖宽限期。89
- 重启后的自动恢复,可把 RegisterApplicationRestart 与 ARSO(自动登录)合在一起。崩溃、无响应、更新触发的重启都有恢复路径。1011
- 断电完全没有通知。标准模式是写完整临时文件、刷新、再用 ReplaceFile 交换;但 ReplaceFile 跨断电也不保证原子性,因此备份(.bak)加上载入时验证是一套。事后隔离可从事件记录(1074/41/6008)做。121314
用一句话说,本文的结论是:「随时保持通知一来就能在几秒内打烊的状态,并且用即使没有通知的断电也不坏的方式写档」。
2. 关机时发生什么 ── 四种结束方式
2.1. 注销、关机、重启与断电
从应用的角度看,重要的是两轴:「用户工作阶段怎么结束」以及「核心会怎样」。
| 操作 | 用户工作阶段 | 核心与驱动程序 | 给应用的通知 |
|---|---|---|---|
| 注销 | 结束 | 继续跑 | WM_QUERYENDSESSION(ENDSESSION_LOGOFF)→ WM_ENDSESSION |
| 关机(开启快速启动) | 结束 | 休眠(存进 hiberfil.sys) | WM_QUERYENDSESSION → WM_ENDSESSION,服务收到(PRE)SHUTDOWN |
| 重启 | 结束 | 完全结束;下次是完整启动 | 同上 |
| 断电 | 立刻消失 | 立刻消失 | 没有 |
注销与关机,从应用看几乎是同一件事。WM_QUERYENDSESSION 的 lParam 若设了 ENDSESSION_LOGOFF 位元就是注销;若是 0 就是关机或重启(两者分不出来)。1 也就是说,「只是注销,没关系」这种松懈不成立,正确设计是呼叫同一段收尾代码。
flowchart TB
accTitle: 四种结束方式与给应用的通知
accDescr: 注销、关机与重启都会送到 WM_QUERYENDSESSION 到 WM_ENDSESSION 的通知,收尾在几秒内做完。只有断电完全没有通知,因此用第 8 章的写入设计与 UPS 来准备
signout["注销"] --> notified["QUERY → ENDSESSION"]
shutdown["关机"] --> notified
reboot["重启"] --> notified
poweroff["断电"] --> none["无通知:写入 + UPS"]
notified --> cleanup["几秒内收尾"]
图 1: 注销、关机与重启都会送到 WM_QUERYENDSESSION 到 WM_ENDSESSION 的通知,收尾在几秒内做完。只有断电完全没有通知,因此用第 8 章的写入设计与 UPS 来准备。
2.2. 「关机没好」的真正原因 ── 混合关机
表里容易漏看的是第二列。从 Windows 8 起的客户端 OS,支持休眠的 PC 默认开启快速启动(混合关机),「关机」的行为变了。用户工作阶段仍会照常注销,但核心工作阶段不会关闭;连同装置驱动程序一起存进休眠文件(hiberfil.sys),下次启动原样还原。这样启动较快,但核心与驱动程序状态即使拔电也还在。2 不过这是有条件的行为。休眠本身被关掉(powercfg /hibernate off)、原则或电源选项关掉快速启动、以及 Windows Server 上,关机仍是传统的完整关机。某台 PC 走哪条路,可从电源选项的「开启快速启动」复选框,或 powercfg /a(可用的睡眠状态)是否列出「Fast Startup」判断。
flowchart TB
accTitle: 关机操作时核心会怎样
accDescr: 关机操作依快速启动是否开启分成完整关机或核心休眠,重启则一定走完整启动
op["关机"]
restart["重启"]
op -->|"快速启动开"| hybrid["工作阶段结束 + 核心休眠"]
op -->|"休眠关 / Server"| full["完整关机"]
hybrid --> resume["下次:还原核心"]
full --> boot["下次:完整启动"]
restart --> boot
图 2: 关机操作依快速启动是否开启分成完整关机或核心休眠,重启则一定走完整启动。
另一方面,「重启」一定走完整启动周期。例如驱动程序更新后,你需要全新的状态。2 由此,现场常听到的几种现象就对上了。
- 「关机再加电,装置麻烦还在」── 核心与驱动程序只是从休眠还原,并没有重设
- 「重启之后就好了」── 因为完整启动把它们初始化了
- 设备 PC 的事故处理步骤应写「重启」,不要写「关电再开」
若要从命令行明确做完整关机,用 shutdown /s(Shutdown.exe 默认就是完整关机);若要默认的混合行为,用 shutdown /s /hybrid。2 不建议关掉快速启动。应用侧应假设「关机时核心可能只是在休眠」──例如不要用 OS 启动时间去估「累计运转时间」──并设计成两种都不会坏(快速启动开或关依环境而异)。
3. GUI 应用该怎么做 ── WM_QUERYENDSESSION 与 WM_ENDSESSION
3.1. 两则消息怎么分工
有窗口与消息队列的应用,会分两阶段收到工作阶段结束通知。1
- WM_QUERYENDSESSION──查询:「可以结束吗?」应用应立刻回 TRUE;DefWindowProc 的默认回应也是 TRUE。这里不要开始收尾。
- WM_ENDSESSION(wParam=TRUE)──已确定的通知:「工作阶段真的要结束了」。收尾在这里做。
对 WM_QUERYENDSESSION 回 FALSE 可以中止关机,但文件写得很清楚:「应回 TRUE 并尊重用户的意图」;回了 FALSE 的应用仍会在全屏 UI 被标成「正在阻止关机的应用」。控制台应用与没有可见窗口的应用本来就不能中止关机,5 秒内没回应就会被自动结束。14
flowchart TB
accTitle: 两阶段工作阶段结束通知的流程
accDescr: 对 WM_QUERYENDSESSION 查询回 TRUE 后以 WM_ENDSESSION 确定并做收尾。用 FALSE 拒绝会把应用标成正在阻止关机,约 5 秒没回应可能被强制继续
q["WM_QUERYENDSESSION"]
q -->|"TRUE(原则)"| e["WM_ENDSESSION(已确定)"]
q -->|"FALSE(拒绝)"| blocked["标成正在阻止关机"]
q -->|"约 5 秒没回"| hung["当成当住"]
e --> cleanup["在这里收尾"]
cleanup --> term["处理程序结束"]
hung --> term
blocked -->|"强制继续"| term
blocked -->|"取消"| cont["关机中止"]
图 3: 对 WM_QUERYENDSESSION 查询回 TRUE 后以 WM_ENDSESSION 确定并做收尾。用 FALSE 拒绝会把应用标成正在阻止关机,约 5 秒没回应可能被强制继续。
3.2. 不回应会怎样 ── 5 秒这道墙
WM_QUERYENDSESSION 与 WM_ENDSESSION 都可以把回应延后大约 5 秒。超过之后,系统会显示「这个应用正在阻止关机」画面,用户可选择强制继续(=强制结束应用)。4 被强制结束的处理程序没有第二次机会把档存完。
因此设计重点是这两点。
- 把收尾压在 5 秒内做完的量。 Microsoft 自己也建议平常就频繁存档,让关机时要存的变少,并把未存数据存到暂存位置、下次启动再还原。1
- 关机过程中不要跳出确认对话框。你坐在那里等「要保存吗?」的时候,5 秒就过了。静默落到安全侧(自动保存)。
3.3. 在 WinForms 与 WPF 的实现
在 .NET 桌面应用里,这些消息会被转成架构事件。WinForms 会引发 FormClosing,CloseReason 告诉你是不是关机造成的。
// WinForms:关机/注销时也会引发 FormClosing
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.WindowsShutDown)
{
// 只做幂等的快照保存。不要显示对话框。
// 也不要设 e.Cancel = true(拒绝)。
SaveWorkingStateToTempFile();
return;
}
// 用户按 × 这类一般关闭,这里可以确认
}
在 WPF,对应的是 Application.SessionEnding 事件(XAML 的 SessionEnding 属性,或覆写 OnSessionEnding)。
flowchart TB
accTitle: WinForms/WPF 事件如何对应到消息
accDescr: WM_QUERYENDSESSION 的查询阶段对应 WinForms FormClosing 与 WPF SessionEnding,那里最多只做幂等快照保存。已确定的 WM_ENDSESSION 没有对应事件,因此用 WndProc 或挂钩接收,做只有确定后才能做的收尾
q["WM_QUERYENDSESSION"] --> fc["WinForms:FormClosing"]
q --> se["WPF:SessionEnding"]
fc -.-> idem["只做幂等快照"]
se -.-> idem
e["WM_ENDSESSION"] --> hook["无事件:WndProc 挂钩"]
hook -.-> final["确定后再收尾"]
图 4: WM_QUERYENDSESSION 的查询阶段对应 WinForms FormClosing 与 WPF SessionEnding,那里最多只做幂等快照保存。已确定的 WM_ENDSESSION 没有对应事件,因此用 WndProc 或挂钩接收,做只有确定后才能做的收尾。
// WPF:App.xaml.cs
protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
{
base.OnSessionEnding(e);
// 可以分辨 ReasonSessionEnding.Logoff / Shutdown,
// 但基准是两种都跑同一段快照保存
SaveWorkingStateToTempFile();
// 没有例外理由就不要设 e.Cancel = true
}
这里有一个注意点。FormClosing(CloseReason.WindowsShutDown)与 WPF 的 SessionEnding 都对应查询阶段(WM_QUERYENDSESSION)。若另一个应用拒绝,关机会中止,你的应用继续跑。因此这些事件里能做的,是即使关机中止也无害、跑几次结果都一样的幂等快照保存。若需要「只有真的要结束才能做的收尾」(断线、交还资源等),直接在 WndProc 挂已确定的 WM_ENDSESSION(wParam=TRUE)并在那里做。
无论哪条路径,把本体收进共用的「快照保存」函数,让正常结束、关机、以及(可能的话)崩溃的还原数据用同一格式写,下次启动的还原逻辑就是一条路。即使崩溃也要留下信息的设计,见「Windows 应用因程式错误的例外掉下也要确实留下日志」。
4. 真的必须挡住时 ── ShutdownBlockReasonCreate
写光碟或韧体这类「做到一半被切断就会实体损坏」的操作是例外。这里的正确做法是:不可中断操作开始时用 ShutdownBlockReasonCreate 注册原因字符串,结束时立刻呼叫 ShutdownBlockReasonDestroy。关机被要求时,那个原因会显示在「这个应用正在阻止关机」画面,用户可决定继续或取消。3
flowchart TB
accTitle: 用 ShutdownBlockReasonCreate 保护的流程
accDescr: 不可中断操作开始时注册原因;保护期间若来了关机要求,全屏显示原因并对 WM_QUERYENDSESSION 以 FALSE 拒绝。用户可取消或强制继续,操作结束时清除原因
begin["开始不可中断工作"] --> reg["ShutdownBlockReasonCreate"]
reg --> work["在工作线程跑"]
work --> done["结束:Destroy"]
req["这段期间关机"] --> show["显示原因 + FALSE"]
show -->|"取消"| work
show -->|"强制继续"| kill["处理程序结束"]
图 5: 不可中断操作开始时注册原因;保护期间若来了关机要求,全屏显示原因并对 WM_QUERYENDSESSION 以 FALSE 拒绝。用户可取消或强制继续,操作结束时清除原因。
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool ShutdownBlockReasonCreate(IntPtr hWnd, string pwszReason);
[DllImport("user32.dll", SetLastError = true)]
static extern bool ShutdownBlockReasonDestroy(IntPtr hWnd);
// 从建立主窗口的线程呼叫(从其他线程会失败)
_criticalOperationInProgress = true;
ShutdownBlockReasonCreate(this.Handle, "Writing measurement data to a file");
try
{
// 不可中断操作在工作线程跑。若在 UI 线程同步跑,
// 消息泵会停,处理程序会在下面的 WM_QUERYENDSESSION
// 拒绝代码跑到之前就被当成「无响应」强制继续
await Task.Run(() => WriteMeasurementData());
}
finally
{
ShutdownBlockReasonDestroy(this.Handle);
_criticalOperationInProgress = false;
}
// 另外,只在保护期间对 WM_QUERYENDSESSION 以 FALSE 拒绝
protected override void WndProc(ref Message m)
{
const int WM_QUERYENDSESSION = 0x0011;
if (m.Msg == WM_QUERYENDSESSION && _criticalOperationInProgress)
{
m.Result = IntPtr.Zero; // 拒绝。注册的原因字符串会显示在全屏 UI
return;
}
base.WndProc(ref m);
}
这里容易误解的是角色分工。ShutdownBlockReasonCreate 只做注册原因字符串;它本身并不挡住关机。真正把关机拦住的,是上面这种在保护旗标立着时对 WM_QUERYENDSESSION 回 FALSE 的自己的处理。两者当一套用,操作结束立刻两边都清掉。另外,被保护的操作本身放在工作线程,让 UI 线程能处理消息──拒绝机制要等消息送到才会动(即便如此,用户和 OS 仍可强制继续,因此「如果没挡住」也不坏的写入设计──第 8 章──仍然必要)。
营运上有三个注意点。
- 原因字符串要短而具体。用户很急,只会读几秒。文件自己举的适当例子是 “Burning a CD”。3
- 不要让它注册整个应用生命周期。 API 假定的是「只在不可中断操作进行中」。
- 不要用「挡得住」当设计前提。用户可选择强制继续,强制关机(ENDSESSION_CRITICAL)根本不会等。文件写得很清楚:”Applications should not depend on being able to block shutdown”。4
5. 控制台应用与背景处理程序该怎么做
5.1. SetConsoleCtrlHandler 与较短的宽限期
控制台应用收不到窗口消息,控制信号会送到用 SetConsoleCtrlHandler 注册的处理函数。各信号的默认宽限期如下。5
| 信号 | 何时发生 | 默认宽限期 |
|---|---|---|
| CTRL_C_EVENT / CTRL_BREAK_EVENT | Ctrl+C / Ctrl+Break | 没有超时 |
| CTRL_CLOSE_EVENT | 关闭控制台、工作管理员「结束工作」(从「详细数据」选项卡强制杀处理程序是没有通知的立即结束,不在此表) | 约 5 秒 |
| CTRL_SHUTDOWN_EVENT | 系统关机(服务处理程序) | 约 20 秒 |
有两点要注意。第一,实质上只有以服务执行的处理程序能收到 CTRL_LOGOFF_EVENT 与 CTRL_SHUTDOWN_EVENT。互动工作阶段里的应用在注销时就被结束,等这些信号的设计不成立。5 第二,载入了 gdi32.dll 或 user32.dll 的处理程序,即使你当它是控制台应用,也会被当成 Windows 应用,LOGOFF/SHUTDOWN 处理例程不会被呼叫。官方变通是建立隐藏窗口,接收 WM_QUERYENDSESSION/WM_ENDSESSION。6
flowchart TB
accTitle: 各控制台信号的宽限期
accDescr: Ctrl+C 与 Ctrl+Break 没有明确超时;关闭控制台约 5 秒,给服务处理程序的关机信号约 20 秒;超过就强制结束处理程序
ctrlc["CTRL_C / BREAK"] -->|"无超时"| handler["HandlerRoutine 收尾"]
closeev["CTRL_CLOSE"] -->|"约 5 秒"| handler
shutev["CTRL_SHUTDOWN"] -->|"约 20 秒"| handler
handler --> timeout["宽限期后强制结束"]
图 6: Ctrl+C 与 Ctrl+Break 没有明确超时;关闭控制台约 5 秒,给服务处理程序的关机信号约 20 秒;超过就强制结束处理程序。
// 控制台应用:Ctrl+C 与关闭控制台时收尾
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool SetConsoleCtrlHandler(HandlerRoutine handler, bool add);
delegate bool HandlerRoutine(int ctrlType); // 2 = CTRL_CLOSE_EVENT
static readonly HandlerRoutine s_handler = OnCtrlEvent; // 保住参考,避免被 GC 回收
static bool OnCtrlEvent(int ctrlType)
{
// 只做 5 秒内做完的收尾
FlushAndCloseDataFile();
return false; // 交给默认处理例程;处理程序结束
}
static void Main()
{
SetConsoleCtrlHandler(s_handler, add: true);
// ...
}
5.2. .NET 的陷阱 ── 不要依赖 ProcessExit
在 .NET 里,「在 AppDomain.ProcessExit 收尾就好」长期是现成模式,但从 .NET 10 起运行时不再提供默认的结束信号处理例程,CTRL_CLOSE_EVENT/CTRL_SHUTDOWN_EVENT 既不触发 ProcessExit 也不触发 AssemblyLoadContext.Unloading。OS 默认处理例程会立刻结束处理程序。7
flowchart TB
accTitle: .NET 10 之后 ProcessExit 怎么变
accDescr: 到 .NET 9 为止,运行时的默认信号处理例程接到结束信号、引发 ProcessExit 再结束。从 .NET 10 起运行时不提供默认处理例程,OS 默认处理立刻结束处理程序,要自己注册处理例程
sig["CTRL_CLOSE / SHUTDOWN"] --> old9["到 .NET 9:ProcessExit"]
sig --> new10["从 .NET 10:立刻结束"]
new10 -.-> alt["自己注册处理例程"]
图 7: 到 .NET 9 为止,运行时的默认信号处理例程接到结束信号、引发 ProcessExit 再结束。从 .NET 10 起运行时不提供默认处理例程,OS 默认处理立刻结束处理程序,要自己注册处理例程。
改走各应用模型的正规路径。
- GUI 应用:前一章的 FormClosing / SessionEnding
- Generic Host(含 Worker Service):IHostApplicationLifetime 与 BackgroundService.StopAsync。用 HostOptions.ShutdownTimeout 把停止宽限期写清楚
- 裸控制台应用:SetConsoleCtrlHandler(或用 PosixSignalRegistration 订阅 SIGINT/SIGTERM 对等信号)
flowchart TB
accTitle: 各应用模型在哪里接收结束通知
accDescr: GUI 应用用 FormClosing 与 SessionEnding,已确定的工作再用 WM_ENDSESSION 挂钩;Generic Host 用 IHostApplicationLifetime 与 StopAsync;裸控制台应用用 SetConsoleCtrlHandler 或 PosixSignalRegistration。依赖 ProcessExit 在外部信号路径上不会触发
model{"哪种应用模型?"}
model -->|"GUI"| gui["FormClosing / SessionEnding"]
model -->|"非 GUI"| other{"Host 还是控制台?"}
gui -.-> guihook["ENDSESSION 挂钩"]
other -->|"Host"| host["Lifetime + StopAsync"]
other -->|"控制台"| con["SetConsoleCtrlHandler"]
host -.-> hostto["设置 ShutdownTimeout"]
con -.-> ngx["不要依赖 ProcessExit"]
图 8: GUI 应用用 FormClosing 与 SessionEnding,已确定的工作再用 WM_ENDSESSION 挂钩;Generic Host 用 IHostApplicationLifetime 与 StopAsync;裸控制台应用用 SetConsoleCtrlHandler 或 PosixSignalRegistration。依赖 ProcessExit 在外部信号路径上不会触发。
宽限期依路径而异──GUI 与关闭控制台约 5 秒,服务是第 6 章的 SCM 宽限期(约 20 秒,或 PRESHUTDOWN 的设置值),Ctrl+C 没有明确超时。但每条路径的宽限期都有限、不能指望,因此设计主轴是平常就是「每个处理检查点都已存好」,而不是「在结束事件里拼命做」。
6. Windows 服务该怎么做 ── SHUTDOWN 与 PRESHUTDOWN
6.1. 两种关机通知
服务不受注销影响,但关机与重启时会被停止。通知以控制码从 Service Control Manager(SCM)送来,接收前要宣告接受旗标。8
| 宣告 | 会送到的通知 | 时机与宽限期 |
|---|---|---|
| SERVICE_ACCEPT_SHUTDOWN | SERVICE_CONTROL_SHUTDOWN | 关机处理中通知。默认约 20 秒,上限 WaitToKillServiceTimeout |
| SERVICE_ACCEPT_PRESHUTDOWN | SERVICE_CONTROL_PRESHUTDOWN | 在 SHUTDOWN 之前通知。SCM 等到服务停止或超时 |
flowchart TB
accTitle: 给服务的关机通知顺序
accDescr: 关机开始时,宣告了 PRESHUTDOWN 的服务先以设置的宽限期收到通知,然后再送默认约 20 秒的 SHUTDOWN 通知,宽限期到期就结束处理程序
start["关机开始"] --> pre["PRESHUTDOWN(若有宣告)"]
pre --> shut["SHUTDOWN(约 20 秒)"]
shut --> kill["宽限期到期 → 结束"]
图 9: 关机开始时,宣告了 PRESHUTDOWN 的服务先以设置的宽限期收到通知,然后再送默认约 20 秒的 SHUTDOWN 通知,宽限期到期就结束处理程序。
PRESHUTDOWN 超时可用 ChangeServiceConfig2(SERVICE_CONFIG_PRESHUTDOWN_INFO)设置;默认是 Windows 10 Creators Update(组建 15063)起 10 秒,之前是 3 分钟。9 若还抱着「PRESHUTDOWN 有 3 分钟」的旧知识,在现行 OS 上宽限期只有预期的 1/18。另外,PRESHUTDOWN 会让整个系统的关机停在那个区间,因此文件也说它「只应在特殊情况使用」。8
处理例程侧的实务也很重要。控制处理例程必须在 30 秒内返回;花时间的停止工作交给另一条线程,回报 SERVICE_STOP_PENDING,立刻返回。8
// Win32 服务:接受 PRESHUTDOWN,把停止工作交给工作线程
g_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PRESHUTDOWN;
DWORD WINAPI ServiceHandlerEx(DWORD control, DWORD, LPVOID, LPVOID)
{
switch (control)
{
case SERVICE_CONTROL_PRESHUTDOWN:
case SERVICE_CONTROL_STOP:
ReportStatus(SERVICE_STOP_PENDING, /*waitHintMs*/ 3000);
SetEvent(g_stopEvent); // 告诉工作线程停止并立刻返回
return NO_ERROR;
}
return ERROR_CALL_NOT_IMPLEMENTED;
}
// 工作线程侧:若收尾比 waitHint 还长,持续定期回报
// SERVICE_STOP_PENDING 并递增 dwCheckPoint。
// SCM 依 waitHint 与检查点前进判断「还活着而且有进度」。
// 若回报停了,可能被当成当住并继续关机。做完一定要回报 SERVICE_STOPPED
6.2. 不依赖宽限期的设计
从服务侧改写宽限期上限 WaitToKillServiceTimeout 来延长,明确不建议。文件要求的是反过来──服务应尽快做完收尾,让靠 UPS 供电的机器能在电池耗尽前完成关机。指引是平常就频繁存档、让未存数据最少,关机时不要花时间释放内存,通知网络对端时也不要等太久。另外,关机时 SCM 默认不考虑相依关系,因此停止处理必须「即使相依的服务已经倒下也还能动」。8
flowchart TB
accTitle: 不依赖宽限期的停止处理设计
accDescr: 若在每个处理检查点都存档、让未存数据始终最少,停止通知来时的收尾几秒就做完。把一切留到结束才存的设计塞不进宽限期,强制结束就丢数据
good["每个检查点都存"] --> gstop["停止 → 存一点差额 → 完成"]
bad["结束时才存一切"] --> bstop["停止 → 存档赶不上宽限"]
bstop --> killed["强制结束 → 数据遗失"]
图 10: 若在每个处理检查点都存档、让未存数据始终最少,停止通知来时的收尾几秒就做完。把一切留到结束才存的设计塞不进宽限期,强制结束就丢数据。
在 .NET Worker Service(UseWindowsService)里,SERVICE_CONTROL_STOP 与 SHUTDOWN 会被转成主机停止,并呼叫 BackgroundService.StopAsync。本文撰写时的现成实现接受 STOP/SHUTDOWN 一族;若也需要 PRESHUTDOWN,就要扩充处理例程。无论哪种,把 HostOptions.ShutdownTimeout 写清楚,并在几秒内做完 StopAsync。一般如何做服务,见「Windows 服务的建立与维运」。
7. 重启后自动恢复
对设备 PC 或无人值守 PC,设计范围不只是「扛过关机」,还有「重启后自己回来」。
7.1. RegisterApplicationRestart 与恢复回呼
若已呼叫 RegisterApplicationRestart,应用会被注册为崩溃(未处理例外)、无响应、更新触发的应用重启、以及更新触发的 OS 重启的重启候补。可以注册重启用的命令行参数,因此若包含「当时开着哪个档」与「哪个还原点」,重启后就能从停下的地方继续。10
要掌握的规格如下。10
- 注册必须在问题发生前完成(更新情境里,处理 WM_QUERYENDSESSION 是最后机会)
- 为避免重启回圈,执行未满 60 秒的处理程序不会被重启
- 以提升权限执行的处理程序不是自动重启候补(没有提升同意就无法重建处理程序)。需要提升的应用的自动恢复,做法是 UI 维持标准权限、把特权工作隔离到服务,或用工作排程器「以最高权限执行」这类明确启动路径
- 崩溃或当住后的重启要经用户同意;更新后的重启是自动的
- 要跨过 OS 重启恢复,要求重启的那一方(安装程式等)必须以 EWX_RESTARTAPPS / SHUTDOWN_RESTARTAPPS 旗标呼叫关机 API
若也注册 RegisterApplicationRecoveryCallback,崩溃时 WER(Windows Error Reporting)会呼叫回呼,给你一段宽限期去存进行中的数据。不过若保存花时间,必须在注册时指定的 ping 间隔内持续呼叫 ApplicationRecoveryInProgress,否则恢复工作会做到一半被切断。存完后用 ApplicationRecoveryFinished 通知完成。应用更新时「替换使用中的档并重启」是 Restart Manager 的领域,细节见「如何替换使用中的 exe/DLL」。
7.2. ARSO ── 更新重启后的自动登录
Windows Update 重启后,若没有人登录,用户工作阶段的应用不会回来。补这段空隙的是 ARSO(Winlogon Automatic Restart Sign-On)。Windows Update 开始重启时,会安全地保存最后一位互动用户的认证数据、设置 Autologon,重启后自动让该用户登录,然后锁定画面。11 也有 shutdown /g 这类要求重启并恢复已注册应用的命令。有些环境会用组织策略(DisableAutomaticRestartSignOn 等)关掉它,因此设计无人值守恢复时,把这个设置当一套检查。若背景工作一直需要依赖用户工作阶段的自动启动,一开始就做成 Windows 服务才对。
flowchart TB
accTitle: 应用在重启后自动恢复的路径
accDescr: 若在问题发生前用 RegisterApplicationRestart 注册,崩溃或无响应时经用户同意后重启应用,更新触发的重启则在 ARSO 自动登录并锁定画面后重启。执行未满 60 秒与提升权限的处理程序不在范围内
reg["RegisterApplicationRestart"]
reg --> crash["崩溃或当住"]
reg --> update["更新重启"]
crash -->|"同意"| restart["应用重启"]
update --> arso["ARSO 登录 + 锁定"]
arso --> restart
restart -.-> limits["不包含:未满 60 秒/提升权限"]
图 11: 若在问题发生前用 RegisterApplicationRestart 注册,崩溃或无响应时经用户同意后重启应用,更新触发的重启则在 ARSO 自动登录并锁定画面后重启。执行未满 60 秒与提升权限的处理程序不在范围内。
8. 扛住完全没有通知的断电 ── 写入设计与 UPS
8.1. 「无论何时被切断都不坏」的写法 ── 临时文件 + ReplaceFile
跳电、电源供应器故障、插头被拔,既没有 WM_ENDSESSION 也没有 PRESHUTDOWN。只要对设置或测量结果「就地覆写原文件」,写到一半断电就可能留下新旧混在一起的损坏文件。
标准模式是在同一卷写完整的临时文件再交换。ReplaceFile 把「存到新档 → 把原文件挪开 → 重新命名 → 删除」包成单一 API,也会带过原文件的建立时间、ACL、替代数据流等属性(三个档必须在同一卷)。12 .NET 的 File.Replace 就是直接呼叫它。
flowchart TB
accTitle: 用临时文件与 ReplaceFile 保存并恢复的流程
accDescr: 保存时写完整临时文件、刷新、再用 ReplaceFile 交换,旧内容留在 .bak。下次启动验证主文件,坏了就回退 .bak
subgraph save["保存时"]
w["写完整临时文件"] --> f["刷新到磁盘"]
f --> r["ReplaceFile → .bak"]
end
subgraph startup["下次启动"]
v["验证主文件"]
v -->|"完好"| use["直接使用"]
v -->|"损坏"| bak["回退到 .bak"]
end
r -.->|"任一步断电"| v
图 12: 保存时写完整临时文件、刷新、再用 ReplaceFile 交换,旧内容留在 .bak。下次启动验证主文件,坏了就回退 .bak。
// 设置与数据的标准模式:写完整临时文件再交换,并留下旧内容
public static void SaveAtomically(string path, string content)
{
string dir = Path.GetDirectoryName(path)!;
string tmp = Path.Combine(dir, Path.GetRandomFileName()); // 在同一卷建立
try
{
using (var fs = new FileStream(tmp, FileMode.CreateNew, FileAccess.Write))
using (var writer = new StreamWriter(fs))
{
writer.Write(content);
writer.Flush();
fs.Flush(flushToDisk: true); // 等同 FlushFileBuffers。把 OS 缓冲区
// 写出到磁盘(装置侧快取的限制见 8.2)
}
if (File.Exists(path))
File.Replace(tmp, path, path + ".bak"); // 呼叫 ReplaceFile。旧内容留成 .bak
else
File.Move(tmp, path);
}
catch
{
// 做到一半失败时不要留下临时文件。定期保存反复失败
// 会让卷堆满完整复本
try { File.Delete(tmp); } catch { /* 删除失败时优先抛出原本的例外 */ }
throw;
}
}
这样一来,平常运作下永远能读到「完整旧文件」或「完整新文件」。不过 ReplaceFile 是多步骤的命名空间操作,跨断电的原子性规格并不保证。所以上面的例子才留备份(.bak)──读取侧在启动时验证主文件、坏了就回退备份,这是一套。附加写入的记录或 CSV 不能用这招,那些用「一行=一笔纪录,读取时丢掉坏掉的最后一行」这种把损坏算进去的格式。
8.2. WriteFile 成功不代表已到磁盘
另一个前提是:即使 WriteFile 回成功,数据可能还只在 OS 快取里。Windows 把文件读写放进系统缓冲区,再用延迟写入定期反映到磁盘。要确定数据到磁盘,要嘛用 FlushFileBuffers 明确刷新,要嘛在 CreateFile 指定 FILE_FLAG_WRITE_THROUGH 让每次写入穿过快取。文件系统中继数据一律会被快取,因此确认中继数据也需要刷新或 write-through。13
不过每次都呼叫 FlushFileBuffers 没效率,文件也鼓励考虑 FILE_FLAG_NO_BUFFERING+WRITE_THROUGH,而不是频繁呼叫。13 实务上,「只在交易检查点或即将关闭文件时刷新」是务实的折衷。这一层的机制──快取管理员、延迟写入,以及「刷新了仍可能没到磁盘」的硬件快取──深入见「快取管理员:你的 WriteFile 究竟何时送达磁盘」。
8.3. UPS 与电池监视 ── 把断电变成关机
设备 PC 对抗断电的真正手段是 UPS。把 UPS 的角色想成不是「阻止停电」,而是把「没有通知的断电」变成「有通知的计划关机」。设计是两段。
- 设计宽限期:UPS 电池撑住时间 >「检测切到电池 → 应用与服务收尾 → OS 关机完成」的总和。若服务停止处理太慢,这个不等式就不成立(第 6.2 节)
- 检测:从交流电切到电池、以及剩余容量下降,会以 PBT_APMPOWERSTATUSCHANGE 事件通知。有窗口的应用以 WM_POWERBROADCAST 接收;没有窗口的服务宣告 SERVICE_ACCEPT_POWEREVENT,在 HandlerEx 以 SERVICE_CONTROL_POWEREVENT 接收(WM_POWERBROADCAST 不会送到服务控制处理例程)。收到后呼叫 GetSystemPowerStatus,检查 ACLineStatus(是否在交流电上)与 BatteryLifePercent,导入中断测量、保存、要求关机15
flowchart TB
accTitle: 用 UPS 把断电变成计划关机的流程
accDescr: 停电让 UPS 切到电池时会通知 PBT_APMPOWERSTATUSCHANGE,检查电源状态后,保存加上关机要求,把没有通知的断电变成有通知的计划关机
outage["停电"] --> ups["UPS 切到电池"]
ups --> pbt["PBT_APMPOWERSTATUSCHANGE"]
pbt --> check["GetSystemPowerStatus"]
check --> saveop["中断并保存"]
saveop --> req["要求关机"]
req --> normal["平常的通知流程(3–6)"]
图 13: 停电让 UPS 切到电池时会通知 PBT_APMPOWERSTATUSCHANGE,检查电源状态后,保存加上关机要求,把没有通知的断电变成有通知的计划关机。
典型的 USB 连接 UPS 在 Windows 上看起来像电池,因此可用这套标准 API 检测。若厂商管理软件有「剩余 N% 时关 OS」功能,也要确认门槛与应用的收尾时间对得上。从睡眠或休眠恢复,以及长时间运行的问题,是另一条轴,见「睡眠、休眠、Modern Standby 与长时间运行的应用」。
9. 怎么验证 ── 安全地试关机
关机处理很容易变成「写了,却从没在接近正式的条件下试过」。准备一套安全验证的步骤。
- 在测试机或 VM 上试:不要先在正式设备 PC 上试。在有 Hyper-V 检查点(快照)的测试环境,反复关机、重启、强制断电(关掉 VM)。不过 VM「关电」只重现「客体 OS 毫无预告地停」;它不重现实体磁盘挥发性快取消失、或控制器相依的损坏。若要以设备 PC 出货,最终检查是在等同正式的硬件上做真正的切断电源测试
- 用注销做快速检查:WM_QUERYENDSESSION → WM_ENDSESSION 路径在注销时也会跑(唯一差别是 lParam 设了 ENDSESSION_LOGOFF 位元),因此可在开发机方便确认收尾代码的行为1
- 完整关机与混合分别试:分别试
shutdown /s /t 0(完整)、shutdown /s /hybrid /t 0(默认行为)、shutdown /r /t 0(重启)2 - 量收尾花多久:在收尾函数开头与结尾把时间戳写进记录,量是否塞进 5 秒(或服务的设置宽限期)
flowchart TB
accTitle: 要验证的操作与各能确认什么
accDescr: 注销方便检查通知路径;关机命令的完整、混合与重启确认正式通知路径与宽限期;VM 关电测试突然停止的耐受力;实体切断电源测试是含实体保存的最终检查
signtest["注销"] -.-> path1["QUERY → ENDSESSION 路径"]
signtest --> shuttest["shutdown /s /hybrid /r"]
shuttest -.-> path2["正式路径 + 宽限"]
shuttest --> vmtest["VM 关电"]
vmtest -.-> path3["客体突然停止"]
vmtest --> hwtest["实体切断电源"]
hwtest -.-> path4["含保存(最终)"]
图 14: 注销方便检查通知路径;关机命令的完整、混合与重启确认正式通知路径与宽限期;VM 关电测试突然停止的耐受力;实体切断电源测试是含实体保存的最终检查。
事后隔离时,事件记录(系统)有用。正常关机或重启会记录 事件ID 1074(哪个处理程序为谁、因何开始关机)。突然断电或崩溃没有 1074,下次启动会记录 事件ID 41(Kernel-Power)与 6008(前一次系统关机非预期)。14「夜里发生了什么」从这里开始。
# 检查近期关机相关事件的历史
Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 1074, 6008, 41 } -MaxEvents 20 |
Select-Object TimeCreated, Id, ProviderName, Message |
Format-List
若 1074 显示「Windows Update 的重启」且应用数据坏了,问题在收尾代码。6008/41 只显示「非预期关机」;蓝白崩溃(崩溃)或强制重设也会记,不只断电。若 41 的 BugcheckCode 非零就是崩溃;若是 0 且也没有内存倾印,断电的可能性较高──从周围信息隔离原因,一旦知道是断电,下一步就是第 8 章的写入设计与 UPS。
10. 总结
- 关机是「迟早会来的正常事件」。通知后的宽限期原则上只有大约 5 秒,因此前提是频繁自动保存,让「结束时要做的事」最少。
- 从 Windows 8 起的客户端 OS,若开启快速启动,「关机」是混合关机,核心只是在休眠。真正完整重设的只有「重启」──事故处理步骤请写「重启」。
- GUI 应用对 WM_QUERYENDSESSION 立刻回 TRUE,已确定的收尾放在 WM_ENDSESSION。WinForms/WPF 的 FormClosing 与 SessionEnding 对应查询阶段,那里最多只做幂等快照保存。关机过程中不要跳出对话框。
- 真的不可中断的操作,用 ShutdownBlockReasonCreate 显示原因来保护。但任何地方都不保证你挡得住。
- 控制台应用用 SetConsoleCtrlHandler 接收通知;服务用 SERVICE_ACCEPT_PRESHUTDOWN/SHUTDOWN。现行 OS 上 PRESHUTDOWN 默认宽限期是 10 秒。在 .NET,停止依赖 ProcessExit,改走符合应用模型的正规路径。
- 重启后的恢复可用 RegisterApplicationRestart(加上恢复回呼)与 ARSO 做成无人值守。
- 断电没有通知。用临时文件 + ReplaceFile 交换(搭配备份 + 载入时验证)、检查点刷新,以及「把断电变成计划关机」的 UPS 来准备。
flowchart TB
accTitle: 关机处理的整体图像
accDescr: 有通知的结束,用几秒内能打烊的收尾回应,并导入重启后自动恢复;没有通知的断电,用无论何时被切断都不坏的写法与 UPS 准备,并含实体硬件验证。这两根支柱就是本文的结论
ending{"怎么结束"}
ending -->|"有通知"| pillar1["几秒收尾(3–6)"]
ending -->|"无通知"| pillar2["安全写入 + UPS(8)"]
pillar1 --> recover["重启后自动恢复(7)"]
pillar2 --> verifytest["在硬件上验证(9)"]
图 15: 有通知的结束,用几秒内能打烊的收尾回应,并导入重启后自动恢复;没有通知的断电,用无论何时被切断都不坏的写法与 UPS 准备,并含实体硬件验证。这两根支柱就是本文的结论。
- 在 VM 与注销上安全验证,事后用事件ID 1074/41/6008 隔离。
下次替应用加功能时,问自己一次:如果这段工作做到一半 WM_ENDSESSION 来了,或电源被拔掉,下次启动还剩下什么?把那个答案写进设计,就是以后不用在设备 PC 前面抱头站一早上的最短路径。
相关文章
- 如何替换使用中的 exe/DLL ── Restart Manager 与自动更新的「文件使用中」问题
- Windows 服务的建立与维运 ── 从工作排程器的取舍到 BackgroundService 服务化
- 睡眠・休眠・Modern Standby 与长时间运行的应用 ── 用设计预防「半夜停止运转」
- Windows I/O 的深层(第 4 回) ── 快取管理员:你的 WriteFile 究竟何时送达磁盘
- Windows 应用因程式错误的例外掉下也要确实留下日志
- Windows 应用安全处理子行程的 checklist - Job Object、结束传播、标准输入输出、watchdog 的最佳实务
相关咨询领域
小村软件有限公司承接设备 PC 与长时间运行应用的关机与断电对策设计与实现、从 Windows Update 重启或注销开始的数据损坏与「早上已经停了」事故的根本原因调查,以及 Windows 服务停止处理与自动恢复的设计审查。从「每次关机好像都会坏、不知道从哪下手」这个阶段开始就可以。
参考链接
-
Microsoft Learn, WM_QUERYENDSESSION message. 关于工作阶段结束时会送出 WM_QUERYENDSESSION,应用应回 TRUE 并尊重用户意图(DefWindowProc 默认也是 TRUE);关于收尾应延到 WM_ENDSESSION;关于 5 秒后系统会显示正在阻止关机的应用 UI、用户可强制结束;关于 lParam 里 ENDSESSION_LOGOFF/CLOSEAPP/CRITICAL 位元的意义;关于关机与重启分不出来;以及应频繁存档、让结束时要存的变少。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Fast startup causes hibernation or shutdown to fail in Windows 10 or Windows 8.1. 关于快速启动时核心工作阶段不会关闭、被当成休眠,核心与装置驱动程序状态存进 hiberfil.sys;关于「重启」一定走完整启动,因为需要全新的 Windows 状态;关于快速启动默认开启、不建议关掉;以及 Shutdown.exe 默认是完整关机,/hybrid 选项才是混合行为。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, ShutdownBlockReasonCreate function (winuser.h). 关于在不可中断操作开始时呼叫以注册原因字符串、结束时呼叫 ShutdownBlockReasonDestroy;关于只能从建立窗口的线程呼叫;以及用户只会读原因几秒,因此字符串应短而清楚。 ↩ ↩2 ↩3
-
Microsoft Learn, Shutdown Changes for Windows Vista. 关于对 WM_QUERYENDSESSION/WM_ENDSESSION 的回应各可延后 5 秒、之后用户可选择继续或取消;关于控制台应用或没有可见窗口的应用不能中止关机,5 秒没回应或回 FALSE 会被自动结束;关于若需要挡住应以 ShutdownBlockReasonCreate 注册原因;以及应用不可依赖挡得住关机。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, HandlerRoutine callback function. 关于用 SetConsoleCtrlHandler 注册的处理例程会收到的 CTRL_C/BREAK/CLOSE/LOGOFF/SHUTDOWN 事件;关于 CTRL_CLOSE_EVENT 默认超时约 5000 毫秒、服务处理程序的 CTRL_SHUTDOWN_EVENT 约 20000 毫秒;关于 CTRL_LOGOFF/SHUTDOWN_EVENT 实质上只有服务会收到,因为互动应用在注销时就被结束;以及处理例程在另一条线程上跑。 ↩ ↩2 ↩3
-
Microsoft Learn, SetConsoleCtrlHandler function. 关于载入了 gdi32.dll 或 user32.dll 的处理程序会被当成 Windows 应用,CTRL_LOGOFF_EVENT/CTRL_SHUTDOWN_EVENT 处理例程不会被呼叫;关于变通是建立隐藏窗口并处理 WM_QUERYENDSESSION/WM_ENDSESSION;以及信号处理期间控制台函数可能无法正确运作。 ↩ ↩2
-
Microsoft Learn, .NET runtime no longer provides default termination signal handlers. 关于从 .NET 10 起运行时不再为 Windows 的 CTRL_SHUTDOWN_EVENT/CTRL_CLOSE_EVENT(Unix 的 SIGTERM/SIGHUP 对等)提供默认处理例程;关于 OS 默认处理会立刻结束应用,AppDomain.ProcessExit 与 AssemblyLoadContext.Unloading 不再触发;以及应在较高层函数库或应用代码注册符合应用模型的信号处理。 ↩ ↩2
-
Microsoft Learn, Service Control Handler Function. 关于宣告 SERVICE_ACCEPT_PRESHUTDOWN 的服务先收到 SERVICE_CONTROL_PRESHUTDOWN,然后宣告 SERVICE_ACCEPT_SHUTDOWN 的服务收到 SERVICE_CONTROL_SHUTDOWN;关于关机时默认宽限期约 20 秒、OS 重启时上限是 WaitToKillServiceTimeout;关于不应延长这个值;关于控制处理例程应在 30 秒内返回、回报 STOP_PENDING 与等待提示、把长工作交给另一条线程;关于考量 UPS 运作应尽快做完收尾;以及关机时 SCM 默认不考虑相依关系。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, SERVICE_PRESHUTDOWN_INFO structure (winsvc.h). 关于 PRESHUTDOWN 通知后 SCM 等到服务停止或超时;关于默认超时从 Windows 10 Creators Update(组建 15063)起是 10 秒、之前是 3 分钟;关于用 ChangeServiceConfig2 设置;以及 SERVICE_STOP_PENDING 期间可继续更新状态。 ↩ ↩2
-
Microsoft Learn, RegisterApplicationRestart function (winbase.h). 关于可为崩溃、无响应、更新、以及伴随更新的电脑重启注册重启;关于可指定重启用的命令行参数;关于必须在问题发生前注册,更新情境里处理 WM_QUERYENDSESSION 是最后机会;关于执行未满 60 秒的处理程序不会被重启;关于崩溃或当住后的重启要经用户同意;以及跨过 OS 重启需要以 EWX_RESTARTAPPS/SHUTDOWN_RESTARTAPPS 关机。 ↩ ↩2 ↩3
-
Microsoft Learn, Winlogon automatic restart sign-on (ARSO). 关于 Windows Update 开始自动重启时会保存最后一位互动用户的认证数据并设置 Autologon;关于重启后自动让用户登录并锁定工作阶段;关于成功登录后删除已保存的认证数据;以及可用组策略(DisableAutomaticRestartSignOn 等)设置。 ↩ ↩2
-
Microsoft Learn, ReplaceFileW function (winbase.h). 关于 ReplaceFile 把相当于「存到新档、暂时重新命名原文件、重新命名新档、删除原文件」的多个步骤包成单一函数;关于它保留原文件的建立时间、DACL、加密、压缩、具名数据流等属性;以及备份、被替换的档与替换档必须在同一卷。 ↩ ↩2
-
Microsoft Learn, File Caching. 关于写入默认进系统快取、再以延迟写入反映到磁盘;关于 FILE_FLAG_WRITE_THROUGH 立刻写到磁盘;关于 FlushFileBuffers 可明确刷新;以及文件系统中继数据一律被快取,因此确认中继数据需要刷新或 write-through。 ↩ ↩2 ↩3
-
Microsoft Learn, Troubleshoot unexpected reboots using system event logs. 关于正常重启会记录事件ID 1074(哪个处理程序为谁、因何开始关机);关于非预期重启会记录事件ID 41(Kernel-Power)与 6008(前一次关机非预期);以及这些ID可隔离重启种类。 ↩ ↩2
-
Microsoft Learn, PBT_APMPOWERSTATUSCHANGE event. 关于电池与交流电切换或剩余容量下降时,此事件经 WM_POWERBROADCAST 通知;以及收到后应呼叫 GetSystemPowerStatus,检查 SYSTEM_POWER_STATUS 的 ACLineStatus、BatteryFlag、BatteryLifePercent 等栏位。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
Win32 线程池 API ── 用 CreateThreadpoolWork 实现不自建线程的并发
原生代码里是否到处调用 CreateThread?本文根据一手资料讲解 Vista 重新设计的 Win32 线程池 API——work、timer、wait、io 四种对象、清理组,以及回调中禁止做的事。
命名管道实务 ── 从设计到安全的 Windows 标准 IPC
以实务视角讲解 Windows 标准进程间通信——命名管道。根据一手资料整理字节模式与消息模式的选择、同时处理多客户端的服务器设计、ACL 与模拟的安全性,以及 .NET 的命名管道流。
从睡眠恢复后损坏的应用 ── Windows 电源事件机制与扛得住恢复的业务应用
打开笔记本后业务应用的连接全断了——原因是设计从未考虑睡眠。本文根据一手资料整理 WM_POWERBROADCAST 通知流程、Modern Standby 行为、断开/重连设计、睡眠抑制与调查命令。
DllMain 与加载器锁 ── 「DLL 初始化里什么都别做」的真正原因
为何不得从 DllMain 调用 LoadLibrary 或与其他线程同步。本文根据一手资料说明加载器锁如何串行化每一条 DLL 通知、结构上必然死锁的经典场景、推迟初始化的正确设计,以及如何调查挂起。
「无响应」的真正含义 ── Windows 如何判定应用已挂起,以及如何设计不挂起的应用
Windows 的「无响应」是操作系统判定窗口已 5 秒未取出消息并换成幽灵窗口的机制。本文说明该判定的内部、挂起的经典原因、把重活移出 UI 线程的设计,以及调查挂起的步骤。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
常见问题
汇总了咨询这一主题时常见的问题。
- 「关机」之后没好的问题,「重启」之后却好了。为什么?
- 从 Windows 8 起的客户端 OS,在开启快速启动时(多数支持休眠的 PC 默认如此),「关机」走的是混合关机。用户会注销,但核心与驱动程序状态会存进休眠文件,下次启动原样还原。也就是说,OS 核心并没有被重设。「重启」则一定走完整启动,驱动程序与服务的麻烦才会被清掉。隔离步骤请写「重启」,不要写「关机再加电」。若要从命令行做完整关机,可用 shutdown /s。
- 能不能挡住关机,等到应用存完文件?
- 可以暂时要求等待,但不能可靠地挡住。只在不可中断的操作进行中,用 ShutdownBlockReasonCreate 注册原因字符串,那个原因会出现在「这个应用正在阻止关机」画面,用户可决定继续或取消。但用户仍可选择强制继续,强制关机或更新触发的重启也可能完全不等。因此正解不是「挡住」,而是频繁自动保存以降低风险数据量,再加上从结束通知起几秒内做完的收尾设计。
- Windows 服务停很久。能不能延长关机宽限期?
- 默认接收 SERVICE_CONTROL_SHUTDOWN 时,宽限期大约 20 秒,取决于 WaitToKillServiceTimeout 注册表值。不建议应用自己改这个值来延长。若需要更长宽限期,可宣告 SERVICE_ACCEPT_PRESHUTDOWN 并接收 SERVICE_CONTROL_PRESHUTDOWN;会比其他人更早收到通知,超时可用 ChangeServiceConfig2 设置(Windows 10 Creators Update 起默认 10 秒,之前是 3 分钟)。不过 PRESHUTDOWN 会让整个关机停在那个区间,因此只限真正需要的情况,根本做法仍是把停止工作本身设计成几秒内做完。
- 在 .NET 的 AppDomain.ProcessExit 做关机收尾安全吗?
- 建议不要依赖它。过去运行时会注册默认信号处理例程,CTRL_CLOSE_EVENT 与 CTRL_SHUTDOWN_EVENT 会触发 ProcessExit;从 .NET 10 起运行时不再提供默认的结束信号处理,那些路径也不再触发 ProcessExit。请依应用模型走对应的通知路径:GUI 应用用 FormClosing 或 SessionEnding(那是查询阶段通知,只做幂等保存;必须在工作阶段确定结束后才能做的收尾,放进 WM_ENDSESSION 挂钩);Generic Host / Worker Service 用 IHostApplicationLifetime 与 StopAsync;控制台应用用 SetConsoleCtrlHandler 或 PosixSignalRegistration。
- 要怎么避免突然断电把文件弄坏?
- 断电完全没有通知,所以只能用「无论何时被切断都不坏」的写法。基准是不要就地覆写原文件:在同一卷写完整的临时文件、刷新,再用 ReplaceFile(.NET 的 File.Replace)交换。平常运作下,读到的不是完整旧文件就是完整新文件;但 ReplaceFile 跨断电的原子性规格并不保证,因此要留备份(第三个参数),并在载入时验证主文件、坏了就回退备份。另外,WriteFile 成功不代表数据已到磁盘,重要检查点要用 FlushFileBuffers 或 FILE_FLAG_WRITE_THROUGH 确认写入。设备 PC 的标准做法是搭配 UPS,检测切到电池后导入安全关机。