從應用程式看 Windows 關機 ── 正確扛住結束通知、重新啟動與斷電
· Go Komura · Windows, 關機, Windows 開發, Windows 服務, 設備用 PC, 資料完整性, 長時間運作, UPS
更新紀錄(僅初版,2026年08月21日 發布)
- 初次發布
引用本文(DOI(已登錄存檔): 10.5281/zenodo.22176549)
以下 DOI 指向先前登錄的存檔,內容可能與目前正文不同。引用目前正文時,請使用本頁網址。
Go Komura(2026)。〈從應用程式看 Windows 關機 ── 正確扛住結束通知、重新啟動與斷電〉。小村軟體有限公司。 https://comcomponent.com/zh-TW/blog/windows-shutdown-handling-for-apps/
- DOI(已登錄存檔)
- 10.5281/zenodo.22176549
- DOI(上次登錄版本)
- 10.5281/zenodo.22176550
「夜間的 Windows Update 重新啟動,把正在量測的檔案弄壞了」「從共用 PC 登出後,編輯到一半的內容就不見了」。要防止這類事故,就不能把關機當成例外,而必須把「從外部被要求結束」設計成應用程式的正常行為。
不過,光有接收結束通知的程式碼並不夠。收到通知後能用的時間很短,而突如其來的斷電根本不會有通知。本文的主軸,是把平時的儲存、簡短的收尾、下次啟動時的復原串成一條線。
本文面向中小企業的資訊人員與 Windows 應用程式開發者,尤其是處理設備用 PC 與長時間運作應用程式的人,整理從通知路徑的選擇、實作、重新啟動後的復歸、斷電的準備,一直到驗證為止。技術依據是原文所參照、2026 年 8 月當下的 Microsoft Learn 一次資訊。
1. 先講結論 ── 在等通知之前,先把該存的量降下來
「隨時維持在收到通知後幾秒內就能結束的狀態,而且即使通知沒有來,也能在下次啟動時復原」。這就是因應關機的基本方針。
不要等收到結束通知才儲存大量資料,而是在每個處理段落就儲存,讓結束時剩下的差額變小。GUI 應用程式與關閉主控台的情況,大約 5 秒是重要的參考基準。服務有另一套寬限期,但這些都不是「一定會等你做完」的時間。123
1.1. 選出自己應用程式的通知路徑
| 應用程式的形態 | 接收結束要求的地方 | 首先要掌握的事 | 要讀的章節 |
|---|---|---|---|
| Win32 的 GUI 應用程式 | WM_QUERYENDSESSION 與 WM_ENDSESSION |
對查詢原則上立刻回 TRUE。結束確定後的收尾放在 WM_ENDSESSION |
第 3 章 |
| WinForms / WPF | FormClosing / SessionEnding,必要時再加上訊息掛鉤 |
這兩個事件屬於查詢階段。無法取消的收尾要分到確定通知 | 第 3 章 |
| 純主控台應用程式 | SetConsoleCtrlHandler 等 |
關閉、登出與關機各自被通知的條件並不相同 | 第 5 章 |
| Windows 服務 | 來自 SCM 的 SHUTDOWN / PRESHUTDOWN |
宣告接受旗標,控制處理常式要立刻返回 | 第 6 章 |
| Generic Host / Worker Service | IHostApplicationLifetime 與 StopAsync |
把收尾集中到 Host 的停止路徑,並明確指定 ShutdownTimeout |
第 5、6 章 |
在 .NET 上,請不要只依賴 AppDomain.ProcessExit。 從 .NET 10 起,執行階段不再為 CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT 提供預設處理常式。這條外部訊號路徑,和從 Main 返回之類的正常結束是兩回事。細節在 5.2 節說明。4
1.2. 在通知處理之外,還需要兩項準備
收到通知後,不要問使用者「要儲存嗎?」,而是用簡短的收尾直接結束。真的無法中斷的處理要暫時擋住,這在第 4 章說明;重新啟動後的自動復歸則在第 7 章說明。
另一項是,即使是沒有通知的斷電,也要留下讀得出來的資料。寫入暫存檔、排清、替換、備份、啟動時驗證,這些在第 8 章整理。實作完通知路徑之後,請一併確認第 8 章的儲存與復原設計,以及第 9 章的驗證。
flowchart TB
accTitle: 結束通知與斷電共通的儲存設計
accDescr: 以平時的儲存把未儲存的差額縮小,有通知時做簡短的收尾,沒有通知時則經過已儲存資料的驗證與復原後恢復作業
daily["每個段落都儲存"] --> event{"結束時是否有通知"}
event -->|"有"| close["只存剩下的差額並結束"]
event -->|"沒有"| lost["當場無法收尾"]
close --> boot["下次啟動時驗證與復原"]
lost --> boot
boot --> resume["恢復作業"]
圖 1:不是只在收到通知時才拼命,而是把平時的儲存到下次啟動串成一條線。
圖中實線表示始終成立的關係,虛線表示附帶條件的關係(成立條件寫在詳細頁面中各關係的說明中)。關係的完整清單(共 14 條,附依據與可信度)以及主要概念的定義,彙整在知識地圖詳細頁面(日文)。資料:JSON-LD / Turtle
2. 先做出區別 ── 登出、關機、重新啟動、斷電
2.1. 使用者工作階段與核心要分開看
把「什麼東西結束了」分成兩塊,因應方式就清楚了。使用者工作階段是該使用者的應用程式運作的範圍。另一方面,核心與驅動程式屬於 OS 那一側,在使用者登出之後仍會繼續運作。
| 操作 | 使用者工作階段 | 核心與驅動程式 | 通知的處理 |
|---|---|---|---|
| 登出 | 結束 | 繼續運作 | GUI 會收到結束查詢與確定通知。服務不會因登出而停止 |
| 啟用快速啟動時的關機 | 結束 | 把休眠狀態存進 hiberfil.sys |
GUI 的工作階段結束通知,以及送給服務的關機通知 |
| 重新啟動 | 結束 | 完全結束,下次走完整開機 | 同上 |
| 突然斷電 | 立即失去 | 立即失去 | 沒有通知 |
在 GUI 的 WM_QUERYENDSESSION 中,lParam 的 ENDSESSION_LOGOFF 位元代表登出。lParam 為 0 就是關機或重新啟動,兩者無法區分。lParam 要當成位元遮罩來處理。1
對應用程式未儲存的資料來說,登出和關機需要同樣的準備。不要分成「登出的話就不必存」,而要接到共用的儲存處理。 不過主控台與服務的通知條件並不會因此相同,請分別確認第 5、6 章的路徑。
flowchart TB
accTitle: 把登出與系統結束分開思考
accDescr: 登出會結束使用者的應用程式但服務仍繼續運作,系統關機或重新啟動時服務也成為停止對象,而斷電則對兩者都沒有通知
signout["登出"] --> user["使用者的應用程式結束"]
signout -.-> alive["服務繼續運作"]
system["關機與重新啟動"] --> user
system --> svc["服務也是停止對象"]
power["斷電"] --> none["兩者都沒有通知"]
圖 2:用登出可以測試 GUI 的結束處理,但不等於連服務的停止處理也測過了。
2.2. 「關機沒好、重新啟動就好了」的原因
從 Windows 8 起的用戶端 OS,在多數支援休眠的 PC 上,快速啟動預設是啟用的。在這種組態下關機時,使用者雖然會登出,但核心與裝置驅動程式的狀態會存進休眠檔,並在下次啟動時還原。就算切掉電源,OS 的狀態也未必全部重設了。5
這是有條件的行為。在停用休眠的環境(powercfg /hibernate off)、以原則或電源選項關掉快速啟動的環境,以及 Windows Server 上,關機仍是傳統的完整關機。請確認電源選項的設定,並用 powercfg /a 確認快速啟動是否可用。
另一方面,「重新啟動」一定會執行完整的開機週期。在釐清驅動程式異常的步驟裡,請明確寫「重新啟動」,而不是「關掉電源再開」。5
flowchart TB
accTitle: 快速啟動與完整開機
accDescr: 啟用快速啟動的關機會保存並還原核心與驅動程式的狀態,而完整關機或重新啟動則以完整開機進行初始化
s["關機"] --> q{"是否使用快速啟動"}
q -->|"是"| save["讓核心與驅動程式休眠"]
save --> restore["下次啟動時還原狀態"]
q -->|"否"| full["完整關機"]
full --> boot["下次完整開機時初始化"]
r["重新啟動"] --> boot
圖 3:切掉電源,和重設核心或驅動程式,並不是同一件事。
若要用命令明確指定完整關機,是 shutdown /s;要混合行為則是 shutdown /s /hybrid。Shutdown.exe 的預設是完整關機,因此重點是不要以為它和畫面上的「關機」一樣。5
用停用快速啟動來因應並不受推薦。應用程式要做到啟用或停用都能運作。舉例來說,設備的「累計運轉時間」不要只從 OS 的啟動時刻推算,而要把核心狀態可能被沿用這件事納入設計。
3. GUI 應用程式 ── 把查詢與結束確定分開
3.1. WM_QUERYENDSESSION 是詢問,WM_ENDSESSION 是結果
對擁有視窗與訊息佇列的應用程式,工作階段結束會分兩個階段通知。1
| 訊息 | 意義 | 要做的事 |
|---|---|---|
WM_QUERYENDSESSION |
詢問「可以結束嗎」 | 原則上立刻回 TRUE。DefWindowProc 的預設也是 TRUE |
WM_ENDSESSION,wParam=TRUE |
工作階段的結束已經確定 | 進行簡短的儲存、中斷連線等收尾 |
WM_ENDSESSION,wParam=FALSE |
工作階段結束被取消 | 應用程式繼續運作。不要做只有在確定後才能做的收尾 |
就算自己對查詢回了 TRUE,也不保證一定會結束。 可能有別的應用程式拒絕,導致整個結束被取消。若在這時切斷連線、交出必要的資源,沒有結束的應用程式就動不了了。把收尾分到確定通知的理由就在這裡。1
flowchart TB
accTitle: GUI 的查詢與結束結果
accDescr: 即使對 WM_QUERYENDSESSION 回 TRUE,結束仍可能被其他應用程式取消,因此只有在 WM_ENDSESSION 的 wParam 為 TRUE 時才做確定後的收尾
query["WM_QUERYENDSESSION"] --> reply["原則上立刻回 TRUE"]
reply --> result["WM_ENDSESSION"]
result --> yes{"wParam 是 TRUE 嗎"}
yes -->|"是"| cleanup["確定後的收尾"]
yes -->|"否"| running["結束取消,繼續運作"]
圖 4:不要把「允許結束」的回覆,和「結束已確定」的通知混為一談。
雖然有時可以回 FALSE 拒絕結束,但原則是尊重使用者的結束意圖。拒絕的應用程式會被顯示成「正在阻止關機的應用程式」。主控台或沒有可見視窗的應用程式也有限制,在一般組態下若 5 秒內沒有回應,就可能被自動強制結束。阻擋要當成第 4 章的例外情況來處理,不要拿來做一般的儲存。6
3.2. 大約 5 秒不是「能存到最後」的保證
在 WM_QUERYENDSESSION 與 WM_ENDSESSION 的各個階段,若回應拖過大約 5 秒,系統就會顯示正在阻止關機的應用程式畫面,使用者可以選擇強制繼續。被強制結束之後,沒有機會再把儲存做完。6
對策是平時就儲存,減少結束時的差額。未儲存的工作狀態先撤到暫時的位置,下次啟動時再還原。不要設計成在關機過程中跳出確認對話方塊等使用者。 一般的結束確認,和來自 OS 的結束要求,要分開處理。1
flowchart TB
accTitle: 把結束時的工作變小
accDescr: 平時就儲存的設計在結束時剩下的差額很小,而把資料囤在記憶體直到結束的設計塞不進短暫的寬限期,有被強制結束而遺失的風險
good["平時就儲存"] --> small["結束時剩下的差額很小"]
small --> fast["用簡短的收尾結束"]
bad["囤在記憶體直到結束"] --> large["結束時一次儲存"]
large --> risk["寬限期不足與強制結束的風險"]
圖 5:要在幾秒內結束,準備工作必須在結束通知之前就做好。
3.3. 在 WinForms 與 WPF 上,要把儲存和無法取消的收尾分開
在 WinForms 上對應的是 FormClosing 的 CloseReason.WindowsShutDown,在 WPF 上是 Application.SessionEnding。WPF 也可以從 XAML 的 SessionEnding 屬性登錄,或用覆寫 OnSessionEnding 來處理。
不過,兩者都是查詢階段的事件。在這裡能做的,僅止於「即使被取消也無害、執行幾次結果都相同」的冪等快照儲存。中斷連線這類只有在結束確定後才能做的處理,要用 WinForms 的 WndProc 或 WPF 的掛鉤接到 WM_ENDSESSION(wParam=TRUE) 再做。
flowchart TB
accTitle: WinForms 與 WPF 結束處理的分工
accDescr: 在 FormClosing 與 SessionEnding 儲存即使被取消也安全的快照,再掛鉤 WM_ENDSESSION 的 TRUE 來做無法取消的收尾
notify["查詢階段"] --> forms["FormClosing"]
notify --> wpf["SessionEnding"]
forms --> snapshot["冪等的快照儲存"]
wpf --> snapshot
final["WM_ENDSESSION 的 TRUE"] --> hook["用 WndProc 或掛鉤接收"]
hook --> cleanup["中斷連線等確定後的處理"]
圖 6:不要把結束確定後的工作也塞進框架的事件裡。
下面兩個例子,是在查詢階段只儲存工作狀態的部分。程式碼範例是實作的節錄,儲存函式的內容、失敗時的處理、事件登錄等都要由應用程式端自行準備。
// WinForms:關機或登出時同樣會呼叫 FormClosing
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.WindowsShutDown)
{
// 只做冪等的快照儲存。不要跳出對話方塊。
// 也不要設定 e.Cancel = true(拒絕)。
SaveWorkingStateToTempFile();
return;
}
// 使用者按 × 關閉之類的一般情況,才可以在這裡確認
}
// WPF:App.xaml.cs
protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
{
base.OnSessionEnding(e);
// 雖然可以區分 ReasonSessionEnding.Logoff / Shutdown,
// 但基本上兩者都執行相同的快照儲存
SaveWorkingStateToTempFile();
// 除非有非常充分的理由,否則不要設定 e.Cancel = true
}
兩者都要把正常結束、關機,可能的話連當機時所用的資料,收斂到共用的儲存函式與格式。只要不為每條儲存路徑各做一種格式,下次啟動時的還原邏輯也能只留一份。當機時留下資訊的設計,在Windows 應用程式當機時留下記錄與傾印的設計中說明。
4. 只對無法中斷的處理,做暫時性的阻擋
4.1. 登錄原因和拒絕結束是兩件不同的工作
像燒錄光碟或寫入韌體這種一中斷就會實體損壞的處理是例外。在處理開始時用 ShutdownBlockReasonCreate 登錄原因,完成時用 ShutdownBlockReasonDestroy 解除。 登錄的原因會顯示在「正在阻止關機的應用程式」畫面上。7
不過,光是登錄原因字串並不會擋住關機。要搭配一個保護中的旗標,只在那段期間對 WM_QUERYENDSESSION 回 FALSE。處理結束後,原因和保護中旗標兩者都要解除。
flowchart TB
accTitle: 暫時性結束阻擋的分工
accDescr: 只在無法中斷的處理進行中搭配原因登錄與查詢拒絕,完成後兩者都解除,但擋不住使用者強制繼續
start["無法中斷的處理開始"] --> reason["登錄原因並設定保護旗標"]
reason --> work["在工作者執行緒處理"]
work --> done["完成,解除原因與旗標"]
work -.-> request["這段期間收到結束查詢"]
request --> refuse["回 FALSE 並顯示原因"]
refuse --> choice{"使用者的判斷"}
choice -->|"取消"| keep["繼續運作"]
choice -->|"強制繼續"| terminate["可能被結束"]
圖 7:只登錄原因不等於拒絕,就算實作了拒絕,也擋不住強制繼續。
4.2. 讓 UI 執行緒保持在能接收結束要求的狀態
原因的登錄與解除,要從建立目標視窗的執行緒呼叫。從其他執行緒呼叫會失敗。7 另一方面,無法中斷的長時間處理本身要移到工作者執行緒。因為若用同步處理把 UI 執行緒塞住,在用來拒絕的訊息被處理之前,就會先變成「沒有回應」。
[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, "正在將量測資料寫入檔案");
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);
}
這個例子是把原因登錄、在工作者執行緒處理、拒絕查詢組合起來的骨架。包含 API 失敗在內的錯誤處理需要另外撰寫。原因字串要短而具體,而且不要在應用程式執行期間一直登錄著,只限定在真正無法中斷的處理進行期間。7
即使如此,使用者仍可以選擇強制繼續。而且還有 ENDSESSION_CRITICAL 等強制結束的路徑,因此不可以把「擋得住」當成資料完整性的前提。擋不住時的準備,就是第 8 章的儲存與復原設計。6
5. 主控台與 .NET ── 確認通知送達的條件
5.1. 用 SetConsoleCtrlHandler 接收的通知與寬限期
在主控台應用程式上,控制訊號會送到用 SetConsoleCtrlHandler 登錄的處理常式。和 GUI 的訊息處理不同,處理常式是在另一條執行緒上執行。2
| 訊號 | 發生的場合 | 預設寬限期 |
|---|---|---|
CTRL_C_EVENT / CTRL_BREAK_EVENT |
Ctrl+C / Ctrl+Break | 沒有明確的逾時 |
CTRL_CLOSE_EVENT |
關閉主控台、工作管理員的「結束工作」等 | 約 5 秒 |
CTRL_SHUTDOWN_EVENT |
系統關機時的服務處理程序 | 約 20 秒 |
從工作管理員「詳細資料」索引標籤等處強制結束處理程序,屬於沒有通知的立即結束,不在這張表的範圍內。2
互動式工作階段內的主控台應用程式,不要設計成等待 CTRL_LOGOFF_EVENT 或 CTRL_SHUTDOWN_EVENT。 互動式應用程式在登出時就會被結束,因此實際上只有以服務形式運作的處理程序收得到這些訊號。此外,只要載入 gdi32.dll 或 user32.dll,就會被當成 Windows 應用程式,LOGOFF / SHUTDOWN 類的處理常式不會被呼叫。這種情況下的官方變通做法,是建立隱藏視窗並接收 WM_QUERYENDSESSION / WM_ENDSESSION。28
flowchart TB
accTitle: 主控台能收到結束通知的條件
accDescr: 互動式主控台就算收得到關閉通知也不能期待登出或關機通知,而即使是服務,只要載入 GUI 用的 DLL 就需要另一條通知路徑
console["主控台處理程序"] --> close["關閉會送到控制處理常式"]
console --> q{"是否等待 LOGOFF 與 SHUTDOWN"}
q -->|"互動式工作階段"| no["不能依賴這個通知"]
q -->|"服務"| dll{"是否載入了 GUI 用的 DLL"}
dll -->|"否"| signal["處理對應的控制訊號"]
dll -->|"是"| window["用隱藏視窗接收"]
圖 8:不要把「處理了主控台關閉」直接當成「處理了關機」。
下面是因應 Ctrl+C 與關閉主控台的節錄。保留委派以避免被記憶體回收,在簡短的收尾之後交給預設處理常式。光是登錄這個,並不保證互動式應用程式連關機通知也收得到。
// 主控台應用程式:在 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; // 保留參考以免被記憶體回收
static bool OnCtrlEvent(int ctrlType)
{
// 只做 5 秒內能完成的收尾
FlushAndCloseDataFile();
return false; // 交給預設處理常式,處理程序就會結束
}
static void Main()
{
SetConsoleCtrlHandler(s_handler, add: true);
// ...
}
5.2. 從 .NET 10 起,不要把收尾只放在 ProcessExit
從 .NET 10 起,執行階段不再為 CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT 提供預設處理常式。若沒有自己或上層函式庫的處理常式,就會由 OS 的預設處理結束,這條路徑上 AppDomain.ProcessExit 與 AssemblyLoadContext.Unloading 都不會觸發。4
這是針對外部結束訊號的預設行為變更。並不是說從 Main 返回這類正常結束時 ProcessExit 也不再觸發。
flowchart TB
accTitle: 解除對 .NET 預設結束處理常式的依賴
accDescr: 過去的執行階段會把目標訊號接到 ProcessExit,但 .NET 10 起不再提供預設處理常式,因此要改用應用程式模型的通知路徑
signal["CLOSE 與 SHUTDOWN 訊號"] --> old["過去執行階段的預設處理"]
old --> event["通知 ProcessExit 等"]
signal --> modern[".NET 10 起沒有預設處理"]
modern --> own{"應用程式端有處理嗎"}
own -->|"沒有"| os["由 OS 預設處理結束"]
own -->|"有"| handle["交給符合模型的收尾"]
圖 9:區分正常結束與外部訊號造成的結束,並依自己的應用程式模型準備必要的處理常式。
收尾要集中到符合應用程式模型的地方。GUI 是第 3 章的事件與確定通知,Generic Host 是 IHostApplicationLifetime 與 BackgroundService.StopAsync,純主控台則是 SetConsoleCtrlHandler 或 PosixSignalRegistration。使用後者時,也要依目標的結束路徑挑選 SIGINT、SIGTERM、SIGHUP 等對應的訊號。4
在 Generic Host 上要明確指定 HostOptions.ShutdownTimeout。但光靠 Host 這一側的設定,並不會延長 GUI、主控台、SCM 各自外側的寬限期。就算 Ctrl+C 沒有明確的逾時,斷電或其他結束路徑仍需要準備。不論哪一種模型,「保持已儲存的狀態、把通知後的工作變小」這個方針都是共通的。
flowchart TB
accTitle: Host 的停止處理與外側的結束寬限期
accDescr: Generic Host 的停止集中在 StopAsync,但設定 HostOptions 的逾時並不會延長 OS 或 SCM 的結束寬限期,因此兩邊都要確認
outer["來自 OS 或 SCM 的結束要求"] --> host["Host 的停止路徑"]
host --> stop["在 StopAsync 收尾"]
stop --> inner["設定 Host 端的停止時間"]
outer -.-> limit["外側的寬限期另外存在"]
inner --> check["實測是否能短時間內完成"]
limit --> check
圖 10:Host 端與 OS 端的時間限制要分開確認,不能只靠調長設定就了事。
6. Windows 服務 ── 控制處理常式要立刻返回
6.1. SHUTDOWN 與 PRESHUTDOWN 的差別
服務不會因為登出而停止,但在關機與重新啟動時會成為停止對象。通知來自 SCM(服務控制管理員),而且必須宣告與所要接收的控制碼對應的旗標。3
| 要宣告的旗標 | 送達的控制碼 | 用法區別 |
|---|---|---|
SERVICE_ACCEPT_SHUTDOWN |
SERVICE_CONTROL_SHUTDOWN |
一般的關機通知。預設寬限期約 20 秒,取決於 WaitToKillServiceTimeout |
SERVICE_ACCEPT_PRESHUTDOWN |
SERVICE_CONTROL_PRESHUTDOWN |
比一般的 SHUTDOWN 更早通知。SCM 會等到停止或等到所設定的逾時 |
flowchart TB
accTitle: 送給服務的結束通知階段
accDescr: SCM 先通知接受 PRESHUTDOWN 的服務並等待停止或逾時,之後才進入一般的 SHUTDOWN 通知
start["系統開始關機"] --> pre["通知接受 PRESHUTDOWN 的服務"]
pre --> wait["等待停止或所設定的期限"]
wait --> shut["通知接受 SHUTDOWN 的服務"]
shut --> proceed["寬限期過後進入系統結束"]
圖 11:通知的先後和等待時間是不同的條件,PRESHUTDOWN 一樣有設定好的期限。
PRESHUTDOWN 的逾時要用 ChangeServiceConfig2(SERVICE_CONFIG_PRESHUTDOWN_INFO) 設定。預設值從 Windows 10 Creators Update(組建 15063)起是 10 秒,之前是 3 分鐘。若以為「用 PRESHUTDOWN 就一定有 3 分鐘」,就拿不到期待中的時間。9
PRESHUTDOWN 會讓整個系統的關機停下來等,因此只在真正需要時才用。由服務端改寫一般的 WaitToKillServiceTimeout 來延長,同樣不受推薦。3
6.2. 把接收通知的處理和實際停止的處理分開
控制處理常式必須在 30 秒內返回,但不要想成「可以用滿 30 秒」,而要做成下達停止指示後立刻返回的結構。長時間的處理交給另一條執行緒,並回報 SERVICE_STOP_PENDING。3
// 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 拖得更久,就一邊遞增 dwCheckPoint,
// 一邊定期持續回報 SERVICE_STOP_PENDING。SCM 會依 waitHint 與檢查點的前進
// 判斷「還活著而且有進展」。一旦停止回報,就會被當成沒有回應,關機可能
// 繼續往下走。完成後一定要回報 SERVICE_STOPPED
在工作者執行緒端,若收尾比等待提示拖得更久,就一邊推進 dwCheckPoint 一邊回報 SERVICE_STOP_PENDING。進度回報一旦停止,就可能被判定為沒有回應。完成後回報 SERVICE_STOPPED,到這裡才算走完停止處理。39
flowchart TB
accTitle: 服務控制處理常式與工作者執行緒的分工
accDescr: 控制處理常式回報停止中並向工作者執行緒發出信號後立刻返回,由工作者執行緒進行收尾與進度回報,最後回報已停止
handler["控制處理常式"] --> pending["回報 STOP_PENDING"]
pending --> signal["向工作者執行緒發出停止信號"]
signal --> back["處理常式立刻返回"]
signal --> worker["工作者執行緒進行收尾"]
worker --> report["拖久時回報進度"]
report --> stopped["完成時回報 STOPPED"]
圖 12:不要用冗長的停止處理塞住接收通知的執行緒。
6.3. 即使相依對象先停止,也要能在短時間內結束
關機時的 SCM 預設不考慮服務的相依關係就發出通知。停止處理要能處理相依的服務已經無法使用的情況,也不要過度等待網路對象的回應。比起花時間釋放記憶體,優先在短時間內把必要的資料落定。3
這個方針在與 UPS 的關係上也很重要。服務把等待拉得愈長,就愈難在電池耗盡前完成整個 OS 的關機。比起增加寬限期,用每個段落的儲存來減少停止時的工作才是基本做法。3
用 UseWindowsService 執行 .NET 的 Worker Service 時,STOP / SHUTDOWN 會被轉換成主機的停止,接到 BackgroundService.StopAsync。原文撰寫當下的標準實作並不接受 PRESHUTDOWN,若有需要就得自行擴充處理常式的實作。明確指定 HostOptions.ShutdownTimeout、讓 StopAsync 本身在幾秒內結束,這個方針是一樣的。整體的實作請參考Windows 服務的建立與維運。
7. 重新啟動後的復歸 ── 湊齊登錄、還原資料與登入
7.1. RegisterApplicationRestart 是復歸路徑的事前登錄
在設備用 PC 或無人運轉的情境下,不只要存好再結束,還要把「重新啟動後恢復作業」也設計進去。RegisterApplicationRestart 是在當機(未處理的例外)、沒有回應、更新造成的應用程式重新啟動、更新造成的 OS 重新啟動這幾種場合,把應用程式登錄為重新啟動對象的 API。10
可以在重新啟動時的命令列引數裡指定原本開著的檔案或還原點。不過,光是登錄並不代表在任何情況下都能自動復歸。
| 確認項目 | 條件與限制 |
|---|---|
| 登錄的時機 | 在問題發生之前。更新情境裡,處理 WM_QUERYENDSESSION 期間是最後機會 |
| 防止重新啟動迴圈 | 啟動未滿 60 秒的處理程序不會被重新啟動 |
| 當機與沒有回應 | 要經過使用者同意才重新啟動。更新造成的重新啟動則是自動 |
| 跨越 OS 重新啟動時 | 發起端必須帶著 EWX_RESTARTAPPS / SHUTDOWN_RESTARTAPPS 呼叫關機 API |
| 提升權限的處理程序 | 不會成為自動重新啟動的對象,需要另一條明確的啟動路徑 |
需要提升權限的應用程式,可以把 UI 留在標準權限、將特權處理分離到服務,或利用工作排程器「以最高權限執行」的工作等,來設計復歸路徑。10
7.2. 復原回呼與 ARSO 的角色各不相同
併用 RegisterApplicationRecoveryCallback 之後,當機時 WER(Windows 錯誤報告)會呼叫復原回呼,讓你儲存作業中的資料。儲存持續進行的期間,要在登錄的 ping 間隔內呼叫 ApplicationRecoveryInProgress,完成時再用 ApplicationRecoveryFinished 通知。進度通知一旦中斷,復原處理就可能被中止。
flowchart TB
accTitle: 復原回呼中的儲存與進度通知
accDescr: 被 WER 呼叫的復原回呼要在儲存期間於 ping 間隔內持續呼叫 ApplicationRecoveryInProgress,完成時再通知 ApplicationRecoveryFinished
reg["事先登錄復原回呼"] --> crash["當機,由 WER 呼叫"]
crash --> save["儲存作業中的資料"]
save --> progress["在 ping 間隔內通知進度"]
progress --> completed{"儲存完成了嗎"}
completed -->|"還沒"| save
completed -->|"完成"| done["通知復原完成"]
progress -.-> timeout["中斷就可能被終止"]
圖 13:不只是儲存,還要把進度與完成通知給 WER。
更新時替換使用中的檔案並重新啟動,是 Restart Manager 的職責。這在如何替換使用中的 exe/DLL中說明。
另一方面,在 OS 重新啟動後把使用者工作階段拉回來的機制是 ARSO(Winlogon 自動重新啟動登入)。Windows Update 開始自動重新啟動時,會安全地保存最後一位互動使用者的認證資料並設定 Autologon,在重新啟動後讓該使用者登入並鎖定畫面。11
flowchart TB
accTitle: 重新啟動登錄、登入與資料還原
accDescr: 即使事先做了重新啟動登錄,當機時仍需要使用者同意,而 OS 重新啟動後的使用者應用程式還需要 ARSO 等機制把工作階段拉回來並還原已儲存的狀態
reg["在問題發生前登錄重新啟動"] --> crash["當機或沒有回應"]
crash --> consent["取得使用者同意"]
consent --> app["重新啟動應用程式"]
reg --> update["帶著必要旗標的 OS 重新啟動"]
update --> session["用 ARSO 等把工作階段拉回"]
session --> app
app --> data["讀取儲存的還原點"]
session -.-> policy["確認原則與啟動條件"]
圖 14:不只做應用程式的重新啟動登錄,也要備好工作階段與作業狀態回得來的路徑。
shutdown /g 是指示重新啟動並重新開啟已登錄應用程式的命令。ARSO 有時會被 DisableAutomaticRestartSignOn 之類的組織原則停用,因此要連同無人復歸的需求一起確認。需要一直運作的背景處理,與其依賴使用者自動登入,不如做成 Windows 服務來執行更合適。11
8. 沒有通知的斷電 ── 把儲存與載入成對設計
8.1. 暫存檔、替換、備份、啟動時驗證要湊成一組
斷路器跳脫、電源供應器故障、插頭被拔掉,這些都不會有 WM_ENDSESSION,也沒有 PRESHUTDOWN。如果是直接覆寫原檔,一旦中途被切斷,就可能留下新舊內容混在一起的檔案。
基本做法是把內容完整寫進同一磁碟區上的暫存檔、排清之後再替換。ReplaceFile 把相當於「存到新檔、把原檔挪開、重新命名、刪除」的多個步驟包成一個函式,也會沿用建立時間、ACL、替代資料流等屬性。被替換的檔案、替換用的檔案與備份必須放在同一個磁碟區。.NET 的 File.Replace 呼叫的就是這個 API。12
flowchart TB
accTitle: 檔案儲存與下次啟動的復原
accDescr: 寫進同一磁碟區的暫存檔並排清、留下備份後替換,啟動時再驗證主檔,必要時回退到備份
tmp["同一磁碟區的暫存檔"] --> write["寫到最後並排清"]
write --> replace["替換,舊內容存為 bak"]
replace -.-> boot["下次啟動"]
boot --> valid{"主檔正常嗎"}
valid -->|"是"| main["讀取主檔"]
valid -->|"否"| backup["回退到備份"]
圖 15:不只實作安全的寫法,也要一併實作壞掉時的讀法。
// 設定與資料儲存的標準做法:先把暫存檔寫完再替換,並保留舊內容
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;
}
}
就算這個例子叫做 SaveAtomically,它也不保證跨越斷電的原子性。 在一般運作情況下,可以做到讀得到完整的舊檔或新檔;但 ReplaceFile 是多個步驟的名稱空間操作,規格上並未保證面對突然斷電的原子性。正因如此才要留下 .bak,並連「啟動時驗證主檔、壞掉就回退到備份」的載入處理一起實作。12
例子裡的 catch,是為了避免一般失敗時暫存檔累積把磁碟區塞滿,並不是期待斷電時這段處理會跑。對於附加型的記錄檔或 CSV,不要直接套用整檔替換的方式,而要採用把損壞方式納入考量的格式,例如「一行寫成一筆記錄,載入時丟掉損壞的最後一行」。
8.2. 區分 WriteFile 成功與資料抵達磁碟
即使 WriteFile 成功,資料仍可能還留在 OS 的快取裡。Windows 通常會先寫進系統緩衝區,再以延遲寫入反映到磁碟。在重要的段落,要用 FlushFileBuffers 排清,或在 CreateFile 時用 FILE_FLAG_WRITE_THROUGH 指定立即寫入。檔案系統的中繼資料同樣會被快取,因此要落定也牽涉到排清或 write-through。13
不過,write-through 和「不使用 OS 快取」並不是同一回事。頻繁呼叫 FlushFileBuffers 效率不佳,必要時可考慮搭配 FILE_FLAG_NO_BUFFERING。實務上比較可行的設計,是只在交易的段落或關檔前這類對資料保全重要的位置排清。13
flowchart TB
accTitle: 寫入成功與持久化的分界
accDescr: 一般的寫入會先進 OS 快取再延遲抵達儲存裝置,即使用排清或 write-through 促使落定,裝置端快取的限制依然存在
write["WriteFile 成功"] --> cache["可能還在 OS 快取裡"]
cache --> delayed["延遲寫入"]
cache --> flush["段落處的排清"]
delayed --> device["反映到儲存裝置"]
flush --> device
device -.-> limit["裝置端快取的極限"]
圖 16:不要把 API 成功、OS 快取落定、耐得住斷電當成同一件事。
裝置端的揮發性快取同樣有極限,不能斷言「只要排清過,在任何硬體上都能完全耐住斷電」。快取管理員、延遲寫入與硬體快取之間的關係,在快取管理員:你的 WriteFile 究竟何時送達磁碟中有詳細說明。
8.3. UPS 是用來把斷電接到計畫性關機
UPS 的職責不是消除停電,而是把沒有通知的斷電,轉換成有通知的計畫性關機。必要的條件是以下的關係。
UPS 的電池續航時間 > 切換的偵測 + 應用程式與服務的收尾 + OS 完成關機所需的時間
交流電源與電池之間的切換、以及剩餘電量下降,會透過 PBT_APMPOWERSTATUSCHANGE 通知。GUI 用 WM_POWERBROADCAST 接收,服務則要先宣告 SERVICE_ACCEPT_POWEREVENT,再用 HandlerEx 接收 SERVICE_CONTROL_POWEREVENT。WM_POWERBROADCAST 並不會送到服務的控制處理常式。14
收到之後,用 GetSystemPowerStatus 確認 ACLineStatus 與 BatteryLifePercent,再接到中斷量測、儲存、要求關機等動作。14
flowchart TB
accTitle: 從 UPS 偵測到關機完成
accDescr: 用適當的通知路徑偵測 UPS 切到電池,確認電源狀態後進入儲存與關機,並設計成整體能落在電池續航時間之內
outage["停電,UPS 切到電池"] --> notice["由對應路徑發出電源通知"]
notice --> check["確認電源狀態與剩餘電量"]
check --> save["中斷量測並儲存"]
save --> req["向 OS 要求關機"]
req --> done["完成收尾與 OS 結束"]
done -.-> time["讓整體落在續航時間內"]
圖 17:不只是應用程式,連 OS 結束為止的時間都要收進 UPS 的續航時間內。
像 USB 連接的一般 UPS 這種在 Windows 看來就是電池的組態,可以用標準 API 監控。若廠商的管理軟體有「剩餘電量 N% 時關機」的功能,也要確認那個臨界值和收尾時間是否一致。從睡眠與休眠復歸是另一個面向的主題,請參考睡眠、休眠、Modern Standby 與長時間運作的應用程式。
9. 驗證 ── 確認通知、時間、復原結果三件事
9.1. 不要在正式機上,而要在驗證機上重現結束路徑
不要直接拿正式的設備用 PC 來試,而要使用驗證機或 Hyper-V 之類的虛擬機器。在虛擬機器上先取得檢查點,並用可以丟掉的驗證資料反覆測試。
| 要試的操作 | 要確認的事 |
|---|---|
| 登出 | GUI 的 WM_QUERYENDSESSION → WM_ENDSESSION 與儲存處理。不能取代服務停止的驗證 |
shutdown /s /t 0 |
完整關機時的行為 |
shutdown /s /hybrid /t 0 |
使用快速啟動的組態下的混合行為 |
shutdown /r /t 0 |
伴隨完整開機的重新啟動,以及之後的復歸 |
| 關閉 VM 電源 | 即使客體 OS 沒有通知就停止,下次啟動時能不能復原 |
| 在等同正式環境的實機上斷電 | 包含實體儲存裝置與控制器在內的耐受力 |
登出雖然有 ENDSESSION_LOGOFF 位元會被設起來的差異,但可以輕鬆確認 GUI 的通知路徑。完整關機、混合關機、重新啟動的命令不要混在一起,要分開試。15
flowchart TB
accTitle: 讓關機驗證逐步擴大
accDescr: 先在驗證環境試 GUI 通知與各種結束操作,再用 VM 確認突然停止後的復原,最後在等同正式環境的實機上確認含儲存裝置的斷電耐受力
prep["準備驗證機與資料"] --> notify["試通知路徑與結束操作"]
notify --> time["測量收尾的時間"]
time --> vm["用 VM 突然停止確認復原"]
vm --> real["含實機儲存裝置一起驗證"]
real --> check["確認下次啟動的資料與復歸"]
圖 18:不只試能不能正常結束,也要試突然停止之後能復原什麼。
關閉 VM 電源能重現的,就只到「客體毫無預警地停止」為止。實體磁碟揮發性快取的消失、以及取決於控制器的損壞方式無法重現,因此若要以設備用 PC 出貨,最終確認就要使用等同正式環境的硬體。
在收尾函式的開頭與結尾留下時間記錄,實測是否落在 GUI 等的約 5 秒、或服務所設定的寬限期之內。不只確認是否正常結束,還要確認下次啟動時載入了什麼、能從哪裡繼續。
9.2. 夜裡發生了什麼,要從事件記錄來釐清
在 Windows 的 System 記錄中,事件識別碼 1074 會記下發起關機的處理程序、使用者與理由。非預期的關機則會在下次啟動時記下 41(Kernel-Power)與 6008。15
# 確認關機相關事件的近期歷程
Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 1074, 6008, 41 } -MaxEvents 20 |
Select-Object TimeCreated, Id, ProviderName, Message |
Format-List
如果 1074 顯示的是 Windows Update 的重新啟動,資料卻壞了,就先檢查結束通知的處理與儲存路徑。另一方面,不可以只憑 41 或 6008 就斷定是斷電。它們表示的是非預期的結束,藍色畫面(當機)或強制重設同樣是候選原因。15
flowchart TB
accTitle: 從結束事件記錄挑出調查對象
accDescr: 從 1074 確認發起的處理程序與理由,41 與 6008 則當成非預期結束的線索,與 BugcheckCode、傾印等周邊資訊對照
log["System 事件記錄"] --> normal["1074:發起的處理程序與理由"]
normal --> cleanup["調查通知處理與儲存路徑"]
log --> unexpected["41、6008:非預期的結束"]
unexpected --> evidence["確認 BugcheckCode 與傾印"]
evidence --> classify["釐清是當機還是斷電等"]
圖 19:41 與 6008 不是斷電本身的證明,而是進一步調查的入口。
41 的 BugcheckCode 若不是 0,就是當機的線索。若是 0 而且也沒有記憶體傾印,就比較可能是斷電,但仍要對照周邊資訊判斷。確定是斷電就重點檢查第 8 章的儲存設計與 UPS,若是計畫性的結束,就重點檢查第 3 到 6 章的通知與收尾。
10. 總結 ── 不只設計結束處理,還要設計到下次啟動
關機對策,不是「結束時只跑一次的事件處理常式」這種層次的事。重點是把平時的儲存 → 簡短的收尾 → 下次啟動時的驗證與復原串成一條線。
| 要重新檢視的地方 | 設計要點 |
|---|---|
| 平時的處理 | 頻繁儲存,減少結束時剩下的差額 |
| GUI 的結束通知 | 對查詢原則上立刻回 TRUE。把冪等的儲存和確定後的收尾分開 |
| 主控台與服務 | 使用符合應用程式模型的通知。不要只依賴 .NET 的 ProcessExit 或延長寬限期 |
| 無法中斷的處理 | 只在必要期間搭配原因登錄與拒絕。也要為強制繼續做好準備 |
| 儲存與載入 | 除了暫存檔、排清、替換之外,還要備好備份與啟動時驗證 |
| 重新啟動之後 | 確認重新啟動登錄、還原資料,以及登入或服務的啟動路徑 |
在啟用快速啟動的關機下,核心與驅動程式有可能是從休眠回來的。釐清異常時要明確寫「重新啟動」,應用程式端則要做到完整關機與混合關機兩種情況都能運作。5
flowchart TB
accTitle: 從結束因應到復原的最終確認
accDescr: 在一般處理中保持已儲存的狀態,收到結束通知時做小規模的收尾,即使沒有通知也驗證留下的資料並在下次啟動時復原
daily["平時就保持已儲存的狀態"] --> endq{"有沒有結束通知"}
endq -->|"有"| short["用簡短的收尾結束"]
endq -->|"沒有"| prior["只能靠最後已儲存的狀態"]
short --> nextboot["下次啟動時驗證與復原"]
prior --> nextboot
nextboot --> restart["在必要的條件下恢復作業"]
圖 20:不要把結束事件的實作,和平時的儲存與下次啟動的復原切開來看。
最後,在驗證機上試通知路徑與所需時間,並確認突然停止之後的復原。事後調查時,以 1074 / 41 / 6008 為線索,釐清究竟是計畫性的結束還是非預期的結束。
下次要加功能時,請確認「如果在這段處理進行中收到結束通知,或是電源被拔掉,下次啟動時會剩下什麼」。把這個答案納入設計,就是對「到了早上才發現資料損壞」這種事故的準備。
相關文章
- 如何替換使用中的 exe/DLL ── Restart Manager 與自動更新的「檔案使用中」問題
- Windows 服務的建立與維運 ── 從與工作排程器的取捨到 BackgroundService 服務化
- 睡眠、休眠、Modern Standby 與長時間運作的應用程式 ── 用設計預防「半夜停止運轉」
- Windows I/O 的深層(第 4 回)── 快取管理員:你的 WriteFile 究竟何時送達磁碟
- Windows 應用程式當機時留下記錄與傾印的設計
- Windows 應用程式安全處理子處理程序的檢查清單
相關諮詢領域
小村軟體有限公司承接設備用 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
-
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 ↩4
-
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 ↩6 ↩7
-
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 ↩3
-
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, Shutdown Changes for Windows Vista. 關於對 WM_QUERYENDSESSION/WM_ENDSESSION 的回應各可延後 5 秒、之後使用者可選擇繼續或取消;關於主控台應用程式或沒有可見視窗的應用程式不能中止關機,5 秒沒回應或回 FALSE 會被自動結束;關於若需要阻擋應以 ShutdownBlockReasonCreate 登錄原因;以及應用程式不可依賴「擋得住關機」這件事。 ↩ ↩2 ↩3
-
Microsoft Learn, ShutdownBlockReasonCreate function (winuser.h). 關於在不可中斷作業開始時呼叫以登錄原因字串、結束時呼叫 ShutdownBlockReasonDestroy;關於只能從建立視窗的執行緒呼叫;以及使用者只會讀原因幾秒,因此字串應短而清楚。 ↩ ↩2 ↩3
-
Microsoft Learn, SetConsoleCtrlHandler function. 關於載入了 gdi32.dll 或 user32.dll 的處理程序會被當成 Windows 應用程式,CTRL_LOGOFF_EVENT/CTRL_SHUTDOWN_EVENT 處理常式不會被呼叫;關於變通是建立隱藏視窗並處理 WM_QUERYENDSESSION/WM_ENDSESSION;以及訊號處理期間主控台函式可能無法正確運作。 ↩
-
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
-
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
-
Microsoft Learn, PBT_APMPOWERSTATUSCHANGE event. 關於電池與交流電切換或剩餘容量下降時,此事件經 WM_POWERBROADCAST 通知;以及收到後應呼叫 GetSystemPowerStatus,檢查 SYSTEM_POWER_STATUS 的 ACLineStatus、BatteryFlag、BatteryLifePercent 等欄位。 ↩ ↩2
-
Microsoft Learn, Troubleshoot unexpected reboots using system event logs. 關於正常重新啟動會記錄事件識別碼 1074(哪個處理程序為誰、因何開始關機);關於非預期重新啟動會記錄事件識別碼 41(Kernel-Power)與 6008(前一次關機非預期);以及可用這些識別碼區分重新啟動的種類。 ↩ ↩2
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
快速啟動的真面目 ── Windows 的「關機」為什麼和重新啟動不一樣
Windows 的「關機」預設會變成混合關機,核心與驅動程式被保存到休眠檔,並在下次開機時還原。本文說明為什麼有些問題只有重新啟動才會好、對運作時間・更新・Wake on LAN 的影響、確認方法以及是否停用的判斷。
Time Travel Debugging ── 把長期運轉中無法重現的問題「錄下來」再倒回去
一個月才出現一次的問題,當機傾印只拍得到結果。本文說明如何用 WinDbg 的 Time Travel Debugging(TTD) 錄下執行並倒回,涵蓋 TTD.exe 的錄製設計、環形緩衝區、TTD.Calls 查詢,以及和傾印的分工。
引數為什麼會壞掉 ── Windows 命令列引數的規則
在 Windows 上並不存在引數的陣列,傳給 CreateProcess 的是一條字串,分割由接收端負責。本文說明 CommandLineToArgvW・CRT・.NET 的分割規則,以及 .NET 的 ArgumentList 與 C++ 中正確的組裝方式。
Windows 列印驅動程式停止提供 ── 業務應用程式的報表與標籤列印如何因應
Microsoft 正分階段推動 v3/v4 列印驅動程式的停止提供,從 2026 年 7 月起會優先選用 IPP 類別驅動程式。本文整理 Windows protected print mode 之下會消失哪些東西,並以判斷表梳理業務應用程式的報表、標籤列印中相依處的盤點...
WinRT 就是 COM —— IInspectable、.winmd、語言投影,以及 WinUI 至今仍立在二進位契約之上的原因
WinRT 不是受管理的執行階段,而是在 COM 之上加了中繼資料(.winmd)與語言投影的 ABI。本文從 IUnknown 與 IInspectable 的關係,一路談到桌面應用程式裡 HWND 初始化與 package identity 的卡關之處。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
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,偵測到切換成電池供電後接到安全關機。