היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 29 Aug 2026)
- פרסום ראשון
סיימתם את ה-UI ב-Task Manager, ובכל זאת אי אפשר לפתוח מחדש את המצלמה. אפליקציית הניטור נעלמה, ובכל זאת ה-helper של ה-SDK עדיין מחזיק את ה-COM port. הפעלה מחדש של ה-parent מייצרת instance שני, ובעיות מופיעות בזיכרון המשותף וב-named pipes. ברגע שמבודדים SDK של מכשיר ב-process נפרד, נתקלים בכשלי “אחרי שה-parent מת” האלה.
נקודת ההתחלה לחשיבה על הסיבה היא שב-Windows, child ו-grandchild processes לא מסתיימים אוטומטית כש-parent process יוצא. יחס parent-child בהפעלה וניהול משך החיים ביציאה הם שני דברים שונים. הכלי שסוגר את הפער הזה הוא ה-Job Object.
המאמר הזה קודם מאשר למה טיפול היציאה של ה-parent לבדו אינו מספיק, ואז מסדר איך מכניסים processes ל-Job, מדיניות הסיום, ושיטת הניטור. לבסוף הוא מחבר אותם לכשלים שקורים באינטגרציית מכשירים ולהליך החקירה.
קהל היעד הוא מפתחי WinForms / WPF / שירותים שמבודדים SDK של מכשיר ב-process נפרד. סביבת הבסיס היא Windows 10/11 (לחלקים שמשתמשים ב-nested jobs וב-PROC_THREAD_ATTRIBUTE_JOB_LIST), והקוד מוצג ב-C++ (Win32 API) וב-C# (.NET 6 ואילך). רמת הקושי היא בינונית.
המאמר הזה מרחיב את מאמרי “Not Responding”, כיבוי, שינה/חזרה ו-named pipes, ומטפל ב-משך החיים מחוץ ל-process.
1. קודם המסקנות
נקודת ההתחלה של התכנון היא להחליט “מה אסור שישאר מאחור, ומה רוצים לשמור” לפני שמחליטים “איך מסיימים”.
Job Object הוא מנגנון שהופך process tree ליחידה אחת. עם זאת, סיום בכוח של הצאצאים ברגע שה-parent נעלם ושמירת dumps או מצב סופי של אותם צאצאים לא, כמו שהם, הולכים יחד. מחליטים קודם אם אוספים את המכשיר שה-process מחזיק או שומרים חומר אבחון.
- היחידה שמנהלת משך חיים היא ה-Job, לא ייחוס parent-child. הוא מצמיד מגבלות, התראות וסיום גורף לקבוצת processes. ברגע ש-process משויך הוא לא יכול לעזוב עד שהוא יוצא, ומ-Windows 8 ואילך אפשר לקנן Jobs.12
- סוגרים את חברות ה-Job לפני שה-child רץ. אם עושים Assign אחרי ההפעלה, מפספסים את הנכדים שנולדים באמצע.
CREATE_SUSPENDEDו-JOB_LISTביצירה סוגרים חלונות race שונים (פרק 4). - איסוף אוטומטי ושמירת מידע אבחון נבחרים כמדיניות סיום. תנאי ההפעלה של KillOnJobClose הוא “handle ה-Job האחרון נסגר”. זה חזק מול קריסת parent אבל חלש לניתוח post-mortem של הצאצאים שמסתיימים בכוח יחד איתו, כך שאם צריך את שניהם, צד הניטור אוסף קודם ומסיים אחר כך (פרק 5).3
מה שאפליקציית מדידה רוצה אינו ההריגה עצמה. זה לא להשאיר process שמחזיק את המכשיר, ולהיות מסוגלים לצפות ביציאות חריגות.
| מה רוצים לדעת | פרקים לקרוא |
|---|---|
| למה טיפול היציאה של ה-parent לבדו אינו מספיק | פרקים 2–3: יחס parent-child ותפקיד ה-Job |
| איך מנהלים צאצאים בלי לפספס | פרקים 4–6: יצירה, מדיניות סיום, ניטור |
| מה הופך לבעיה עם SDKs ושירותים | פרקים 7–9: מקרי כשל, מגבלות משאבים, קינון ו-breakaway |
| מה לחקור בשטח, ואיך לבחור | פרקים 10–11: הליך חקירה וטבלת החלטה |
מפת הידע למטה היא לסקירת איך האלמנטים קשורים זה לזה. אם מעדיפים להתחיל מהמנגנון, ממשיכים לפרק 2.
ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 21, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle
2. למה WaitForExit אינו מספיק
“המתנה” ליציאה וקשירת משכי חיים הם דברים שונים
Process.WaitForExit() הוא API ל”ה-parent ממתין שה-child ייצא”. מה שהמאמר הזה שוקל הוא הכיוון ההפוך: מה לעשות עם ה-child כשה-parent מת קודם. עם יחס parent-child של Windows לבדו, יציאת ה-parent לא מועברת ל-child.
גם Process.Kill() ו-CloseMainWindow() לבדם לא מטפלים ב-grandchild processes או ב-device handles שאותם נכדים מחזיקים. ה-handles של ה-process שיצא משוחררים, אבל הבעיה היא ה-handles שמוחזקים על ידי הצאצאים ששרדו.
Kill(entireProcessTree: true) של .NET עובר על הצאצאים ומסיים אותם, אבל הוא יכול לפספס processes שנוצרו במהלך המנייה או אחרי שה-parent מת קודם, והוא אף פעם לא נקרא ברגע שה-parent עצמו קרס. צריך מנגנון שלא תלוי רק בקוד ה-cleanup של ה-parent.
לכל אחת מהסיבות העיקריות ש-parent יוצא, הנה מה שנשאר בשטח.
| למה ה-parent מת | מה קורה ל-child | מה נשאר בשטח |
|---|---|---|
| UI נסגר בלחצן X, טיפול יציאה לא הושלם | כלום (אובייקט ה-Process רק עובר dispose) |
helper process, נעילת מצלמה |
| רק ה-parent נהרג ב-Task Manager | ה-child ממשיך לחיות | COM port, USB, זיכרון משותף |
| קריסה מ-unhandled exception | אין הבטחה ש-finally של ה-parent רץ |
קבצים זמניים, נעילות בלעדיות |
| timeout של עצירת שירות | ה-SCM מטפל ב-parent בלבד | ילדים שנשארים ב-session 0 |
flowchart TB
accTitle: הפער בין משך החיים של ה-parent לבין משך התפיסה של המכשיר
accDescr: כש-parent process יוצא, ההמתנה של ה-parent ואובייקט ה-Process שלו נעלמים, אבל ה-child ו-grandchild processes ממשיכים לחיות, וגם האחיזה שלהם ב-device handles, named pipes וקבצי נעילה נשארת
parent["parent process יוצא"] --> gone["נעלם: המתנת ה-parent, אובייקט Process"]
parent --> live["נשאר: child ו-grandchild processes"]
live --> dev["אחיזה ב-device handles"]
live --> pipe["צד ה-server של named pipes"]
live --> lock["קבצי נעילה, זיכרון משותף"]
איור 1: משך החיים של ה-parent ומשך התפיסה של המכשיר לא מסונכרנים. קוד ה-cleanup של ה-parent הכי פחות סביר שירוץ בדיוק כשה-parent מת מוות חריג.
היעד הוא המקרה שבו “מערכת ההפעלה רצה ורק ה-parent יצא”
הזרימות שבהן כיבוי או שינה עוצרים את כל מערכת ההפעלה שייכות ל-מאמר הכיבוי ול-מאמר שינה/חזרה. המאמר הזה מטפל ב-המקרה שבו מערכת ההפעלה נשארת בריאה ורק ה-parent יוצא. בסביבות מדידה זה המקרה התכוף יותר, ואת ה-processes שנשארים קשה יותר להבחין בהם.
3. מה זה Job Object
הפעולות הבסיסיות הן Create, Assign, Set ו-Query
Job Object הוא kernel object שמנהל קבוצת processes כיחידה אחת. חלוקת הפעולות הבסיסיות לפי תפקיד נותנת את ארבע אלה.14
| API | תפקיד |
|---|---|
CreateJobObject |
ליצור Job שעדיין אף process לא שייך אליו |
AssignProcessToJobObject |
לשייך process ל-Job |
SetInformationJobObject |
להגדיר מגבלות והגדרות אחרות |
QueryInformationJobObject |
לקרוא נתוני accounting כמו זמן CPU, page faults ומספר processes |
החברות בלתי הפיכה; process לא יכול לעזוב עד שהוא יוצא. גם, נתוני ה-accounting כוללים סכומים שהצטברו מ-processes שכבר יצאו.
child ש-process חבר יוצר עם CreateProcess שייך לאותו Job כברירת מחדל. כלומר, ליבת הערך של Job היא שנכדים ונינים נכנסים אוטומטית.1 הנתיבים שבורחים מחברות, כמו breakaway והפעלות proxy דרך WMI, נבדקים בנפרד בפרק 9.
flowchart TB
accTitle: מבנה בסיסי של Job Object
accDescr: child ו-grandchild שייכים ל-Job Object שה-parent process יצר, וה-Job אוכף מגבלות, שולח התראות ל-completion port, ומבצע סיום גורף לפי process tree
parent["parent process"] --> job["Job Object"]
job --> child["child (מארח SDK של מכשיר)"]
child --> gc1["grandchild (helper של הספק)"]
job -.-> lim["מגבלות (זיכרון, CPU)"]
job -.-> note["התראות (completion port)"]
job -.-> kill["סיום גורף"]
איור 2: Job הוא מיכל שמספק שלושה דברים לפי process tree: מגבלות, התראות וסיום גורף.
הבדלי דור של מערכת ההפעלה, ואיך זה שונה מ-sandbox
ב-Windows 7 ומוקדם יותר process יכול היה להשתייך ל-job אחד בלבד; מ-Windows 8 ואילך קינון (חברות מרובה) הפך לאפשרי.5 גוף המאמר מניח Windows 10/11; הנקודות שצריך לשים לב אליהן ב-Windows 7 ומוקדם יותר מכוסות בפרק 9 וב-FAQ.
הכנסת process ל-Job לא הופכת אותו ל-container או ל-sandbox. אי אפשר להגביל גישת רשת, וה-access token (הרשאות) הוא מנגנון נפרד. גם הגבלות UI לבדן לא יכולות ליצור גבול אבטחה. התפקיד במאמר הזה הוא אך ורק להפוך את משך החיים ואת המשאבים של process tree ליחידה אחת.
4. הדרך הנכונה פנימה — ה-race בין יצירה ל-assignment
אם עושים Assign אחרי ההפעלה, נכדים נולדים באמצע
child process יכול להוליד נכדים באלפיות השנייה הראשונות אחרי שהוא מתחיל לרוץ. SDK שמפעיל את ה-helper שלו הוא המקרה הטיפוסי. אם מקבלים את ה-PID אחרי Process.Start() ואז עושים Assign, כל grandchild שנולד לפני ה-Assign נגמר מחוץ ל-Job.
flowchart TB
accTitle: assignment אחרי שה-child מתחיל לרוץ מפספס נכדים
accDescr: ה-child כבר רץ מיד אחרי Process.Start, וכל grandchild שהוא מוליד בחלון ה-race לפני שקוראים ל-AssignProcessToJobObject נגמר מחוץ ל-Job
s["ה-child מתחיל לרוץ ב-Process.Start"] --> w["חלון race עד Assign"]
w --> g["נכדים שנולדים בחלון הזה"]
g --> out["ממשיכים לרוץ מחוץ ל-Job"]
s --> a2["AssignProcessToJobObject"]
a2 --> in2["רק נכדים שנולדים אחר כך נכנסים"]
איור 3: חלון ה-race עשוי להיות רק כמה אלפיות שנייה, אבל הפעלת ה-helper של ה-SDK קורה בדיוק שם.
הליך A: יוצרים מושהים, משייכים, ואז מריצים
השיטה הקלאסית עם התאימות הרחבה ביותר משתמשת ב-CREATE_SUSPENDED. היא סוגרת את החלון שבו ה-child רץ קודם ומוליד נכדים בסדר הבא.67
- יוצרים את ה-Job עם
CreateJobObject - מגדירים את המגבלות קודם עם
SetInformationJobObject - קוראים ל-
CreateProcessעםCREATE_SUSPENDED(ה-thread הראשוני לא רץ) - מכניסים עם
AssignProcessToJobObject - אם זה נכשל, לא עושים Resume; קוראים ל-
TerminateProcessבמקום (לא נותנים להוראה אחת לרוץ מחוץ ל-Job) - מריצים עם
ResumeThread
מה שהליך A סוגר הוא חלון ה-race מול יצירת נכדים. החלון מול קריסה של ה-parent עצמו נשאר. אם ה-parent קורס בין שלבים 3 ו-4, child מושהה שעדיין לא ב-Job נשאר מאחור. הוא לא רץ, אבל הוא גם לא נעלם לבד.
אם רוצים לסגור את החלון הזה כולל עמידות לקריסת parent, משתמשים בהליך B למטה.
flowchart TB
accTitle: הליך להפעלה עם SUSPENDED ואז הכנסת ה-child ל-Job
accDescr: יוצרים את ה-Job ומגדירים מגבלות, מפעילים את ה-child עם CREATE_SUSPENDED, משייכים עם AssignProcessToJobObject, עוצרים עם TerminateProcess בלי Resume אם זה נכשל, ומריצים עם ResumeThread אם זה מצליח
a["יוצרים את ה-Job עם CreateJobObject"] --> b["מגבלות עם SetInformationJobObject"]
b --> c["מפעילים child עם CREATE_SUSPENDED"]
c --> d["AssignProcessToJobObject"]
d -->|"הצלחה"| e["מריצים עם ResumeThread"]
d -->|"כישלון"| f["מסיימים מיד, בלי Resume"]
איור 4: השלד של הליך A. מימוש שמשמיט את הענף “אם נכשל, לא מריצים” מייצר process תועה רק כשדברים משתבשים.
// C++: הליבה המינימלית של הליך A (טיפול בשגיאות הוא שלד בלבד)
HANDLE job = CreateJobObjectW(nullptr, nullptr); // unnamed בסדר. לא עושים inheritable
if (!job) return HRESULT_FROM_WIN32(GetLastError());
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = {};
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation,
&limits, sizeof(limits))) {
DWORD err = GetLastError(); // שומרים לפני ש-CloseHandle דורס
CloseHandle(job); // לא מריצים את ה-child ב-Job בלי מגבלות
return HRESULT_FROM_WIN32(err);
}
STARTUPINFOW si = { sizeof(si) };
PROCESS_INFORMATION pi = {};
if (!CreateProcessW(exePath, cmdline, nullptr, nullptr, FALSE,
CREATE_SUSPENDED, nullptr, nullptr, &si, &pi)) {
DWORD err = GetLastError();
CloseHandle(job); // לא מדליפים את handle ה-Job בניסיונות הפעלה חוזרים
return HRESULT_FROM_WIN32(err);
}
if (!AssignProcessToJobObject(job, pi.hProcess)) {
DWORD err = GetLastError(); // שומרים לפני ש-Terminate דורס
TerminateProcess(pi.hProcess, 1); // לא נותנים לו לרוץ מחוץ ל-Job
// סוגרים את ה-handles ומעלים את err כשגיאה
} else if (ResumeThread(pi.hThread) == (DWORD)-1) {
DWORD err = GetLastError();
TerminateProcess(pi.hProcess, 1); // לא משאירים אותו מושהה
// סוגרים את ה-handles ומעלים את err כשגיאה
}
CloseHandle(pi.hThread);
// בהצלחה, הבעלות על job ו-pi.hProcess עוברת לאובייקט ניהול משך החיים של הקורא
// (המקבילה של ה-wrapper ב-C# בפרק 4). סגירת job יורה KillOnJobClose, ו-pi.hProcess
// משמש בפרק 6 כדי לקבוע "מי מת"
הליך B: ב-Windows 10 ואילך, יוצרים את ה-process כבר בתוך ה-Job
שמים את handle ה-Job על רשימת המאפיינים של STARTUPINFOEX עם PROC_THREAD_ATTRIBUTE_JOB_LIST. ואז מעבירים אותו ל-CreateProcess עם הדגל EXTENDED_STARTUPINFO_PRESENT. בלי הדגל, המבנה לא מפורש כמורחב, והמאפיינים מתעלמים.89
בשיטה הזו, ה-process שייך ל-Job לפני שה-thread הראשוני שלו רץ. חלון ה-race של “נוצר, אבל עדיין לא חבר” כבר לא קיים בכלל, כך שלא נחוצים לא SUSPENDED ולא הענף שמשייך אחרי יצירה ומטפל בכישלון.
| שיטה | מתי החברות נסגרת | הסתייגויות שנשארות |
|---|---|---|
| הליך A: SUSPENDED → Assign → Resume | אחרי שה-child נוצר, לפני שה-thread הראשוני שלו רץ | אם ה-parent קורס לפני ה-Assign, child עצור נשאר מאחור |
| הליך B: יצירה עם JOB_LIST | ביצירת ה-process | דורש Windows 10 ואילך. מציינים את רשימת המאפיינים ואת דגל ה-startup המורחב |
flowchart TB
accTitle: איך חלונות ה-race של הליך A והליך B נבדלים
accDescr: הליך A יוצר את ה-process מושהה ואז עושה Assign ו-Resume, כך שהוא צריך ענף שמסיים בכישלון, ואילו הליך B יוצר את ה-process עם ה-Job על רשימת המאפיינים, כך שהוא כבר חבר ברגע שהוא נולד ואין לו לא חלון race ולא ענף כישלון
a1["הליך A: יוצרים מושהים"] --> a2["מצטרפים עם Assign"]
a2 --> a3["מתחילים עם Resume"]
a2 -.-> a4["נחוץ ענף כישלון"]
b1["הליך B: יוצרים עם רשימת מאפיינים"] --> b2["חבר ברגע שהוא נולד"]
b2 -.-> b3["אין חלון race, אין ענף כישלון"]
איור 5: הליך A הוא “מכניסים, ואז מריצים”; הליך B הוא “נולד כבר בפנים”. אם אפשר להניח Windows 10 ואילך, הסיבה לבחור בו היא בדיוק נוכחות או היעדר חלון ה-race וענף הכישלון.
שלושה מימושים להימנע מהם בשני ההליכים
- קבלת ה-PID אחרי
Process.Start()ואז הכנסה (הנכדים יוצאים קודם) - מתן הורשה של handle ה-Job ל-child (ה-child ממשיך להחזיק את ה-handle גם אחרי שה-parent מת, כך ש-KillOnJobClose כבר לא נורה — פרק 5)
- בליעת כישלון Assign והמשך תפעול (process מכשיר מחוץ ל-Job הוא מה שמניע את הכשל הבא)
ב-.NET, מבטאים את בעל handle ה-Job בקוד
ל-System.Diagnostics.Process אין מושג של Job, ואין wrapper רשמי. כותבים wrapper דק עם P/Invoke או CsWin32.
הנקודה היא לעטוף את handle ה-Job ב-SafeHandle ולהפוך אותו ל-IDisposable. סגירת handle ה-Job האחרון ב-Dispose() יורה KillOnJobClose. כוונת התכנון ש”משך החיים של ה-wrapper הוא משך החיים של עץ ה-child” יכולה להיות מבוטאת כבעלות.
למטה שלד שמראה בעלות על handle. היצירה נעשית בצד ה-P/Invoke של הליך A/B, ופרויקט אמיתי גם צריך תצורת CsWin32, סימוני unsafe, וכן הלאה.
// C#: wrapper דק שאחראי רק לבעלות על handle ה-Job (היצירה עוברת ב-P/Invoke של הליך A/B)
sealed class ChildProcessJob : IDisposable
{
private readonly SafeFileHandle _job; // ממשיכים להחזיק בשדה
public ChildProcessJob()
{
_job = PInvoke.CreateJobObject(default, null);
if (_job.IsInvalid) throw new Win32Exception();
var limits = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
limits.BasicLimitInformation.LimitFlags =
JOB_OBJECT_LIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!PInvoke.SetInformationJobObject(_job,
JOBOBJECTINFOCLASS.JobObjectExtendedLimitInformation,
&limits, (uint)sizeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)))
{
int err = Marshal.GetLastWin32Error(); // שומרים לפני Dispose
_job.Dispose(); // לא מוציאים Job בלי KillOnJobClose
throw new Win32Exception(err);
}
}
public void Dispose() => _job.Dispose(); // העץ שמתחת מסתיים כאן
}
ולהפך, אם סוגרים את ה-handle בלי להתכוון לסגור אותו, מסיימים את עץ ה-child. שומרים הפניה ל-wrapper לכל משך החיים של ה-parent. אם לא, עץ ה-child נמחק בלי סיבה ברגע שה-GC אוסף את ה-SafeHandle.
5. KillOnJobClose — “מחזיקים” ו”מתים יחד”
ההדק הוא “סגירת ה-handle האחרון”, לא “מות ה-parent”
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE הוא דגל מגבלה שמסיים כל process תחת ה-Job כש-handle ה-Job האחרון נסגר.3
בין אם ה-parent יוצא על exception, נהרג ב-Task Manager, או נעצר בכוח כשירות, הליבה סוגרת את כל ה-handles של אותו process. אם זה היה handle ה-Job האחרון, הילדים והנכדים מסתיימים. החוזק של המנגנון הזה הוא שהוא לא מניח שטיפול היציאה של ה-parent רץ.10
עם זאת, אם נותנים ל-child לרשת את handle ה-Job, ה-handle נשאר אחרי שה-parent יוצא. במקרה כזה זה לא “ה-handle האחרון”, ועץ ה-child לא מסתיים.
flowchart TB
accTitle: ציר הזמן של KillOnJobClose
accDescr: בין אם ה-parent יוצא כרגיל, קורס, או מסתיים בכוח, הליבה סוגרת את כל ה-handles של ה-parent, ואם זה היה handle ה-Job האחרון ה-Job נסגר ועץ ה-processes שמתחת מסתיים בבת אחת
die["ה-parent נעלם (כולל קריסה)"] --> close["הליבה סוגרת את כל ה-handles"]
close --> last{"handle ה-Job האחרון?"}
last -->|"כן"| killall["מסיימים את כל העץ שמתחת"]
last -->|"לא (מורש)"| stay["הילדים ממשיכים לחיות"]
איור 6: ההדק הוא “ה-handle האחרון נסגר”, לא “ה-parent מת”. לכן אסור שה-handle יורש על ידי ה-child.
בוחרים בין איסוף מיידי לבין שמירת חומר אבחון
מה שמאבדים בסיום בכוח אינו רק האחיזה במכשיר. מאבדים גם את ההזדמנות לקחת crash dump של הילדים והנכדים שמסתיימים יחד עם ה-parent, את הפריים התקף האחרון, ואת ההזדמנות לשטוף קובץ מדידה שנכתב באמצע.
ה-crash dump של ה-parent עצמו הוא עניין אחר. WER מטפל ב-unhandled exception כל עוד ה-parent עדיין חי, כך שאפשר לכתוב את ה-dump של ה-parent לפני שה-handles נסגרים. מה שעומד כאן הוא חומר ה-post-mortem של הילדים והנכדים בצד שמסתיים בכוח.
| מדיניות | מתאים ל | מה מאבדים |
|---|---|---|
| עם KillOnJobClose | אתרים שבהם פתיחה כפולה של המכשיר היא התוצאה הגרועה ביותר | חומר post-mortem, הדגימות האחרונות |
| בלי (ניטור בלבד) | אתרים שבהם dumps ולוגים הם נכס | processes יתומים ופורטים מוחזקים אם מזניחים |
בלי, ועוד process ניטור שקורא ל-TerminateJobObject |
כשיש שירות בקרה נפרד | המימוש משוכפל |
כקלט להחלטה, מציגים קודם “מה אסור שישאר מאחור” ו”מה רוצים לשמור”.
מה אסור שישאר מאחור: מצלמה או digitizer פתוחים, שימוש בלעדי בפורט serial או USB, צד ה-server של named pipe, סשן dongle רישיון, זיכרון משותף וקבצי נעילה.
מה רוצים לשמור: crash dumps, הפריים או המונים התקפים האחרונים, ההזדמנות לשלוח פקודה שמחזירה את המכשיר למצב בטוח (אם אפשר, שולחים אותה לפני ההריגה).
flowchart TB
accTitle: להרוג, או רק לנטר?
accDescr: אם פתיחה כפולה של המכשיר היא התוצאה הגרועה ביותר, מגדירים KillOnJobClose; אם crash dumps והפריים האחרון הם נכס, לא מגדירים ומנטרים במקום; אם יש שירות בקרה נפרד, קוראים ל-TerminateJobObject משם
q{"מה מגינים עליו ברגע שהוא מת?"} -->|"שחרור המכשיר קודם"| k["עם KillOnJobClose"]
q -->|"dumps ומצב סופי הם נכס"| m["ניטור בלבד (לא הורגים)"]
q -->|"יש שירות בקרה נפרד"| t["צד הניטור קורא ל-TerminateJobObject"]
איור 7: בוחרים לפי “מה מגינים עליו ברגע שהוא מת”, לא לפי “להרוג או לא”. אם רוצים את שניהם, מגיעים לסידור של השורה השלישית, שבו צד הניטור לוקח את ה-dump קודם ואז מפרק את העץ.
מעבירים את handle ה-Job לצד הניטור כל עוד ה-parent חי
השורה השלישית בטבלה, הסידור שבו process ניטור מסיים את העץ עם TerminateJobObject, צריכה הכנה. צד הניטור חייב לקבל את handle ה-Job לפני שה-parent מת.
ה-Job שנוצר בהליך של המאמר הזה הוא unnamed, כך שאין דרך להגיע אליו מבחוץ ברגע שה-parent נעלם. יש שתי דרכים להעביר אותו.
| שיטה | מה לעשות כל עוד ה-parent חי |
|---|---|
| משכפלים את handle ה-Job ה-unnamed | מעבירים את ה-handle ל-process הניטור עם DuplicateHandle |
| משתמשים ב-Job named | יוצרים אותו named מההתחלה; צד הניטור פותח אותו עם OpenJobObject ומחזיק אותו |
כי שמות יכולים להתנגש גלובלית, כוללים GUID ייחודי או דומה. אם שוכחים את ההכנה הזו, לצד הניטור אין אמצעי לסיים את העץ גם כשהוא מזהה את החריגה של ה-parent.
לוקחים dumps והופכים את המכשיר לבטוח לפני סיום בכוח
child שמסתיים ב-KillOnJobClose לא מקבל אזהרה, בדיוק כמו עם TerminateProcess. לא קורה unhandled exception, כך שגם אם WER (LocalDumps) מוגדר על ה-child, לא נשאר dump של הסיום בכוח הזה. מה ש-WER יכול לתפוס הוא המקרה שבו ה-child יוצא בגלל קריסה משלו.
אם הדרישה היא “גם איסוף אוטומטי וגם dumps”, מעבירים את האחריות לסיום לצד הניטור. הסדר הוא: לוקחים את ה-dump כל עוד היעד חי, מחזירים את המכשיר למצב בטוח אם נחוץ, ולבסוף מסיימים עם TerminateJobObject.
flowchart TB
accTitle: פירוק כך שאיסוף אוטומטי ו-dumps מתקיימים יחד
accDescr: כש-process הניטור מזהה חריגה, הוא לוקח dump קודם, שולח פקודה שמחזירה את המכשיר למצב בטוח אם נחוץ, ולבסוף מפרק את העץ עם TerminateJobObject, כך שאיסוף אוטומטי וניתוח post-mortem מתקיימים יחד
det["צד הניטור מזהה חריגה"] --> dmp["לוקחים את ה-dump קודם"]
dmp --> safe["מחזירים את המכשיר למצב בטוח"]
safe --> term["מפרקים עם TerminateJobObject"]
איור 8: התשובה היחידה ל”גם איסוף אוטומטי וגם dumps”. הופכים את הסדר והיעד ל-dump כבר לא קיים.
בסידור שבו KillOnJobClose מסיים בכוח את ה-child ברגע שה-parent נעלם, גם ל-child אין הזדמנות לשלוח פקודת “החזירו את המכשיר למצב בטוח”. למכשירים שצריכים את זה, בוחרים בסידור של השורה השלישית, ונותנים לצד הניטור לשלוח את פקודת ה-safing קודם ואז לקרוא ל-TerminateJobObject.11
6. ממתינים ל”ריק” עם completion port
המתנה על handle ה-Job לבדה לא יכולה לאשר שעץ יצא
handle ה-Job לא הופך ל-signaled כשכל ה-processes שמתחתיו יצאו. הוא מסומן רק כשכל ה-processes הסתיימו כי חרגו ממגבלת זמן ה-job.12
כדי “להמשיך ברגע שעץ ה-child ריק”, משייכים I/O completion port (IOCP) ל-Job.1314 עושים את השיוך כשה-Job ריק, לפני שמכניסים process כלשהו. אם משייכים באמצע, אפשר לפספס את ההתראות של processes שמצבם השתנה במהלך השיוך.15
צופים ביצירה, יציאה, חריגה ואפס עם ארבע הודעות
| הודעה | מה היא אומרת |
|---|---|
JOB_OBJECT_MSG_NEW_PROCESS |
process הצטרף ל-Job. גם מזהה יצירת נכדים |
JOB_OBJECT_MSG_EXIT_PROCESS |
process יצא |
JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS |
process יצא עם קוד יציאה חריג כמו access violation. חשוב במיוחד באפליקציות מדידה13 |
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO |
מספר ה-processes הפעילים הגיע ל-0 |
כל מה שחבילת NEW_PROCESS נושאת הוא ה-PID החדש. היא לא אומרת את יחס parent-child, כלומר מי הוליד את ה-process. אם צריך את הייחוס, מוסיפים אמצעי אחר כמו ETW.
sequenceDiagram
accTitle: זרימת התראות completion port
accDescr: ה-Job שולח הודעות יצירת process, יציאה, יציאה חריגה ואפס ל-completion port, thread ניטור ייעודי מקבל אותן עם GetQueuedCompletionStatus, ורק התוצאות מועברות ל-UI thread
participant J as Job Object
participant P as completion port
participant W as thread ניטור
participant U as UI thread
J->>P: NEW / EXIT_PROCESS
J->>P: ABNORMAL_EXIT / ZERO
W->>P: ממתינים לחבילת completion
P-->>W: הודעה ו-PID
W-->>U: מעבירים רק את התראת התוצאה
איור 9: מריצים GetQueuedCompletionStatus על thread ייעודי. ממתינים על ה-UI thread, וה-UI הופך ל-“Not Responding” בכל פעם ש-child מתנהג רע.
ממתינים על thread ייעודי ועל port ייעודי, ושואלים את ה-accounting ב-timeout
מריצים את לולאת הניטור הזו על completion port שנוצר אך ורק להתראות ה-Job. אם רוכבים על אותו port כמו I/O קיים, החבילה כבר יצאה מהתור עד ש-GetQueuedCompletionStatus חוזר. אם זורקים אותה עם continue כי המפתח שונה, בעל ה-I/O ההוא ממתין להשלמה לנצח. אותו דבר לענף החבילה שנכשלה.
אם בכל זאת משתפים, צריך מנגנון נפרד שמעביר לכל בעלים לפי מפתח. המאמר הזה מפריד את ה-ports ומעביר רק תוצאות ל-UI thread.
הוא גם מניח שJob אחד הוא דור הפעלה אחד, שנוצר מחדש בכל הפעלה. אם עושים reuse בין ניסיונות, TotalProcesses כולל את הדור הקודם, וההגנה שמבחינה בין Job ריק לפני הפעלה מפסיקה לעבוד.
// C++: שלד של thread הניטור (timeout + accounting כביטוח מול התראות שפספסו)
DWORD msg; ULONG_PTR key; LPOVERLAPPED info;
bool treeEmpty = false;
while (!treeEmpty) {
if (!GetQueuedCompletionStatus(iocp, &msg, &key, &info, 5000)) {
if (info != nullptr) continue; // חבילת completion של I/O שנכשל. ממשיכים לנטר
if (GetLastError() != WAIT_TIMEOUT) break; // port נהרס ודומה: עוצרים
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION acct = {};
if (QueryInformationJobObject(job, JobObjectBasicAccountingInformation,
&acct, sizeof(acct), nullptr))
treeEmpty = (acct.TotalProcesses > 0 && // לא טועים ב-Job ריק לפני הפעלה כהשלמה
acct.ActiveProcesses == 0); // ביטוח להתראת ZERO שנפלה
// הנחה: ה-Job נוצר מחדש בכל הפעלה (1 Job = דור הפעלה 1).
// אם עושים reuse ל-Job בין ניסיונות, TotalProcesses עדיין סופר את הדור
// הקודם, וההגנה הזו לא יכולה להבחין בין הדורות
continue; // בכישלון שאילתה, לא מסיקים שזה ריק
}
if ((HANDLE)key != job) continue; // התאמה מול CompletionKey ששימש בשיוך.
// מניחים שה-port הזה נוצר אך ורק לניטור Job
// (ראו בטקסט למטה. זריקה כזו על port משותף
// גורמת לבעלי I/O אחר להמתין לנצח)
DWORD pid = (DWORD)(UINT_PTR)info; // חלק מההודעות נושאות PID
switch (msg) {
case JOB_OBJECT_MSG_NEW_PROCESS: /* לוג יצירת grandchild */ break;
case JOB_OBJECT_MSG_EXIT_PROCESS: /* לוג יציאה */ break;
case JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS:/* יציאה חריגה: הולכים לבדוק dump */ break;
case JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO: treeEmpty = true; break;
} // ה-break של ה-switch לבדו לא מסיים את ההמתנה
}
התראות, accounting ו-handles כל אחד קובע מידע שונה
להתראות אין עקרונית הבטחת מסירה. היחידות המובטחות הן ההתראות על המגבלות שמוגדרות עם JobObjectNotificationLimitInformation. אי אפשר להסיק “אין התראה = זה לא קרה”.15
מצב מצטבר כמו “האם זה הפך לריק” מאושר גם ב-polling של נתוני ה-accounting. אבל accounting הוא קבוצת מונים מצטברים, כך שאי אפשר לשחזר את ה-PID, את קוד היציאה, או אם היציאה הייתה חריגה, ל-EXIT/ABNORMAL_EXIT שנפל. זה התחום של החזקת process handles ושל ETW.
גם ACTIVE_PROCESS_ZERO אינו ראיה ליציאה תקינה. המספר עשוי להגיע לאפס דרך סיום בכוח, וההודעה עצמה לא מבחינה. שופטים את איכות היציאה לפי EXIT/ABNORMAL_EXIT וקוד היציאה.
לא מחליטים שזה אותו process לפי PID לבדו
ה-PID בחבילת completion עושה reuse. אלא אם מחזיקים process handle, אין הבטחה שה-PID עדיין מפנה לאותו process.13
מה ש-pi.hProcess שמוחזק בפרק 4 יכול לנעוץ הוא רק ה-PID של ה-child שהפעלתם ישירות. לנכדים שה-SDK מוליד, קוראים ל-OpenProcess בנקודה ש-NEW_PROCESS מגיע כדי לקבל handle, ומכאן ואילך מתאימים מול ה-handle הזה.
גם אז, נשאר חלון קצר בין ההתראה ל-Open. אם ה-PID עושה reuse במרווח הזה, תופסים process אחר. כשנדרשת זיהוי קפדני של ה-process הבודד, מצליבים עם טלמטריה שנושאת זמן יצירה, כמו אירועי process-start של ETW.
flowchart TB
accTitle: ניטור שלא נשען על התראות לבדן
accDescr: כי הודעות completion port הן להתראה ואין להן הבטחת מסירה, משלבים אותן עם polling של נתוני accounting והחזקת process handles כדי להתכונן לפספוסים ול-reuse של PID
n["התראות completion port"] --> miss["אין הבטחת מסירה (מטרת התראה)"]
miss --> poll["עושים polling גם לנתוני accounting"]
n --> pid["PIDs עושים reuse"]
pid --> hold["מחזיקים process handles"]
איור 10: התראות הן הנתיב הראשי; accounting ו-handles הם הביטוח. רק עם שניהם אפשר לומר ש”צופים”.
אגב, מימושים ש”הורגים את ה-thread שממתין שה-Job יתרוקן עם TerminateThread” פעם הסתובבו, אבל עם completion port אין צורך לסיים בכוח את ה-thread הממתין. גם Raymond Chen פרסם מאמר שכותב מחדש את הדפוס הישן הזה לגישת completion port.16
7. כשלים שקורים בפועל במדידה ובאינטגרציית מכשירים
כאן המנגנונים שכוסו עד כאן מוחלים על שישה כשלים שקורים בסביבות מכשירים. לכל מקרה, בנוסף לסיבה ולתיקון, מאשרים “מה נשאר מאחור”.
כשל 1: רק ה-parent מת, והמצלמה נשארת פתוחה
בהתקנה שבה ל-SDK של הספק יש helper process להעברת פריימים, הריגת UI ה-parent ב-Task Manager משאירה רק את ה-helper. ה-parent שמופעל מחדש מקבל device busy במהלך אתחול ה-SDK. בחלק מהאתרים שום דבר לא מתאושש עד שמחזור חשמל למכשיר.
מה שנשאר: ה-helper process והפתיחה הבלעדית של המצלמה.
flowchart TB
accTitle: רק ה-parent מת והמצלמה נשארת פתוחה
accDescr: סיום בכוח של ה-UI מסיר את ה-parent, אבל helper process של ה-SDK נשאר מחזיק את handle המצלמה, כך שה-parent שמופעל מחדש נכשל בפתיחה מחדש עם device busy
kill9["מסיימים את ה-UI ב-Task Manager"] --> dead["ה-parent נעלם"]
dead --> helper["helper של ה-SDK נשאר"]
helper --> busy["עדיין מחזיק את המצלמה"]
busy --> fail["device busy אחרי הפעלה מחדש"]
איור 11: הפנים האמיתיות של “ה-process נעלם, ובכל זאת אי אפשר לפתוח את המכשיר”. האשם לעתים קרובות אינו ה-process ששמו הוצג ב-Task Manager.
כשל 2: grandchild נגמר מחוץ ל-Job
יש שני נתיבים, והתיקונים חייבים להיות מופרדים.
(a) ה-SDK משתמש ב-Job משלו. זה המקרה שבו הצד השני כבר ב-Job אחר. ב-Windows 7 זה job אחד לכל process, כך שה-Assign שלכם נכשל. ב-Windows 8 ואילך קינון יכול לאסוף אותו, אבל אם ל-Job שלכם יש הגבלות UI, הקינון עצמו בלתי אפשרי (פרק 9).
(b) ה-SDK מוליד את ה-grandchild עם CREATE_BREAKAWAY_FROM_JOB. זה עובד רק כשה-Job שלכם מאפשר BREAKAWAY_OK, וה-grandchild נולד מחוץ לעץ מההתחלה. גם קינון לא יכול לאסוף אותו. אם לא מאפשרים, יצירת ה-SDK נכשלת, כך שאם הניטור בעדיפות, הגישה הבסיסית היא לא לאפשר ולזהות את זה ככישלון.
מה שנשאר: grandchild שרץ מחוץ לניטור, ו-Assign שכישלונו נבלע.
flowchart TB
accTitle: שני נתיבים שבהם grandchild נגמר מחוץ ל-Job
accDescr: בנתיב שבו ה-SDK משתמש ב-Job משלו, קינון יכול לאסוף ב-Windows 8 ואילך אבל נכשל אם מוגדרות הגבלות UI, ואילו בנתיב שבו ה-SDK מוליד את ה-grandchild עם breakaway, זה עובד רק אם אפשרתם וה-grandchild מחוץ לעץ מההתחלה
g["grandchild נגמר מחוץ ל-Job"] --> ja["(a) ה-SDK משתמש ב-Job משלו"]
g --> jb["(b) נוצר עם breakaway"]
ja --> nest["Win8+ אוסף בקינון"]
nest -.-> ui["נכשל עם הגבלות UI"]
jb --> allow["עובד רק אם מותר"]
allow -.-> out["grandchild מחוץ לעץ מההתחלה"]
איור 12: אותו “יציאה החוצה”, אבל (a) משאיר מקום לאסוף בקינון, בעוד (b) נסגר כבלתי ניתן לשחזור ברגע שמאפשרים. התיקון מתחיל בזיהוי הנתיב.
כשל 3: process מכשיר שהופעל משירות
כששירות נעצר, ה-SCM ממתין ל-parent בלבד; הילדים והנכדים שהופעלו ב-session 0 הם מחוץ להיקף טיפול העצירה. Job + KillOnJobClose מכסה גם את האזור מחוץ להיקף הזה. תכנון של סידור שחוצה שירות וסשן אינטראקטיבי מושאר ל-מאמר גבול המשתמש.
מה שנשאר: processes שנשארים ב-session 0, וזיהוי שווא בבדיקת instance כפול בהפעלה הבאה.
כשל 4: child שמת חודש אחר כך
אם לא רושמים מעת לעת את ה-accounting של ה-Job (PeakJobMemoryUsed, מוני I/O, מספר processes כולל), אי אפשר לעקוב אחר כך “איזה דור של ה-helper התחיל להתנפח, ומתי”.17 המבנה של מוות חודש אחר כך מ-handle leak מנותח ב-מאמר הכשל הארוך של מצלמה תעשייתית, וה-accounting של ה-Job הוא נקודת הכניסה לחקירה הזו.
מה שנשאר: לוגים שאינם מספיקים לקבוע את הסיבה.
כשל 5: טיפול היציאה של ה-parent ממתין שה-child ייצא
אם קוראים ל-WaitForExit או ממתינים שהעץ ייצא על ה-UI thread, ה-parent הופך ל-“Not Responding” ביום שה-child קופא. משאירים את המתנת היציאה ל-thread ה-IOCP, ושמים על ה-UI רק התקדמות ולחצן ביטול. המנגנון כמתואר ב-מאמר “Not Responding”.
מה שנשאר: parent שנתקע יחד עם ה-child.
flowchart TB
accTitle: לא ממתינים ליציאה על UI thread של ה-parent
accDescr: המתנה ליציאת ה-child על ה-UI thread מפיצה hang של child ל-Not Responding של ה-parent, לכן משאירים את המתנת היציאה ל-thread ניטור ה-IOCP ושמים על ה-UI thread רק תצוגת התקדמות ולחצן ביטול
w2["ממתינים ליציאת child על ה-UI thread"] --> h2["היום שה-child נתקע"]
h2 --> f2["גם ה-parent Not Responding (נגרר)"]
ok2["ממתינים על thread ה-IOCP"] --> u2["ל-UI יש רק התקדמות וביטול"]
איור 13: הצד שצופה בחריגה של ה-child אסור שיקפא בגלל החריגה של ה-child. רק הפרדת איפה ממתינים מסירה את ה-hang הנלווה.
כשל 6: נכשל רק תחת debugger
כלי פיתוח ומפעילים עשויים כבר להכניס את parent process שלכם לאיזה Job. ב-Windows 8 ואילך קינון בדרך כלל מציל, אבל במחשבי מכשיר שרצים 7 או מוקדם יותר ה-Assign הופך ל-ERROR_ACCESS_DENIED, ומייצר הבדלים כמו “זה לא עובד רק במכונת הפיתוח” או “רק בייצור”. הצעד הראשון הסטנדרטי הוא לבדוק את החברות שלכם עם IsProcessInJob.18
מה שנשאר: זמן אימות שבוזבז בלי לנעוץ את סיבת הבדל הסביבה.
8. אילו מגבלות להצמיד
למגבלות יש מטרה ותופעות לוואי
מצמצמים את הרשימה למגבלות שחשובות באפליקציית מדידה, הנה הסיבות להשתמש בכל אחת ותופעות הלוואי.319
| מגבלה | למה משתמשים | אם מגזימים |
|---|---|---|
| KILL_ON_JOB_CLOSE | לשחרר את המכשיר כשה-parent נעלם | חומר post-mortem נעלם |
| ACTIVE_PROCESS | לעצור ריבוי פרוע של ילדי ה-SDK | גם helpers לגיטימיים נדחים ביצירה |
| JOB_MEMORY / PROCESS_MEMORY | תקרה לדליפות ריצה ארוכה | הקצאה של מאגרי תמונה ענקיים מתחילה להיכשל |
| DIE_ON_UNHANDLED_EXCEPTION | בלי דיאלוגי שגיאה במכונות unattended | דיבאג אינטראקטיבי הופך לכואב |
| בקרת CPU rate | למנוע מ-child של עיבוד תמונה להרעיב את ה-UI | דדליינים של פריימים מוחמצים |
| BREAKAWAY_OK | להשאיר נתיב בריחה ל-SDK שצריך job משלו | processes נעלמים מהניטור |
| הגבלות UI | הידוק דמוי sandbox | הקינון נשבר (פרק 9) |
מגבלות התראה הן לתצפית; מגבלות אכיפה הן לדחייה ולסיום
מפרידים “מגבלות רופפות להתראה” מ”מגבלות שעוצרות כשחורגים”. JobObjectNotificationLimitInformation רק מודיע על החריגה; ה-process ממשיך לרוץ.15
מגבלות Extended Limit נאכפות, אבל צורת האכיפה שונה לפי מגבלה.3
| מגבלה | מה קורה כשחורגים |
|---|---|
| מגבלת זיכרון | פעולת ה-commit שהייתה חורגת נכשלת. ה-process עצמו נשאר חי |
| ACTIVE_PROCESS | היצירה או ה-assignment שהיו חורגים נכשלים. process שחרג ב-assignment מסתיים |
| זמן process (PROCESS_TIME) | רק ה-process שחרג מסתיים |
| זמן Job (JOB_TIME) | מגבלה על הערך המצטבר; כברירת מחדל כל ה-processes תחת ה-Job מסתיימים |
מגדירים את אלה בלי לדעת את ההבדל ותאבחנו לא נכון “helper שנעלם מיד אחרי יצירה” כתקלה אחרת. לתפעול ארוך, הסדר הבטוח הוא לצפות קודם עם מגבלות התראה, ולהחליט על מגבלות האכיפה ברגע שהמגמה ידועה.
flowchart TB
accTitle: מגבלות להתראה ומגבלות לאכיפה
accDescr: מגבלה של JobObjectNotificationLimitInformation רק מודיעה על החריגה וה-process ממשיך לרוץ, ואילו מגבלות Extended Limit נאכפות: מגבלת זיכרון מכשילה את הפעולה, ACTIVE_PROCESS מכשיל יצירה ו-assignment, ומגבלות זמן מסיימות את ה-process
lim2{"מטרת המגבלה?"} -->|"לצפות"| ntf["מגבלת התראה: ממשיך לרוץ כשחורגים"]
lim2 -->|"לעצור"| enf["מגבלת אכיפה: דוחים או מסיימים"]
ntf --> log2["מזהים את הדור מלוגי accounting"]
enf --> die2["כישלון commit, יצירה נדחית, סיום"]
איור 14: אותה “מגבלה”, אבל התראה ואכיפה הם דברים שונים, והאכיפה עובדת אחרת לפי מגבלה. שמים מגבלת אכיפה בלי לצפות קודם, והיא נורה בשווא על שיא של תפעול רגיל.
הקשר בין בקרת CPU rate לעיבוד מחזורי מושאר ל-מאמר soft real-time; כאן לא הולכים רחוק יותר מנגד ל”process המכשיר אוכל את ה-UI”.
9. קינון, Breakaway, וצד שכבר ב-Job
חושבים על קינון כ”הכלה של קבוצות processes”
כללי הקינון ב-Windows 8 ואילך אפשר לארגן לארבעה.5
- ה-job ההורה הוא הקבוצה הרחבה יותר, ו-job הילד הוא תת-קבוצה שלו (שיוך בסדר ששובר את ההכלה הזו נכשל)
- למגבלות המשאבים העיקריות, המגבילה ביותר לאורך השרשרת נכנסת לתוקף
- Job עם הגבלות UI לא יכול להיות מקונן
- התראות מועברות גם ל-completion ports של כל job הורה לאורך השרשרת (ל-job הילד לא חייב להיות port)
flowchart TB
accTitle: היררכיית jobs מקוננים ומגבלות בתוקף
accDescr: ה-job ההורה הוא הקבוצה הרחבה יותר ו-job הילד הוא תת-קבוצה שלו, ולמגבלות המשאבים העיקריות הערך המגביל ביותר לאורך השרשרת נכנס לתוקף. Job עם הגבלות UI לא יכול להיות מקונן
pj["job הורה (קבוצה רחבה יותר)"] --> cj["job ילד (תת-קבוצה)"]
cj --> pr["processes חברים"]
pj -.-> eff["מגבלה בתוקף = הערך המגביל ביותר"]
cj -.-> eff
ui["Job עם הגבלות UI"] -.-> no["לא יכול להיות מקונן"]
איור 15: חושבים על קינון כ”הכלה של קבוצות”. הגבלות UI שוברות קינון, כך שבטוח יותר לא להצמיד אותן ל-Job שמשמש לניהול משך חיים.
Breakaway הוא הנתיב ליצירת process מחוץ ל-Job מההתחלה
Breakaway הוא הנתיב הלגיטימי שבו צאצאים שנוצרים עם CreateProcess עוזבים את העץ.3
| הגדרה בצד ה-Job | תנאי שה-child ייולד מחוץ ל-Job |
|---|---|
JOB_OBJECT_LIMIT_BREAKAWAY_OK |
יוצרים אותו עם CREATE_BREAKAWAY_FROM_JOB שצוין |
SILENT_BREAKAWAY_OK |
אין צורך בדגל. כל child נולד בחוץ |
לפעמים זה נחוץ כי ה-SDK משתמש ב-Job משלו. עם זאת, process שנולד בנתיב הזה מוצא גם מסיום גורף וגם מניטור. אם מאפשרים, מחליטים גם מי מנהל את מה שברח.
flowchart TB
accTitle: הנתיב החוצה מהעץ ב-breakaway
accDescr: כש-BREAKAWAY_OK מוגדר על ה-Job, grandchild שנוצר עם CREATE_BREAKAWAY_FROM_JOB נולד מחוץ ל-Job ונעלם מהיקף הסיום הגורף והניטור
j2["Job (עם BREAKAWAY_OK)"] --> c2["child process"]
c2 -->|"יצירה רגילה"| in3["גם grandchild ב-Job"]
c2 -->|"יצירה עם BREAKAWAY"| out3["grandchild יוצא מחוץ ל-Job"]
out3 --> lost["מחוץ לניטור ולסיום גורף"]
איור 16: ל-breakaway שני פנים: “נתיב בריחה ל-SDK שצריך אותו” ו”חור בניטור”. אם מצמידים, מחליטים מי משגיח על מה שברח.
הפעלת proxy דרך WMI אי אפשר למנוע גם כש-breakaway אסור
נתיבים שבהם process צד שלישי מפעיל בשמכם, כמו Win32_Process.Create של WMI, הם עניין אחר. ה-parent בפועל הוא ספק ה-WMI, כך שה-process שנולד מחוץ ל-Job מההתחלה. איסור breakaway לא סוגר את החור הזה.1
אם ה-SDK משתמש בנתיב הזה בודקים לא מלוג NEW_PROCESS אלא מיחסי parent-child ב-Process Explorer.
בודקים קודם חברות ב-Job קיים ואת מגבלות Windows 7 ומוקדם יותר
כשהצד השני כבר ב-Job (כשל 6), ההליך הוא: בודקים עם IsProcessInJob → אם אפשר להקים קינון, עושים Assign כמו שהוא → אם אי אפשר (Windows 7, או הגבלות UI), משנים את התכנון.18
ב-Windows 7 ומוקדם יותר, אי אפשר לעשות Assign שני לצד שכבר שייך ל-Job אחר. BREAKAWAY_OK אינו דגל שמסיר process שכבר שויך אחר כך. נתיב הבריחה הזה עובד רק כשצד ה-SDK מבקש breakaway כשהוא יוצר את ה-child. אם אי אפשר לצפות לזה, משנים את התכנון לפני ההפעלה על ההנחה של “process אחד, job אחד”.12
מחליטים אחרון אם להכניס את ה-parent עצמו ל-Job
לבסוף, מילה על תכנון “הכנסת ה-process שלכם ל-Job שלכם”. אם גם ה-parent עצמו מונח מתחת, אז כשה-parent קורס גם הוא נכלל ביעדי KillOnJobClose, ומשך החיים של כל העץ תואם לגמרי. אבל זה חרב פיפיות שהופך ל-סיום המוני לא מכוון אם טועים בטיפול ב-handle ה-Job, כך שבטוח יותר להתחיל עם “parent בחוץ, רק עץ ה-child בפנים”.
10. איך חוקרים
חוקרים בסדר חברות → לוגים של accounting והתראות → תפיסת מכשיר. זה הליך האישור ללא להסיק “כלום לא נשאר” רק מהסתכלות על שמות processes.
- Process Explorer: למאפייני ה-process יש לשונית Job, שמראה את ה-Job שה-process שייך אליו ואת המגבלות שלו. זו הדרך המהירה ביותר לאשר “באיזה Job ה-helper הזה”
IsProcessInJob: נקודת הכניסה לבדיקת החברות שלכם או של הצד השני מהקוד18QueryInformationJobObject: רושמים מעת לעת Basic Accounting (מספר processes כולל, זמן CPU) ו-Extended Limit (PeakJobMemoryUsedוכן הלאה)417- שומרים את לוג ה-completion port בקובץ: ציר הזמן של NEW_PROCESS / EXIT / ABNORMAL_EXIT הופך לראיה היחידה בחקירה חודש אחר כך
- לא בודקים שאריות לפי PID: על מה מסתכלים הוא device handles, שמות pipes, וקבצי נעילה. “אין process נראה ב-Task Manager” לא אומר “המכשיר שוחרר”
flowchart TB
accTitle: הליך לחקירת שאריות
accDescr: קודם מאשרים חברות עם IsProcessInJob ועם לשונית Job של Process Explorer, קוראים את ה-accounting עם QueryInformationJobObject, ולבסוף שופטים שאריות לפי device handles, שמות pipes וקבצי נעילה ולא לפי אם process קיים
s1["מאשרים חברות עם IsProcessInJob"] --> s2["לשונית Job של Process Explorer"]
s2 --> s3["accounting עם QueryInformationJobObject"]
s3 --> s4["שופטים שאריות לפי device handles ושמות pipes"]
איור 17: חוקרים בסדר “חברות → accounting → תפיסה”. לא מסיקים “כלום לא נשאר” רק מהסתכלות על שמות processes.
11. מדריך גס לבחירה (טבלת החלטה)
כאן הבחירות עד כאן מסוכמות לפי מצב. חוזרים לפרק 5 למדיניות הסיום, לפרק 6 להנחות הניטור, ולפרק 9 לקשר עם Jobs קיימים.
| מצב | המלצה |
|---|---|
| גוף ה-UI ו-SDK המכשיר פוצלו ל-processes נפרדים | מכניסים ל-Job, ומנטרים עם completion port |
| המקרה הגרוע ביותר הוא שהמכשיר מוחזק אחרי שה-parent נעלם | מגדירים KillOnJobClose |
| הפריים או ה-dump ברגע הקריסה הוא נכס | לא מגדירים KillOnJobClose; צד הניטור הופך דברים לבטוחים ואז קורא ל-TerminateJobObject |
| SDK הספק מוליד helpers | יוצרים עם SUSPENDED או JOB_LIST, ורושמים NEW_PROCESS |
Assign מחזיר ERROR_ACCESS_DENIED |
בודקים קודם את ה-job הקיים ואם קינון אפשרי. חושדים בהגבלות UI |
| מולידים ילדים בסשן האינטראקטיבי משירות | לא מצמידים הגבלות UI. חוזרים לתכנון ב-מאמר גבול המשתמש |
| ממתינים ליציאת נכדים על ה-UI thread | עוצרים. מעבירים ל-thread ה-IOCP |
12. סיכום
משך החיים של child אינו משך החיים של אובייקט ה-Process של ה-parent. גם יציאת ה-parent לא מועברת ל-child אוטומטית. Job Object הוא המנגנון שהופך את ה-process tree הזה ליחידה אחת ומטפל במגבלות, בהתראות ובסיום גורף.
כדי לנהל עד הנכדים, סוגרים חברות לפני שה-child רץ. הליך A הוא SUSPENDED + Assign; הליך B, ב-Windows 10 ואילך, הוא JOB_LIST. KillOnJobClose עובד בסגירת handle ה-Job האחרון לא משנה למה ה-parent יצא, אבל הוא גם מאבד את חומר ה-post-mortem של הצאצאים שמסתיימים יחד איתו.
בדיוק לכן צריך להציג קודם “מה אסור שישאר מאחור אחרי הקריסה” ו”מה רוצים לשמור”, ולבחור בין איסוף מיידי לבין סיום אחרי שצד הניטור אסף והפך דברים לבטוחים. זה הליך התכנון שהמאמר הזה רוצה להעביר.
הדבר הבא לכתוב יהיה הפרטים של הפעלת processes בין session 0 לסשן האינטראקטיבי, או המתנה על מכשירים עם overlapped I/O. ברגע שיש שליטה ב”משך החיים החיצוני” של process, משך החיים של I/O מחכה הבא בתור.
מאמרים קשורים
- רשימת בדיקה לניהול בטוח של child processes באפליקציית Windows
- מה Not Responding באמת אומר — איך Windows מחליט שיש hang, ואיך לתכנן אפליקציות שלא נתקעות
- Named Pipes בפועל — ה-IPC הסטנדרטי של Windows, מתכנון עד אבטחה
- Handle leak באפליקציית מצלמה תעשייתית שקורסת אחרי ריצה ארוכה
- soft real-time ב-Windows רגיל: מדריך מעשי
- “אותו מחשב” אינו אותה סביבת הרצה — גבול המשתמש שמפריד AppData, HKCU, DPAPI ו-Credentials
תחומי ייעוץ קשורים
KomuraSoft LLC מטפלת בתכנון בידוד processes של אפליקציות Windows שעובדות עם מצלמות, מכשירי מדידה ומכשירי serial/USB, בחקירת תקלות תפיסת מכשיר כמו helpers של SDK שנשארים ו-device busy, ובבניית מנגנוני ניטור והתאוששות אוטומטית לאפליקציות ריצה ארוכה. אפשר להתייעץ גם ממקרה בודד של “מפעילים מחדש את ה-parent ואי אפשר לפתוח את המכשיר”.
קישורים
-
Microsoft Learn, Job Objects. על כך ש-Job Object הוא kernel object שמנהל קבוצת processes כיחידה אחת, child processes שנוצרים על ידי process חבר משויכים לאותו Job כברירת מחדל (למעט דרך Win32_Process.Create), שני דגלי המגבלה ל-breakaway, סיום גורף עם TerminateJobObject, ואיך לנהל process tree בסביבות שבהן קינון אינו זמין. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, AssignProcessToJobObject function (jobapi2.h). על כך שהשיוך בין process ל-Job בלתי הפיך, job אחד לכל process ב-Windows 7 ומוקדם יותר עם חברות מרובה (קינון) אפשרית מ-Windows 8, והמגבלות בתוקף והתפשטות breakaway תחת קינון. ↩ ↩2
-
Microsoft Learn, JOBOBJECT_BASIC_LIMIT_INFORMATION structure (winnt.h). על דגלי המגבלה JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE (מסיים את כל ה-processes כש-handle ה-Job האחרון נסגר), ACTIVE_PROCESS (תקרה על processes פעילים בו-זמנית), JOB_MEMORY (תקרת commit לכל ה-job), DIE_ON_UNHANDLED_EXCEPTION, ו-BREAKAWAY_OK / SILENT_BREAKAWAY_OK. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, JOBOBJECT_BASIC_ACCOUNTING_INFORMATION structure (winnt.h). על כך שה-Job שומר נתוני accounting כמו מספר processes כולל, זמן CPU ומספר page faults, כולל סכומים שהצטברו מ-processes שיצאו, וקבלתם עם QueryInformationJobObject. ↩ ↩2
-
Microsoft Learn, Nested Jobs. על כך ש-jobs מקוננים יוצרים היררכיית parent-child (job הילד הוא תת-קבוצה של ה-processes של job ההורה), Job עם הגבלות UI לא יכול לקנן, המגבלה בתוקף היא הערך המגביל ביותר לאורך השרשרת, התראות נשלחות לכל completion ports של שרשרת job ההורה, וההיררכיה מסתיימת מהרמה הנמוכה ביותר. ↩ ↩2
-
Microsoft Learn, Process Creation Flags. על CREATE_SUSPENDED (יוצרים את ה-thread הראשוני מושהה ולא מריצים אותו עד ResumeThread) ו-CREATE_BREAKAWAY_FROM_JOB (ה-Job של הקורא חייב שיהיה לו JOB_OBJECT_LIMIT_BREAKAWAY_OK). ↩
-
Raymond Chen, Closing the race window between creating a suspended process and putting it in a job (The Old New Thing). על ההליך הקלאסי של יצירה עם CREATE_SUSPENDED ואז הכנסת ה-process ל-Job, ואיך סוגרים את חלון ה-race שלו. ↩
-
Microsoft Learn, UpdateProcThreadAttribute function (processthreadsapi.h). על כך ש-PROC_THREAD_ATTRIBUTE_JOB_LIST משייך handles של Job ל-child process שנוצר בסדר שצוין, ועל כך שהוא נתמך ב-Windows 10 / Windows Server 2016 ואילך. ↩
-
Raymond Chen, A more direct and mistake-free way of creating a process in a job object (The Old New Thing). על שימוש ב-PROC_THREAD_ATTRIBUTE_JOB_LIST כדי לגרום ל-process להשתייך ל-Job מרגע היצירה. ↩
-
Raymond Chen, Destroying all child processes (and grandchildren) when the parent exits (The Old New Thing). על הסידור שמסיים צאצאים יחד כשה-parent נעלם באמצעות Job עם KILL_ON_JOB_CLOSE, ועל החשיבות של לא לתת ל-handle ה-Job להיות מורש. ↩
-
Microsoft Learn, TerminateJobObject function (jobapi2.h). על סיום בכוח של כל ה-processes שמשויכים ל-Job, כאילו קראו ל-TerminateProcess על כל אחד בנפרד. ↩
-
Microsoft Learn, Job Objects - Managing Job Objects. על כך שאובייקט ה-Job הופך ל-signaled כשכל ה-processes מסתיימים בגלל חריגה ממגבלת זמן ה-job, ה-Job נהרס כש-handle האחרון נסגר, וסגירה גורמת לסיום כל ה-processes החברים כש-KILL_ON_JOB_CLOSE צוין. ↩
-
Microsoft Learn, JOBOBJECT_ASSOCIATE_COMPLETION_PORT structure (winnt.h). על רשימת ההודעות שנשלחות ל-completion port כמו JOB_OBJECT_MSG_NEW_PROCESS / EXIT_PROCESS / ABNORMAL_EXIT_PROCESS / ACTIVE_PROCESS_ZERO, קודי היציאה שנשפטים כיציאות חריגות, אי אפשר לשלול reuse של PID להודעות שמחזירות PID אלא אם מחזיקים process handle, ומסירת התראות אינה מובטחת. ↩ ↩2 ↩3
-
Raymond Chen, How do I wait until all processes in a job have exited? (The Old New Thing). על כך שהמתנה על handle ה-Job לא יכולה לזהות “הפך לריק”, ועל הצורך להמתין ל-JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO על ה-completion port. ↩
-
Microsoft Learn, Job Objects - Job Limits and Notifications. על כך שעדיף לשייך את ה-completion port כשה-Job לא פעיל (מצמצם את האפשרות לפספס התראות ל-processes שמצבם משתנה במהלך השיוך), מסירת הודעות אינה מובטחת למעט המגבלות שמוגדרות עם JobObjectNotificationLimitInformation, ו-processes ממשיכים לרוץ אחרי חריגה ממגבלת התראה. ↩ ↩2 ↩3
-
Raymond Chen, Removing the TerminateThread from code that waits for a job object to empty (The Old New Thing). על כתיבה מחדש של הדפוס הישן של הריגת ה-thread הממתין עם TerminateThread להמתנה מבוססת completion port. ↩
-
Microsoft Learn, JOBOBJECT_EXTENDED_LIMIT_INFORMATION structure (winnt.h). על הגדרת מגבלות זיכרון לפי process ולפי job, וקבלת שיא זיכרון עם PeakProcessMemoryUsed / PeakJobMemoryUsed. ↩ ↩2
-
Microsoft Learn, IsProcessInJob function (jobapi.h). על קביעה אם process רץ ב-Job שצוין (או בכל Job). ↩ ↩2 ↩3
-
Microsoft Learn, JOBOBJECT_CPU_RATE_CONTROL_INFORMATION structure (winnt.h). על שליטה ב-CPU rate (חלק ממחזורי שעון או משקל) לפי Job. ↩
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
למה arguments נשברים — כללי command-line arguments ב-Windows
Windows מעביר ל-CreateProcess מחרוזת אחת שהמקבל מפצל. מכסה את כללי CommandLineToArgvW, CRT ו-.NET, ArgumentList, ובניה ב-C++.
Named Pipes בפועל — ה-IPC הסטנדרטי של Windows, מתכנון עד אבטחה
מדריך מעשי ל-Named Pipes, ה-IPC הסטנדרטי ב-Windows. המאמר מסדר לפי מקורות ראשוניים את הבחירה בין byte mode ל-message mode, תכנון server ל...
Win32 Thread Pool API — מקביליות בלי CreateThread, דרך CreateThreadpoolWork
מפזרים קריאות CreateThread בכל הקוד ה-native? המאמר מסביר את ה-Win32 Thread Pool API שעוצב מחדש ב-Vista: ארבעת האובייקטים work, timer, wa...
DllMain ו-loader lock — הסיבה האמיתית שאומרים לא לעשות כלום ב-initialization של DLL
למה אסור לקרוא ל-LoadLibrary או להסתנכרן עם threads מתוך DllMain. המאמר מסביר ממקורות ראשוניים איך loader lock מסדר כל DLL notification, ...
Spurious wakeup — למה condition variable מתעורר בלי notify, ואיך לחכות נכון ב-Windows
Wait של condition variable יכול להתעורר בלי notify (spurious wakeup). למה Windows מתיר זאת, וצורת ה-wait הנכונה עם while ו-predicate ב-Wi...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
יישומים עסקיים, חיבור התקנים וכלי תקשורת, מהגדרת הדרישות ועד הפיתוח.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- האם Job Object הוא container או sandbox?
- לא. Job Object הוא kernel object שמצמיד מגבלות, התראות וסיום גורף לקבוצת processes. הוא יכול להטיל תקרות על זיכרון, CPU ומספר processes, אבל הוא לא יכול להגביל גישת רשת, וה-access token (הרשאות) לא משתנה. יש לו הגבלות UI, אבל הן לבדן לא עושות אותו לגבול אבטחה. אם המטרה היא isolation או אבטחה, צריך לשלב אותו עם מנגנון אחר כמו AppContainer או containers. מה שהמאמר הזה מכסה הוא השימוש בטיפול במשך החיים ובמשאבים של process tree כיחידה אחת.
- Process.Kill() לא מספיק?
- Process.Kill() מסיים רק את אותו process אחד; הוא לא מגיע ל-grandchild processes. Kill(entireProcessTree: true) ב-.NET Core 3.0 ואילך עובר על הצאצאים ומסיים אותם, אבל כי הוא מונה את ה-process tree מיחסי parent-child באותו רגע, הוא יכול לפספס processes שנולדו במהלך המנייה ו-processes שהייחוס שלהם נחתך כי ה-parent מת קודם. גם, אף אחת מהשיטות לא נקראת כשה-parent עצמו קורס. אם רוצים שצאצאים ייאספו לא משנה אם ה-parent חי או מת, Job Object ועליו KillOnJobClose, שמפקיד את משך החיים לליבה, הוא הבחירה האמינה.
- אם ה-parent ב-Job, האם ה-child נכנס ל-Job אוטומטית?
- כברירת מחדל, כן. child process שנוצר עם CreateProcess על ידי process ששייך ל-Job שייך אוטומטית לאותו Job. החריג הוא breakaway. אם JOB_OBJECT_LIMIT_BREAKAWAY_OK מוגדר על ה-Job וה-child נוצר עם הדגל CREATE_BREAKAWAY_FROM_JOB, או אם JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK מוגדר, ה-child נולד מחוץ ל-Job. שימו לב גם ש-processes שנוצרים דרך Win32_Process.Create של WMI אינם משויכים ל-Job.
- אפשר להוציא process מ-Job אחרי שהכניסו אותו?
- לא. השיוך שנעשה ב-AssignProcessToJobObject הוא בלתי הפיך, והחברות נמשכת עד שה-process יוצא. אפשרויות התכנון הן לכן שלוש: לא להכניס, ליצור אותו בחוץ מההתחלה עם breakaway, או לקנן Job נפרד. אין "להוציא אחר כך". הבלתי-הפיכות הזו היא גם הסיבה שה-Job צריך להיות מוכן לפני היצירה.
- האם KillOnJobClose עובד גם כשה-parent קורס?
- כן. JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE הוא מנגנון שמסיים את ה-processes שמתחת כש-handle ה-Job האחרון נסגר, ובין אם ה-parent יוצא כרגיל, מת על unhandled exception, או נהרג ב-Task Manager, הליבה סוגרת את ה-handles כחלק מ-cleanup של ה-process, ולכן זה נורה. עם זאת, אם נותנים ל-child process לרשת את handle ה-Job, ה-handle שהילד מחזיק שורד את מות ה-parent, כך שזה לא ה-handle האחרון וזה לא נורה. לא נותנים ל-handle ה-Job להיות מורש.
- האם התראות completion port תמיד מגיעות?
- לא תמיד. התיעוד הרשמי קובע במפורש שלמעט התראות על המגבלות שמוגדרות עם JobObjectNotificationLimitInformation, מסירת הודעות ל-completion port אינה מובטחת. התראה שלא הגיעה אינה אומרת שהאירוע לא קרה. לניטור שצריך ודאות, משלבים עם polling של נתוני accounting דרך QueryInformationJobObject, ומחזיקים את process handles בעצמכם כדי לקבוע אם process חי או מת.
- יש ל-.NET API רשמי ל-Job Object?
- לא. ל-System.Diagnostics.Process אין מושג של Job, וגם ל-BCL אין wrapper. התשובה המעשית היא לקרוא ל-CreateJobObject / SetInformationJobObject / AssignProcessToJobObject דרך P/Invoke, או לייצר את החתימות עם CsWin32, ה-source generator של Microsoft, ולכתוב wrapper דק. אם עוטפים את handle ה-Job ב-SafeHandle וסוגרים אותו ב-Dispose של IDisposable, המשמעות של KillOnJobClose, "משך החיים של ה-wrapper = משך החיים של עץ ה-child processes", מופיעה ישירות בקוד.
- איך מתכננים למחשב מכשיר שרץ Windows 7?
- ב-Windows 7 ומוקדם יותר process יכול להשתייך ל-Job אחד בלבד, וקינון אינו אפשרי. אם ה-SDK של הצד השני משתמש ב-Job משלו, ה-AssignProcessToJobObject שלכם נכשל. JOB_OBJECT_LIMIT_BREAKAWAY_OK הוא דגל שמאפשר ל-process שכבר ב-Job שלכם ליצור child מחוץ ל-Job עם CREATE_BREAKAWAY_FROM_JOB; זה לא קסם שמעביר Assign שני אחרי שה-process כבר בפנים. כלומר, נתיב הבריחה קיים רק כשצד ה-SDK מבקש breakaway בזמן היצירה. אם אי אפשר לצפות לזה, האפשרות היחידה היא לשנות את התכנון לפני ההפעלה על ההנחה שיש רק Job אחד, שלכם. התיעוד של Microsoft גם מראה איך לנהל את העץ עם שני דגלי מגבלת breakaway בסביבות שבהן קינון אינו זמין. עם זאת, זו מערכת הפעלה מחוץ לתמיכה, ולכן אם בכלל אפשר, מעבר קודם.