5
votes

Recently I've had some problems with my system running out of memory. It took a while to discover what was going on, but I eventually determined that when I copied large amounts of data to my machine from a file share an equivalently large amount of memory was put into 'Standby'. Task Manager doesn't appear to show the Standby memory usage, but Resource Monitor does. At first I could only get the memory back by rebooting, but I eventually I discovered that the SysInternals guys had written a great utility to free the memory (link below).

Here is a brief blurb on Standby memory:

The Standby list contains unmodified pages that have been removed from process working sets, which effectively makes the Standby list a cache. If a process needs a page that is on the Standby list, the memory manager immediately returns the page to its working set. All pages on the Standby list are available for memory allocation requests. If a process requests memory, the memory manager can take a page from the Standby list, initialize it, and allocate it to the calling process. This is called repurposing a page. Pages on the Standby list are often from recently used files. By keeping these pages on the Standby list, the memory manager reduces the need to read information from the disk. Disk reads can decrease system responsiveness.

(this is from the document here: Memory Sizing Guidance

Here is a link to the tool: RAMMap

My Question Is:

Does anyone have an idea how do this programmatically? Ideally I'd like to use C#, but I would appreciate any pointers that might help me get to an answer.

Thanks!

3
Clearly its possible to do this programmatically since there already is a tool. You just have to figure out how to do it, so figure out how the tool works, and you have solved that problem.Security Hound
Indeed. But I don't have the source code, hence my post.chrismead
Mark Russinovich never shares his secrets.Hans Passant
Yeah, Mr. Russinovich is an impressive guy. I see why Microsoft decided to buy his company.chrismead

3 Answers

4
votes

Here is my code, it is a console application that has to be run with administrator privileges. Code is C#. Uses EmptyWorkingSet and MemoryPurgeStandbyList.

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Threading;
using System.Collections.Generic;

namespace FreeMemory
{
//Declaration of structures
//SYSTEM_CACHE_INFORMATION
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct SYSTEM_CACHE_INFORMATION
{
    public uint CurrentSize;
    public uint PeakSize;
    public uint PageFaultCount;
    public uint MinimumWorkingSet;
    public uint MaximumWorkingSet;
    public uint Unused1;
    public uint Unused2;
    public uint Unused3;
    public uint Unused4;
}

//SYSTEM_CACHE_INFORMATION_64_BIT
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct SYSTEM_CACHE_INFORMATION_64_BIT
{
    public long CurrentSize;
    public long PeakSize;
    public long PageFaultCount;
    public long MinimumWorkingSet;
    public long MaximumWorkingSet;
    public long Unused1;
    public long Unused2;
    public long Unused3;
    public long Unused4;
}

//TokPriv1Luid
[StructLayout(LayoutKind.Sequential, Pack = 1)]
internal struct TokPriv1Luid
{
    public int Count;
    public long Luid;
    public int Attr;
}
public class Program
{
    //Declaration of constants
    const int SE_PRIVILEGE_ENABLED = 2;
    const string SE_INCREASE_QUOTA_NAME = "SeIncreaseQuotaPrivilege";
    const string SE_PROFILE_SINGLE_PROCESS_NAME = "SeProfileSingleProcessPrivilege";
    const int SystemFileCacheInformation = 0x0015;
    const int SystemMemoryListInformation = 0x0050;
    const int MemoryPurgeStandbyList = 4;
    const int MemoryEmptyWorkingSets = 2;

    //Import of DLL's (API) and the necessary functions 
    [DllImport("advapi32.dll", SetLastError = true)]
    internal static extern bool LookupPrivilegeValue(string host, string name, ref long pluid);

    [DllImport("advapi32.dll", SetLastError = true)]
    internal static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall, ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen);

    [DllImport("ntdll.dll")]
    public static extern UInt32 NtSetSystemInformation(int InfoClass, IntPtr Info, int Length);

    [DllImport("psapi.dll")]
    static extern int EmptyWorkingSet(IntPtr hwProc);

    //Function to clear working set of all processes
    public static void EmptyWorkingSetFunction()
    {
        //Declaration of variables
        string ProcessName = string.Empty;
        Process[] allProcesses = Process.GetProcesses();
        List<string> successProcesses = new List<string>();
        List<string> failProcesses = new List<string>();

        //Cycle through all processes
        for (int i = 0; i < allProcesses.Length; i++)
        {
            Process p = new Process();
            p = allProcesses[i];
            //Try to empty the working set of the process, if succesfull add to successProcesses, if failed add to failProcesses with error message
            try
            {
                ProcessName = p.ProcessName;
                EmptyWorkingSet(p.Handle);
                successProcesses.Add(ProcessName);
            }
            catch (Exception ex)
            {
                failProcesses.Add(ProcessName + ": " + ex.Message);
            }
        }

        //Print the lists with successful and failed processes
        Console.WriteLine("SUCCESSFULLY CLEARED PROCESSES: " + successProcesses.Count);
        Console.WriteLine("-------------------------------");
        for (int i = 0; i < successProcesses.Count; i++)
        {
            Console.WriteLine(successProcesses[i]);
        }
        Console.WriteLine();

        Console.WriteLine("FAILED CLEARED PROCESSES: " + failProcesses.Count);
        Console.WriteLine("-------------------------------");
        for (int i = 0; i < failProcesses.Count; i++)
        {
            Console.WriteLine(failProcesses[i]);
        }
        Console.WriteLine();
    }

    //Function to check if OS is 64-bit or not, returns boolean
    public static bool Is64BitMode()
    {
        return Marshal.SizeOf(typeof(IntPtr)) == 8;
    }

    //Function used to clear file system cache, returns boolean
    public static void ClearFileSystemCache(bool ClearStandbyCache)
    {
        try
        {
            //Check if privilege can be increased
            if (SetIncreasePrivilege(SE_INCREASE_QUOTA_NAME))
            {
                uint num1;
                int SystemInfoLength;
                GCHandle gcHandle;
                //First check which version is running, then fill structure with cache information. Throw error is cache information cannot be read.
                if (!Is64BitMode())
                {
                    SYSTEM_CACHE_INFORMATION cacheInformation = new SYSTEM_CACHE_INFORMATION();
                    cacheInformation.MinimumWorkingSet = uint.MaxValue;
                    cacheInformation.MaximumWorkingSet = uint.MaxValue;
                    SystemInfoLength = Marshal.SizeOf(cacheInformation);
                    gcHandle = GCHandle.Alloc(cacheInformation, GCHandleType.Pinned);
                    num1 = NtSetSystemInformation(SystemFileCacheInformation, gcHandle.AddrOfPinnedObject(), SystemInfoLength);
                    gcHandle.Free();
                }
                else
                {
                    SYSTEM_CACHE_INFORMATION_64_BIT information64Bit = new SYSTEM_CACHE_INFORMATION_64_BIT();
                    information64Bit.MinimumWorkingSet = -1L;
                    information64Bit.MaximumWorkingSet = -1L;
                    SystemInfoLength = Marshal.SizeOf(information64Bit);
                    gcHandle = GCHandle.Alloc(information64Bit, GCHandleType.Pinned);
                    num1 = NtSetSystemInformation(SystemFileCacheInformation, gcHandle.AddrOfPinnedObject(), SystemInfoLength);
                    gcHandle.Free();
                }
                if (num1 != 0)
                    throw new Exception("NtSetSystemInformation(SYSTEMCACHEINFORMATION) error: ", new Win32Exception(Marshal.GetLastWin32Error()));
            }

            //If passes paramater is 'true' and the privilege can be increased, then clear standby lists through MemoryPurgeStandbyList
            if (ClearStandbyCache && SetIncreasePrivilege(SE_PROFILE_SINGLE_PROCESS_NAME))
            {
                int SystemInfoLength = Marshal.SizeOf(MemoryPurgeStandbyList);
                GCHandle gcHandle = GCHandle.Alloc(MemoryPurgeStandbyList, GCHandleType.Pinned);
                uint num2 = NtSetSystemInformation(SystemMemoryListInformation, gcHandle.AddrOfPinnedObject(), SystemInfoLength);
                gcHandle.Free();
                if (num2 != 0)
                    throw new Exception("NtSetSystemInformation(SYSTEMMEMORYLISTINFORMATION) error: ", new Win32Exception(Marshal.GetLastWin32Error()));
            }
        }
        catch (Exception ex)
        {
            Console.Write(ex.ToString());
        }
    }

    //Function to increase Privilege, returns boolean
    private static bool SetIncreasePrivilege(string privilegeName)
    {
        using (WindowsIdentity current = WindowsIdentity.GetCurrent(TokenAccessLevels.Query | TokenAccessLevels.AdjustPrivileges))
        {
            TokPriv1Luid newst;
            newst.Count = 1;
            newst.Luid = 0L;
            newst.Attr = SE_PRIVILEGE_ENABLED;

            //Retrieves the LUID used on a specified system to locally represent the specified privilege name
            if (!LookupPrivilegeValue(null, privilegeName, ref newst.Luid))
                throw new Exception("Error in LookupPrivilegeValue: ", new Win32Exception(Marshal.GetLastWin32Error()));

            //Enables or disables privileges in a specified access token
            int num = AdjustTokenPrivileges(current.Token, false, ref newst, 0, IntPtr.Zero, IntPtr.Zero) ? 1 : 0;
            if (num == 0)
                throw new Exception("Error in AdjustTokenPrivileges: ", new Win32Exception(Marshal.GetLastWin32Error()));
            return num != 0;
        }
    }

    //MAIN Program
    static void Main(string[] args)
    {
        //Clear working set of all processes
        EmptyWorkingSetFunction();

        //Clear file system cache
        ClearFileSystemCache(true);

        //Waiting for input of user to close program
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}
}
2
votes

The secret seems to be in the Process Hacker source code (which is in c language). Looking at the code, you'll see a promising command MemoryPurgeStandbyList which seems to be called when we choose the "empty standby list" option in the GUI.

memlists.c(227, 35): command = MemoryPurgeStandbyList;
ntexapi.h(1475, 5): MemoryPurgeStandbyList,

http://processhacker.sourceforge.net/

Also available here as a command line version.

2
votes

This outlines the basic steps required to achieve this in C#

using System;
using System.Runtime.InteropServices;
using System.Security.Principal;

public static class MemoryUtility
{
   private static void ClearStandbyCache()
   {
     SetIncreasePrivilege(SE_PROFILE_SINGLE_PROCESS_NAME);

     int iReturn;
     int iSize = Marshal.SizeOf(ClearStandbyPageList);

     GCHandle gch = GCHandle.Alloc(ClearStandbyPageList, GCHandleType.Pinned);
     iReturn = NtSetSystemInformation(SYSTEMMEMORYLISTINFORMATION, gch.AddrOfPinnedObject(), iSize);
     gch.Free();

     if (iReturn != 0)
       Console.WriteLine("Empty Standby List failed");
     else
       Console.WriteLine("Empty Standby List success");
   }



   [DllImport("NTDLL.dll", SetLastError = true)]
   internal static extern int NtSetSystemInformation(int SystemInformationClass, IntPtr SystemInfo, int SystemInfoLength);

   //SystemInformationClass values
   private static int SYSTEMCACHEINFORMATION = 0x15;
   private static int SYSTEMMEMORYLISTINFORMATION = 80;

   //SystemInfo values
   private static int ClearStandbyPageList = 4;

}

Also here's a link to some documentation at a site where SetIncreasePrivilege method is documented and also contains many useful resources on the topic: http://www.pinvoke.net/default.aspx/ntdll/NtSetSystemInformation.html