I would like to introduce you to Process Governor – a new tool I added to my .NET diagnostics toolkit. This application allows you to set a limit on a memory committed by a process. In 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:
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));
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 port. 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, so 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) {
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));
}
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:
if (pid == 0) {
CheckResult(ApiMethods.ResumeThread(pi.hThread));
CloseHandle(pi.hThread);
}
The process should be now running and we can wait for its completion:
if (ApiMethods.WaitForSingleObject(hProcess, ApiMethods.INFINITE) == 0xFFFFFFFF) {
throw new Win32Exception();
}
The source code and the application can be downloaded from the .NET diagnostics toolkit codeplex site. 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.