首页 > 解决方案 > Windows 进程如何检测到它即将达到其内存限制?

问题描述

如果进程的内存受到JOBOBJECT_EXTENDED_LIMIT_INFORMATION 的限制。ProcessMemoryLimit(“...指定进程可以提交的虚拟内存的限制...”),进程如何检测何时接近此限制?最明显的(对我来说)是定期检查Process.VirtualMemorySize64(“为相关进程分配的虚拟内存量,以字节为单位”)。这是正确的措施吗?还有 WorkingSet64、PrivateMemorySize64、PagedSystemMemorySize64、PagedMemorySize64 和 NonPagedSystemMemorySize64。

标签: c#.netwinapimemoryprocess

解决方案


@Ben 在评论中确实回答了这个问题:您可以使用SetInformationJobObjectJob Objects中的概述)。这是我的代码,它扩展了这个问题以提供一个可用于限制进程内存的类,并在指定的阈值处提供回调:

#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Threading;
using CommonLib;
using QWeb.QWebLib;

namespace QWeb.QhostLib
{
    /// <summary>
    /// A Class to enable the use of Win32 [Job Objects](https://docs.microsoft.com/en-us/windows/win32/procthread/job-objects)
    /// taken from: https://stackoverflow.com/a/9164742/5626740
    ///
    /// 1. Create a MemoryLimitedJobObject, specifying the memory limit (CreateJobObject)
    /// 2. Add the process to be limited to the job (AddProcess)
    /// 3. [optional] TellMeWhenMemoryUsedExceeds() to register a callback for when memory
    ///    use exceeds some threshold.
    /// </summary>
    public unsafe class MemoryLimitedJobObject : IDisposable
    {
        [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
        static extern IntPtr CreateJobObject(IntPtr a, string? lp_name);

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool SetInformationJobObject(IntPtr h_job, JobObjectInfoType info_type, void *lp_job_object_info, int cb_job_object_info_length);

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);

        static readonly ListenToJobLimitViolations listenToJobLimitViolations = new ListenToJobLimitViolations();

        IntPtr jobHandle;
        bool disposed;

        /// <summary>Creates a Job Object with limited memory, processes can later be added to this Job Object</summary>
        /// <param name="memory_limit_bytes">The limit to be set on the Job Object</param>
        public MemoryLimitedJobObject(ulong memory_limit_bytes)
        {
            this.jobHandle = CreateJobObject(IntPtr.Zero, null);

            var extended_info = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
                BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION {
                    LimitFlags = 0x0100 // JOB_OBJECT_LIMIT_PROCESS_MEMORY
                },
                ProcessMemoryLimit = new UIntPtr(memory_limit_bytes)
            };
            if (!SetInformationJobObject(jobHandle, JobObjectInfoType.ExtendedLimitInformation, &extended_info, sizeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)))
                throw new ApplicationException($"Unable to set information.  Error: {Marshal.GetLastWin32Error()}");
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (disposed)
                return;

            if (disposing) { }

            Close();
            disposed = true;
        }

        public void Close()
        {
            listenToJobLimitViolations.Forget(jobHandle);
            Win32.CloseHandle(jobHandle);
            this.jobHandle = IntPtr.Zero;
        }

        /// <summary>As soon as the process hits memory_report_trigger_bytes of RAM we will
        /// call memory_trigger_exceeded with the number of bytes in use.  This will not be called
        /// again.</summary>
        public void TellMeWhenMemoryUsedExceeds(ulong memory_report_trigger_bytes, Action<ulong> memory_trigger_exceeded)
        {
            // Tell Windows to notify us when this job exceeds its limit.
            var limit_info = new JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION {
                JobMemoryLimit = memory_report_trigger_bytes,
                LimitFlags = 0x00000200 // JOB_OBJECT_LIMIT_JOB_MEMORY
            };
            if (!SetInformationJobObject(jobHandle, JobObjectInfoType.NotificationLimitInformation, &limit_info, sizeof(JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION)))
                throw new Win32Exception(Marshal.GetLastWin32Error());

            // Tell Windows that any asynchronous notifications from this job should come to the completion
            // port we created earlier.
            var async_completion_port_handle = listenToJobLimitViolations.KnowAbout(jobHandle, memory_trigger_exceeded);
            var port_info = new JOBOBJECT_ASSOCIATE_COMPLETION_PORT {
                CompletionKey = jobHandle,  // so we can tell 
                CompletionPort = async_completion_port_handle
            };
            if (!SetInformationJobObject(jobHandle, JobObjectInfoType.AssociateCompletionPortInformation, &port_info, sizeof(JOBOBJECT_ASSOCIATE_COMPLETION_PORT)))
                throw new Win32Exception(Marshal.GetLastWin32Error());
        }

        public bool AddProcess(IntPtr process_handle)
        {
            return AssignProcessToJobObject(jobHandle, process_handle);
        }
    }

    /// <summary>
    /// Runs a Thread that is shared between all MemoryLimitedJobObjects.  It gets notifications from Windows
    /// when a process exceeds its memory threshold, which lets us warn people that the process probably
    /// needs more space.
    /// </summary>
    public class ListenToJobLimitViolations
    {
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern IntPtr CreateIoCompletionPort(IntPtr file_handle, IntPtr existing_completion_port, UIntPtr completion_key, uint number_of_concurrent_threads);

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern unsafe bool GetQueuedCompletionStatus(IntPtr completion_port, out uint number_of_bytes, out IntPtr completion_key, void* overlapped, uint milliseconds);

        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool QueryInformationJobObject(IntPtr job_handle, JobObjectInfoType job_object_information_class, out JOBOBJECT_LIMIT_VIOLATION_INFORMATION job_object_information, int job_object_information_length, out uint return_length);

        // Extracted from winnt.h in Windows SDK
        public const int JOB_OBJECT_MSG_NOTIFICATION_LIMIT = 11;

        readonly IntPtr asyncCompletionPortHandle;
        readonly Dictionary<IntPtr, Action<ulong>> jobsRegisteredForNotification = new();

        internal ListenToJobLimitViolations()
        {
            // Create a asynchronous completion port to receive events for this job, and start
            // monitoring it.
            this.asyncCompletionPortHandle = CreateIoCompletionPort(Win32.INVALID_HANDLE_VALUE, IntPtr.Zero, UIntPtr.Zero, 0);
            if (asyncCompletionPortHandle == IntPtr.Zero)
                throw new Win32Exception(Marshal.GetLastWin32Error());
            new Thread(() => {
                try {
                    MonitorCompletionPort(asyncCompletionPortHandle);
                } catch (Exception ex) {
                    Error.Report(nameof(MonitorCompletionPort), ex);
                }
            }) {
                IsBackground = true,
                Name = "ListenToJobLimitViolations"
            }.Start();
        }

        unsafe void MonitorCompletionPort(IntPtr async_completion_port_handle)
        {
            while (true) {
                NativeOverlapped* native_overlapped;  // documented as being set to whatever was provided when the async operation was
                                                      // started, which doesn't apply here.  So I figure it's not our job to dispose.
                if (!GetQueuedCompletionStatus(async_completion_port_handle, out var bytes_read, out var job_handle, &native_overlapped, uint.MaxValue))
                    throw new Win32Exception(Marshal.GetLastWin32Error());
                // Identify what sort of message we're getting.  winnt.h says
                // "These values are returned via the lpNumberOfBytesTransferred parameter"!
                if (bytes_read == JOB_OBJECT_MSG_NOTIFICATION_LIMIT) {
                    if (!QueryInformationJobObject(job_handle, JobObjectInfoType.LimitViolationInformation, out var limit_violation, sizeof(JOBOBJECT_LIMIT_VIOLATION_INFORMATION), out var _))
                        throw new Win32Exception(Marshal.GetLastWin32Error());
                    Action<ulong> trigger_action;
                    lock (jobsRegisteredForNotification) {
                        trigger_action = jobsRegisteredForNotification[job_handle];
                    }
                    trigger_action(limit_violation.JobMemory);
                }
            }
        }

        internal IntPtr KnowAbout(IntPtr job_handle, Action<ulong> memory_trigger_exceeded)
        {
            lock (jobsRegisteredForNotification) {
                jobsRegisteredForNotification.Add(job_handle, memory_trigger_exceeded);
                return asyncCompletionPortHandle;  // caller must register their Job with this, to direct notifications here
            }
        }

        internal void Forget(IntPtr job_handle)
        {
            lock (jobsRegisteredForNotification) {
                jobsRegisteredForNotification.Remove(job_handle);
            }
        }
    }

    #region Helper classes

    [StructLayout(LayoutKind.Sequential)]
    struct IO_COUNTERS
    {
        public ulong ReadOperationCount;
        public ulong WriteOperationCount;
        public ulong OtherOperationCount;
        public ulong ReadTransferCount;
        public ulong WriteTransferCount;
        public ulong OtherTransferCount;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct JOBOBJECT_BASIC_LIMIT_INFORMATION
    {
        public long PerProcessUserTimeLimit;
        public long PerJobUserTimeLimit;
        public uint LimitFlags;
        public UIntPtr MinimumWorkingSetSize;
        public UIntPtr MaximumWorkingSetSize;
        public uint ActiveProcessLimit;
        public UIntPtr Affinity;
        public uint PriorityClass;
        public uint SchedulingClass;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct SECURITY_ATTRIBUTES
    {
#pragma warning disable IDE1006 // Naming Styles
        public uint nLength;
        public IntPtr lpSecurityDescriptor;
        public int bInheritHandle;
#pragma warning restore IDE1006 // Naming Styles
    }

    [StructLayout(LayoutKind.Sequential)]
    struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
    {
        public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
        public IO_COUNTERS IoInfo;
        public UIntPtr ProcessMemoryLimit;
        public UIntPtr JobMemoryLimit;
        public UIntPtr PeakProcessMemoryUsed;
        public UIntPtr PeakJobMemoryUsed;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION
    {
        public ulong IoReadBytesLimit;
        public ulong IoWriteBytesLimit;
        public long PerJobUserTimeLimit;
        public ulong JobMemoryLimit;
        public int RateControlTolerance;
        public int RateControlToleranceInterval;
        public uint LimitFlags;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct JOBOBJECT_ASSOCIATE_COMPLETION_PORT
    {
        public IntPtr CompletionKey;
        public IntPtr CompletionPort;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct JOBOBJECT_LIMIT_VIOLATION_INFORMATION
    {
        public uint LimitFlags;
        public uint ViolationLimitFlags;
        public ulong IoReadBytes;
        public ulong IoReadBytesLimit;
        public ulong IoWriteBytes;
        public ulong IoWriteBytesLimit;
        public long PerJobUserTime;
        public long PerJobUserTimeLimit;
        public ulong JobMemory;
        public ulong JobMemoryLimit;
        public int RateControlTolerance;
        public int RateControlToleranceLimit;
    }

    public enum JobObjectInfoType
    {
        AssociateCompletionPortInformation = 7,
        ExtendedLimitInformation = 9,
        NotificationLimitInformation = 12,
        LimitViolationInformation = 13
    }
    #endregion
}

推荐阅读