Killing all child processes when the parent exits (Job Object)

 
 
  • Gérald Barré

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:

Shell
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:

Shell
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.

NativeMethods.txt
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:

C#
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.

Shell
dotnet add package Meziantou.Framework.Win32.Jobs
C#
// 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!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub