Revision history (first version, published Aug 29, 2026)
- First published
Cite this article(DOI: 10.5281/zenodo.22640263)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). What Is Left After the Parent Dies — Keeping Child Processes in a Job Object. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22640263 https://comcomponent.com/en/blog/windows-job-object-child-process-lifetime/
- DOI (latest version)
- 10.5281/zenodo.22640263
- DOI (this version)
- 10.5281/zenodo.22640264
You ended the UI in Task Manager, yet the camera cannot be reopened. The monitoring app is gone, yet the SDK’s helper is still holding the COM port. Restarting the parent produces a second instance, and problems appear in the shared memory and named pipes. Once you isolate a device SDK in a separate process, you run into these “after the parent dies” failures.
The starting point for thinking about the cause is that on Windows, child and grandchild processes do not terminate automatically when the parent process exits. The parent-child relationship at launch and lifetime management at exit are two different things. The tool that fills this gap is the Job Object.
This article first confirms why the parent’s exit handling alone is not enough, then organizes how to put processes into a Job, the termination policy, and the monitoring method. Finally, it connects these to the failures that occur in device integration and to the investigation procedure.
Intended readers are WinForms / WPF / service developers who isolate a device SDK in a separate process. The prerequisite environment is Windows 10/11 (for the parts that use nested jobs and PROC_THREAD_ATTRIBUTE_JOB_LIST), and the code is shown in C++ (Win32 API) and C# (.NET 6 or later). The difficulty is intermediate.
This article extends the “Not Responding”, shutdown, sleep/resume, and named pipes articles, and deals with the lifetime outside the process.
1. The Bottom Line First
The starting point of the design is deciding “what must not be left behind, and what you want to keep” before deciding “how to terminate”.
A Job Object is a mechanism that makes a process tree one unit. However, forcibly terminating the descendants at the moment the parent disappears and keeping those descendants’ dumps or final state do not, as they stand, go together. Decide whether you are reclaiming the device the process holds or preserving diagnostic material first.
- The unit that manages lifetime is the Job, not the parent-child lineage. It attaches limits, notifications, and bulk termination to a group of processes. Once a process is assigned it cannot leave until it exits, and on Windows 8 and later Jobs can be nested.12
- Settle the Job membership before the child runs. If you Assign after launch, you miss the grandchildren born in between.
CREATE_SUSPENDEDandJOB_LISTat creation close different race windows (Chapter 4). - Automatic reclamation and preserving diagnostic information are chosen as a termination policy. The trigger condition for KillOnJobClose is “the last Job handle closes”. It is strong against a parent crash but weak for post-mortem analysis of the descendants that are forcibly terminated along with it, so if you need both, the monitoring side collects first and terminates afterward (Chapter 5).3
What a measurement app wants is not the killing itself. It is not leaving behind a process that holds the device, and being able to observe abnormal exits.
| What you want to know | Chapters to read |
|---|---|
| Why the parent’s exit handling alone is not enough | Chapters 2–3: the parent-child relationship and the role of the Job |
| How to manage descendants without missing any | Chapters 4–6: creation, termination policy, monitoring |
| What becomes a problem with SDKs and services | Chapters 7–9: failure cases, resource limits, nesting and breakaway |
| What to investigate in the field, and how to choose | Chapters 10–11: investigation procedure and decision table |
The knowledge map below is for reviewing how the elements relate to one another. If you would rather start from the mechanism, go on to Chapter 2.
In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (21 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle
2. Why WaitForExit Is Not Enough
“Waiting” for an Exit and Tying Lifetimes Together Are Different Things
Process.WaitForExit() is an API for “the parent waits for the child to exit”. What this article considers is the opposite direction: what to do with the child when the parent dies first. With the Windows parent-child relationship alone, the parent’s exit is not conveyed to the child.
Process.Kill() and CloseMainWindow() do not, by themselves, take care of grandchild processes or the device handles those grandchildren hold either. The handles of the process that exited are released, but the problem is the handles held by the descendants that survived.
.NET’s Kill(entireProcessTree: true) walks the descendants and terminates them, but it can miss processes created during the enumeration or after the parent died first, and it is never called once the parent itself has crashed. You need a mechanism that does not depend solely on the parent’s cleanup code.
For each of the main reasons a parent exits, here is what is left in the field.
| Why the parent dies | What happens to the child | What is left in the field |
|---|---|---|
| UI closed with the X button, exit handling incomplete | Nothing (the Process object is merely disposed) |
Helper process, camera lock |
| Only the parent killed in Task Manager | The child keeps living | COM port, USB, shared memory |
| Crash from an unhandled exception | No guarantee that the parent’s finally runs |
Temporary files, exclusive locks |
| Service stop timeout | The SCM takes care of the parent only | Children left behind in session 0 |
flowchart TB
accTitle: The gap between the parent's lifetime and the device's occupancy lifetime
accDescr: When the parent process exits, the parent's wait and its Process object disappear, but the child and grandchild processes keep living, and their hold on device handles, named pipes, and lock files remains as well
parent["Parent process exits"] --> gone["Gone: parent's wait, Process object"]
parent --> live["Left: child and grandchild processes"]
live --> dev["Hold on device handles"]
live --> pipe["Server side of named pipes"]
live --> lock["Lock files, shared memory"]
Figure 1: The parent’s lifetime and the device’s occupancy lifetime are out of step. The parent’s cleanup code is least likely to run precisely when the parent dies an abnormal death.
The Target Is the Case Where “the OS Is Running and Only the Parent Exited”
The flows in which shutdown or sleep stops the whole OS belong to the shutdown article and the sleep/resume article. This article deals with the case where the OS stays healthy and only the parent exits. In measurement environments this is the more frequent case, and the leftover processes are harder to notice.
3. What a Job Object Is
The Basic Operations Are Create, Assign, Set, and Query
A Job Object is a kernel object that manages a group of processes as one unit. Dividing the basic operations by role gives the following four.14
| API | Role |
|---|---|
CreateJobObject |
Create a Job that no process belongs to yet |
AssignProcessToJobObject |
Assign a process to the Job |
SetInformationJobObject |
Set limits and other settings |
QueryInformationJobObject |
Read accounting information such as CPU time, page faults, and process count |
Membership is irreversible; a process cannot leave until it exits. Also, the accounting information includes totals accumulated by processes that have already exited.
A child that a member process creates with CreateProcess belongs to the same Job by default. In other words, the core of a Job’s value is that grandchildren and great-grandchildren enter automatically.1 The paths that escape membership, such as breakaway and proxy launches via WMI, are examined separately in Chapter 9.
flowchart TB
accTitle: Basic structure of a Job Object
accDescr: A child and a grandchild belong to the Job Object the parent process created, and the Job enforces limits, sends notifications to a completion port, and performs bulk termination per process tree
parent["Parent process"] --> job["Job Object"]
job --> child["Child (device SDK host)"]
child --> gc1["Grandchild (vendor helper)"]
job -.-> lim["Limits (memory, CPU)"]
job -.-> note["Notifications (completion port)"]
job -.-> kill["Bulk termination"]
Figure 2: A Job is a container that provides three things per process tree: limits, notifications, and bulk termination.
OS Generation Differences, and How It Differs from a Sandbox
On Windows 7 and earlier a process could belong to only one job; from Windows 8 on, nesting (multiple membership) became possible.5 The body of this article assumes Windows 10/11; the points to watch on Windows 7 and earlier are covered in Chapter 9 and the FAQ.
Putting a process into a Job does not make it a container or a sandbox. Network access cannot be restricted, and the access token (privileges) is a separate mechanism. UI restrictions alone cannot create a security boundary either. The role in this article is strictly to make the lifetime and resources of a process tree one unit.
4. The Right Way In — The Race Between Creation and Assignment
If You Assign After Launch, Grandchildren Are Born in Between
A child process can spawn grandchildren in the first few milliseconds after it starts running. An SDK launching its helper is the typical case. If you obtain the PID after Process.Start() and then Assign, any grandchild born before the Assign ends up outside the Job.
flowchart TB
accTitle: Assigning after the child starts running misses grandchildren
accDescr: The child is already running right after Process.Start, and any grandchild it spawns during the race window before AssignProcessToJobObject is called ends up outside the Job
s["Child starts running at Process.Start"] --> w["Race window until Assign"]
w --> g["Grandchildren born in this window"]
g --> out["Keep running outside the Job"]
s --> a2["AssignProcessToJobObject"]
a2 --> in2["Only grandchildren born afterward get in"]
Figure 3: The race window may be only a few milliseconds, but the SDK helper launch happens exactly there.
Procedure A: Create Suspended, Assign, Then Run
The classic method with the widest compatibility uses CREATE_SUSPENDED. It closes the window in which the child runs first and spawns grandchildren with the following order.67
- Create the Job with
CreateJobObject - Set the limits first with
SetInformationJobObject - Call
CreateProcesswithCREATE_SUSPENDED(the initial thread does not run) - Put it in with
AssignProcessToJobObject - If that fails, do not Resume; call
TerminateProcesson the spot (do not let a single instruction run outside the Job) - Run it with
ResumeThread
What Procedure A closes is the race window against grandchild creation. The window against a crash of the parent itself remains. If the parent crashes between steps 3 and 4, a suspended child that is not yet in the Job is left behind. It does not run, but it does not disappear on its own either.
If you want to close this window including resilience to a parent crash, use Procedure B below.
flowchart TB
accTitle: Procedure for launching with SUSPENDED and then putting the child in the Job
accDescr: Create the Job and set limits, launch the child with CREATE_SUSPENDED, assign it with AssignProcessToJobObject, stop it with TerminateProcess without resuming if that fails, and run it with ResumeThread if it succeeds
a["Create the Job with CreateJobObject"] --> b["Limits with SetInformationJobObject"]
b --> c["Launch child with CREATE_SUSPENDED"]
c --> d["AssignProcessToJobObject"]
d -->|"success"| e["Run with ResumeThread"]
d -->|"failure"| f["Terminate at once, no Resume"]
Figure 4: The skeleton of Procedure A. An implementation that omits the “if it fails, do not run it” branch produces a stray process only when things go wrong.
// C++: the minimal core of Procedure A (error handling is skeleton only)
HANDLE job = CreateJobObjectW(nullptr, nullptr); // Unnamed is fine. Do not make it 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(); // Save it before CloseHandle overwrites it
CloseHandle(job); // Do not run the child in a Job without limits
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); // Do not leak the Job handle on launch retries
return HRESULT_FROM_WIN32(err);
}
if (!AssignProcessToJobObject(job, pi.hProcess)) {
DWORD err = GetLastError(); // Save it before Terminate overwrites it
TerminateProcess(pi.hProcess, 1); // Do not let it run outside the Job
// Close the handles and raise err as the error
} else if (ResumeThread(pi.hThread) == (DWORD)-1) {
DWORD err = GetLastError();
TerminateProcess(pi.hProcess, 1); // Do not leave it behind suspended
// Close the handles and raise err as the error
}
CloseHandle(pi.hThread);
// On success, ownership of job and pi.hProcess moves to the caller's lifetime
// management object (the equivalent of the C# wrapper in Chapter 4). Closing job
// fires KillOnJobClose, and pi.hProcess is used in Chapter 6 to settle "who died"
Procedure B: On Windows 10 and Later, Create the Process Already Inside the Job
Put the Job handle on the attribute list of STARTUPINFOEX with PROC_THREAD_ATTRIBUTE_JOB_LIST. Then pass it to CreateProcess with the EXTENDED_STARTUPINFO_PRESENT flag. Without the flag, the structure is not interpreted as the extended one, and the attributes are ignored.89
With this method, the process belongs to the Job before its initial thread runs. The race window of “created, but not yet a member” no longer exists at all, so neither SUSPENDED nor the branch that Assigns after creation and handles the failure is needed.
| Method | When membership is settled | Remaining caveats |
|---|---|---|
| Procedure A: SUSPENDED → Assign → Resume | After the child is created, before its initial thread runs | If the parent crashes before the Assign, a stopped child is left behind |
| Procedure B: create with JOB_LIST | At process creation | Requires Windows 10 or later. Specify the attribute list and the extended startup flag |
flowchart TB
accTitle: How the race windows of Procedure A and Procedure B differ
accDescr: Procedure A creates the process suspended and then Assigns and Resumes, so it needs a branch that terminates on failure, whereas Procedure B creates the process with the Job on the attribute list, so it is already a member the moment it is born and has neither a race window nor a failure branch
a1["Procedure A: create suspended"] --> a2["Join with Assign"]
a2 --> a3["Start with Resume"]
a2 -.-> a4["Failure branch required"]
b1["Procedure B: create with attribute list"] --> b2["Member the moment it is born"]
b2 -.-> b3["No race window, no failure branch"]
Figure 5: Procedure A is “put it in, then run it”; Procedure B is “born already inside”. If you can assume Windows 10 or later, the reason to choose it is precisely the presence or absence of the race window and the failure branch.
Three Implementations to Avoid in Either Procedure
- Obtaining the PID after
Process.Start()and then putting it in (the grandchildren get out first) - Letting the child inherit the Job handle (the child keeps holding the handle even after the parent dies, so KillOnJobClose no longer fires — Chapter 5)
- Swallowing an Assign failure and continuing operation (a device process outside the Job is what drives the next failure)
In .NET, Express the Owner of the Job Handle in Code
System.Diagnostics.Process has no concept of a Job, and there is no official wrapper. Write a thin wrapper with P/Invoke or CsWin32.
The point is to wrap the Job handle in a SafeHandle and make it IDisposable. Closing the last Job handle in Dispose() fires KillOnJobClose. The design intent that “the wrapper’s lifetime is the child tree’s lifetime” can be expressed as ownership.
Below is a skeleton that shows handle ownership. Creation is done on the P/Invoke side of Procedure A/B, and a real project also needs CsWin32 configuration, unsafe designations, and so on.
// C#: a thin wrapper responsible only for owning the Job handle (creation goes through the P/Invoke of Procedure A/B)
sealed class ChildProcessJob : IDisposable
{
private readonly SafeFileHandle _job; // Keep holding it in a field
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(); // Save it before Dispose
_job.Dispose(); // Do not hand out a Job without KillOnJobClose
throw new Win32Exception(err);
}
}
public void Dispose() => _job.Dispose(); // The tree underneath terminates here
}
Conversely, if you close the handle without meaning to close it, you terminate the child tree. Keep a reference to the wrapper for the parent’s entire lifetime. If you do not, the child tree is wiped out for no reason the moment the GC collects the SafeHandle.
5. KillOnJobClose — “Keeping” and “Dying Together”
The Trigger Is “the Last Handle Closing”, Not “the Parent’s Death”
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE is a limit flag that terminates every process under the Job when the last Job handle is closed.3
Whether the parent exits on an exception, is killed in Task Manager, or is force-stopped as a service, the kernel closes all of that process’s handles. If that was the last Job handle, the children and grandchildren terminate. The strength of this mechanism is that it does not assume the parent’s exit handling runs.10
However, if you let the child inherit the Job handle, the handle remains after the parent exits. In that case it is not “the last handle”, and the child tree does not terminate.
flowchart TB
accTitle: KillOnJobClose timeline
accDescr: Whether the parent exits normally, crashes, or is force-terminated, the kernel closes all of the parent's handles, and if that was the last Job handle the Job closes and the process tree underneath is terminated at once
die["Parent disappears (including a crash)"] --> close["Kernel closes all handles"]
close --> last{"Last Job handle?"}
last -->|"Yes"| killall["Terminate the whole tree underneath"]
last -->|"No (inherited)"| stay["Children keep living"]
Figure 6: The trigger is “the last handle closed”, not “the parent died”. That is why the handle must not be inherited by the child.
Choose Between Reclaiming Immediately and Preserving Diagnostic Material
What you lose with forced termination is not just the hold on the device. You also lose the chance to take a crash dump of the children and grandchildren that are terminated along with the parent, the last valid frame, and the chance to flush a half-written measurement file.
The parent’s own crash dump is a different matter. WER handles the unhandled exception while the parent is still alive, so the parent’s dump can be written before the handles are closed. What is at issue here is the post-mortem material of the children and grandchildren on the side being forcibly terminated.
| Policy | Suited to | What you lose |
|---|---|---|
| With KillOnJobClose | Sites where a double open of the device is the worst outcome | Post-mortem material, the last samples |
| Without (monitoring only) | Sites where dumps and logs are assets | Orphaned processes and held ports if neglected |
Without, plus a monitoring process calling TerminateJobObject |
When a separate control service exists | The implementation is duplicated |
As input for the decision, list “what must not be left behind” and “what you want to keep” first.
What must not be left behind: an open camera or digitizer, exclusive use of a serial or USB port, the server side of a named pipe, a license dongle session, shared memory and lock files.
What you want to keep: crash dumps, the last valid frame or counters, the chance to send a command that returns the device to a safe state (if possible, send it before killing).
flowchart TB
accTitle: Kill, or monitor only?
accDescr: If a double open of the device is the worst outcome, set KillOnJobClose; if crash dumps and the last frame are assets, do not set it and monitor instead; if a separate control service exists, call TerminateJobObject from there
q{"What do you protect the moment it dies?"} -->|"Releasing the device comes first"| k["With KillOnJobClose"]
q -->|"Dumps and final state are assets"| m["Monitor only (do not kill)"]
q -->|"A separate control service exists"| t["Monitoring side calls TerminateJobObject"]
Figure 7: Choose by “what do you protect the moment it dies”, not by “kill or not”. If you want both, you end up with the third row’s arrangement, in which the monitoring side takes the dump first and then tears the tree down.
Hand the Job Handle to the Monitoring Side While the Parent Is Alive
The third row of the table, the arrangement in which a monitoring process terminates the tree with TerminateJobObject, needs preparation. The monitoring side must obtain the Job handle before the parent dies.
The Job created by this article’s procedure is unnamed, so there is no way to reach it from outside once the parent is gone. There are two ways to hand it over.
| Method | What to do while the parent is alive |
|---|---|
| Duplicate the unnamed Job’s handle | Pass the handle to the monitoring process with DuplicateHandle |
| Use a named Job | Create it named from the start; the monitoring side opens it with OpenJobObject and holds it |
Because names can collide globally, include a unique GUID or similar. If you forget this preparation, the monitoring side has no means of terminating the tree even when it detects the parent’s abnormality.
Take Dumps and Make the Device Safe Before Forced Termination
A child terminated by KillOnJobClose gets no warning, just as with TerminateProcess. No unhandled exception occurs, so even if WER (LocalDumps) is configured on the child, no dump of that forced termination is left. What WER can catch is the case where the child exits because of its own crash.
If the requirement is “both automatic reclamation and dumps”, move the responsibility for terminating to the monitoring side. The order is: take the dump while the target is alive, return the device to a safe state if necessary, and finally terminate with TerminateJobObject.
flowchart TB
accTitle: Tearing down so that automatic reclamation and dumps coexist
accDescr: When the monitoring process detects an abnormality, it takes a dump first, sends a command that returns the device to a safe state if necessary, and finally tears the tree down with TerminateJobObject, so that automatic reclamation and post-mortem analysis coexist
det["Monitoring side detects an abnormality"] --> dmp["Take the dump first"]
dmp --> safe["Return the device to a safe state"]
safe --> term["Tear down with TerminateJobObject"]
Figure 8: The only answer to “both automatic reclamation and dumps”. Reverse the order and the target to dump no longer exists.
In an arrangement where KillOnJobClose forcibly terminates the child at the moment the parent disappears, the child also has no chance to send a “return the device to a safe state” command. For devices that need it, choose the third row’s arrangement, and have the monitoring side send the safing command first and then call TerminateJobObject.11
6. Waiting for “Empty” with a Completion Port
Waiting on the Job Handle Alone Cannot Confirm That the Tree Has Exited
The Job handle does not become signaled when all the processes under it have exited. It is signaled only when all the processes were terminated because the job time limit was exceeded.12
To “move on once the child tree is empty”, associate an I/O completion port (IOCP) with the Job.1314 Do the association while the Job is empty, before putting any process in. If you associate it midway, you may miss the notifications of processes whose state changed during the association.15
Observe Creation, Exit, Abnormality, and Zero with Four Messages
| Message | What it tells you |
|---|---|
JOB_OBJECT_MSG_NEW_PROCESS |
A process joined the Job. Also detects the creation of grandchildren |
JOB_OBJECT_MSG_EXIT_PROCESS |
A process exited |
JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS |
A process exited with an abnormal exit code such as an access violation. Especially important in measurement apps13 |
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO |
The active process count reached 0 |
All a NEW_PROCESS packet carries is the new PID. It does not tell you the parent-child relationship, that is, who spawned the process. If you need the lineage, add another means such as ETW.
sequenceDiagram
accTitle: Flow of completion port notifications
accDescr: The Job posts process creation, exit, abnormal exit, and zero messages to the completion port, a dedicated monitoring thread receives them with GetQueuedCompletionStatus, and only the results are passed to the UI thread
participant J as Job Object
participant P as Completion port
participant W as Monitoring thread
participant U as UI thread
J->>P: NEW / EXIT_PROCESS
J->>P: ABNORMAL_EXIT / ZERO
W->>P: Wait for completion packet
P-->>W: Message and PID
W-->>U: Pass only the result notification
Figure 9: Run GetQueuedCompletionStatus on a dedicated thread. Wait on the UI thread, and the UI goes “Not Responding” every time a child misbehaves.
Wait on a Dedicated Thread and a Dedicated Port, and Query the Accounting on Timeout
Run this monitoring loop on a completion port created solely for the Job’s notifications. If you piggyback on the same port as existing I/O, the packet has already been dequeued by the time GetQueuedCompletionStatus returns. If you discard it with continue because the key differs, the owner of that I/O waits for its completion forever. The same goes for the failed-packet branch.
If you do share, you need a separate mechanism that delivers to each owner by key. This article separates the ports and passes only results to the UI thread.
It also assumes that one Job is one launch generation, recreated on every launch. If you reuse it across retries, TotalProcesses includes the previous generation, and the guard that distinguishes an empty Job before launch stops working.
// C++: skeleton of the monitoring thread (timeout + accounting as insurance against missed notifications)
DWORD msg; ULONG_PTR key; LPOVERLAPPED info;
bool treeEmpty = false;
while (!treeEmpty) {
if (!GetQueuedCompletionStatus(iocp, &msg, &key, &info, 5000)) {
if (info != nullptr) continue; // Completion packet of a failed I/O. Keep monitoring
if (GetLastError() != WAIT_TIMEOUT) break; // Port destroyed and the like: stop
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION acct = {};
if (QueryInformationJobObject(job, JobObjectBasicAccountingInformation,
&acct, sizeof(acct), nullptr))
treeEmpty = (acct.TotalProcesses > 0 && // Do not mistake an empty pre-launch Job for completion
acct.ActiveProcesses == 0); // Insurance for a dropped ZERO notification
// Assumption: the Job is recreated on every launch (1 Job = 1 launch generation).
// If the Job is reused across retries, TotalProcesses still counts the previous
// generation, and this guard cannot tell the generations apart
continue; // On query failure, do not conclude it is empty
}
if ((HANDLE)key != job) continue; // Match against the CompletionKey used at association.
// This port is assumed to be created solely for Job monitoring
// (see the text below. Discarding like this on a shared port
// makes the owners of other I/O wait forever)
DWORD pid = (DWORD)(UINT_PTR)info; // Some messages carry a PID
switch (msg) {
case JOB_OBJECT_MSG_NEW_PROCESS: /* log grandchild creation */ break;
case JOB_OBJECT_MSG_EXIT_PROCESS: /* log exit */ break;
case JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS:/* abnormal exit: go check for a dump */ break;
case JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO: treeEmpty = true; break;
} // The switch's break alone does not end the wait
}
Notifications, Accounting, and Handles Each Settle Different Information
Notifications carry no delivery guarantee in principle. The only guaranteed ones are the notifications of the limits set with JobObjectNotificationLimitInformation. You cannot conclude “no notification = it did not happen”.15
Aggregate state such as “has it become empty” is confirmed by polling the accounting information as well. But accounting is a set of aggregate counters, so it cannot reconstruct the PID, the exit code, or whether the exit was abnormal for a dropped EXIT/ABNORMAL_EXIT. That is the domain of holding process handles and of ETW.
ACTIVE_PROCESS_ZERO is not evidence of a normal exit either. The count may have reached zero through forced termination, and the message itself does not distinguish. Judge the quality of the exit by EXIT/ABNORMAL_EXIT and the exit code.
Do Not Decide It Is the Same Process by PID Alone
The PID in a completion packet is reused. Unless you hold a process handle, there is no guarantee that the PID still refers to the same process.13
What the pi.hProcess held in Chapter 4 can pin down is only the PID of the child you launched directly. For grandchildren spawned by the SDK, call OpenProcess at the point NEW_PROCESS arrives to obtain a handle, and from then on match against that handle.
Even so, a short window remains between the notification and the Open. If the PID is reused in that interval, you grab a different process. When strict identification of the individual process is required, cross-check with telemetry that carries a creation time, such as ETW process-start events.
flowchart TB
accTitle: Monitoring that does not rely on notifications alone
accDescr: Because completion port messages are for notification and carry no delivery guarantee, combine them with polling of accounting information and holding process handles to prepare for misses and PID reuse
n["Completion port notifications"] --> miss["No delivery guarantee (notification purpose)"]
miss --> poll["Poll accounting information too"]
n --> pid["PIDs are reused"]
pid --> hold["Hold process handles"]
Figure 10: Notifications are the main path; accounting and handles are the insurance. Only with both can you say you are “observing”.
Incidentally, implementations that “kill the thread waiting for the Job to empty with TerminateThread” once circulated, but with a completion port there is no need to forcibly terminate the waiting thread. Raymond Chen has also published an article rewriting this old pattern to the completion port approach.16
7. Failures That Actually Happen in Measurement and Device Integration
Here the mechanisms covered so far are applied to six failures that happen in device environments. For each case, in addition to the cause and the remedy, we confirm “what was left behind”.
Failure 1: Only the Parent Dies, and the Camera Stays Open
In a setup where the vendor SDK has a helper process for frame transfer, killing the parent UI in Task Manager leaves only the helper. The restarted parent gets device busy during SDK initialization. At some sites nothing recovers until the device is power-cycled.
What was left: the helper process and the exclusive open of the camera.
flowchart TB
accTitle: Only the parent dies and the camera stays open
accDescr: Force-ending the UI removes the parent, but the SDK helper process remains holding the camera handle, so the restarted parent fails to reopen with device busy
kill9["End the UI in Task Manager"] --> dead["Parent disappears"]
dead --> helper["SDK helper remains"]
helper --> busy["Still holding the camera"]
busy --> fail["device busy after restart"]
Figure 11: The true face of “the process is gone, yet the device cannot be opened”. The culprit is often not the process whose name was shown in Task Manager.
Failure 2: A Grandchild Ends Up Outside the Job
There are two paths, and the remedies must be separated.
(a) The SDK uses its own Job. This is the case where the other party is already in a different Job. On Windows 7 it is one job per process, so your Assign fails. On Windows 8 and later nesting can pick it up, but if your Job has UI restrictions, nesting itself is impossible (Chapter 9).
(b) The SDK spawns the grandchild with CREATE_BREAKAWAY_FROM_JOB. This works only when your Job allows BREAKAWAY_OK, and the grandchild is born outside the tree from the start. Nesting cannot pick it up either. If you do not allow it, the SDK’s creation fails, so if monitoring is the priority, the basic approach is not to allow it and to detect it as a failure.
What was left: a grandchild running outside the monitoring, and an Assign whose failure was swallowed.
flowchart TB
accTitle: Two paths by which a grandchild ends up outside the Job
accDescr: On the path where the SDK uses its own Job, nesting can pick it up on Windows 8 and later but fails if UI restrictions are set, whereas on the path where the SDK spawns the grandchild with breakaway, it works only if you allowed it and the grandchild is outside the tree from the start
g["Grandchild ends up outside the Job"] --> ja["(a) SDK uses its own Job"]
g --> jb["(b) Created with breakaway"]
ja --> nest["Win8+ picks it up by nesting"]
nest -.-> ui["Fails with UI restrictions"]
jb --> allow["Works only if allowed"]
allow -.-> out["Grandchild outside the tree from the start"]
Figure 12: The same “getting out”, but (a) leaves room to pick it up by nesting, while (b) is settled as unrecoverable the moment you allow it. The remedy starts with identifying the path.
Failure 3: A Device Process Launched from a Service
When a service stops, the SCM waits for the parent only; the children and grandchildren spawned in session 0 are outside the scope of the stop handling. Job + KillOnJobClose covers even this out-of-scope area. The design of an arrangement that spans a service and the interactive session is left to the user boundary article.
What was left: leftover processes in session 0, and a false positive in the duplicate-instance check at the next launch.
Failure 4: A Child That Dies a Month Later
If you do not periodically record the Job’s accounting (PeakJobMemoryUsed, I/O counters, total process count), you cannot trace afterward “which generation of the helper started to swell, and when”.17 The structure of dying a month later from a handle leak is as dissected in the industrial camera long-run failure article, and the Job’s accounting is the entry point to that investigation.
What was left: logs insufficient to settle the cause.
Failure 5: The Parent’s Exit Handling Waits for the Child to Exit
If you call WaitForExit or wait for the tree to exit on the UI thread, the parent goes “Not Responding” on the day the child freezes. Leave the exit wait to the IOCP thread, and put only progress and an abort button on the UI. The mechanism is as described in the “Not Responding” article.
What was left: a parent hung along with the child.
flowchart TB
accTitle: Do not wait for exit on the parent's UI thread
accDescr: Waiting for the child to exit on the UI thread propagates a child hang into the parent's Not Responding, so leave the exit wait to the IOCP monitoring thread and put only a progress display and an abort button on the UI thread
w2["Wait for child exit on the UI thread"] --> h2["The day the child hangs"]
h2 --> f2["Parent also Not Responding (dragged along)"]
ok2["Wait on the IOCP thread"] --> u2["UI has only progress and abort"]
Figure 13: The side that observes the child’s abnormality must not freeze because of the child’s abnormality. Just separating where you wait removes the collateral hang.
Failure 6: Fails Only Under a Debugger
Development tools and launchers may have already put your parent process into some Job. On Windows 8 and later nesting usually saves you, but on device PCs running 7 or earlier the Assign becomes ERROR_ACCESS_DENIED, producing differences such as “it does not work only on the development machine” or “only in production”. The standard first step is to check your own membership with IsProcessInJob.18
What was left: verification time spent unable to pin down the cause of the environment difference.
8. Which Limits to Attach
Limits Have a Purpose and Side Effects
Restricting the list to the limits that matter in a measurement app, here are the reasons to use each and the side effects.319
| Limit | Why you use it | If you overdo it |
|---|---|---|
| KILL_ON_JOB_CLOSE | Release the device when the parent disappears | Post-mortem material disappears |
| ACTIVE_PROCESS | Stop a runaway multiplication of the SDK’s children | Even legitimate helpers are refused creation |
| JOB_MEMORY / PROCESS_MEMORY | A cap on long-run leaks | Allocation of huge image buffers starts to fail |
| DIE_ON_UNHANDLED_EXCEPTION | No error dialogs on unattended machines | Interactive debugging becomes painful |
| CPU rate control | Keep an image-processing child from starving the UI | Frame deadlines are missed |
| BREAKAWAY_OK | Leave an escape route for an SDK that needs its own job | Processes vanish from monitoring |
| UI restrictions | Sandbox-like tightening | Nesting breaks (Chapter 9) |
Notification Limits Are for Observation; Enforced Limits Are for Refusal and Termination
Separate “loose limits for notification” from “limits that stop things when exceeded”. JobObjectNotificationLimitInformation only notifies you of the excess; the process keeps running.15
The Extended Limit limits are enforced, but the form of enforcement differs per limit.3
| Limit | What happens when it is exceeded |
|---|---|
| Memory limit | The commit operation that would exceed it fails. The process itself stays alive |
| ACTIVE_PROCESS | The creation or assignment that would exceed it fails. A process that exceeded it by assignment is terminated |
| Process time (PROCESS_TIME) | Only the process that exceeded it is terminated |
| Job time (JOB_TIME) | A limit on the aggregate value; by default all processes under the Job are terminated |
Set these without knowing the difference and you will misdiagnose “a helper that vanishes right after creation” as some other fault. For long-running operation, the safe order is to observe with notification limits first, and decide the enforced limits once the trend is known.
flowchart TB
accTitle: Limits for notification and limits for enforcement
accDescr: A JobObjectNotificationLimitInformation limit only notifies of the excess and the process keeps running, whereas Extended Limit limits are enforced: a memory limit fails the operation, ACTIVE_PROCESS fails creation and assignment, and time limits terminate the process
lim2{"Purpose of the limit?"} -->|"Observe"| ntf["Notification limit: keeps running when exceeded"]
lim2 -->|"Stop"| enf["Enforced limit: refuse or terminate"]
ntf --> log2["Identify the generation from accounting logs"]
enf --> die2["Commit failure, creation refused, termination"]
Figure 14: The same “limit”, but notification and enforcement are different things, and enforcement works differently per limit. Put up an enforced limit without observing first, and it fires falsely on a peak of normal operation.
The relationship between CPU rate control and periodic processing is left to the soft real-time article; here we go no further than a countermeasure for “the device process eating the UI”.
9. Nesting, Breakaway, and a Counterpart Already in a Job
Think of Nesting as “Containment of Process Sets”
The rules of nesting on Windows 8 and later can be organized into four.5
- The parent job is the wider set, and the child job is a subset of it (assigning in an order that breaks this containment fails)
- For the main resource limits, the most restrictive one along the chain takes effect
- A job with UI restrictions cannot be nested
- Notifications are also delivered to the completion ports of every parent job along the chain (the child job need not have a port)
flowchart TB
accTitle: Nested job hierarchy and effective limits
accDescr: The parent job is the wider set and the child job is a subset of it, and for the main resource limits the most restrictive value along the chain takes effect. A job with UI restrictions cannot be nested
pj["Parent job (wider set)"] --> cj["Child job (subset)"]
cj --> pr["Member processes"]
pj -.-> eff["Effective limit = most restrictive value"]
cj -.-> eff
ui["Job with UI restrictions"] -.-> no["Cannot be nested"]
Figure 15: Think of nesting as “containment of sets”. UI restrictions break nesting, so it is safer not to attach them to a Job used for lifetime management.
Breakaway Is the Path for Creating a Process Outside the Job from the Start
Breakaway is the legitimate path by which descendants created with CreateProcess leave the tree.3
| Job-side setting | Condition for the child to be born outside the Job |
|---|---|
JOB_OBJECT_LIMIT_BREAKAWAY_OK |
Create it with CREATE_BREAKAWAY_FROM_JOB specified |
SILENT_BREAKAWAY_OK |
No flag needed. Every child is born outside |
Sometimes it is needed because the SDK uses a Job of its own. However, a process born by that path is excluded from both bulk termination and monitoring. If you allow it, decide who manages what has escaped, too.
flowchart TB
accTitle: The path out of the tree by breakaway
accDescr: When BREAKAWAY_OK is set on the Job, a grandchild created with CREATE_BREAKAWAY_FROM_JOB is born outside the Job and vanishes from the scope of bulk termination and monitoring
j2["Job (with BREAKAWAY_OK)"] --> c2["Child process"]
c2 -->|"Normal creation"| in3["Grandchild also in the Job"]
c2 -->|"Creation with BREAKAWAY"| out3["Grandchild goes outside the Job"]
out3 --> lost["Outside monitoring and bulk termination"]
Figure 16: Breakaway has two faces: “an escape route for an SDK that needs it” and “a hole in monitoring”. If you attach it, decide who looks after what has escaped.
A Proxy Launch via WMI Cannot Be Prevented Even with Breakaway Forbidden
Paths on which a third-party process launches on your behalf, such as WMI’s Win32_Process.Create, are a different matter. The actual parent is the WMI provider, so the process born is outside the Job from the start. Forbidding breakaway does not close this hole.1
Whether the SDK uses this path is checked not from the NEW_PROCESS log but from the parent-child relationships in Process Explorer.
Check Membership in an Existing Job and the Windows 7-and-Earlier Constraints First
When the counterpart is already in a Job (Failure 6), the procedure is: check with IsProcessInJob → if nesting can be set up, Assign as is → if it cannot (Windows 7, or UI restrictions), change the design.18
On Windows 7 and earlier, you cannot Assign a counterpart that already belongs to another Job a second time. BREAKAWAY_OK is not a flag that removes an already-assigned process afterward. That escape route works only when the SDK side requests breakaway when it creates the child. If that cannot be expected, change the design before launch on the assumption of “one process, one job”.12
Decide Last Whether to Put the Parent Itself in the Job
Finally, a word on the design of “putting your own process into your own Job”. If the parent itself is also placed underneath, then when the parent crashes it too is included in KillOnJobClose’s targets, and the lifetime of the whole tree matches completely. But it is a double-edged sword that turns into an unintended mass termination if you get the handling of the Job handle wrong, so it is safer to start with “parent outside, only the child tree inside”.
10. How to Investigate
Investigate in the order membership → accounting and notification logs → device occupancy. This is the confirmation procedure for not concluding “nothing is left” merely by eyeballing process names.
- Process Explorer: the process properties have a Job tab, which shows the Job the process belongs to and its limits. This is the fastest way to confirm “which Job is this helper in”
IsProcessInJob: the entry point for checking your own or the counterpart’s membership from code18QueryInformationJobObject: periodically record Basic Accounting (total process count, CPU time) and Extended Limit (PeakJobMemoryUsedand so on)417- Keep the completion port log in a file: the NEW_PROCESS / EXIT / ABNORMAL_EXIT timeline becomes the only evidence in an investigation a month later
- Do not check for leftovers by PID: what to look at is device handles, pipe names, and lock files. “No process visible in Task Manager” does not mean “the device has been released”
flowchart TB
accTitle: Procedure for investigating leftovers
accDescr: First confirm membership with IsProcessInJob and the Job tab of Process Explorer, read the accounting with QueryInformationJobObject, and finally judge leftovers by device handles, pipe names, and lock files rather than by whether a process exists
s1["Confirm membership with IsProcessInJob"] --> s2["Job tab of Process Explorer"]
s2 --> s3["Accounting with QueryInformationJobObject"]
s3 --> s4["Judge leftovers by device handles and pipe names"]
Figure 17: Investigate in the order “membership → accounting → occupancy”. Do not conclude “nothing is left” merely by eyeballing process names.
11. A Rough Guide to Choosing (Decision Table)
Here the choices so far are summarized by situation. Go back to Chapter 5 for the termination policy, Chapter 6 for the monitoring assumptions, and Chapter 9 for the relationship with existing Jobs.
| Situation | Recommendation |
|---|---|
| The UI body and the device SDK have been split into separate processes | Put them in a Job, and monitor with a completion port |
| The worst case is the device being held after the parent disappears | Set KillOnJobClose |
| The frame or dump at the moment of the crash is an asset | Do not set KillOnJobClose; the monitoring side makes things safe and then calls TerminateJobObject |
| The vendor SDK spawns helpers | Create with SUSPENDED or JOB_LIST, and log NEW_PROCESS |
Assign returns ERROR_ACCESS_DENIED |
Check the existing job and whether nesting is possible first. Suspect UI restrictions |
| Spawning children in the interactive session from a service | Do not attach UI restrictions. Go back to the design in the user boundary article |
| Waiting for grandchildren to exit on the UI thread | Stop. Move it to the IOCP thread |
12. Summary
A child’s lifetime is not the lifetime of the parent’s Process object. Nor is the parent’s exit conveyed to the child automatically. A Job Object is the mechanism that makes this process tree one unit and handles limits, notifications, and bulk termination.
To manage down to the grandchildren, settle membership before the child runs. Procedure A is SUSPENDED + Assign; Procedure B, on Windows 10 and later, is JOB_LIST. KillOnJobClose works by the last Job handle closing regardless of why the parent exited, but it also loses the post-mortem material of the descendants terminated along with it.
That is exactly why you should list “what must not be left behind after the crash” and “what you want to keep” first, and choose between immediate reclamation and termination after the monitoring side has collected and made things safe. This is the design procedure this article wants to convey.
The next thing to write would be the specifics of launching processes across session 0 and the interactive session, or waiting on devices with overlapped I/O. Once you have the “outer lifetime” of a process under control, the lifetime of I/O is waiting next.
Related Articles
- A Checklist for Safely Handling Child Processes in Windows Apps — Best Practices for Job Objects, Exit Propagation, Standard I/O, and Watchdogs
- Why Windows Apps Go “Not Responding” — The Message Loop and How Hangs Happen
- Named Pipes in Practice — Windows’ Standard IPC from Design to Security
- Dying a Month Later from a Handle Leak — Anatomy of a Long-Run Failure in an Industrial Camera App (Part 1)
- How Far Can Windows Go with Soft Real-Time — A Practical Guide
- “The Same PC” Is Not the Same Execution Environment — The User Boundary Separating AppData, HKCU, DPAPI, and Credentials
Related Consulting Areas
KomuraSoft LLC handles the process-isolation design of Windows apps that work with cameras, measuring instruments, and serial/USB devices, the investigation of device-occupancy trouble such as leftover SDK helpers and device busy, and building monitoring and automatic-recovery mechanisms for long-running apps. Feel free to consult us even from a single case of “restart the parent and the device cannot be opened”.
- Windows Application Development
- Technical Consulting & Design Review
- Bug Investigation & Root-Cause Analysis
- Contact Us
References
-
Microsoft Learn, Job Objects. On a Job Object being a kernel object that manages a group of processes as one unit, child processes created by a member process being associated with the same Job by default (except via Win32_Process.Create), the two limit flags for breakaway, bulk termination with TerminateJobObject, and how to manage a process tree in environments where nesting is unavailable. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, AssignProcessToJobObject function (jobapi2.h). On the association between a process and a Job being irreversible, one job per process on Windows 7 and earlier with multiple membership (nesting) possible from Windows 8, and the effective limits and propagation of breakaway under nesting. ↩ ↩2
-
Microsoft Learn, JOBOBJECT_BASIC_LIMIT_INFORMATION structure (winnt.h). On the limit flags JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE (terminate all processes when the last Job handle closes), ACTIVE_PROCESS (a cap on simultaneously active processes), JOB_MEMORY (a commit cap for the whole job), DIE_ON_UNHANDLED_EXCEPTION, and BREAKAWAY_OK / SILENT_BREAKAWAY_OK. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, JOBOBJECT_BASIC_ACCOUNTING_INFORMATION structure (winnt.h). On the Job keeping accounting information such as total process count, CPU time, and page fault count, including totals accumulated by processes that have exited, and obtaining it with QueryInformationJobObject. ↩ ↩2
-
Microsoft Learn, Nested Jobs. On nested jobs forming a parent-child hierarchy (the child job is a subset of the parent job’s processes), a job with UI restrictions set being unable to nest, the effective limit being the most restrictive value along the chain, notifications being sent to all completion ports of the parent job chain, and the hierarchy being terminated from the lowest level. ↩ ↩2
-
Microsoft Learn, Process Creation Flags. On CREATE_SUSPENDED (create the initial thread suspended and do not run it until ResumeThread) and CREATE_BREAKAWAY_FROM_JOB (the caller’s Job must have 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). On the classic procedure of creating with CREATE_SUSPENDED and then putting the process in a Job, and how to close its race window. ↩
-
Microsoft Learn, UpdateProcThreadAttribute function (processthreadsapi.h). On PROC_THREAD_ATTRIBUTE_JOB_LIST assigning Job handles to the created child process in the specified order, and it being supported on Windows 10 / Windows Server 2016 and later. ↩
-
Raymond Chen, A more direct and mistake-free way of creating a process in a job object (The Old New Thing). On using PROC_THREAD_ATTRIBUTE_JOB_LIST to make a process belong to a Job from the moment of creation. ↩
-
Raymond Chen, Destroying all child processes (and grandchildren) when the parent exits (The Old New Thing). On the arrangement that terminates descendants together when the parent disappears using a Job with KILL_ON_JOB_CLOSE, and the importance of not letting the Job handle be inherited. ↩
-
Microsoft Learn, TerminateJobObject function (jobapi2.h). On forcibly terminating all processes associated with the Job, as if TerminateProcess had been called on each individually. ↩
-
Microsoft Learn, Job Objects - Managing Job Objects. On the Job object becoming signaled when all processes are terminated for exceeding the job time limit, the Job being destroyed when the last handle closes, and closure causing termination of all member processes when KILL_ON_JOB_CLOSE is specified. ↩
-
Microsoft Learn, JOBOBJECT_ASSOCIATE_COMPLETION_PORT structure (winnt.h). On the list of messages sent to the completion port such as JOB_OBJECT_MSG_NEW_PROCESS / EXIT_PROCESS / ABNORMAL_EXIT_PROCESS / ACTIVE_PROCESS_ZERO, the exit codes judged to be abnormal exits, PID reuse being impossible to rule out for messages that return a PID unless a process handle is held, and delivery of notifications not being guaranteed. ↩ ↩2 ↩3
-
Raymond Chen, How do I wait until all processes in a job have exited? (The Old New Thing). On waiting on the Job handle being unable to detect “became empty”, and the need to wait for JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO on the completion port. ↩
-
Microsoft Learn, Job Objects - Job Limits and Notifications. On associating the completion port while the Job is inactive being preferable (reducing the possibility of missing notifications for processes whose state changes during association), message delivery not being guaranteed except for the limits set with JobObjectNotificationLimitInformation, and processes continuing to run after exceeding a notification limit. ↩ ↩2 ↩3
-
Raymond Chen, Removing the TerminateThread from code that waits for a job object to empty (The Old New Thing). On rewriting the old pattern of killing the waiting thread with TerminateThread into a completion-port-based wait. ↩
-
Microsoft Learn, JOBOBJECT_EXTENDED_LIMIT_INFORMATION structure (winnt.h). On setting per-process and per-job memory limits, and obtaining peak memory with PeakProcessMemoryUsed / PeakJobMemoryUsed. ↩ ↩2
-
Microsoft Learn, IsProcessInJob function (jobapi.h). On determining whether a process is running in the specified Job (or in any Job). ↩ ↩2 ↩3
-
Microsoft Learn, JOBOBJECT_CPU_RATE_CONTROL_INFORMATION structure (winnt.h). On controlling the CPU rate (share of cycles or weight) per Job. ↩
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Why Arguments Break — The Rules of Windows Command-Line Arguments
Windows passes CreateProcess a single string that the receiver splits. Covers the CommandLineToArgvW, CRT, and .NET rules, ArgumentList, ...
Named Pipes in Practice — Windows' Standard IPC from Design to Security
A practical guide to named pipes, Windows' standard IPC. Covers byte vs. message mode, servers that handle multiple clients, ACL and impe...
The Win32 Thread Pool API — Concurrency That Does Not Create Threads, with CreateThreadpoolWork
Calling CreateThread everywhere in native code? A primary-source guide to the Win32 thread pool API: the work, timer, wait, and io object...
DllMain and the Loader Lock — The Real Reason You Are Told to "Do Nothing in DLL Initialization"
Why DllMain must not call LoadLibrary or wait on threads: the loader lock serializes DLL notifications, typical deadlocks, deferred initi...
Spurious Wakeups — Why a Condition Variable Wakes "Without a Notification" and How to Wait Correctly on Windows
Condition variable waits can wake with no notification (spurious wakeups). Why Windows allows it, and the correct while-and-predicate wai...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
Frequently Asked Questions
Common questions about the topic of this article.
- Is a Job Object a container or a sandbox?
- No. A Job Object is a kernel object that attaches limits, notifications, and bulk termination to a group of processes. It can impose caps on memory, CPU, and process count, but it cannot restrict network access, and the access token (privileges) does not change. It has UI restrictions, but those alone do not make it a security boundary. If isolation or security is the goal, you need to combine it with another mechanism such as AppContainer or containers. What this article covers is the use of treating the lifetime and resources of a process tree as one unit.
- Isn't Process.Kill() good enough?
- Process.Kill() terminates only that one process; it does not reach grandchild processes. Kill(entireProcessTree: true) in .NET Core 3.0 and later walks the descendants and terminates them, but because it enumerates the process tree from the parent-child relationships at that moment, it can miss processes born during the enumeration and processes whose lineage was cut because the parent died first. Also, neither method is called when the parent itself crashes. If you want descendants reclaimed regardless of whether the parent lives or dies, a Job Object plus KillOnJobClose, which entrusts the lifetime to the kernel, is the reliable choice.
- If the parent is in a Job, does the child enter the Job automatically?
- By default, yes. A child process created with CreateProcess by a process that belongs to a Job automatically belongs to the same Job. The exception is breakaway. If JOB_OBJECT_LIMIT_BREAKAWAY_OK is set on the Job and the child is created with the CREATE_BREAKAWAY_FROM_JOB flag, or if JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK is set, the child is born outside the Job. Note also that processes created via WMI's Win32_Process.Create are not associated with the Job.
- Can a process be removed from a Job once it has been put in?
- No. The association made by AssignProcessToJobObject is irreversible, and membership continues until the process exits. The design options are therefore three: do not put it in, create it outside from the start with breakaway, or nest a separate Job. There is no "remove it later". This irreversibility is also the reason the Job should be prepared before creation.
- Does KillOnJobClose work even when the parent crashes?
- Yes. JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE is a mechanism that terminates the processes underneath when the last Job handle is closed, and whether the parent exits normally, dies on an unhandled exception, or is killed in Task Manager, the kernel closes the handles as part of process cleanup, so it fires. However, if you let the child process inherit the Job handle, the handle the child holds survives the parent's death, so it is not the last handle and it does not fire. Do not let the Job handle be inherited.
- Are completion port notifications always delivered?
- Not always. The official documentation states explicitly that, except for notifications of the limits set with JobObjectNotificationLimitInformation, delivery of messages to the completion port is not guaranteed. A notification that did not arrive does not mean the event did not happen. For monitoring that needs certainty, combine it with polling of the accounting information via QueryInformationJobObject, and hold the process handles yourself to settle whether a process is alive or dead.
- Does .NET have an official Job Object API?
- No. System.Diagnostics.Process has no concept of a Job, and the BCL has no wrapper either. The practical answer is to call CreateJobObject / SetInformationJobObject / AssignProcessToJobObject through P/Invoke, or to generate the signatures with CsWin32, Microsoft's source generator, and write a thin wrapper. If you wrap the Job handle in a SafeHandle and close it in IDisposable's Dispose, the meaning of KillOnJobClose, "the wrapper's lifetime = the child process tree's lifetime", appears directly in the code.
- How should I design for a device PC running Windows 7?
- On Windows 7 and earlier a process can belong to only one Job, and nesting is not possible. If the other party's SDK uses a Job of its own, your AssignProcessToJobObject fails. JOB_OBJECT_LIMIT_BREAKAWAY_OK is a flag that allows a process already in your Job to create a child outside the Job with CREATE_BREAKAWAY_FROM_JOB; it is not magic that lets a second Assign through after the process is already in. In other words, the escape route exists only when the SDK side requests breakaway at creation time. If that cannot be expected, the only option is to change the design before launch on the assumption that there is only one Job, your own. Microsoft's documentation also shows how to manage the tree with the two breakaway limit flags in environments where nesting is unavailable. That said, it is an OS out of support, so if at all possible, migration comes first.