Tắt Windows nhìn từ ứng dụng của bạn — Sống sót đúng với thông báo thoát, khởi động lại và mất điện
· Go Komura · Windows, Tắt máy, Phát triển Windows, Dịch vụ Windows, PC thiết bị, Toàn vẹn dữ liệu, Vận hành dài hạn, UPS
“Sau một lần khởi động lại đêm của Windows Update, ứng dụng đo trên PC thiết bị chết giữa lần ghi, và tới sáng tệp đo đã hỏng.” “Ai đó đăng xuất khỏi một PC dùng chung rồi phàn nàn các sửa chưa lưu đã biến mất.” — Với ứng dụng Windows chạy dài, hai cuộc tư vấn này là kinh điển.
Điểm chung của cả hai hiện trường là coi tắt máy như “một sự kiện bất thường không nên xảy ra”. Trong thực tế, tuy nhiên, từ các lần khởi động lại tự động của Windows Update, đăng xuất người dùng, và tắt do UPS cho tới mất điện không báo, các sự kiện cắt thực thi từ ngoài ứng dụng sẽ tới, sớm hay muộn. Bạn không ngăn chúng tới được. Thứ bạn ngăn được là “mất dữ liệu khi chúng tới”.
May là Windows có cơ chế thông báo cho ứng dụng trước khi tắt, với ứng dụng GUI, ứng dụng console, và dịch vụ đều vậy. Hướng tới nhân viên IT ở doanh nghiệp vừa và nhỏ cùng các nhà phát triển ứng dụng Windows (đặc biệt ứng dụng PC thiết bị và chạy dài), bài viết này sắp xếp cách nhận những thông báo đó, cách thiết kế dọn dẹp “xong trong vài giây”, phục hồi tự động sau khởi động lại, và cách chuẩn bị cho một lần mất điện không mang thông báo — tất cả dựa trên nguồn gốc Microsoft Learn tính tới tháng 8 năm 2026.
1. Kết luận trước
- Thiết kế tắt máy như “một sự kiện bình thường sẽ tới, sớm hay muộn”. Thời gian bạn dùng được sau khi nhận thông báo, về nguyên tắc, chỉ khoảng 5 giây, nên một thiết kế hốt hoảng lưu hết tại chỗ sẽ sụp. Điều kiện tiên quyết là tự lưu thường xuyên để “phần chênh phải lưu lúc tắt” luôn nhỏ.1
- Trên các OS máy khách từ Windows 8 trở đi, khi fast startup bật (mặc định trên hầu hết PC hỗ trợ ngủ đông), “Shut down” là hybrid shutdown, và kernel chỉ đang ngủ đông. Thứ duy nhất được đặt lại đầy đủ là “Restart”. Đó là lý do thật của “tôi tắt rồi không khá, rồi tôi khởi động lại thì khá”.2
- Một ứng dụng GUI nên trả TRUE ngay cho WM_QUERYENDSESSION, và dọn dẹp trong WM_ENDSESSION. Về nguyên tắc bạn không được trả FALSE (từ chối).1
- Chỉ khi bạn thật sự có một thao tác không thể gián đoạn bạn mới nên hiện một lý do bằng ShutdownBlockReasonCreate. Ngay cả khi đó người dùng và OS vẫn có thể buộc tiếp, nên thiết kế giả định “chúng ta chặn được” không đứng.34
- Một ứng dụng console nhận thông báo bằng SetConsoleCtrlHandler. Thời gian ân hạn còn ngắn hơn — mặc định 5 giây cho đóng console. Cũng có một cái bẫy: trong một tiến trình đã nạp gdi32.dll hoặc user32.dll, một số sự kiện này không tới.56
- Dọn dẹp dựa vào AppDomain.ProcessExit của .NET không chạy, từ .NET 10 trở đi, trên các đường mà tiến trình bị “kết thúc từ ngoài”. Trên một lần thoát bình thường như trở về từ Main nó vẫn chạy như trước, nhưng vì runtime không còn cung cấp xử lý mặc định cho các tín hiệu kết thúc như đóng console và tắt máy, dọn dẹp trên những đường đó phải chuyển sang thông báo khớp mô hình ứng dụng.7
- Một dịch vụ Windows có thể nhận SERVICE_ACCEPT_PRESHUTDOWN sớm hơn, và với thời gian ân hạn cấu hình được, so với SERVICE_ACCEPT_SHUTDOWN (khoảng 20 giây ân hạn). Thời gian chờ PRESHUTDOWN mặc định, tuy nhiên, đã rút còn 10 giây từ Windows 10 Creators Update trở đi, nên dù sao bạn cũng cần thiết kế không dựa quá cứng vào ân hạn.89
- Phục hồi tự động sau khởi động lại đạt được bằng cách kết hợp RegisterApplicationRestart với ARSO (đăng nhập tự động). Các đường phục hồi được cung cấp cho sự cố, không phản hồi, và khởi động lại do cập nhật.1011
- Mất điện không mang thông báo nào cả. Mẫu chuẩn là ghi trọn vào một tệp tạm, xả, rồi đổi bằng ReplaceFile, nhưng vì ReplaceFile cũng không bảo đảm tính nguyên tử xuyên một lần mất điện, một đường phục hồi bản sao lưu (.bak) cộng xác thực lúc tải là một phần của bộ. Cô lập sau việc có thể làm từ nhật ký sự kiện (1074/41/6008).121314
Trong một câu, kết luận của bài là: “luôn giữ một trạng thái từ đó bạn đóng cửa hàng được trong vài giây khi thông báo tới, và ghi theo cách không vỡ ngay cả trên một lần mất điện không mang thông báo”.
2. Điều gì xảy ra lúc tắt — Bốn cách kết thúc
2.1. Đăng xuất, tắt máy, khởi động lại, và mất điện
Từ góc nhìn ứng dụng, điều quan trọng là hai trục: “phiên người dùng kết thúc thế nào” và “điều gì xảy ra với kernel”.
| Thao tác | Phiên người dùng | Kernel và driver | Thông báo tới ứng dụng |
|---|---|---|---|
| Đăng xuất | Kết thúc | Tiếp tục chạy | WM_QUERYENDSESSION (ENDSESSION_LOGOFF) → WM_ENDSESSION |
| Tắt máy (fast startup bật) | Kết thúc | Ngủ đông (lưu vào hiberfil.sys) | WM_QUERYENDSESSION → WM_ENDSESSION, (PRE)SHUTDOWN tới dịch vụ |
| Khởi động lại | Kết thúc | Kết thúc hoàn toàn; lần khởi động tiếp là khởi động đầy đủ | Giống trên |
| Mất điện | Biến ngay | Biến ngay | Không có |
Đăng xuất và tắt máy, từ góc nhìn ứng dụng, gần như cùng một sự kiện. Nếu bit ENDSESSION_LOGOFF được đặt trong lParam của WM_QUERYENDSESSION thì đó là đăng xuất; nếu là 0 thì đó là tắt hoặc khởi động lại (bạn không tách được hai cái).1 Nói cách khác, sự chủ quan “chỉ là đăng xuất, chúng ta sẽ ổn” không đứng, và thiết kế đúng là cùng mã dọn dẹp được gọi.
flowchart TB
accTitle: Bốn cách kết thúc, và thông báo tới ứng dụng
accDescr: Đăng xuất, tắt máy, và khởi động lại gửi thông báo WM_QUERYENDSESSION tới WM_ENDSESSION, và dọn dẹp xong trong vài giây. Chỉ mất điện là không có thông báo nào, nên bạn chuẩn bị bằng thiết kế ghi ở Chương 8 và UPS
signout["Đăng xuất"] --> notified["QUERY → ENDSESSION"]
shutdown["Tắt máy"] --> notified
reboot["Khởi động lại"] --> notified
poweroff["Mất điện"] --> none["Không thông báo: ghi + UPS"]
notified --> cleanup["Dọn dẹp trong vài giây"]
Hình 1: Đăng xuất, tắt máy, và khởi động lại gửi thông báo WM_QUERYENDSESSION tới WM_ENDSESSION, và dọn dẹp xong trong vài giây. Chỉ mất điện là không có thông báo nào, nên bạn chuẩn bị bằng thiết kế ghi ở Chương 8 và UPS.
2.2. Lý do thật của “tôi tắt rồi không khá” — hybrid shutdown
Hàng dễ bỏ sót là hàng thứ hai trong bảng. Trên các OS máy khách từ Windows 8 trở đi, fast startup (hybrid shutdown) bật mặc định trên PC hỗ trợ ngủ đông, và hành vi của “Shut down” đã đổi. Đăng xuất phiên người dùng vẫn xảy ra như thường, nhưng phiên kernel không bị đóng; nó được lưu, driver thiết bị và tất cả, vào tệp ngủ đông (hiberfil.sys) rồi khôi phục nguyên si ở lần khởi động tiếp. Điều đó làm khởi động nhanh hơn, nhưng trạng thái kernel và driver sống sót ngay cả sau khi bạn cắt điện.2 Đây, tuy nhiên, là hành vi có điều kiện. Trong môi trường ngủ đông tự bị tắt (powercfg /hibernate off), nơi chính sách hoặc Power Options đã tắt fast startup, và trên Windows Server, tắt máy là tắt đầy đủ thông thường. Bạn biết một PC đang chạy theo hướng nào từ hộp “Turn on fast startup” trong Power Options, hoặc từ việc powercfg /a (các trạng thái ngủ khả dụng) có liệt kê “Fast Startup” hay không.
flowchart TB
accTitle: Điều gì xảy ra với kernel trên thao tác Shut down
accDescr: Một thao tác Shut down tách thành tắt đầy đủ hoặc ngủ đông kernel tùy fast startup có bật hay không, và Restart luôn làm khởi động đầy đủ
op["Shut down"]
restart["Restart"]
op -->|"Fast startup bật"| hybrid["Phiên kết thúc + ngủ đông kernel"]
op -->|"Ngủ đông tắt / Server"| full["Tắt đầy đủ"]
hybrid --> resume["Tiếp: khôi phục kernel"]
full --> boot["Tiếp: khởi động đầy đủ"]
restart --> boot
Hình 2: Một thao tác Shut down tách thành tắt đầy đủ hoặc ngủ đông kernel tùy fast startup có bật hay không, và Restart luôn làm khởi động đầy đủ.
“Restart”, ngược lại, luôn chạy một chu kỳ khởi động hoàn chỉnh. Sau một lần cập nhật driver, chẳng hạn, bạn cần một trạng thái hoàn toàn mới.2 Từ đó, vài hiện tượng bạn nghe hiện trường rơi vào chỗ.
- “Tôi tắt rồi bật lại, nhưng sự cố thiết bị không hết” — kernel và driver chỉ được khôi phục từ ngủ đông; chúng không được đặt lại
- “Nó khá hơn sau khi tôi khởi động lại” — vì một lần khởi động đầy đủ đã khởi tạo chúng
- Quy trình sự cố PC thiết bị nên nói “Restart”, không phải “tắt rồi bật”
Nếu muốn làm một lần tắt đầy đủ tường minh từ dòng lệnh, shutdown /s (mặc định của Shutdown.exe là tắt đầy đủ); nếu muốn hành vi hybrid mặc định, shutdown /s /hybrid.2 Tắt fast startup không được khuyến nghị. Phía ứng dụng nên giả định “lúc tắt kernel có thể chỉ đang ngủ đông” — ví dụ, đừng ước “thời gian chạy tích lũy” từ thời điểm khởi động OS — và thiết kế sao cho không vỡ theo hướng nào (fast startup bật hay tắt khác nhau theo môi trường).
3. Ứng dụng GUI nên hành xử thế nào — WM_QUERYENDSESSION và WM_ENDSESSION
3.1. Hai thông điệp tách việc thế nào
Một ứng dụng có cửa sổ và hàng thông điệp được thông báo kết thúc phiên theo hai giai đoạn.1
- WM_QUERYENDSESSION — một truy vấn: “kết thúc được chứ?” Ứng dụng nên trả TRUE ngay; phản hồi mặc định của DefWindowProc cũng là TRUE. Đừng bắt đầu dọn dẹp ở đây.
- WM_ENDSESSION (wParam=TRUE) — một thông báo đã cam kết: “phiên thật sự đang kết thúc”. Dọn dẹp xảy ra ở đây.
Trả FALSE cho WM_QUERYENDSESSION có thể hủy tắt máy, nhưng tài liệu nói rõ “bạn nên trả TRUE và tôn trọng ý định của người dùng”, và một ứng dụng đã trả FALSE vẫn bị lộ trên UI toàn màn hình như “một ứng dụng đang ngăn tắt máy”. Ứng dụng console và ứng dụng không có cửa sổ hiện không thể hủy tắt máy ngay từ đầu, và nếu không trả lời trong 5 giây chúng bị kết thúc tự động.14
flowchart TB
accTitle: Luồng thông báo kết thúc phiên hai giai đoạn
accDescr: Trả TRUE cho truy vấn WM_QUERYENDSESSION cam kết bằng WM_ENDSESSION và dọn dẹp chạy. Từ chối bằng FALSE hiện ứng dụng như đang ngăn tắt máy, và khoảng 5 giây không trả lời có thể buộc tiếp
q["WM_QUERYENDSESSION"]
q -->|"TRUE(quy tắc)"| e["WM_ENDSESSION(đã cam kết)"]
q -->|"FALSE(từ chối)"| blocked["Hiện như đang chặn tắt máy"]
q -->|"Không trả lời ~5s"| hung["Coi như treo"]
e --> cleanup["Dọn dẹp ở đây"]
cleanup --> term["Tiến trình thoát"]
hung --> term
blocked -->|"Buộc tiếp"| term
blocked -->|"Hủy"| cont["Tắt máy bị hủy"]
Hình 3: Trả TRUE cho truy vấn WM_QUERYENDSESSION cam kết bằng WM_ENDSESSION và dọn dẹp chạy. Từ chối bằng FALSE hiện ứng dụng như đang ngăn tắt máy, và khoảng 5 giây không trả lời có thể buộc tiếp.
3.2. Điều gì xảy ra nếu bạn không trả lời — bức tường 5 giây
Trên cả WM_QUERYENDSESSION và WM_ENDSESSION, bạn có thể trì hoãn phản hồi khoảng 5 giây. Vượt quá, hệ thống hiện màn hình “This app is preventing shutdown”, và người dùng có thể chọn buộc tiếp (= buộc kết thúc ứng dụng).4 Một tiến trình bị kết thúc cưỡng bức không được thêm cơ hội để xong lần lưu.
Các điểm thiết kế vì vậy là hai điều này.
- Giữ dọn dẹp ở lượng xong trong 5 giây. Chính Microsoft khuyến nghị lưu dữ liệu thường xuyên trong vận hành thường để ít hơn phải lưu lúc tắt, và lưu dữ liệu chưa lưu vào một vị trí tạm để khôi phục ở lần khởi chạy tiếp.1
- Đừng đưa hộp thoại xác nhận trong lúc tắt. Trong khi bạn ngồi chờ “Do you want to save?”, 5 giây trôi. Hãy lặng lẽ nghiêng về phía an toàn (tự lưu).
3.3. Triển khai trong WinForms và WPF
Trong một ứng dụng desktop .NET, các thông điệp này được dịch thành sự kiện khung. Trong WinForms, FormClosing được phát, và CloseReason cho bạn biết tắt máy có phải nguyên nhân không.
// WinForms: FormClosing is also raised on shutdown / sign-out
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.WindowsShutDown)
{
// Do only an idempotent snapshot save. Do not show a dialog.
// Do not set e.Cancel = true (refuse) either.
SaveWorkingStateToTempFile();
return;
}
// For ordinary closes such as the user clicking the × button, you may confirm here
}
Trong WPF, sự kiện Application.SessionEnding (thuộc tính XAML SessionEnding, hoặc ghi đè OnSessionEnding) tương ứng.
flowchart TB
accTitle: Cách sự kiện WinForms/WPF ánh xạ tới các thông điệp
accDescr: Pha truy vấn của WM_QUERYENDSESSION ánh xạ tới WinForms FormClosing và WPF SessionEnding, và việc bạn làm ở đó nhiều nhất là một lần lưu ảnh chụp lũy đẳng. Không có sự kiện tương ứng cho WM_ENDSESSION đã cam kết, nên hãy nhận nó trong WndProc hoặc một hook và làm dọn dẹp chỉ chạy được sau cam kết
q["WM_QUERYENDSESSION"] --> fc["WinForms: FormClosing"]
q --> se["WPF: SessionEnding"]
fc -.-> idem["Chỉ ảnh chụp lũy đẳng"]
se -.-> idem
e["WM_ENDSESSION"] --> hook["Không sự kiện: hook WndProc"]
hook -.-> final["Dọn dẹp sau cam kết"]
Hình 4: Pha truy vấn của WM_QUERYENDSESSION ánh xạ tới WinForms FormClosing và WPF SessionEnding, và việc bạn làm ở đó nhiều nhất là một lần lưu ảnh chụp lũy đẳng. Không có sự kiện tương ứng cho WM_ENDSESSION đã cam kết, nên hãy nhận nó trong WndProc hoặc một hook và làm dọn dẹp chỉ chạy được sau cam kết.
// WPF: App.xaml.cs
protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
{
base.OnSessionEnding(e);
// You can distinguish ReasonSessionEnding.Logoff / Shutdown,
// but the baseline is to run the same snapshot save in either case
SaveWorkingStateToTempFile();
// Do not set e.Cancel = true unless you have an exceptional reason
}
Có một lưu ý ở đây. Cả FormClosing (CloseReason.WindowsShutDown) lẫn SessionEnding của WPF đều tương ứng pha truy vấn (WM_QUERYENDSESSION). Nếu một ứng dụng khác từ chối, tắt máy bị hủy và ứng dụng của bạn tiếp tục chạy. Vì vậy việc bạn được phép làm trong các sự kiện này là một lần lưu ảnh chụp lũy đẳng không hại nếu tắt máy bị hủy và cho cùng kết quả dù chạy bao nhiêu lần. Nếu bạn cần “dọn dẹp chỉ được làm khi chúng ta thật sự đang kết thúc” (ngắt kết nối, trả tài nguyên, và tương tự), hãy hook WM_ENDSESSION đã cam kết (wParam=TRUE) trực tiếp trong WndProc và làm ở đó.
Trên mọi đường, hãy gói thân vào một hàm “lưu ảnh chụp” chung và ghi dữ liệu khôi phục cho thoát bình thường, tắt máy, và (nếu được) sự cố theo cùng định dạng, để logic khôi phục ở lần khởi chạy tiếp là một đường. Thiết kế để lại thông tin ngay cả khi sự cố nằm trong “Thiết kế ứng dụng Windows để lại nhật ký và dump khi chúng sự cố”.
4. Nếu bạn thật sự phải chặn — ShutdownBlockReasonCreate
Các thao tác vỡ vật lý nếu bị cắt giữa chừng, như ghi CD hoặc firmware, là ngoại lệ. Thực hành đúng ở đây là đăng ký một chuỗi lý do bằng ShutdownBlockReasonCreate khi thao tác không thể gián đoạn bắt đầu, và gọi ShutdownBlockReasonDestroy ngay khi nó xong. Khi tắt máy được yêu cầu, lý do đó hiện trên màn hình “This app is preventing shutdown”, và người dùng quyết tiếp tục hay hủy.3
flowchart TB
accTitle: Luồng bảo vệ bằng ShutdownBlockReasonCreate
accDescr: Đăng ký một lý do khi thao tác không thể gián đoạn bắt đầu; nếu một yêu cầu tắt tới khi đang được bảo vệ, lý do hiện toàn màn hình và WM_QUERYENDSESSION bị từ chối bằng FALSE. Người dùng có thể hủy hoặc buộc tiếp, và lý do được gỡ khi thao tác xong
begin["Bắt đầu việc không thể gián đoạn"] --> reg["ShutdownBlockReasonCreate"]
reg --> work["Chạy trên một thread thợ"]
work --> done["Xong: Destroy"]
req["Tắt máy trong lúc này"] --> show["Hiện lý do + FALSE"]
show -->|"Hủy"| work
show -->|"Buộc tiếp"| kill["Tiến trình thoát"]
Hình 5: Đăng ký một lý do khi thao tác không thể gián đoạn bắt đầu; nếu một yêu cầu tắt tới khi đang được bảo vệ, lý do hiện toàn màn hình và WM_QUERYENDSESSION bị từ chối bằng FALSE. Người dùng có thể hủy hoặc buộc tiếp, và lý do được gỡ khi thao tác xong.
[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);
// Call from the thread that created the main window (it fails from other threads)
_criticalOperationInProgress = true;
ShutdownBlockReasonCreate(this.Handle, "Writing measurement data to a file");
try
{
// Run the uninterruptible operation on a worker thread. If you run it
// synchronously on the UI thread the message pump stops, and the process
// is force-continued as "Not Responding" before the WM_QUERYENDSESSION
// refusal code below can run
await Task.Run(() => WriteMeasurementData());
}
finally
{
ShutdownBlockReasonDestroy(this.Handle);
_criticalOperationInProgress = false;
}
// Also, refuse WM_QUERYENDSESSION with FALSE only while protected
protected override void WndProc(ref Message m)
{
const int WM_QUERYENDSESSION = 0x0011;
if (m.Msg == WM_QUERYENDSESSION && _criticalOperationInProgress)
{
m.Result = IntPtr.Zero; // Refuse. The registered reason string is shown in the full-screen UI
return;
}
base.WndProc(ref m);
}
Sự hiểu nhầm dễ ở đây là tách vai trò. Tất cả ShutdownBlockReasonCreate làm là đăng ký một chuỗi lý do; bản thân nó không dừng tắt máy. Thứ thực sự giữ tắt máy lại là xử lý của bạn trả FALSE cho WM_QUERYENDSESSION khi một cờ bảo vệ đang đặt, như trên. Hãy dùng hai cái như một bộ, và gỡ cả hai ngay khi thao tác xong. Ngoài ra, hãy chạy bản thân thao tác được bảo vệ trên một thread thợ và giữ thread UI xử lý được thông điệp — cơ chế từ chối chỉ hoạt động khi thông điệp tới (và ngay cả khi đó người dùng và OS vẫn có thể buộc tiếp, nên thiết kế ghi không vỡ “nếu nó không dừng” — Chương 8 — vẫn cần).
Có ba lưu ý vận hành.
- Giữ chuỗi lý do ngắn và cụ thể. Người dùng đang vội và chỉ đọc vài giây. Chính tài liệu đưa “Burning a CD” làm ví dụ thích hợp.3
- Đừng để nó đăng ký suốt đời ứng dụng. “Chỉ khi một thao tác không thể gián đoạn đang chạy” là điều API giả định.
- Đừng thiết kế trên giả định bạn chặn được. Người dùng có thể chọn buộc tiếp, và một lần tắt cưỡng bức (ENDSESSION_CRITICAL) sẽ không chờ ngay từ đầu. Tài liệu nói rõ: “Applications should not depend on being able to block shutdown”.4
5. Ứng dụng console và tiến trình nền nên hành xử thế nào
5.1. SetConsoleCtrlHandler và thời gian ân hạn ngắn
Một ứng dụng console không nhận được thông điệp cửa sổ, nên các tín hiệu điều khiển tới một hàm handler đăng ký bằng SetConsoleCtrlHandler. Thời gian ân hạn mặc định mỗi tín hiệu như sau.5
| Tín hiệu | Khi nào xảy ra | Ân hạn mặc định |
|---|---|---|
| CTRL_C_EVENT / CTRL_BREAK_EVENT | Ctrl+C / Ctrl+Break | Không hết thời gian chờ |
| CTRL_CLOSE_EVENT | Đóng console, “End task” của Task Manager (một lần giết tiến trình cưỡng bức từ tab “Details” là thoát ngay không thông báo, và nằm ngoài bảng này) | Khoảng 5 giây |
| CTRL_SHUTDOWN_EVENT | Tắt hệ thống (tiến trình dịch vụ) | Khoảng 20 giây |
Có hai điểm cần để ý. Thứ nhất, về căn bản chỉ một tiến trình chạy như dịch vụ mới nhận được CTRL_LOGOFF_EVENT và CTRL_SHUTDOWN_EVENT. Một ứng dụng trong phiên tương tác bị kết thúc lúc đăng xuất, nên thiết kế chờ các tín hiệu này không đứng.5 Thứ hai, một tiến trình đã nạp gdi32.dll hoặc user32.dll bị coi như ứng dụng Windows ngay cả nếu bạn nghĩ nó là ứng dụng console, và các handler LOGOFF/SHUTDOWN không được gọi. Cách chính thức là tạo một cửa sổ ẩn và nhận WM_QUERYENDSESSION/WM_ENDSESSION.6
flowchart TB
accTitle: Thời gian ân hạn mỗi tín hiệu console
accDescr: Ctrl+C và Ctrl+Break không có hết thời gian chờ tường minh; đóng console có khoảng 5 giây và tín hiệu tắt tới một tiến trình dịch vụ có khoảng 20 giây; vượt quá thì buộc kết thúc tiến trình
ctrlc["CTRL_C / BREAK"] -->|"Không hết thời gian chờ"| handler["Dọn dẹp HandlerRoutine"]
closeev["CTRL_CLOSE"] -->|"Khoảng 5s"| handler
shutev["CTRL_SHUTDOWN"] -->|"Khoảng 20s"| handler
handler --> timeout["Giết cưỡng bức sau ân hạn"]
Hình 6: Ctrl+C và Ctrl+Break không có hết thời gian chờ tường minh; đóng console có khoảng 5 giây và tín hiệu tắt tới một tiến trình dịch vụ có khoảng 20 giây; vượt quá thì buộc kết thúc tiến trình.
// Console app: clean up on Ctrl+C and console close
[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; // Keep a reference so GC does not collect it
static bool OnCtrlEvent(int ctrlType)
{
// Do only cleanup that finishes within 5 seconds
FlushAndCloseDataFile();
return false; // Proceed to the default handler; the process exits
}
static void Main()
{
SetConsoleCtrlHandler(s_handler, add: true);
// ...
}
5.2. Cái bẫy .NET — đừng dựa vào ProcessExit
Trong .NET từ lâu đã có mẫu sẵn “chỉ việc dọn trong AppDomain.ProcessExit”, nhưng từ .NET 10 runtime không còn cung cấp handler tín hiệu kết thúc mặc định, và cả ProcessExit lẫn AssemblyLoadContext.Unloading đều không kích trên CTRL_CLOSE_EVENT/CTRL_SHUTDOWN_EVENT. Handler mặc định của OS chỉ việc kết thúc tiến trình ngay.7
flowchart TB
accTitle: ProcessExit đổi thế nào trong .NET 10
accDescr: Tới .NET 9 handler tín hiệu mặc định của runtime nhận tín hiệu kết thúc, phát ProcessExit, rồi thoát. Từ .NET 10 runtime không cung cấp handler mặc định, xử lý mặc định của OS kết thúc tiến trình ngay, và bạn tự đăng ký một handler
sig["CTRL_CLOSE / SHUTDOWN"] --> old9["Tới .NET 9: ProcessExit"]
sig --> new10["Từ .NET 10: thoát ngay"]
new10 -.-> alt["Tự đăng ký một handler"]
Hình 7: Tới .NET 9 handler tín hiệu mặc định của runtime nhận tín hiệu kết thúc, phát ProcessExit, rồi thoát. Từ .NET 10 runtime không cung cấp handler mặc định, xử lý mặc định của OS kết thúc tiến trình ngay, và bạn tự đăng ký một handler.
Thay vào đó, hãy chuyển sang đường chuẩn của từng mô hình ứng dụng.
- Ứng dụng GUI: FormClosing / SessionEnding từ chương trước
- Generic Host (gồm Worker Service): IHostApplicationLifetime và BackgroundService.StopAsync. Làm ân hạn dừng tường minh bằng HostOptions.ShutdownTimeout
- Ứng dụng console trần: SetConsoleCtrlHandler (hoặc đăng ký các tương đương SIGINT/SIGTERM bằng PosixSignalRegistration)
flowchart TB
accTitle: Mỗi mô hình ứng dụng nhận thông báo thoát ở đâu
accDescr: Ứng dụng GUI dùng FormClosing và SessionEnding cộng một hook WM_ENDSESSION cho việc đã cam kết; Generic Host dùng IHostApplicationLifetime và StopAsync; ứng dụng console trần dùng SetConsoleCtrlHandler hoặc PosixSignalRegistration. Dựa vào ProcessExit không kích trên các đường tín hiệu ngoài
model{"Mô hình ứng dụng nào?"}
model -->|"GUI"| gui["FormClosing / SessionEnding"]
model -->|"Không GUI"| other{"Host hay console?"}
gui -.-> guihook["Hook ENDSESSION"]
other -->|"Host"| host["Lifetime + StopAsync"]
other -->|"Console"| con["SetConsoleCtrlHandler"]
host -.-> hostto["Đặt ShutdownTimeout"]
con -.-> ngx["Đừng dựa vào ProcessExit"]
Hình 8: Ứng dụng GUI dùng FormClosing và SessionEnding cộng một hook WM_ENDSESSION cho việc đã cam kết; Generic Host dùng IHostApplicationLifetime và StopAsync; ứng dụng console trần dùng SetConsoleCtrlHandler hoặc PosixSignalRegistration. Dựa vào ProcessExit không kích trên các đường tín hiệu ngoài.
Thời gian ân hạn khác theo đường — khoảng 5 giây cho GUI và đóng console, ân hạn SCM của Chương 6 cho dịch vụ (khoảng 20 giây, hoặc giá trị đã cấu hình cho PRESHUTDOWN), và không hết thời gian chờ tường minh cho Ctrl+C. Trên mọi đường, tuy nhiên, ân hạn có hạn và không thể trông cậy, nên trục thiết kế là trường hợp thường là “đã lưu tại mỗi mốc xử lý”, không phải “làm hết sức trong sự kiện thoát”.
6. Dịch vụ Windows nên hành xử thế nào — SHUTDOWN và PRESHUTDOWN
6.1. Hai loại thông báo tắt máy
Một dịch vụ không bị ảnh hưởng bởi đăng xuất, nhưng bị dừng lúc tắt và khởi động lại. Thông báo tới như một mã điều khiển từ Service Control Manager (SCM), và nhận nó đòi tuyên bố một cờ chấp nhận.8
| Tuyên bố | Thông báo tới | Thời điểm và ân hạn |
|---|---|---|
| SERVICE_ACCEPT_SHUTDOWN | SERVICE_CONTROL_SHUTDOWN | Được thông báo trong xử lý tắt. Mặc định khoảng 20 giây, trần WaitToKillServiceTimeout |
| SERVICE_ACCEPT_PRESHUTDOWN | SERVICE_CONTROL_PRESHUTDOWN | Được thông báo trước SHUTDOWN. SCM chờ tới khi dịch vụ dừng hoặc hết thời gian chờ |
flowchart TB
accTitle: Thứ tự thông báo tắt máy tới một dịch vụ
accDescr: Khi tắt bắt đầu, các dịch vụ đã tuyên bố PRESHUTDOWN được thông báo trước với ân hạn đã cấu hình, rồi thông báo SHUTDOWN được gửi với mặc định khoảng 20 giây, và tiến trình bị kết thúc khi ân hạn hết
start["Tắt máy bắt đầu"] --> pre["PRESHUTDOWN(nếu đã tuyên bố)"]
pre --> shut["SHUTDOWN(khoảng 20s)"]
shut --> kill["Ân hạn hết → thoát"]
Hình 9: Khi tắt bắt đầu, các dịch vụ đã tuyên bố PRESHUTDOWN được thông báo trước với ân hạn đã cấu hình, rồi thông báo SHUTDOWN được gửi với mặc định khoảng 20 giây, và tiến trình bị kết thúc khi ân hạn hết.
Thời gian chờ PRESHUTDOWN cấu hình được bằng ChangeServiceConfig2 (SERVICE_CONFIG_PRESHUTDOWN_INFO); mặc định là 10 giây từ Windows 10 Creators Update (build 15063) trở đi, và 3 phút trước đó.9 Nếu bạn vẫn làm việc từ kiến thức cũ rằng “PRESHUTDOWN cho bạn 3 phút”, trên OS hiện tại bạn chỉ có 1/18 ân hạn bạn kỳ vọng. Ngoài ra, PRESHUTDOWN giữ tắt cả hệ thống trong khoảng đó, nên tài liệu cũng nói nó “should be used only in special circumstances”.8
Thực hành phía handler cũng quan trọng. Handler điều khiển phải trả về trong 30 giây; hãy để việc dừng tốn thời gian cho một thread khác, báo SERVICE_STOP_PENDING, và trả về ngay.8
// Win32 service: accept PRESHUTDOWN and leave stop work to a worker
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); // Tell the worker to stop and return immediately
return NO_ERROR;
}
return ERROR_CALL_NOT_IMPLEMENTED;
}
// Worker side: if cleanup runs longer than waitHint, keep reporting
// SERVICE_STOP_PENDING periodically while incrementing dwCheckPoint.
// The SCM judges "still alive and making progress" from waitHint and
// checkpoint advance. If reporting stops it can be treated as hung and
// shutdown can proceed. Always report SERVICE_STOPPED when finished
6.2. Một thiết kế không dựa vào ân hạn
Kéo dài trần ân hạn, WaitToKillServiceTimeout, bằng cách viết lại từ phía dịch vụ được nói rõ là không khuyến nghị. Tài liệu đòi điều ngược — một dịch vụ nên xong dọn dẹp càng nhanh càng tốt để một máy chạy UPS hoàn tất tắt trước khi pin chết. Hướng dẫn là lưu thường xuyên trong vận hành thường để dữ liệu chưa lưu được tối thiểu, không tiêu thời gian giải phóng bộ nhớ lúc tắt, và không chờ quá lâu một câu trả lời khi thông báo một bên mạng. Ngoài ra, SCM lúc tắt mặc định không xét phụ thuộc, nên xử lý dừng vẫn phải chạy “ngay cả nếu một dịch vụ bạn phụ thuộc đã xuống”.8
flowchart TB
accTitle: Thiết kế xử lý dừng không dựa vào ân hạn
accDescr: Nếu bạn lưu tại mỗi mốc xử lý để dữ liệu chưa lưu luôn tối thiểu, dọn dẹp khi thông báo dừng tới xong trong vài giây. Một thiết kế lưu hết lúc thoát sẽ không vừa ân hạn, và một lần kết thúc cưỡng bức làm mất dữ liệu
good["Lưu tại mỗi mốc"] --> gstop["Dừng → lưu mảnh → xong"]
bad["Lưu hết lúc thoát"] --> bstop["Dừng → lưu trượt ân hạn"]
bstop --> killed["Giết cưỡng bức → mất dữ liệu"]
Hình 10: Nếu bạn lưu tại mỗi mốc xử lý để dữ liệu chưa lưu luôn tối thiểu, dọn dẹp khi thông báo dừng tới xong trong vài giây. Một thiết kế lưu hết lúc thoát sẽ không vừa ân hạn, và một lần kết thúc cưỡng bức làm mất dữ liệu.
Trong một .NET Worker Service (UseWindowsService), SERVICE_CONTROL_STOP và SHUTDOWN được dịch thành dừng host, và BackgroundService.StopAsync được gọi. Bản triển khai sẵn tính tới lúc viết chấp nhận họ STOP/SHUTDOWN; nếu bạn cũng cần PRESHUTDOWN bạn sẽ cần một handler mở rộng. Dù sao, hãy làm HostOptions.ShutdownTimeout tường minh và xong StopAsync trong vài giây. Về xây một dịch vụ nói chung, xem “Cách xây và vận hành dịch vụ Windows”.
7. Phục hồi tự động sau khởi động lại
Trên một PC thiết bị hoặc PC không người trông, phạm vi thiết kế không chỉ “sống sót lúc tắt” mà còn “tự trở lại sau khởi động lại”.
7.1. RegisterApplicationRestart và callback phục hồi
Nếu bạn đã gọi RegisterApplicationRestart, ứng dụng được đăng ký là ứng viên khởi động lại cho sự cố (ngoại lệ không xử lý), không phản hồi, khởi động lại ứng dụng do cập nhật, và khởi động lại OS do cập nhật. Bạn có thể đăng ký các đối số dòng lệnh cho lần khởi động lại, nên nếu gồm “tệp nào đang mở” và “điểm khôi phục nào”, bạn có thể tiếp từ chỗ đã dừng sau lần khởi động lại.10
Các đặc tả cần tiếp nhận như sau.10
- Đăng ký phải xong trước khi sự cố xảy ra (trong lúc xử lý WM_QUERYENDSESSION là cơ hội cuối trong một kịch bản cập nhật)
- Để ngăn vòng khởi động lại, một tiến trình đã chạy dưới 60 giây không được khởi động lại
- Một tiến trình chạy elevated không phải ứng viên khởi động lại tự động (tiến trình không tạo lại được nếu không có đồng ý nâng quyền). Phục hồi tự động của một ứng dụng cần nâng quyền được thiết kế bằng cách giữ UI ở quyền chuẩn và cô lập việc đặc quyền trong một dịch vụ, hoặc bằng một đường khởi chạy tường minh như tác vụ Task Scheduler “Run with highest privileges”
- Khởi động lại sau sự cố hoặc treo đi qua đồng ý của người dùng; khởi động lại sau cập nhật là tự động
- Để phục hồi xuyên một lần khởi động lại OS, bên yêu cầu khởi động lại (một trình cài và tương tự) phải gọi API tắt máy với các cờ EWX_RESTARTAPPS / SHUTDOWN_RESTARTAPPS
Nếu bạn cũng đăng ký RegisterApplicationRecoveryCallback, WER (Windows Error Reporting) gọi callback khi sự cố và cho bạn một khoảng ân hạn để lưu dữ liệu đang làm. Nếu lần lưu mất thời gian, tuy nhiên, bạn phải tiếp tục gọi ApplicationRecoveryInProgress trong khoảng ping đã chỉ lúc đăng ký nếu không việc phục hồi bị cắt giữa chừng. Khi lưu xong, hãy thông báo hoàn tất bằng ApplicationRecoveryFinished. “Thay một tệp đang dùng rồi khởi động lại” lúc cập nhật ứng dụng thuộc địa phận Restart Manager, được trình bày chi tiết trong “Cách thay một exe hoặc DLL đang được dùng”.
7.2. ARSO — đăng nhập tự động sau khởi động lại cập nhật
Sau một lần khởi động lại Windows Update, nếu không ai đăng nhập, các ứng dụng phiên người dùng không trở lại. Thứ lấp khoảng trống đó là ARSO (Winlogon Automatic Restart Sign-On). Khi Windows Update bắt đầu một lần khởi động lại, nó lưu an toàn thông tin đăng nhập của người dùng tương tác cuối, cấu hình Autologon, và sau lần khởi động lại tự động đăng nhập người dùng đó rồi khóa màn hình.11 Cũng có lệnh như shutdown /g yêu cầu khởi động lại cộng tiếp các ứng dụng đã đăng ký. Một số môi trường tắt điều này bằng chính sách tổ chức (DisableAutomaticRestartSignOn và tương tự), nên khi thiết kế phục hồi không người, hãy kiểm thiết lập này như một bộ. Và nếu bạn đang dựa vào khởi chạy tự động trong phiên người dùng cho việc nền bạn luôn cần, bước đúng là biến nó thành dịch vụ Windows ngay từ đầu.
flowchart TB
accTitle: Đường ứng dụng phục hồi tự động sau khởi động lại
accDescr: Nếu bạn đăng ký bằng RegisterApplicationRestart trước khi sự cố xảy ra, ứng dụng được khởi động lại sau đồng ý người dùng trên sự cố hoặc không phản hồi, và sau đăng nhập tự động ARSO cùng khóa màn hình trên lần khởi động lại do cập nhật. Các tiến trình dưới 60 giây thời gian chạy và tiến trình elevated nằm ngoài phạm vi
reg["RegisterApplicationRestart"]
reg --> crash["Sự cố hoặc treo"]
reg --> update["Khởi động lại cập nhật"]
crash -->|"Đồng ý"| restart["Khởi động lại ứng dụng"]
update --> arso["Đăng nhập ARSO + khóa"]
arso --> restart
restart -.-> limits["Không: dưới 60s / elevated"]
Hình 11: Nếu bạn đăng ký bằng RegisterApplicationRestart trước khi sự cố xảy ra, ứng dụng được khởi động lại sau đồng ý người dùng trên sự cố hoặc không phản hồi, và sau đăng nhập tự động ARSO cùng khóa màn hình trên lần khởi động lại do cập nhật. Các tiến trình dưới 60 giây thời gian chạy và tiến trình elevated nằm ngoài phạm vi.
8. Sống sót một lần mất điện không mang thông báo — Thiết kế ghi và UPS
8.1. Một lần ghi “không vỡ dù cắt lúc nào” — tệp tạm + ReplaceFile
Một aptomat nhảy, PSU hỏng, hoặc phích bị rút không mang cả WM_ENDSESSION lẫn PRESHUTDOWN. Miễn là bạn “ghi đè tệp gốc tại chỗ” cho thiết lập hoặc kết quả đo, một lần mất điện giữa ghi có thể để lại một tệp hỏng trộn cũ và mới.
Mẫu chuẩn là ghi trọn vào một tệp tạm trên cùng ổ rồi đổi. ReplaceFile đóng gói chuỗi “lưu vào tệp mới → để tệp gốc sang một bên → đổi tên → xóa” thành một API, và cũng mang theo các thuộc tính của tệp gốc như thời gian tạo, ACL, và luồng thay thế (ba tệp phải trên cùng ổ).12 File.Replace của .NET gọi cái này nguyên si.
flowchart TB
accTitle: Luồng lưu và phục hồi với tệp tạm và ReplaceFile
accDescr: Lúc lưu, ghi trọn vào một tệp tạm, xả, rồi đổi bằng ReplaceFile, để nội dung cũ trong .bak. Ở lần khởi chạy tiếp, xác thực tệp chính và rơi về .bak nếu nó hỏng
subgraph save["Lúc lưu"]
w["Ghi một tệp tạm trọn"] --> f["Xả ra đĩa"]
f --> r["ReplaceFile → .bak"]
end
subgraph startup["Ở lần khởi chạy tiếp"]
v["Xác thực tệp chính"]
v -->|"Nguyên vẹn"| use["Dùng nguyên"]
v -->|"Hỏng"| bak["Rơi về .bak"]
end
r -.->|"Mất điện ở bất kỳ bước"| v
Hình 12: Lúc lưu, ghi trọn vào một tệp tạm, xả, rồi đổi bằng ReplaceFile, để nội dung cũ trong .bak. Ở lần khởi chạy tiếp, xác thực tệp chính và rơi về .bak nếu nó hỏng.
// The standard pattern for settings and data: write completely to a temporary file, then swap, and keep the old contents
public static void SaveAtomically(string path, string content)
{
string dir = Path.GetDirectoryName(path)!;
string tmp = Path.Combine(dir, Path.GetRandomFileName()); // Create it on the same volume
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 equivalent. Write the OS buffer
// out to disk (device-side cache limits are in 8.2)
}
if (File.Exists(path))
File.Replace(tmp, path, path + ".bak"); // Calls ReplaceFile. Keep the old contents as .bak
else
File.Move(tmp, path);
}
catch
{
// If we fail mid-way, do not leave the temporary file. Repeated failures
// of a periodic save would fill the volume with complete copies
try { File.Delete(tmp); } catch { /* Prefer the original exception if delete fails */ }
throw;
}
}
Với điều này, vận hành thường luôn để bạn đọc được hoặc “một tệp cũ trọn” hoặc “một tệp mới trọn”. ReplaceFile, tuy nhiên, là một thao tác không gian tên nhiều bước, và tính nguyên tử xuyên một lần mất điện không được đặc tả bảo đảm. Đó là vì sao ví dụ trên giữ một bản sao lưu (.bak) — phía đọc xác thực tệp chính lúc khởi chạy và rơi về bản sao lưu nếu nó hỏng, như một bộ. Bạn không dùng cái này cho nhật ký chỉ-nối hoặc CSV, nên những thứ đó dùng một định dạng xây hỏng vào trong, như “một dòng = một bản ghi, và bỏ dòng cuối hỏng lúc đọc”.
8.2. Thành công từ WriteFile không phải đã tới đĩa
Tiền đề kia là ngay cả nếu WriteFile trả thành công, dữ liệu vẫn có thể chỉ nằm trong bộ đệm OS. Windows đặt đọc và ghi tệp lên bộ đệm hệ thống rồi phản ánh chúng ra đĩa định kỳ bằng ghi lười. Để đưa dữ liệu ra đĩa chắc, hoặc xả tường minh bằng FlushFileBuffers, hoặc chỉ định FILE_FLAG_WRITE_THROUGH lúc CreateFile để mỗi lần ghi xuyên bộ đệm. Siêu dữ liệu hệ thống tệp luôn được đệm, nên xác nhận siêu dữ liệu cũng cần xả hoặc write-through.13
Gọi FlushFileBuffers mỗi lần thì kém hiệu quả, tuy nhiên, và tài liệu cũng khuyến khích cân nhắc FILE_FLAG_NO_BUFFERING+WRITE_THROUGH thay vì gọi thường xuyên.13 Trong thực tế, “chỉ xả tại một mốc giao dịch hoặc ngay trước khi đóng tệp” là thỏa hiệp thực tế. Cơ chế tầng này — cache manager, ghi lười, và sự thật “tôi đã xả mà nó vẫn có thể chưa tới đĩa” vì bộ đệm phần cứng — được trình bày sâu trong “Cache Manager: WriteFile của bạn thực sự tới đĩa khi nào?”.
8.3. UPS và giám sát pin — biến mất điện thành tắt máy
Đối sách thật với mất điện trên PC thiết bị là UPS. Hãy nghĩ vai trò của UPS không phải “chặn cúp điện” mà là biến “một lần mất điện không thông báo” thành “một lần tắt có kế hoạch có thông báo”. Thiết kế là bộ hai giai đoạn.
- Thiết kế thời gian ân hạn: thời gian giữ pin UPS > tổng “phát hiện chuyển sang pin → dọn dẹp ứng dụng và dịch vụ → tắt OS xong”. Nếu xử lý dừng dịch vụ chậm, phương trình này không còn đứng (Mục 6.2)
- Phát hiện: Chuyển từ AC sang pin, và sụt dung lượng còn lại, được thông báo bằng sự kiện PBT_APMPOWERSTATUSCHANGE. Ứng dụng có cửa sổ nhận nó như WM_POWERBROADCAST; dịch vụ không cửa sổ tuyên bố SERVICE_ACCEPT_POWEREVENT và nhận nó như SERVICE_CONTROL_POWEREVENT trong HandlerEx (WM_POWERBROADCAST không tới handler điều khiển dịch vụ). Khi nhận, hãy gọi GetSystemPowerStatus, kiểm ACLineStatus (có đang AC không) và BatteryLifePercent, rồi dẫn vào ngắt đo, lưu, và yêu cầu tắt15
flowchart TB
accTitle: Luồng biến mất điện thành tắt có kế hoạch bằng UPS
accDescr: Khi cúp điện chuyển UPS sang pin, PBT_APMPOWERSTATUSCHANGE được thông báo, trạng thái nguồn được kiểm, và lưu cộng một yêu cầu tắt biến một lần mất điện không thông báo thành một lần tắt có kế hoạch có thông báo
outage["Cúp điện"] --> ups["UPS sang pin"]
ups --> pbt["PBT_APMPOWERSTATUSCHANGE"]
pbt --> check["GetSystemPowerStatus"]
check --> saveop["Ngắt và lưu"]
saveop --> req["Yêu cầu tắt"]
req --> normal["Luồng thông báo thường(3–6)"]
Hình 13: Khi cúp điện chuyển UPS sang pin, PBT_APMPOWERSTATUSCHANGE được thông báo, trạng thái nguồn được kiểm, và lưu cộng một yêu cầu tắt biến một lần mất điện không thông báo thành một lần tắt có kế hoạch có thông báo.
Một UPS USB điển hình hiện với Windows như một viên pin, nên bạn phát hiện được bằng API chuẩn này. Nếu phần mềm quản lý của nhà cung cấp có tính năng “tắt OS khi còn N%”, cũng hãy kiểm ngưỡng khớp thời gian dọn dẹp của ứng dụng bạn. Thức dậy từ ngủ hoặc ngủ đông, và các vấn đề chạy dài, là một trục riêng, nằm trong “Ngủ, ngủ đông, Modern Standby, và ứng dụng chạy dài”.
9. Cách xác minh — Thử tắt máy an toàn
Xử lý tắt máy dễ thành “chúng tôi đã viết nhưng chưa bao giờ thử trong điều kiện tương đương sản xuất”. Hãy giữ một quy trình để xác minh nó an toàn.
- Thử trên máy thử hoặc VM: Đừng thử trước trên PC thiết bị sản xuất. Trong môi trường thử với checkpoint Hyper-V (ảnh chụp), hãy lặp tắt, khởi động lại, và mất điện cưỡng bức (tắt nguồn VM). Một lần “power off” VM, tuy nhiên, chỉ tái hiện “OS khách dừng không báo”; nó không tái hiện sự biến mất của bộ đệm bay hơi đĩa vật lý hay hỏng phụ thuộc bộ điều khiển. Nếu bạn xuất xưởng nó như PC thiết bị, kiểm tra cuối là một lần cắt điện thật trên phần cứng tương đương sản xuất
- Kiểm nhanh bằng đăng xuất: Đường WM_QUERYENDSESSION → WM_ENDSESSION cũng chạy lúc đăng xuất (khác duy nhất là bit ENDSESSION_LOGOFF được đặt trong lParam), nên bạn tiện xác nhận hành vi mã dọn dẹp trên máy phát triển1
- Thử tắt đầy đủ và hybrid riêng: Thử từng cái
shutdown /s /t 0(đầy đủ),shutdown /s /hybrid /t 0(hành vi mặc định), vàshutdown /r /t 0(khởi động lại)2 - Đo dọn dẹp mất bao lâu: Ghi dấu thời gian vào nhật ký ở đầu và cuối hàm dọn dẹp, và đo xem nó có vừa 5 giây (hoặc ân hạn đã cấu hình cho dịch vụ) không
flowchart TB
accTitle: Các thao tác xác minh và thứ mỗi cái xác nhận được
accDescr: Đăng xuất là kiểm tiện đường thông báo; tắt-lệnh đầy đủ, hybrid, và khởi động lại xác nhận đường thông báo sản xuất cùng ân hạn; tắt nguồn VM thử sức chịu dừng đột ngột; một lần cắt điện vật lý là kiểm tra cuối gồm cả lưu trữ vật lý
signtest["Đăng xuất"] -.-> path1["Đường QUERY → ENDSESSION"]
signtest --> shuttest["shutdown /s /hybrid /r"]
shuttest -.-> path2["Đường sản xuất + ân hạn"]
shuttest --> vmtest["Tắt nguồn VM"]
vmtest -.-> path3["Dừng khách đột ngột"]
vmtest --> hwtest["Cắt điện vật lý"]
hwtest -.-> path4["Gồm lưu trữ(cuối)"]
Hình 14: Đăng xuất là kiểm tiện đường thông báo; tắt-lệnh đầy đủ, hybrid, và khởi động lại xác nhận đường thông báo sản xuất cùng ân hạn; tắt nguồn VM thử sức chịu dừng đột ngột; một lần cắt điện vật lý là kiểm tra cuối gồm cả lưu trữ vật lý.
Để cô lập sau việc, nhật ký sự kiện (System) hữu ích. Trên một lần tắt hoặc khởi động lại bình thường, event ID 1074 (tiến trình nào bắt đầu tắt, vì ai, và vì lý do gì) được ghi. Trên mất điện hoặc sự cố đột ngột không có 1074, và ở lần khởi động tiếp event ID 41 (Kernel-Power) và 6008 (The previous system shutdown was unexpected) được ghi.14 “Điều gì xảy ra đêm qua” bắt đầu ở đây.
# Check the recent history of shutdown-related events
Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 1074, 6008, 41 } -MaxEvents 20 |
Select-Object TimeCreated, Id, ProviderName, Message |
Format-List
Nếu 1074 cho “một lần khởi động lại bởi Windows Update” và dữ liệu ứng dụng bị hỏng, vấn đề là mã dọn dẹp. 6008/41 chỉ cho “một lần tắt bất ngờ”; chúng cũng được ghi cho màn hình xanh (sự cố) hoặc đặt lại cưỡng bức, không chỉ mất điện. Nếu BugcheckCode của 41 khác không thì đó là sự cố; nếu là 0 và cũng không có bản dump bộ nhớ, mất điện có khả năng — hãy cô lập nguyên nhân từ thông tin xung quanh, và khi biết đó là mất điện, thiết kế ghi Chương 8 và UPS là bước tiếp.
10. Tóm tắt
- Tắt máy là “một sự kiện bình thường sẽ tới, sớm hay muộn”. Thời gian ân hạn sau thông báo, về nguyên tắc, chỉ khoảng 5 giây, nên điều kiện tiên quyết là tự lưu thường xuyên để “việc bạn làm lúc thoát” được tối thiểu.
- Trên các OS máy khách từ Windows 8 trở đi, nếu fast startup bật, “Shut down” là hybrid shutdown và kernel chỉ đang ngủ đông. Lần đặt lại đầy đủ duy nhất là “Restart” — hãy viết “Restart” vào quy trình sự cố.
- Ứng dụng GUI trả TRUE ngay cho WM_QUERYENDSESSION, và làm dọn dẹp đã cam kết trong WM_ENDSESSION. FormClosing và SessionEnding của WinForms/WPF tương ứng pha truy vấn, nên việc bạn làm ở đó nhiều nhất là một lần lưu ảnh chụp lũy đẳng. Đừng đưa hộp thoại trong lúc tắt.
- Một thao tác thật sự không thể gián đoạn được bảo vệ bằng cách hiện một lý do với ShutdownBlockReasonCreate. Không có, tuy nhiên, bảo đảm nào rằng bạn chặn được.
- Ứng dụng console nhận thông báo bằng SetConsoleCtrlHandler; dịch vụ bằng SERVICE_ACCEPT_PRESHUTDOWN/SHUTDOWN. Ân hạn PRESHUTDOWN mặc định là 10 giây trên OS hiện tại. Trong .NET, hãy thôi dựa vào ProcessExit và chuyển sang đường chuẩn của mô hình ứng dụng.
- Phục hồi sau khởi động lại có thể không người với RegisterApplicationRestart (+ một callback phục hồi) và ARSO.
- Mất điện không mang thông báo. Hãy chuẩn bị bằng đổi tệp-tạm + ReplaceFile (như một bộ với sao lưu + xác thực lúc tải), xả tại các mốc, và một UPS “biến mất điện thành tắt có kế hoạch”.
flowchart TB
accTitle: Toàn cảnh xử lý tắt máy
accDescr: Với một lần kết thúc có thông báo, hãy đáp bằng dọn dẹp đóng cửa hàng được trong vài giây và dẫn vào phục hồi tự động sau khởi động lại; với mất điện không thông báo, hãy chuẩn bị bằng một lần ghi không vỡ dù cắt lúc nào và UPS, rồi xác minh gồm cả phần cứng vật lý. Hai trụ này là kết luận của bài
ending{"Nó kết thúc thế nào"}
ending -->|"Có thông báo"| pillar1["Dọn dẹp vài giây(3–6)"]
ending -->|"Không thông báo"| pillar2["Ghi an toàn + UPS(8)"]
pillar1 --> recover["Tự phục hồi sau khởi động lại(7)"]
pillar2 --> verifytest["Xác minh trên phần cứng(9)"]
Hình 15: Với một lần kết thúc có thông báo, hãy đáp bằng dọn dẹp đóng cửa hàng được trong vài giây và dẫn vào phục hồi tự động sau khởi động lại; với mất điện không thông báo, hãy chuẩn bị bằng một lần ghi không vỡ dù cắt lúc nào và UPS, rồi xác minh gồm cả phần cứng vật lý. Hai trụ này là kết luận của bài.
- Xác minh an toàn trên VM và bằng đăng xuất, và cô lập sau việc bằng event ID 1074/41/6008.
Lần tới bạn thêm một tính năng vào ứng dụng, hãy tự hỏi một lần: nếu WM_ENDSESSION tới giữa việc này, hoặc điện bị rút, thứ gì còn lại ở lần khởi chạy tiếp? Viết câu trả lời đó vào thiết kế là đường ngắn nhất để không bao giờ phải mất một buổi sáng đứng trước PC thiết bị ôm đầu.
Bài viết liên quan
- Cách thay một exe hoặc DLL đang được dùng — Restart Manager và bài toán “File In Use” trong tự cập nhật
- Cách xây và vận hành dịch vụ Windows ── Từ chọn giữa Task Scheduler và dịch vụ tới biến một BackgroundService thành dịch vụ Windows
- Ngủ, ngủ đông, Modern Standby, và ứng dụng chạy dài — Thiết kế quanh “nó đã dừng từ đêm”
- Độ sâu I/O Windows (Phần 4) — Cache Manager: WriteFile của bạn thực sự tới đĩa khi nào?
- Thiết kế ứng dụng Windows để lại nhật ký và dump khi chúng sự cố
- Danh sách kiểm để xử lý tiến trình con an toàn trong ứng dụng Windows
Lĩnh vực tư vấn liên quan
KomuraSoft LLC đảm nhận thiết kế và triển khai đối sách tắt máy và mất điện cho ứng dụng PC thiết bị và chạy dài, điều tra nguyên nhân gốc rễ của hỏng dữ liệu và sự cố “tới sáng nó đã dừng” bắt đầu từ một lần khởi động lại Windows Update hoặc đăng xuất, cùng rà soát thiết kế xử lý dừng dịch vụ Windows và phục hồi tự động. Bắt đầu từ giai đoạn “mỗi lần tắt hình như lại vỡ gì đó, và tôi không biết bắt đầu từ đâu” cũng được.
- Phát triển ứng dụng Windows
- Điều tra lỗi và phân tích nguyên nhân gốc rễ
- Tư vấn kỹ thuật và rà soát thiết kế
- Liên hệ với chúng tôi
Liên kết tham khảo
-
Microsoft Learn, WM_QUERYENDSESSION message. Về việc WM_QUERYENDSESSION được gửi lúc kết thúc phiên và ứng dụng nên trả TRUE rồi tôn trọng ý định của người dùng (mặc định của DefWindowProc cũng là TRUE); về việc dọn dẹp nên được hoãn tới WM_ENDSESSION; về việc sau 5 giây hệ thống hiện UI cho các ứng dụng đang ngăn tắt máy và người dùng có thể buộc kết thúc; ý nghĩa các bit ENDSESSION_LOGOFF/CLOSEAPP/CRITICAL trong lParam; về việc tắt và khởi động lại không phân biệt được; và về việc dữ liệu nên được lưu thường xuyên để ít hơn phải lưu lúc thoát. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Fast startup causes hibernation or shutdown to fail in Windows 10 or Windows 8.1. Về việc với fast startup phiên kernel không bị đóng và được coi như ngủ đông, và trạng thái kernel cùng driver thiết bị được lưu vào hiberfil.sys; về việc “Restart” luôn làm khởi động đầy đủ vì cần một trạng thái Windows hoàn toàn mới; về việc fast startup bật mặc định và tắt nó không được khuyến nghị; và về mặc định của Shutdown.exe là tắt đầy đủ, với tùy chọn /hybrid cho hành vi hybrid. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, ShutdownBlockReasonCreate function (winuser.h). Về việc bạn gọi nó lúc bắt đầu một thao tác không thể gián đoạn để đăng ký một chuỗi lý do và gọi ShutdownBlockReasonDestroy khi xong; về việc nó chỉ gọi được từ thread đã tạo cửa sổ; và về việc người dùng chỉ đọc lý do vài giây, nên chuỗi nên ngắn và rõ. ↩ ↩2 ↩3
-
Microsoft Learn, Shutdown Changes for Windows Vista. Về việc phản hồi WM_QUERYENDSESSION/WM_ENDSESSION có thể bị trì hoãn 5 giây mỗi cái và người dùng khi đó có thể chọn tiếp tục hoặc hủy; về việc ứng dụng console hoặc ứng dụng không cửa sổ hiện không thể hủy tắt máy và bị kết thúc tự động sau 5 giây không trả lời hoặc phản hồi FALSE; về việc nếu cần chặn thì nên đăng ký một lý do bằng ShutdownBlockReasonCreate; và về việc ứng dụng không được dựa vào khả năng chặn tắt máy. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, HandlerRoutine callback function. Các sự kiện CTRL_C/BREAK/CLOSE/LOGOFF/SHUTDOWN mà một handler đăng ký bằng SetConsoleCtrlHandler nhận; về thời gian chờ mặc định của CTRL_CLOSE_EVENT khoảng 5000 mili giây và của CTRL_SHUTDOWN_EVENT trên tiến trình dịch vụ khoảng 20000 mili giây; về việc CTRL_LOGOFF/SHUTDOWN_EVENT về căn bản chỉ được dịch vụ nhận vì ứng dụng tương tác bị kết thúc lúc đăng xuất; và về việc handler chạy trên một thread riêng. ↩ ↩2 ↩3
-
Microsoft Learn, SetConsoleCtrlHandler function. Về việc một tiến trình đã nạp gdi32.dll hoặc user32.dll bị coi như ứng dụng Windows và các handler CTRL_LOGOFF_EVENT/CTRL_SHUTDOWN_EVENT không được gọi; về cách là tạo một cửa sổ ẩn rồi xử lý WM_QUERYENDSESSION/WM_ENDSESSION; và về việc các hàm console có thể không hoạt động đúng trong lúc xử lý tín hiệu. ↩ ↩2
-
Microsoft Learn, .NET runtime no longer provides default termination signal handlers. Về việc từ .NET 10 runtime không còn cung cấp handler mặc định cho Windows CTRL_SHUTDOWN_EVENT/CTRL_CLOSE_EVENT (các tương đương Unix SIGTERM/SIGHUP); về việc xử lý mặc định của OS kết thúc ứng dụng ngay và AppDomain.ProcessExit cùng AssemblyLoadContext.Unloading không còn kích; và về việc xử lý tín hiệu phù hợp mô hình ứng dụng nên được đăng ký trong một thư viện tầng cao hơn hoặc trong mã ứng dụng. ↩ ↩2
-
Microsoft Learn, Service Control Handler Function. Về việc dịch vụ đã tuyên bố SERVICE_ACCEPT_PRESHUTDOWN nhận SERVICE_CONTROL_PRESHUTDOWN trước, rồi dịch vụ SERVICE_ACCEPT_SHUTDOWN nhận SERVICE_CONTROL_SHUTDOWN; về ân hạn mặc định lúc tắt khoảng 20 giây và trần lúc khởi động lại OS là WaitToKillServiceTimeout; về việc không nên kéo dài giá trị này; về việc handler điều khiển nên trả về trong 30 giây, báo STOP_PENDING và wait hint, rồi để việc dài cho thread khác; về việc dọn dẹp nên xong càng nhanh càng tốt với vận hành UPS trong đầu; và về việc SCM lúc tắt mặc định không xét phụ thuộc. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, SERVICE_PRESHUTDOWN_INFO structure (winsvc.h). Về việc sau thông báo PRESHUTDOWN SCM chờ tới khi dịch vụ dừng hoặc hết thời gian chờ; về thời gian chờ mặc định là 10 giây từ Windows 10 Creators Update (build 15063) trở đi và 3 phút trước đó; về việc nó được cấu hình bằng ChangeServiceConfig2; và về việc trạng thái có thể tiếp tục được cập nhật trong SERVICE_STOP_PENDING. ↩ ↩2
-
Microsoft Learn, RegisterApplicationRestart function (winbase.h). Về việc khởi động lại có thể được đăng ký cho sự cố, không phản hồi, cập nhật, và khởi động lại máy kèm cập nhật; về việc đối số dòng lệnh cho lần khởi động lại có thể được chỉ định; về việc đăng ký phải làm trước khi sự cố xảy ra và trong lúc xử lý WM_QUERYENDSESSION là cơ hội cuối trong kịch bản cập nhật; về việc tiến trình dưới 60 giây thời gian chạy không được khởi động lại; về việc khởi động lại sau sự cố hoặc treo đi qua đồng ý người dùng; và về việc xuyên một lần khởi động lại OS đòi tắt máy với EWX_RESTARTAPPS/SHUTDOWN_RESTARTAPPS. ↩ ↩2 ↩3
-
Microsoft Learn, Winlogon automatic restart sign-on (ARSO). Về việc khi Windows Update bắt đầu khởi động lại tự động nó lưu thông tin đăng nhập của người dùng tương tác cuối rồi cấu hình Autologon; về việc sau lần khởi động lại nó tự đăng nhập người dùng và khóa phiên; về việc thông tin đã lưu bị xóa sau đăng nhập thành công; và về việc nó cấu hình được bằng Group Policy (DisableAutomaticRestartSignOn và tương tự). ↩ ↩2
-
Microsoft Learn, ReplaceFileW function (winbase.h). Về việc ReplaceFile đóng gói thành một hàm nhiều bước tương đương “lưu vào tệp mới, tạm đổi tên tệp gốc, đổi tên tệp mới, xóa tệp gốc”; về việc nó giữ các thuộc tính của tệp gốc như thời gian tạo, DACL, mã hóa, nén, và luồng có tên; và về việc bản sao lưu, tệp đang bị thay, và tệp thay thế phải trên cùng ổ. ↩ ↩2
-
Microsoft Learn, File Caching. Về việc các lần ghi vào bộ đệm hệ thống mặc định và được phản ánh ra đĩa bằng ghi lười; về việc FILE_FLAG_WRITE_THROUGH ghi ngay ra đĩa; về việc FlushFileBuffers có thể xả tường minh; và về việc siêu dữ liệu hệ thống tệp luôn được đệm, nên xác nhận siêu dữ liệu đòi xả hoặc write-through. ↩ ↩2 ↩3
-
Microsoft Learn, Troubleshoot unexpected reboots using system event logs. Về việc một lần khởi động lại bình thường ghi event ID 1074 (tiến trình nào bắt đầu tắt, vì ai, và vì lý do gì); về việc một lần khởi động lại bất ngờ ghi event ID 41 (Kernel-Power) và 6008 (lần tắt trước bất ngờ); và về việc các ID này cô lập được loại khởi động lại. ↩ ↩2
-
Microsoft Learn, PBT_APMPOWERSTATUSCHANGE event. Về việc sự kiện này được thông báo qua WM_POWERBROADCAST khi chuyển giữa pin và AC hoặc sụt dung lượng còn lại; và về việc khi nhận bạn nên gọi GetSystemPowerStatus rồi kiểm các trường SYSTEM_POWER_STATUS như ACLineStatus, BatteryFlag, và BatteryLifePercent. ↩
Bài viết liên quan
Các bài viết gần đây có cùng thẻ để tìm hiểu sâu hơn những chủ đề lân cận.
API thread pool Win32 — Đồng thời mà không tạo luồng, với CreateThreadpoolWork
Bạn đang rải các lời gọi CreateThread khắp mã native? Bài viết này giải thích API thread pool Win32 được thiết kế lại từ Vista — bốn đối ...
Named pipe trong thực tiễn — IPC chuẩn của Windows, từ thiết kế đến bảo mật
Hướng dẫn thực tiễn về named pipe, cơ chế giao tiếp giữa các tiến trình chuẩn của Windows. Bài viết sắp xếp, từ nguồn gốc, việc chọn giữa...
Ứng dụng hỏng khi thức dậy từ ngủ — Sự kiện nguồn Windows và cách viết ứng dụng nghiệp vụ sống sót
Bạn mở laptop và kết nối của ứng dụng nghiệp vụ đã chết — nguyên nhân là thiết kế chưa tính đến ngủ. Bài viết này trình bày luồng thông b...
DllMain và Loader Lock — Lý do thật sự bạn bị bảo "đừng làm gì trong khởi tạo DLL"
Vì sao bạn không được gọi LoadLibrary hoặc đồng bộ với luồng khác từ DllMain. Dựa trên nguồn gốc, bài viết giải thích cách loader lock tu...
"Không phản hồi" thực sự là gì — Cách Windows quyết định ứng dụng đã treo, và cách thiết kế ứng dụng không treo
"Không phản hồi" của Windows là cơ chế trong đó hệ điều hành phán rằng một cửa sổ chưa lấy thông điệp trong 5 giây và thay nó bằng cửa sổ...
Chủ đề liên quan
Các trang này đặt chủ đề trong bối cảnh rộng hơn của dịch vụ và quyết định.
Chủ đề kỹ thuật Windows
Cổng vào phát triển Windows, điều tra lỗi và khai thác tài sản hiện có.
Dịch vụ liên quan đến chủ đề này
Bài viết liên quan trực tiếp đến các dịch vụ sau.
Phát triển ứng dụng Windows
Ứng dụng nghiệp vụ, tích hợp thiết bị và công cụ liên lạc, từ yêu cầu đến phát triển.
Câu hỏi thường gặp
Các câu hỏi thường gặp khi tư vấn về chủ đề của bài viết.
- Một vấn đề không hết sau "Shut down" lại hết sau "Restart". Vì sao?
- Trên các OS máy khách từ Windows 8 trở đi, khi fast startup bật (mặc định trên hầu hết PC hỗ trợ ngủ đông), "Shut down" dùng cơ chế gọi là hybrid shutdown. Người dùng được đăng xuất, nhưng trạng thái kernel và driver được lưu vào tệp ngủ đông rồi khôi phục nguyên si ở lần khởi động tiếp. Nói cách khác, lõi OS chưa được đặt lại. "Restart", ngược lại, luôn thực hiện khởi động đầy đủ, nên sự cố driver và dịch vụ được đặt lại. Hãy viết "Restart" vào quy trình cô lập, không phải "Shut down rồi bật lại". Nếu muốn tắt đầy đủ từ dòng lệnh, bạn có thể dùng shutdown /s.
- Tôi có thể dừng tắt máy cho tới khi ứng dụng xong việc lưu không?
- Bạn có thể xin nó chờ tạm, nhưng không thể dừng chắc. Nếu bạn đăng ký một chuỗi lý do bằng ShutdownBlockReasonCreate chỉ khi một thao tác không thể gián đoạn đang chạy, lý do đó hiện trên màn hình "This app is preventing shutdown" và người dùng quyết tiếp tục hay hủy. Người dùng vẫn có thể chọn buộc tiếp, tuy nhiên, và một lần tắt cưỡng bức hoặc khởi động lại do cập nhật có thể không chờ gì cả. Cách đúng vì vậy không phải "chặn", mà là tự lưu thường xuyên để ít dữ liệu gặp rủi ro hơn, cộng dọn dẹp được thiết kế xong trong vài giây từ thông báo thoát.
- Dừng dịch vụ Windows của tôi mất nhiều thời gian. Tôi có thể kéo dài thời gian ân hạn tắt máy không?
- Trong cấu hình mặc định nhận SERVICE_CONTROL_SHUTDOWN, thời gian ân hạn khoảng 20 giây và phụ thuộc giá trị registry WaitToKillServiceTimeout. Viết lại giá trị đó từ phía ứng dụng để kéo dài không được khuyến nghị. Nếu cần ân hạn dài hơn, bạn có thể tuyên bố SERVICE_ACCEPT_PRESHUTDOWN và nhận SERVICE_CONTROL_PRESHUTDOWN; bạn được thông báo sớm hơn các bên khác, và thời gian chờ cấu hình được bằng ChangeServiceConfig2 (mặc định là 10 giây từ Windows 10 Creators Update trở đi, và 3 phút trước đó). PRESHUTDOWN, tuy nhiên, giữ cả lần tắt trong khoảng đó, nên hãy giới hạn ở các vụ bạn thật sự cần, và về căn bản thiết kế bản thân việc dừng sao cho xong trong vài giây.
- Dọn dẹp tắt máy trong AppDomain.ProcessExit của .NET có an toàn không?
- Tôi khuyên đừng dựa vào nó. Về lịch sử runtime đăng ký một handler tín hiệu mặc định, và ProcessExit kích trên CTRL_CLOSE_EVENT và CTRL_SHUTDOWN_EVENT, nhưng từ .NET 10 runtime không còn cung cấp handler tín hiệu kết thúc mặc định, và ProcessExit không còn kích trong các trường hợp đó. Hãy triển khai dọn dẹp trên đường thông báo khớp mô hình ứng dụng: ứng dụng GUI dùng FormClosing hoặc SessionEnding (đó là thông báo pha truy vấn, nên giới hạn ở các lần lưu lũy đẳng; dọn dẹp chỉ chạy được sau khi phiên đã cam kết thuộc về một hook WM_ENDSESSION); Generic Host / Worker Service dùng IHostApplicationLifetime và StopAsync; ứng dụng console dùng SetConsoleCtrlHandler hoặc PosixSignalRegistration.
- Làm sao giữ tệp khỏi bị hỏng bởi mất điện đột ngột?
- Mất điện không mang thông báo nào cả, nên lựa chọn duy nhất là ghi theo cách không vỡ dù điện cắt lúc nào. Đường cơ sở là không ghi đè tệp gốc tại chỗ: ghi trọn vào một tệp tạm trên cùng ổ, xả, rồi đổi bằng ReplaceFile (File.Replace trong .NET). Trong vận hành thường điều này để bạn đọc được hoặc một tệp cũ trọn hoặc một tệp mới trọn, nhưng tính nguyên tử của ReplaceFile xuyên một lần mất điện không được đặc tả bảo đảm, nên hãy giữ bản sao lưu (đối số thứ ba) và triển khai phục hồi lúc tải xác thực tệp chính rồi rơi về bản sao lưu nếu nó hỏng. Ngoài ra, thành công từ WriteFile không nghĩa dữ liệu đã tới đĩa, nên tại các mốc quan trọng hãy xác nhận lần ghi bằng FlushFileBuffers hoặc FILE_FLAG_WRITE_THROUGH. Trên PC thiết bị, bộ chuẩn là kết hợp điều này với UPS, phát hiện chuyển sang pin, và dẫn vào một lần tắt an toàn.