Killing all child processes when the parent exits (Job Object)
A Job Object allows groups of processes to be managed as a unit. This is useful for managing the lifetime of a group of processes, for example, when you want to terminate a group of processes when one of them terminates. It is also useful for managing the resources consumed by a group of processes, for example, when you want to limit the amount of memory consumed by a group of processes.
One use case of Job Objects is to kill all processes of a hierarchy when the root process is killed. In this post, I will show how to use Job Objects in .NET and associate a process with a Job Object.
Let's create a new console application:
dotnet new console
You'll need to use native win32 methods to create the Job Object. Instead of writing the [DllImports]
yourself, you can use the Microsoft.Windows.CsWin32
source generator. I've already written about this package in this post: Generating PInvoke code for Win32 APIs using a Source Generator. You can add the package using the following command:
dotnet add package Microsoft.Windows.CsWin32 --prerelease
The next step is to instruct the source generator to generate the methods you need. You need to create a file named NativeMethods.txt
at the root of the project. This file contains the list of methods and constants you want to use.
CreateJobObject
SetInformationJobObject
AssignProcessToJobObject
JOBOBJECT_EXTENDED_LIMIT_INFORMATION
Finally, you can create the Job Object and associate a process with it. Here's the annotated code:
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Windows.Win32;
using Windows.Win32.System.JobObjects;
// The code generated by Microsoft.Windows.CsWin32 needs unsafe code.
unsafe
{
// Create the Job Object
var atts = new Windows.Win32.Security.SECURITY_ATTRIBUTES
{
// Ensure the handle is not duplicated in the child processes.
// This would break JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE as there
// all child processes would have a handle for the Job Object.
// Thus, closing the root process would not terminate all processes
// in the hierarchy as the Job object would still be referenced by
// other child processes.
bInheritHandle = false,
lpSecurityDescriptor = (void*)null,
nLength = (uint)Marshal.SizeOf<Windows.Win32.Security.SECURITY_ATTRIBUTES>(),
};
using var jobHandle = PInvoke.CreateJobObject(atts, lpName: null);
if (jobHandle.IsInvalid)
throw new Win32Exception(Marshal.GetLastWin32Error());
// Configure the Job Object to kill all processes when the root process is killed.
var info = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION
{
// Kill all processes associated to the job when the last handle is closed
LimitFlags = JOB_OBJECT_LIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
},
};
if (!PInvoke.SetInformationJobObject(
jobHandle,
JOBOBJECTINFOCLASS.JobObjectExtendedLimitInformation,
&info,
(uint)Marshal.SizeOf<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>()))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
// Assign the Job object to the current process.
// As we don't allow child processes to escape from the Job object in the LimitFlags using
// JOB_OBJECT_LIMIT_BREAKAWAY_OK or JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK, all child
// processes will be associated to this Job Object automatically.
if (!PInvoke.AssignProcessToJobObject(jobHandle, Process.GetCurrentProcess().SafeHandle))
throw new Win32Exception(Marshal.GetLastWin32Error());
Process.Start("ChildApp.exe");
Console.ReadKey();
// jobHandle is disposed (using statement), so all processes associated to
// the Job object are killed
}
You can use Process Explorer to view the Job Objects associated to a process and their configuration.
You can now kill the root process and see all child processes get killed automatically.
#NuGet Package
The NuGet package Meziantou.Framework.Win32.Jobs allows to create and configure Job Objects.
dotnet add package Meziantou.Framework.Win32.Jobs
// Create the Job object and assign it to the current process
using var job = new JobObject();
job.SetLimits(new JobObjectLimits()
{
Flags = JobObjectLimitFlags.DieOnUnhandledException |
JobObjectLimitFlags.KillOnJobClose,
});
// You can set additinal restrictions if needed
// job.SetUIRestrictions(...);
// job.SetCpuRateHardCap(...);
// job.SetCpuRate(...);
// job.SetCpuRateWeight(...);
// job.SetNetRateLimits(...);
// job.SetSecurityLimits(...);
// job.SetIoLimits(...);
job.AssignProcess(Process.GetCurrentProcess());
var process = Process.Start("child");
process.WaitForExit();
#Additional resources
Do you have a question or a suggestion about this post? Contact me!