Set process memory limit with Process Governor

Today I would like to introduce you to Process Governor – a new tool in my .NET diagnostics toolkit. This application allows you to set a limit on a memory committed by a process. On Windows committed memory is actually all private memory that the process uses. I wrote this tool to test my .NET applications (including web applications) for memory leaks. With it I can check if under heavy load they won’t throw OutOfMemoryException.

Usage scenarios

The command line syntax is really simple, you just specify a memory limit using the -maxmem switch and then provide your application path and its arguments. You can either start a new process:

procgov.exe -maxmem 30M .\example\TestMemory.exe

or attach to an already running one:

procgov  -maxmem 30M -p 6040

The -p parameter corresponds to the process identifier (PID). The difference between those two is that when you start a process it has the memory constraint already set – so we will receive an OutOfMemory at the moment it occurs. When you attach to a process that already exceeded the memory limit it will be immediately terminated so no OutOfMemory will be thrown.

How it works?

Process Governor uses a system job object to apply constraints to a process. Unfortunately there is no managed API to work with jobs so I needed to import some functions from pinvoke.net. Let’s quickly analyze the source code of this application. Before we can assign a process to a job (and apply constraints on it) we need to obtain its handle. When attaching to a process we will use the OpenProcess function:

hProcess = CheckResult(ApiMethods.OpenProcess(ProcessAccessFlags.AllAccess, false, pid));

If we start a new process we will use the CreateProcess function:

PROCESS_INFORMATION pi;
STARTUPINFO si = new STARTUPINFO();

CheckResult(ApiMethods.CreateProcess(null, String.Join(" ", procargs), IntPtr.Zero, IntPtr.Zero, false,
            CreateProcessFlags.CREATE_SUSPENDED | CreateProcessFlags.CREATE_NEW_CONSOLE,
           IntPtr.Zero, null, ref si, out pi));

I haven’t used the Process.Create managed method as it does not allow to start a process in a suspended state.

After obtaining a process handle we can start working with a job object. First, we need to create it:

var securityAttributes = new SECURITY_ATTRIBUTES();
securityAttributes.nLength = Marshal.SizeOf(securityAttributes);

hJob = CheckResult(ApiMethods.CreateJobObject(ref securityAttributes, "procgov-" + Guid.NewGuid()));

Then we will create a completion port for listening to job events:

// create completion port
hIOCP = CheckResult(ApiMethods.CreateIoCompletionPort(ApiMethods.INVALID_HANDLE_VALUE, IntPtr.Zero, IntPtr.Zero, 1));
var assocInfo = new JOBOBJECT_ASSOCIATE_COMPLETION_PORT {
    CompletionKey = IntPtr.Zero,
    CompletionPort = hIOCP
};
uint size = (uint)Marshal.SizeOf(assocInfo);
CheckResult(ApiMethods.SetInformationJobObject(hJob, JOBOBJECTINFOCLASS.AssociateCompletionPortInformation,
        ref assocInfo, size));

// start listening thread
listener = new Thread(CompletionPortListener);
listener.Start(hIOCP);

The CompletionPortListener method has a simple switch and transforms message identifier to some meaningful messages. That’s a pity that there is no managed API for I/O completion ports. They seem to be one of the greatest mechanism in Windows to asynchronously communicate between threads. The name is a bit misleading as it suggests that there may be used only for I/O operations – the truth is that you may use them to any type of asynchronous communication between threads (the job notifier is one of examples). In our case, as we have only one process run in a job, one listening thread is enough.

Finally we apply a process memory limit (if provided) and assign the newly created process to our job:

if (maxmem > 0.0f) {
    // configure constraints
    var limitInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
        BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION {
            LimitFlags = JobInformationLimitFlags.JOB_OBJECT_LIMIT_PROCESS_MEMORY
                        | JobInformationLimitFlags.JOB_OBJECT_LIMIT_BREAKAWAY_OK
                        | JobInformationLimitFlags.JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK
        },
        ProcessMemoryLimit = (UIntPtr)maxmem
    };
    size = (uint)Marshal.SizeOf(limitInfo);
    CheckResult(ApiMethods.SetInformationJobObject(hJob, JOBOBJECTINFOCLASS.ExtendedLimitInformation,
            ref limitInfo, size));
}

// assign a process to a job to apply constraints
CheckResult(ApiMethods.AssignProcessToJobObject(hJob, hProcess));

Remember that we created a process in a suspended mode so if the PID was not provided we need to resume the main process thread:

// resume process main thread (if it was started by us)
if (pid == 0) {
    CheckResult(ApiMethods.ResumeThread(pi.hThread));
    // and we can close the thread handle
    CloseHandle(pi.hThread);
}

The process should be now running and we can wait for it’s completion:

if (ApiMethods.WaitForSingleObject(hProcess, ApiMethods.INFINITE) == 0xFFFFFFFF) {
    throw new Win32Exception();
}

The source code and the application can be downloaded from the project Github page. I encourage you to have a deeper look at what the job object provides. In my application I’m setting limit on a process committed memory, but you may as well restrict process CPU usage and its working set size. Have fun with jobs and fight with memory leaks 🙂

6 thoughts on “Set process memory limit with Process Governor

  1. codingorca July 14, 2014 / 16:42

    Great article, simple and easy to understand the Job Objects. Thank you for it.

  2. ro.yo.mi October 10, 2018 / 16:19

    Are you able to give some guidenace on how to compile the source code, or where to find an already compiled version?

  3. Thomas September 17, 2019 / 11:57

    Ah, just the on-trick-pony I desperately needed today to hunt down a problem with memory-usage. 10/10 I’ll keep this link. Thanks!

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.