Working with unmanaged memory is not my forte. Thus I am very appreciative of when someone not only reads my blog, but then takes the time to do their own research and leave comments and suggest updates. TLDR: Thank you, Steven Pereyda!
Last year I blogged about how to kill a child process when the parent process is killed. The solution involves using the operating system by invoking Kenral32. However, as mentioned above, there was a small memory leak and a few other optimizations that we should have made; so let's take a look at how to fix those!
Fixes and Optimizations in v2
1. There was a small memory leak.
In the job constructor we used Marshal.AllocHGlobal to create an unmanaged copy of the JobObjectExtendedLimitInformation object, but we never freed that memory. The new constructor how has a finally block that ensures we invoke Marshal.FreeHGlobal.
2. We should use SafeHandles.
.NET has a SafeHandle class that can be used as a wrapper around unmanaged handles, which can help you prevent memory leaks; and I learned how to use this by reading a code project article by the always awesome Stephen Cleary. Please note that in v2 there is a now a JobObjectHandle class that extends SafeHandle, and we use this instead of storing the IntPtr ourselves.
3. We moved the GC.SuppressFinalize into the public dispose method.
Yes, this is the right way to do it. So why didn't I do it that way the first time? Because ReSharper was warning me that I wasn't using the isDisposing param...and hate warnings!
4. Follow the best practices of hosting all external calls in a single Native Methods class.
Microsoft recommends that you keep all of your external calls in one NativeMethods class, and then decorate that class with attributes to avoid security warnings. Sounds like a good idea to me.
v2 Code
using System;
using System.Diagnostics;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Security;
namespace JobManagement
{
[SuppressUnmanagedCodeSecurity]
internal static class NativeMethods
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
public static extern JobObjectHandle CreateJobObject(
IntPtr a,
string lpName);
[DllImport("kernel32.dll")]
public static extern bool SetInformationJobObject(
JobObjectHandle hJob,
JobObjectInfoType infoType,
IntPtr lpJobObjectInfo,
UInt32 cbJobObjectInfoLength);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool AssignProcessToJobObject(
JobObjectHandle job,
IntPtr process);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CloseHandle(IntPtr handle);
}
public sealed class Job : IDisposable
{
private readonly JobObjectHandle _handle;
private bool _disposed;
public Job()
{
_handle = NativeMethods.CreateJobObject(IntPtr.Zero, null);
var info = new JobObjectBasicLimitInformation
{
LimitFlags = 0x2000
};
var extendedInfo = new JobObjectExtendedLimitInformation
{
BasicLimitInformation = info
};
var infoType = typeof(JobObjectExtendedLimitInformation);
var length = Marshal.SizeOf(infoType);
var extendedInfoPtr = IntPtr.Zero;
try
{
extendedInfoPtr = Marshal.AllocHGlobal(length);
Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);
var setResult = NativeMethods.SetInformationJobObject(
_handle,
JobObjectInfoType.ExtendedLimitInformation,
extendedInfoPtr,
(uint)length);
if (setResult)
return;
}
finally
{
if (extendedInfoPtr != IntPtr.Zero)
Marshal.FreeHGlobal(extendedInfoPtr);
}
var lastError = Marshal.GetLastWin32Error();
var message = "Unable to set information. Error: " + lastError;
throw new Exception(message);
}
~Job()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (_disposed)
return;
if (_handle != null && !_handle.IsInvalid)
_handle.Dispose();
_disposed = true;
}
public bool AddProcess(IntPtr processHandle)
{
return NativeMethods.AssignProcessToJobObject(_handle, processHandle);
}
public bool AddProcess(int processId)
{
var process = Process.GetProcessById(processId);
return AddProcess(process.Handle);
}
}
internal class JobObjectHandle : SafeHandle
{
public JobObjectHandle()
: base(IntPtr.Zero, true)
{
}
public override bool IsInvalid
{
[PrePrepareMethod]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
get { return (handle == IntPtr.Zero); }
}
[PrePrepareMethod]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
protected override bool ReleaseHandle()
{
return NativeMethods.CloseHandle(handle);
}
}
#region Helper classes
[StructLayout(LayoutKind.Sequential)]
internal struct IoCounters
{
public UInt64 ReadOperationCount;
public UInt64 WriteOperationCount;
public UInt64 OtherOperationCount;
public UInt64 ReadTransferCount;
public UInt64 WriteTransferCount;
public UInt64 OtherTransferCount;
}
[StructLayout(LayoutKind.Sequential)]
internal struct JobObjectBasicLimitInformation
{
public Int64 PerProcessUserTimeLimit;
public Int64 PerJobUserTimeLimit;
public UInt32 LimitFlags;
public UIntPtr MinimumWorkingSetSize;
public UIntPtr MaximumWorkingSetSize;
public UInt32 ActiveProcessLimit;
public UIntPtr Affinity;
public UInt32 PriorityClass;
public UInt32 SchedulingClass;
}
[StructLayout(LayoutKind.Sequential)]
internal struct JobObjectExtendedLimitInformation
{
public JobObjectBasicLimitInformation BasicLimitInformation;
public IoCounters IoInfo;
public UIntPtr ProcessMemoryLimit;
public UIntPtr JobMemoryLimit;
public UIntPtr PeakProcessMemoryUsed;
public UIntPtr PeakJobMemoryUsed;
}
internal enum JobObjectInfoType
{
AssociateCompletionPortInformation = 7,
BasicLimitInformation = 2,
BasicUIRestrictions = 4,
EndOfJobTimeInformation = 6,
ExtendedLimitInformation = 9,
SecurityLimitInformation = 5,
GroupInformation = 11
}
#endregion
}
Enjoy,
Tom
Awesome! Thank you!
ReplyDelete