כיבוי Windows כפי שנראה מהיישום — לשרוד נכון הודעות יציאה, אתחולים ואובדן חשמל
· Go Komura · Windows, כיבוי, פיתוח Windows, שירותי Windows, מחשבי התקן, שלמות נתונים, פעולה ארוכת-טווח, UPS
“אחרי אתחול לילי של Windows Update, יישום המדידה במחשב ההתקן נפל באמצע כתיבה, ובבוקר קובץ המדידה היה מושחת.” “מישהו התנתק ממחשב משותף והתלונן שעריכות שלא נשמרו נעלמו.” — ליישומי Windows ארוכי-טווח, שני הייעוצים האלה קלאסיים.
מה שמשותף לשני האתרים הוא התייחסות לכיבוי כ”אירוע חריג שלא אמור לקרות”. במציאות, עם זאת, מאתחולים אוטומטיים של Windows Update, התנתקות משתמש וכיבוי ביוזמת UPS ועד אובדן חשמל בלי הודעה, אירועים שחותכים ביצוע מחוץ ליישום יגיעו, במוקדם או במאוחר. אי אפשר למנוע מהם להגיע. מה שאפשר למנוע הוא “לאבד נתונים כשהם מגיעים”.
למרבה המזל, ל-Windows יש מנגנון שמודיע ליישום לפני כיבוי, ליישומי GUI, יישומי קונסול ושירותים כאחד. מיועד לאנשי IT בעסקים קטנים ובינוניים ולמפתחי יישומי Windows (במיוחד מחשבי התקן ויישומים ארוכי-טווח), המאמר הזה מסדר איך לקבל את ההודעות האלה, איך לתכנן ניקוי ש”מסתיים בכמה שניות”, התאוששות אוטומטית אחרי אתחול, ואיך להתכונן לאובדן חשמל שלא מביא הודעה — הכול מעוגן במקורות ראשוניים של Microsoft Learn נכון לאוגוסט 2026.
1. השורה התחתונה קודם
- מתכננים כיבוי כ”אירוע רגיל שיגיע, במוקדם או במאוחר”. הזמן שאפשר להשתמש בו אחרי קבלת ההודעה הוא, כעיקרון, רק כ-5 שניות, ולכן תכנון ששומר הכול בבהילות במקום יתפרק. התנאי המוקדם הוא שמירה אוטומטית תכופה כך ש”ההפרש שחייב להישמר בכיבוי” נשאר קטן.1
- במערכות הפעלה ללקוח מ-Windows 8 ואילך, כשהפעלה מהירה מופעלת (ברירת המחדל ברוב המחשבים שתומכים בהיברנציה), “כיבוי” הוא כיבוי היברידי, והליבה רק נכנסת להיברנציה. הדבר היחיד שמאופס לגמרי הוא “אתחול”. זו הסיבה האמיתית ל”כיביתי ולא השתפר, אחר כך אתחלתי והשתפר”.2
- יישום GUI צריך להחזיר TRUE מיד ל-WM_QUERYENDSESSION, ולעשות ניקוי ב-WM_ENDSESSION. כעיקרון אסור להחזיר FALSE (סירוב).1
- רק כשיש באמת פעולה שאי אפשר להפריע לה מציגים סיבה עם ShutdownBlockReasonCreate. גם אז המשתמש ומערכת ההפעלה יכולים לכפות המשך, ולכן תכנון שמניח “אנחנו יכולים לחסום” לא מחזיק.34
- יישום קונסול מקבל את ההודעה עם SetConsoleCtrlHandler. תקופת החסד קצרה עוד יותר — ברירת מחדל של 5 שניות לסגירת קונסול. יש גם מלכודת: בתהליך שטען gdi32.dll או user32.dll, חלק מהאירועים האלה לא מגיעים.56
- ניקוי שמסתמך על AppDomain.ProcessExit של .NET לא רץ, מ-.NET 10 ואילך, בנתיבים שבהם התהליך “מסתיים מבחוץ”. ביציאה רגילה כמו חזרה מ-Main הוא עדיין רץ כמו קודם, אבל כי זמן הריצה כבר לא מספק טיפול ברירת מחדל לאותות סיום כמו סגירת קונסול וכיבוי, ניקוי בנתיבים האלה חייב לעבור להודעה שמתאימה למודל היישום.7
- שירות Windows יכול לקבל SERVICE_ACCEPT_PRESHUTDOWN מוקדם יותר, ועם תקופת חסד ניתנת להגדרה, מ-SERVICE_ACCEPT_SHUTDOWN (כ-20 שניות חסד). פקיעת הזמן כברירת מחדל של PRESHUTDOWN, עם זאת, קוצרה ל-10 שניות מ-Windows 10 Creators Update ואילך, ולכן בכל מקרה צריך תכנון שלא נשען יותר מדי על תקופת החסד.89
- התאוששות אוטומטית אחרי אתחול אפשר להשיג בשילוב RegisterApplicationRestart עם ARSO (התחברות אוטומטית). נתיבי התאוששות מסופקים לקריסה, לאי-תגובה ולאתחול מונע-עדכון.1011
- אובדן חשמל לא מביא שום הודעה. הדפוס הסטנדרטי הוא לכתוב לגמרי לקובץ זמני, לבצע flush ולהחליף עם ReplaceFile, אבל כי גם ReplaceFile לא מבטיח אטומיות מעבר לאובדן חשמל, נתיב התאוששות של גיבוי (.bak) פלוס אימות בזמן טעינה הוא חלק מהסט. בידוד בדיעבד אפשר מיומן האירועים (1074/41/6008).121314
במשפט אחד, המסקנה של המאמר הזה היא: “תמיד מחזיקים מצב שממנו אפשר לסגור את החנות בכמה שניות כשההודעה מגיעה, וכותבים באופן שלא נשבר גם באובדן חשמל שלא מביא הודעה”.
2. מה קורה בכיבוי — ארבע דרכים להסתיים
2.1. התנתקות, כיבוי, אתחול ואובדן חשמל
מנקודת מבט של היישום, מה שחשוב הם שני צירים: “איך מסתיימת הפעלת המשתמש” ו”מה קורה לליבה”.
| פעולה | הפעלת משתמש | ליבה ומנהלים | הודעה ליישום |
|---|---|---|---|
| התנתקות | מסתיימת | ממשיכה לרוץ | WM_QUERYENDSESSION (ENDSESSION_LOGOFF) ← WM_ENDSESSION |
| כיבוי (הפעלה מהירה מופעלת) | מסתיימת | נכנסת להיברנציה (נשמרת ב-hiberfil.sys) | WM_QUERYENDSESSION ← WM_ENDSESSION, (PRE)SHUTDOWN לשירותים |
| אתחול | מסתיימת | מסתיימת לגמרי; האתחול הבא הוא אתחול מלא | כמו למעלה |
| אובדן חשמל | נעלמת מיד | נעלמת מיד | אין |
התנתקות וכיבוי הם, מנקודת מבט של היישום, כמעט אותו אירוע. אם סיבית ENDSESSION_LOGOFF מוגדרת ב-lParam של WM_QUERYENDSESSION זו התנתקות; אם היא 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 ואילך, הפעלה מהירה (כיבוי היברידי) מופעלת כברירת מחדל במחשבים שתומכים בהיברנציה, וההתנהגות של “כיבוי” השתנתה. התנתקות הפעלת המשתמש עדיין קורית כרגיל, אבל הפעלת הליבה לא נסגרת; היא נשמרת, מנהלי התקנים וכולם, לקובץ ההיברנציה (hiberfil.sys) ומוחזרת כמו שהיא באתחול הבא. זה מאיץ הפעלה, אבל מצב ליבה ומנהלים שורד גם אחרי שחותכים את החשמל.2 זה, עם זאת, התנהגות מותנית. בסביבה שבה היברנציה עצמה מנוטרלת (powercfg /hibernate off), שבה מדיניות או אפשרויות צריכת חשמל כיבו הפעלה מהירה, וב-Windows Server, כיבוי הוא כיבוי מלא מסורתי. אפשר לדעת באיזה כיוון מחשב נתון רץ מתיבת הסימון “הפעל הפעלה מהירה” באפשרויות צריכת חשמל, או מכך אם powercfg /a (מצבי שינה זמינים) מציג “Fast Startup”.
flowchart TB
accTitle: מה קורה לליבה בפעולת כיבוי
accDescr: פעולת כיבוי מתפצלת לכיבוי מלא או היברנציית ליבה לפי האם הפעלה מהירה דולקת, ואתחול תמיד עושה אתחול מלא
op["כיבוי"]
restart["אתחול"]
op -->|"הפעלה מהירה דולקת"| hybrid["ההפעלה מסתיימת + היברנציית ליבה"]
op -->|"היברנציה כבויה / שרת"| full["כיבוי מלא"]
hybrid --> resume["הבא: שחזור ליבה"]
full --> boot["הבא: אתחול מלא"]
restart --> boot
איור 2: פעולת כיבוי מתפצלת לכיבוי מלא או היברנציית ליבה לפי האם הפעלה מהירה דולקת, ואתחול תמיד עושה אתחול מלא.
“אתחול”, לעומת זאת, תמיד מריץ מחזור אתחול שלם. אחרי עדכון מנהל, למשל, צריך מצב חדש לגמרי.2 מזה, כמה תופעות ששומעים בשטח נופלות למקומן.
- “כיביתי והדלקתי מחדש, אבל תקלת ההתקן לא נעלמה” — הליבה והמנהלים רק שוחזרו מהיברנציה; לא אופסו
- “זה השתפר אחרי שאתחלתי” — כי אתחול מלא אתחל אותם
- נהלי תקריות של מחשב התקן צריכים לומר “אתחול”, לא “לכבות ולהדליק”
אם רוצים להפוך כיבוי מלא למפורש משורת הפקודה, shutdown /s (ברירת המחדל של Shutdown.exe היא כיבוי מלא); אם רוצים את ההתנהגות ההיברידית כברירת מחדל, shutdown /s /hybrid.2 ניטרול הפעלה מהירה אינו מומלץ. צד היישום צריך להניח “בכיבוי הליבה עלולה רק להיכנס להיברנציה” — למשל, לא לאמוד “זמן פעולה מצטבר” מזמן האתחול של מערכת ההפעלה — ולתכנן כך שזה לא יישבר בכל מקרה (האם הפעלה מהירה דולקת או כבויה משתנה לפי סביבה).
3. איך יישום GUI צריך להתנהג — WM_QUERYENDSESSION ו-WM_ENDSESSION
3.1. איך שתי ההודעות מפצלות את העבודה
יישום שיש לו חלון ותור הודעות מקבל הודעה על סיום הפעלה בשני שלבים.1
- WM_QUERYENDSESSION — שאילתה: “האם בסדר לסיים?” היישום צריך להחזיר TRUE מיד; תגובת ברירת המחדל של DefWindowProc היא גם TRUE. לא מתחילים ניקוי כאן.
- WM_ENDSESSION (wParam=TRUE) — הודעה מחויבת: “ההפעלה באמת מסתיימת”. הניקוי קורה כאן.
החזרת FALSE ל-WM_QUERYENDSESSION יכולה לבטל כיבוי, אבל התיעוד מפורש ש”צריך להחזיר TRUE ולכבד את כוונת המשתמש”, ויישום שהחזיר FALSE עדיין נחשף ב-UI במסך מלא כ”יישום שמונע כיבוי”. יישומי קונסול ויישומים בלי חלון נראה לא יכולים לבטל כיבוי מלכתחילה, ואם הם לא מגיבים בתוך 5 שניות הם מסתיימים אוטומטית.14
flowchart TB
accTitle: זרימת הודעת סיום ההפעלה הדו-שלבית
accDescr: החזרת TRUE לשאילתת WM_QUERYENDSESSION מחייבת עם 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: החזרת TRUE לשאילתת WM_QUERYENDSESSION מחייבת עם 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 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
}
ב-WPF, אירוע Application.SessionEnding (המאפיין SessionEnding ב-XAML, או דריסת OnSessionEnding) מתאים.
flowchart TB
accTitle: איך אירועי WinForms/WPF ממופים להודעות
accDescr: שלב השאילתה של WM_QUERYENDSESSION ממופה ל-FormClosing ב-WinForms ול-SessionEnding ב-WPF, ומה שעושים שם הוא שמירת תצלום אידמפוטנטית לכל היותר. אין אירוע מתאים ל-WM_ENDSESSION המחויב, לכן מקבלים אותו ב-WndProc או ב-hook ועושים ניקוי שיכול לרוץ רק אחרי חיוב
q["WM_QUERYENDSESSION"] --> fc["WinForms: FormClosing"]
q --> se["WPF: SessionEnding"]
fc -.-> idem["תצלום אידמפוטנטי בלבד"]
se -.-> idem
e["WM_ENDSESSION"] --> hook["אין אירוע: hook של WndProc"]
hook -.-> final["ניקוי אחרי חיוב"]
איור 4: שלב השאילתה של WM_QUERYENDSESSION ממופה ל-FormClosing ב-WinForms ול-SessionEnding ב-WPF, ומה שעושים שם הוא שמירת תצלום אידמפוטנטית לכל היותר. אין אירוע מתאים ל-WM_ENDSESSION המחויב, לכן מקבלים אותו ב-WndProc או ב-hook ועושים ניקוי שיכול לרוץ רק אחרי חיוב.
// 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
}
יש זהירות אחת כאן. גם FormClosing (CloseReason.WindowsShutDown) וגם SessionEnding של WPF מתאימים לשלב השאילתה (WM_QUERYENDSESSION). אם יישום אחר מסרב, הכיבוי מבוטל והיישום שלכם ממשיך לרוץ. לכן מה שמותר לעשות באירועים האלה הוא שמירת תצלום אידמפוטנטית שלא מזיקה אם הכיבוי מבוטל ומייצרת אותה תוצאה כמה פעמים שלא תרוץ. אם צריך “ניקוי שחייב להיעשות רק כשאנחנו באמת מסיימים” (ניתוק, החזרת משאבים וכן הלאה), תופסים את WM_ENDSESSION המחויב (wParam=TRUE) ישירות ב-WndProc ועושים אותו שם.
בכל נתיב, מקפלים את הגוף לפונקציית “שמירת תצלום” משותפת וכותבים נתוני שחזור ליציאה רגילה, לכיבוי ול(אם אפשר) קריסה באותו פורמט, כך שלוגיקת השחזור בהפעלה הבאה היא נתיב אחד. תכנון להשאיר מידע גם בקריסה מכוסה ב”תכנון שמירת יומנים ו-dump בקריסת יישום Windows”.
4. אם באמת חייבים לחסום — ShutdownBlockReasonCreate
פעולות שנשברות פיזית אם חותכים אותן באמצע, כמו כתיבת CD או קושחה, הן החריג. הפרקטיקה הנכונה כאן היא לרשום מחרוזת סיבה עם 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);
// 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);
}
ההבנה השגויה הקלה כאן היא פיצול התפקידים. כל מה ש-ShutdownBlockReasonCreate עושה הוא לרשום מחרוזת סיבה; הוא עצמו לא עוצר כיבוי. מה שבאמת מעכב כיבוי הוא הטיפול שלכם שמחזיר FALSE ל-WM_QUERYENDSESSION בזמן שדגל הגנה מוגדר, כמו למעלה. משתמשים בשניים כסט, ומנקים את שניהם מיד כשהפעולה מסתיימת. גם, מריצים את הפעולה המוגנת עצמה על תהליכון עובד ומשאירים את תהליכון ה-UI מסוגל לעבד הודעות — מנגנון הסירוב עובד רק ברגע שההודעה מגיעה (וגם אז המשתמש ומערכת ההפעלה יכולים לכפות המשך, ולכן תכנון כתיבה שלא נשבר “אם זה לא עצר” — פרק 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 שניות; חריגה מזה מסיימת את התהליך בכפייה.
// 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. מלכודת .NET — לא להסתמך על ProcessExit
ב-.NET היה דפוס מלאי ארוך של “פשוט מנקים ב-AppDomain.ProcessExit”, אבל מ-.NET 10 זמן הריצה כבר לא מספק מטפלי אות סיום כברירת מחדל, ולא ProcessExit ולא AssemblyLoadContext.Unloading נורים ב-CTRL_CLOSE_EVENT/CTRL_SHUTDOWN_EVENT. מטפל ברירת המחדל של מערכת ההפעלה פשוט מסיים את התהליך מיד.7
flowchart TB
accTitle: איך ProcessExit השתנה ב-.NET 10
accDescr: עד .NET 9 מטפל האות כברירת מחדל של זמן הריצה קיבל את אות הסיום, הרים ProcessExit, ואז יצא. מ-.NET 10 זמן הריצה לא מספק מטפל ברירת מחדל, הטיפול כברירת מחדל של מערכת ההפעלה מסיים את התהליך מיד, ורושמים מטפל בעצמכם
sig["CTRL_CLOSE / SHUTDOWN"] --> old9["עד .NET 9: ProcessExit"]
sig --> new10["מ-.NET 10: יציאה מיידית"]
new10 -.-> alt["רושמים מטפל בעצמכם"]
איור 7: עד .NET 9 מטפל האות כברירת מחדל של זמן הריצה קיבל את אות הסיום, הרים ProcessExit, ואז יצא. מ-.NET 10 זמן הריצה לא מספק מטפל ברירת מחדל, הטיפול כברירת מחדל של מערכת ההפעלה מסיים את התהליך מיד, ורושמים מטפל בעצמכם.
במקום זה, עוברים לנתיב הקנוני לכל מודל יישום.
- יישום GUI: FormClosing / SessionEnding מהפרק הקודם
- Generic Host (כולל Worker Service): IHostApplicationLifetime ו-BackgroundService.StopAsync. הופכים את תקופת החסד של העצירה למפורשת עם HostOptions.ShutdownTimeout
- יישום קונסול חשוף: SetConsoleCtrlHandler (או הרשמה למקבילות SIGINT/SIGTERM עם PosixSignalRegistration)
flowchart TB
accTitle: איפה כל מודל יישום מקבל את הודעת היציאה
accDescr: יישום GUI משתמש ב-FormClosing וב-SessionEnding פלוס hook של WM_ENDSESSION לעבודה מחויבת; Generic Host משתמש ב-IHostApplicationLifetime וב-StopAsync; יישום קונסול חשוף משתמש ב-SetConsoleCtrlHandler או PosixSignalRegistration. הסתמכות על ProcessExit לא נורה בנתיבי אות חיצוני
model{"איזה מודל יישום?"}
model -->|"GUI"| gui["FormClosing / SessionEnding"]
model -->|"לא GUI"| other{"Host או קונסול?"}
gui -.-> guihook["hook של ENDSESSION"]
other -->|"Host"| host["Lifetime + StopAsync"]
other -->|"קונסול"| con["SetConsoleCtrlHandler"]
host -.-> hostto["מגדירים ShutdownTimeout"]
con -.-> ngx["לא מסתמכים על ProcessExit"]
איור 8: יישום GUI משתמש ב-FormClosing וב-SessionEnding פלוס hook של WM_ENDSESSION לעבודה מחויבת; Generic Host משתמש ב-IHostApplicationLifetime וב-StopAsync; יישום קונסול חשוף משתמש ב-SetConsoleCtrlHandler או PosixSignalRegistration. הסתמכות על ProcessExit לא נורה בנתיבי אות חיצוני.
תקופת החסד שונה לפי נתיב — כ-5 שניות ל-GUI ולסגירת קונסול, תקופת החסד של SCM בפרק 6 לשירות (כ-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 מקבלים הודעה קודם עם תקופת החסד המוגדרת, אחר כך נשלחת הודעת SHUTDOWN עם ברירת מחדל של כ-20 שניות, והתהליך מסתיים כשתקופת החסד פוקעת
start["כיבוי מתחיל"] --> pre["PRESHUTDOWN(אם הוכרז)"]
pre --> shut["SHUTDOWN(כ-20 שנ׳)"]
shut --> kill["החסד פוקע ← יציאה"]
איור 9: כשכיבוי מתחיל, שירותים שהכריזו PRESHUTDOWN מקבלים הודעה קודם עם תקופת החסד המוגדרת, אחר כך נשלחת הודעת SHUTDOWN עם ברירת מחדל של כ-20 שניות, והתהליך מסתיים כשתקופת החסד פוקעת.
את פקיעת הזמן של PRESHUTDOWN אפשר להגדיר עם ChangeServiceConfig2 (SERVICE_CONFIG_PRESHUTDOWN_INFO); ברירת המחדל היא 10 שניות מ-Windows 10 Creators Update (build 15063) ואילך, ו-3 דקות לפני כן.9 אם עדיין עובדים מהידע הישן ש”PRESHUTDOWN נותן 3 דקות”, במערכת הפעלה נוכחית יש רק 1/18 מתקופת החסד שציפיתם. גם, PRESHUTDOWN מעכב כיבוי של כל המערכת באותו מרווח, ולכן גם התיעוד אומר שהוא “should be used only in special circumstances”.8
גם פרקטיקת צד המטפל חשובה. מטפל הבקרה חייב לחזור בתוך 30 שניות; משאירים עבודת עצירה שלוקחת זמן לתהליכון אחר, מדווחים SERVICE_STOP_PENDING, וחוזרים מיד.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. תכנון שלא נשען על תקופת החסד
הארכת תקרת תקופת החסד, 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. התאוששות אוטומטית אחרי אתחול
במחשב התקן או במחשב בלי השגחה, היקף התכנון אינו רק “לשרוד כיבוי” אלא “לחזור לבד אחרי אתחול”.
7.1. RegisterApplicationRestart ו-callback ההתאוששות
אם קראתם ל-RegisterApplicationRestart, היישום נרשם כמועמד לאתחול עבור קריסה (חריגה לא מטופלת), אי-תגובה, אתחול יישום מונע-עדכון, ואתחול מערכת הפעלה מונע-עדכון. אפשר לרשום ארגומנטי שורת פקודה לאתחול, ולכן אם כוללים “איזה קובץ היה פתוח” ו”איזו נקודת שחזור”, אפשר להמשיך מאיפה שהפסקתם אחרי האתחול.10
המפרטים לקלוט הם כדלקמן.10
- הרישום חייב להסתיים לפני שהבעיה קורה (במהלך טיפול ב-WM_QUERYENDSESSION זו ההזדמנות האחרונה בתרחיש עדכון)
- כדי למנוע לולאת אתחול, תהליך שרץ פחות מ-60 שניות לא מאותחל
- תהליך שרץ בהרשאה מוגבהת אינו מועמד לאתחול אוטומטי (אי אפשר ליצור מחדש את התהליך בלי הסכמת הגבהה). התאוששות אוטומטית של יישום שצריך הגבהה מתוכננת בהשארת ה-UI בהרשאה רגילה ובידוד עבודה מועדפת בשירות, או בנתיב הפעלה מפורש כמו משימת Task Scheduler “הפעל בהרשאות הגבוהות ביותר”
- אתחול אחרי קריסה או תקיעה עובר דרך הסכמת המשתמש; אתחול אחרי עדכון הוא אוטומטי
- כדי להתאושש מעבר לאתחול מערכת הפעלה, הצד שמבקש את האתחול (מתקין וכדומה) חייב לקרוא ל-API הכיבוי עם דגלי EWX_RESTARTAPPS / SHUTDOWN_RESTARTAPPS
אם גם רושמים RegisterApplicationRecoveryCallback, WER (Windows Error Reporting) קורא ל-callback בקריסה ונותן תקופת חסד לשמור נתונים בתהליך. אם השמירה לוקחת זמן, עם זאת, חייבים להמשיך לקרוא ל-ApplicationRecoveryInProgress בתוך מרווח ה-ping שצוין ברישום או שעבודת ההתאוששות נחתכת באמצע. כשהשמירה מסתיימת, מודיעים על השלמה עם 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 File.Replace של .NET קורא לזה כמו שהוא.
flowchart TB
accTitle: זרימת שמירה והתאוששות עם קובץ זמני ו-ReplaceFile
accDescr: בשמירה, כותבים לגמרי לקובץ זמני, מבצעים flush, ומחליפים עם ReplaceFile, ומשאירים את התוכן הישן ב-.bak. בהפעלה הבאה, מאמתים את הקובץ הראשי ונופלים ל-.bak אם הוא שבור
subgraph save["בשמירה"]
w["כתיבת קובץ זמני שלם"] --> f["Flush לדיסק"]
f --> r["ReplaceFile ← .bak"]
end
subgraph startup["בהפעלה הבאה"]
v["אימות הראשי"]
v -->|"שלם"| use["שימוש כמו שהוא"]
v -->|"שבור"| bak["נפילה ל-.bak"]
end
r -.->|"אובדן חשמל בכל שלב"| v
איור 12: בשמירה, כותבים לגמרי לקובץ זמני, מבצעים flush, ומחליפים עם ReplaceFile, ומשאירים את התוכן הישן ב-.bak. בהפעלה הבאה, מאמתים את הקובץ הראשי ונופלים ל-.bak אם הוא שבור.
// 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;
}
}
עם זה, פעולה רגילה תמיד משאירה אתכם מסוגלים לקרוא או “קובץ ישן שלם” או “קובץ חדש שלם”. ReplaceFile, עם זאת, הוא פעולת מרחב שמות רב-שלבית, ואטומיות מעבר לאובדן חשמל אינה מובטחת במפרט. לכן הדוגמה למעלה שומרת גיבוי (.bak) — צד הקריאה מאמת את הקובץ הראשי בהפעלה ונופל לגיבוי אם הוא שבור, כסט. אי אפשר להשתמש בזה ליומני append-only או CSV, ולכן אלה משתמשים בפורמט שבונה שבירה פנימה, כמו “שורה אחת = רשומה אחת, וזורקים שורה אחרונה שבורה בקריאה”.
8.2. הצלחה מ-WriteFile אינה הגעה לדיסק
ההנחה האחרת היא שגם אם WriteFile מחזיר הצלחה, הנתונים עלולים עדיין להיות רק במטמון של מערכת ההפעלה. Windows שם קריאות וכתיבות קבצים על מאגר המערכת ומשקף אותם לדיסק מעת לעת בכתיבה עצלה. כדי להביא נתונים לדיסק בוודאות, או מבצעים flush במפורש עם FlushFileBuffers, או מציינים FILE_FLAG_WRITE_THROUGH ב-CreateFile כך שכל כתיבה עוברת דרך המטמון. מטא-נתונים של מערכת הקבצים תמיד במטמון, ולכן אישור מטא-נתונים גם צריך flush או write-through.13
קריאה ל-FlushFileBuffers בכל פעם אינה יעילה, עם זאת, וגם התיעוד מעודד לשקול FILE_FLAG_NO_BUFFERING+WRITE_THROUGH במקום קריאות תכופות.13 בפועל, “flush רק בנקודת ביקורת עסקה או ממש לפני סגירת הקובץ” הוא פשרה מציאותית. המכניקה של השכבה הזאת — מנהל המטמון, כתיבה עצלה, והעובדה ש”עשיתי flush ועדיין אולי לא הגיע לדיסק” בגלל מטמון חומרה — מכוסה לעומק ב”מנהל המטמון: מתי WriteFile שלכם באמת מגיע לדיסק?”.
8.3. UPS ומעקב סוללה — הפיכת אובדן חשמל לכיבוי
הנגד-מידה האמיתית לאובדן חשמל במחשב התקן היא UPS. חושבים על תפקיד ה-UPS לא כ”עצירת הפסקת חשמל” אלא כהפיכת “אובדן חשמל בלי הודעה” ל”כיבוי מתוכנן עם הודעה”. התכנון הוא תצורה דו-שלבית.
- תכנון תקופת החסד: זמן אחיזת סוללת UPS > הסכום של “זיהוי המעבר לסוללה ← ניקוי יישום ושירות ← השלמת כיבוי מערכת ההפעלה”. אם עיבוד עצירת שירות איטי, המשוואה הזאת כבר לא מחזיקה (סעיף 6.2)
- זיהוי: המעבר מ-AC לסוללה, וירידה בקיבולת שנותרה, מקבלים הודעה באירוע PBT_APMPOWERSTATUSCHANGE. יישום עם חלון מקבל אותו כ-WM_POWERBROADCAST; שירות בלי חלון מכריז SERVICE_ACCEPT_POWEREVENT ומקבל אותו כ-SERVICE_CONTROL_POWEREVENT ב-HandlerEx (WM_POWERBROADCAST לא מגיע למטפל בקרת שירות). בקבלה, קוראים ל-GetSystemPowerStatus, בודקים ACLineStatus (האם על AC) ו-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 מקבל הודעה, מצב החשמל נבדק, ושמירה פלוס בקשת כיבוי הופכים אובדן חשמל בלי הודעה לכיבוי מתוכנן עם הודעה.
UPS טיפוסי מחובר ב-USB מופיע ל-Windows כסוללה, כך שאפשר לזהות אותו עם ה-API הסטנדרטי הזה. אם לתוכנת ניהול של הספק יש תכונת “כיבוי מערכת ההפעלה ב-N% שנותרו”, בודקים גם שהסף מתיישר עם זמן הניקוי של היישום. חזרה משינה או היברנציה, ובעיות ארוכות-טווח, הם ציר נפרד, מכוסה ב”שינה, היברנציה, Modern Standby ויישומים ארוכי-טווח”.
9. איך לאמת — לנסות כיבוי בבטחה
טיפול בכיבוי נוטה להפוך ל”כתבנו אבל אף פעם לא ניסינו בתנאים שקולים לייצור”. מחזיקים הליך לאימות בבטחה.
- מנסים במחשב בדיקה או במכונה וירטואלית: לא מנסים קודם במחשב התקן ייצור. בסביבת בדיקה עם נקודת ביקורת Hyper-V (צילום), חוזרים על כיבוי, אתחול ואובדן חשמל כפוי (כיבוי המכונה הווירטואלית). “כיבוי” של מכונה וירטואלית, עם זאת, רק משחזר “עצירת מערכת ההפעלה האורחת בלי הודעה”; הוא לא משחזר היעלמות מטמון נדיף של דיסק פיזי או שבירה שתלויה בבקר. אם שולחים אותו כמחשב התקן, הבדיקה הסופית היא מבחן ניתוק חשמל אמיתי על חומרה שקולה לייצור
- בדיקה מהירה עם התנתקות: הנתיב WM_QUERYENDSESSION ← WM_ENDSESSION רץ גם בהתנתקות (ההבדל היחיד הוא שסיבית ENDSESSION_LOGOFF מוגדרת ב-lParam), כך שאפשר בנוחות לאשר התנהגות קוד הניקוי במחשב פיתוח1
- מנסים כיבוי מלא והיברידי בנפרד: מנסים
shutdown /s /t 0(מלא),shutdown /s /hybrid /t 0(התנהגות ברירת מחדל), ו-shutdown /r /t 0(אתחול) כל אחד2 - מודדים כמה זמן הניקוי לוקח: כותבים חותמת זמן ליומן בהתחלת פונקציית הניקוי ובסיומה, ומודדים אם זה נכנס ל-5 שניות (או לתקופת החסד המוגדרת לשירות)
flowchart TB
accTitle: פעולות לאימות ומה כל אחת יכולה לאשר
accDescr: התנתקות היא בדיקה נוחה של נתיב ההודעה; כיבוי מלא, היברידי ואתחול של פקודת shutdown מאשרים את נתיב הייצור ואת תקופת החסד; כיבוי מכונה וירטואלית בודק עמידות לעצירה פתאומית; מבחן ניתוק חשמל פיזי הוא הבדיקה הסופית כולל אחסון פיזי
signtest["התנתקות"] -.-> path1["נתיב QUERY ← ENDSESSION"]
signtest --> shuttest["shutdown /s /hybrid /r"]
shuttest -.-> path2["נתיב ייצור + חסד"]
shuttest --> vmtest["כיבוי מכונה וירטואלית"]
vmtest -.-> path3["עצירת אורח פתאומית"]
vmtest --> hwtest["ניתוק חשמל פיזי"]
hwtest -.-> path4["כולל אחסון(סופי)"]
איור 14: התנתקות היא בדיקה נוחה של נתיב ההודעה; כיבוי מלא, היברידי ואתחול של פקודת shutdown מאשרים את נתיב הייצור ואת תקופת החסד; כיבוי מכונה וירטואלית בודק עמידות לעצירה פתאומית; מבחן ניתוק חשמל פיזי הוא הבדיקה הסופית כולל אחסון פיזי.
לבידוד בדיעבד, יומן האירועים (System) שימושי. בכיבוי או אתחול רגיל, מזהה אירוע 1074 (איזה תהליך התחיל כיבוי, עבור מי, ומאיזו סיבה) נרשם. באובדן חשמל פתאומי או קריסה אין 1074, ובאתחול הבא מזהי אירוע 41 (Kernel-Power) ו-6008 (The previous system shutdown was unexpected) נרשמים.14 “מה קרה בלילה” מתחיל כאן.
# 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
אם 1074 מראה “אתחול על ידי Windows Update” ונתוני היישום היו שבורים, הבעיה היא קוד הניקוי. 6008/41 מראים רק “כיבוי לא צפוי”; הם נרשמים גם למסך כחול (קריסה) או לאיפוס כפוי, לא רק לאובדן חשמל. אם BugcheckCode של 41 אינו אפס זו קריסה; אם הוא 0 וגם אין dump זיכרון, אובדן חשמל סביר — מבודדים את הסיבה מהמידע הסובב, וברגע שיודעים שזה היה אובדן חשמל, תכנון הכתיבה של פרק 8 ו-UPS הם הצעד הבא.
10. סיכום
- כיבוי הוא “אירוע רגיל שיגיע, במוקדם או במאוחר”. תקופת החסד אחרי ההודעה היא, כעיקרון, רק כ-5 שניות, ולכן התנאי המוקדם הוא שמירה אוטומטית תכופה כך ש”מה שעושים ביציאה” ממוזער.
- במערכות הפעלה ללקוח מ-Windows 8 ואילך, אם הפעלה מהירה מופעלת, “כיבוי” הוא כיבוי היברידי והליבה רק נכנסת להיברנציה. האיפוס המלא היחיד הוא “אתחול” — כותבים “אתחול” בנוהל התקרית.
- יישום GUI מחזיר TRUE מיד ל-WM_QUERYENDSESSION, ועושה ניקוי מחויב ב-WM_ENDSESSION. FormClosing ו-SessionEnding של WinForms/WPF מתאימים לשלב השאילתה, ולכן מה שעושים שם הוא שמירת תצלום אידמפוטנטית לכל היותר. לא מעלים דיאלוג בזמן כיבוי.
- פעולה שבאמת אי אפשר להפריע לה מוגנת בהצגת סיבה עם ShutdownBlockReasonCreate. אין, עם זאת, שום ערובה שאפשר לחסום.
- יישום קונסול מקבל את ההודעה עם SetConsoleCtrlHandler; שירות עם SERVICE_ACCEPT_PRESHUTDOWN/SHUTDOWN. תקופת החסד כברירת מחדל של PRESHUTDOWN היא 10 שניות במערכת הפעלה נוכחית. ב-.NET, מפסיקים להסתמך על ProcessExit ועוברים לנתיב הקנוני למודל היישום.
- התאוששות אחרי אתחול יכולה להיות בלי השגחה עם RegisterApplicationRestart (+ callback התאוששות) ו-ARSO.
- אובדן חשמל לא מביא הודעה. מתכוננים עם החלפת קובץ זמני + ReplaceFile (כסט עם גיבוי + אימות בזמן טעינה), flush בנקודות ביקורת, ו-UPS ש”הופך אובדן חשמל לכיבוי מתוכנן”.
flowchart TB
accTitle: התמונה הכוללת של טיפול בכיבוי
accDescr: לסיום עם הודעה, מגיבים עם ניקוי שיכול לסגור את החנות בכמה שניות ומובילים להתאוששות אוטומטית אחרי אתחול; לאובדן חשמל בלי הודעה, מתכוננים עם כתיבה שלא נשברת מתי שלא תיחתך ועם UPS, ומאמתים כולל על חומרה פיזית. שני העמודים האלה הם מסקנת המאמר
ending{"איך זה מסתיים"}
ending -->|"עם הודעה"| pillar1["ניקוי בכמה שניות(3–6)"]
ending -->|"בלי הודעה"| pillar2["כתיבה בטוחה + UPS(8)"]
pillar1 --> recover["התאוששות אוטומטית אחרי אתחול(7)"]
pillar2 --> verifytest["אימות על חומרה(9)"]
איור 15: לסיום עם הודעה, מגיבים עם ניקוי שיכול לסגור את החנות בכמה שניות ומובילים להתאוששות אוטומטית אחרי אתחול; לאובדן חשמל בלי הודעה, מתכוננים עם כתיבה שלא נשברת מתי שלא תיחתך ועם UPS, ומאמתים כולל על חומרה פיזית. שני העמודים האלה הם מסקנת המאמר.
- מאמתים בבטחה על מכונה וירטואלית ועם התנתקות, ומבודדים בדיעבד עם מזהי אירוע 1074/41/6008.
בפעם הבאה שמוסיפים תכונה ליישום, שואלים את עצמכם פעם אחת: אם WM_ENDSESSION מגיע באמצע העבודה הזאת, או שהחשמל נשלף, מה נשאר בהפעלה הבאה? כתיבת התשובה הזאת לתכנון היא הנתיב הקצר ביותר לכך שלעולם לא תבלו בוקר עומדים מול מחשב התקן עם הידיים על הראש.
מאמרים קשורים
- איך להחליף exe או DLL שבשימוש — Restart Manager ובעיית “הקובץ בשימוש” בעדכון אוטומטי
- איך לבנות ולהפעיל שירותי Windows — מבחירה בין Task Scheduler לשירותים ועד הפיכת BackgroundService לשירות Windows
- שינה, היברנציה, Modern Standby ויישומים ארוכי-טווח — תכנון סביב “זה נעצר בלילה”
- עומקי ה-I/O של Windows (חלק 4) — מנהל המטמון: מתי WriteFile שלכם באמת מגיע לדיסק?
- תכנון שמירת יומנים ו-dump בקריסת יישום Windows
- רשימת בדיקה לטיפול בטוח בתהליכי ילד ביישום Windows
תחומי ייעוץ קשורים
KomuraSoft LLC מטפלת בתכנון ומימוש של נגד-מידות כיבוי ואובדן חשמל למחשבי התקן וליישומים ארוכי-טווח, בחקירת שורש של השחתת נתונים ותקריות “זה כבר נעצר בבוקר” שמתחילות מאתחול Windows Update או מהתנתקות, ובסקירת תכנון של עיבוד עצירת שירותי Windows ושל התאוששות אוטומטית. בסדר להתחיל משלב של “משהו נשבר בכל פעם שאנחנו מכבים, ואין לי מושג מאיפה להתחיל”.
קישורים
-
Microsoft Learn, WM_QUERYENDSESSION message. על כך ש-WM_QUERYENDSESSION נשלח בסיום הפעלה והיישום צריך להחזיר TRUE ולכבד את כוונת המשתמש (ברירת המחדל של DefWindowProc היא גם TRUE); על כך שיש לדחות ניקוי עד WM_ENDSESSION; על כך שאחרי 5 שניות המערכת מציגה UI ליישומים שמונעים כיבוי והמשתמש יכול לסיים בכפייה; על משמעות סיביות ENDSESSION_LOGOFF/CLOSEAPP/CRITICAL ב-lParam; על כך שאי אפשר להבחין בין כיבוי לאתחול; ועל כך שיש לשמור נתונים לעיתים קרובות כך שפחות חייב להישמר ביציאה. ↩ ↩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. אירועי CTRL_C/BREAK/CLOSE/LOGOFF/SHUTDOWN שמקבל מטפל שנרשם עם SetConsoleCtrlHandler; על כך שפקיעת הזמן כברירת מחדל ל-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 זמן הריצה כבר לא מספק מטפל ברירת מחדל ל-CTRL_SHUTDOWN_EVENT/CTRL_CLOSE_EVENT של Windows (מקבילות SIGTERM/SIGHUP ב-Unix); על כך שהטיפול כברירת מחדל של מערכת ההפעלה מסיים את היישום מיד ו-AppDomain.ProcessExit ו-AssemblyLoadContext.Unloading כבר לא נורים; ועל כך שיש לרשום טיפול באות שמתאים למודל היישום בספרייה ברמה גבוהה יותר או בקוד היישום. ↩ ↩2
-
Microsoft Learn, Service Control Handler Function. על כך ששירות שהכריז SERVICE_ACCEPT_PRESHUTDOWN מקבל קודם SERVICE_CONTROL_PRESHUTDOWN, ואז שירות SERVICE_ACCEPT_SHUTDOWN מקבל SERVICE_CONTROL_SHUTDOWN; על כך שתקופת החסד כברירת מחדל בכיבוי היא כ-20 שניות והתקרה באתחול מערכת הפעלה היא WaitToKillServiceTimeout; על כך שאין להאריך ערך זה; על כך שמטפל הבקרה צריך לחזור בתוך 30 שניות, לדווח STOP_PENDING ורמז המתנה, ולהשאיר עבודה ארוכה לתהליכון אחר; על כך שיש לסיים ניקוי מהר ככל האפשר עם פעולת UPS בראש; ועל כך שה-SCM בכיבוי לא, כברירת מחדל, מתחשב בתלויות. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, SERVICE_PRESHUTDOWN_INFO structure (winsvc.h). על כך שאחרי הודעת PRESHUTDOWN ה-SCM מחכה עד שהשירות נעצר או פקיעת הזמן; על כך שפקיעת הזמן כברירת מחדל היא 10 שניות מ-Windows 10 Creators Update (build 15063) ואילך ו-3 דקות לפני כן; על כך שמגדירים אותה עם ChangeServiceConfig2; ועל כך שאפשר להמשיך לעדכן מצב במהלך SERVICE_STOP_PENDING. ↩ ↩2
-
Microsoft Learn, RegisterApplicationRestart function (winbase.h). על כך שאפשר לרשום אתחול לקריסה, אי-תגובה, עדכון ואתחול מחשב הנלווה לעדכון; על כך שאפשר לציין ארגומנטי שורת פקודה לאתחול; על כך שהרישום חייב להיעשות לפני שהבעיה קורה ובמהלך טיפול ב-WM_QUERYENDSESSION זו ההזדמנות האחרונה בתרחיש עדכון; על כך שתהליך מתחת ל-60 שניות ריצה לא מאותחל; על כך שאתחול אחרי קריסה או תקיעה עובר דרך הסכמת המשתמש; ועל כך שמעבר לאתחול מערכת הפעלה דורש כיבוי עם 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 יכול לבצע flush במפורש; ועל כך שמטא-נתונים של מערכת הקבצים תמיד במטמון, ולכן אישור מטא-נתונים דורש flush או write-through. ↩ ↩2 ↩3
-
Microsoft Learn, Troubleshoot unexpected reboots using system event logs. על כך שאתחול רגיל רושם מזהה אירוע 1074 (איזה תהליך התחיל כיבוי, עבור מי, ומאיזו סיבה); על כך שאתחול לא צפוי רושם מזהה אירוע 41 (Kernel-Power) ו-6008 (הכיבוי הקודם היה לא צפוי); ועל כך שהמזהים האלה יכולים לבודד את סוג האתחול. ↩ ↩2
-
Microsoft Learn, PBT_APMPOWERSTATUSCHANGE event. על כך שהאירוע הזה מקבל הודעה דרך WM_POWERBROADCAST במעבר בין סוללה ל-AC או בירידה בקיבולת שנותרה; ועל כך שבקבלה יש לקרוא ל-GetSystemPowerStatus ולבדוק שדות SYSTEM_POWER_STATUS כמו ACLineStatus, BatteryFlag ו-BatteryLifePercent. ↩
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
Win32 Thread Pool API — מקביליות בלי ליצור תהליכונים, דרך CreateThreadpoolWork
מפזרים קריאות CreateThread בכל הקוד הנייטיב? המאמר מסביר את ה-API של מאגר התהליכונים של Win32 שעוצב מחדש ב-Vista — ארבעת האובייקטים work,...
Named Pipes בפועל — ה-IPC הסטנדרטי של Windows, מתכנון ועד אבטחה
מדריך מעשי ל-Named Pipes, התקשורת הסטנדרטית בין תהליכים ב-Windows. המאמר מסדר ממקורות ראשוניים את הבחירה בין מצב בתים למצב הודעות, תכנון ...
יישומים שנשברים בחזרה משינה — איך אירועי צריכת החשמל של Windows עובדים ואיך לבנות יישומים עסקיים ששורדים אותם
פתחתם את המחשב הנייד והחיבורים של היישום העסקי היו מתים — הסיבה היא תכנון שלא חשב על שינה. המאמר מכסה את זרימת ההודעות WM_POWERBROADCAST,...
DllMain ונעילת הטוען — הסיבה האמיתית שאומרים לכם "לא לעשות כלום באתחול DLL"
למה אסור לקרוא ל-LoadLibrary או להסתנכרן עם תהליכונים אחרים מתוך DllMain. המאמר מסביר ממקורות ראשוניים איך נעילת הטוען מסדרת כל הודעת DLL...
מה "לא מגיב" באמת — איך Windows מחליט שיישום נתקע, ואיך לתכנן יישומים שלא
"לא מגיב" של Windows הוא מנגנון שבו מערכת ההפעלה קובעת שחלון לא שלף הודעה במשך 5 שניות ומחליפה אותו בחלון רפאים. המאמר מכסה את פנים השיפו...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
יישומים עסקיים, חיבור התקנים וכלי תקשורת, מהגדרת הדרישות ועד הפיתוח.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- בעיה שלא נעלמה אחרי "כיבוי" נעלמה אחרי "אתחול". למה?
- במערכות הפעלה ללקוח מ-Windows 8 ואילך, כשהפעלה מהירה מופעלת (ברירת המחדל ברוב המחשבים שתומכים בהיברנציה), "כיבוי" משתמש במנגנון שנקרא כיבוי היברידי. המשתמש מנותק, אבל מצב הליבה והמנהלים נשמר לקובץ ההיברנציה ומוחזר כמו שהוא באתחול הבא. במילים אחרות, ליבת מערכת ההפעלה לא אופסה. "אתחול", לעומת זאת, תמיד מבצע אתחול מלא, ולכן תקלות מנהלים ושירותים מאופסות. כותבים "אתחול" בהליך הבידוד, לא "כיבוי והדלקה מחדש". אם רוצים כיבוי מלא משורת הפקודה, אפשר להשתמש ב-shutdown /s.
- אפשר לעצור כיבוי עד שהיישום מסיים לשמור?
- אפשר לבקש ממנו לחכות זמנית, אבל אי אפשר לעצור אותו באמינות. אם רושמים מחרוזת סיבה עם ShutdownBlockReasonCreate רק בזמן שפעולה שאי אפשר להפריע לה בתהליך, הסיבה הזאת מופיעה במסך "היישום הזה מונע כיבוי" והמשתמש יכול להחליט אם להמשיך או לבטל. המשתמש עדיין יכול לבחור המשך כפוי, וכיבוי כפוי או אתחול מונע-עדכון עלולים לא לחכות בכלל. הגישה הנכונה לכן אינה "חסימה", אלא שמירה אוטומטית תכופה כך שפחות נתונים בסיכון, פלוס ניקוי שמתוכנן להסתיים בכמה שניות מהודעת היציאה.
- עצירת שירות Windows שלי לוקחת הרבה זמן. אפשר להאריך את תקופת החסד של הכיבוי?
- בתצורה כברירת מחדל שמקבלת SERVICE_CONTROL_SHUTDOWN, תקופת החסד היא בערך 20 שניות ותלויה בערך הרישום WaitToKillServiceTimeout. שכתוב הערך הזה מצד היישום כדי להאריך אותו אינו מומלץ. אם צריך תקופת חסד ארוכה יותר, אפשר להכריז SERVICE_ACCEPT_PRESHUTDOWN ולקבל SERVICE_CONTROL_PRESHUTDOWN; מקבלים הודעה מוקדם מאחרים, ואת פקיעת הזמן אפשר להגדיר עם ChangeServiceConfig2 (ברירת המחדל 10 שניות מ-Windows 10 Creators Update ואילך, ו-3 דקות לפני כן). PRESHUTDOWN, עם זאת, מעכב את כל הכיבוי באותו מרווח, לכן מגבילים אותו למקרים שבאמת צריך, וביסוד מתכננים את עבודת העצירה עצמה להסתיים בכמה שניות.
- בטוח לעשות ניקוי כיבוי ב-AppDomain.ProcessExit של .NET?
- אני ממליץ לא להסתמך על זה. היסטורית זמן הריצה רשם מטפל אות ברירת מחדל, ו-ProcessExit נורה ב-CTRL_CLOSE_EVENT וב-CTRL_SHUTDOWN_EVENT, אבל מ-.NET 10 זמן הריצה כבר לא מספק מטפלי אות סיום כברירת מחדל, ו-ProcessExit כבר לא נורה במקרים האלה. מממשים ניקוי בנתיב ההודעה שמתאים למודל היישום: יישומי GUI משתמשים ב-FormClosing או SessionEnding (אלה הודעות שלב שאילתה, לכן מגבילים אותן לשמירות אידמפוטנטיות; ניקוי שיכול לרוץ רק אחרי שההפעלה מחויבת שייך לוו-hook של WM_ENDSESSION); Generic Host / Worker Service משתמשים ב-IHostApplicationLifetime וב-StopAsync; יישומי קונסול משתמשים ב-SetConsoleCtrlHandler או PosixSignalRegistration.
- איך מונעים השחתת קבצים מאובדן חשמל פתאומי?
- אובדן חשמל לא מביא שום הודעה, לכן האפשרות היחידה היא לכתוב באופן שלא נשבר מתי שלא תהיה החיתוך. הבסיס הוא לא לדרוס את הקובץ המקורי במקום: כותבים לגמרי לקובץ זמני על אותו כרך, מבצעים flush, ומחליפים עם ReplaceFile (File.Replace ב-.NET). בפעולה רגילה זה משאיר אתכם מסוגלים לקרוא קובץ ישן שלם או קובץ חדש שלם, אבל האטומיות של ReplaceFile מעבר לאובדן חשמל אינה מובטחת במפרט, לכן שומרים גיבוי (הארגומנט השלישי) ומממשים התאוששות בזמן טעינה שמוודאת את הקובץ הראשי ונופלת לגיבוי אם הוא שבור. גם, הצלחה מ-WriteFile לא אומרת שהנתונים הגיעו לדיסק, לכן בנקודות ביקורת חשובות מאשרים את הכתיבה עם FlushFileBuffers או FILE_FLAG_WRITE_THROUGH. במחשבי התקן התצורה הסטנדרטית היא לשלב את זה עם UPS, לזהות את המעבר לסוללה, ולהוביל לכיבוי בטוח.