|
If you need to constrain or otherwise customize how the CLR uses memory, use the CLR hosting API to implement a memory manager. Implementing a memory manager enables you to customize the following aspects of how memory is managed in your process:
These capabilities are provided by the three interfaces that make up a memory manager: IHostMemoryManager, IHostMalloc, and ICLRMemoryNotificationCallback. As the primary interface in the hosting manager, IHostMemoryManager is the interface the CLR asks the host for during initialization. The CLR determines whether a host implements the memory manager by passing the IID for IHostMemoryManager to the GetHostManager method of the host's implementation of IHostControl. (Refer to Chapter 2 for a complete description of how the CLR determines which managers a particular host implements.) In the next several sections, I provide the details on how to use the capabilities offered by the three interfaces that comprise the memory manager. Virtual Memory ManagementThe CLR relies on the features provided by Microsoft Win32 virtual memory management functions to allocate the memory it needs for its garbage collection heaps and to store other internal CLR data structures. If you implement a memory manager in your host, you must provide the CLR with a set of virtual memory management functions that map to those provided by Win32. As you can see in Table 13-1, IHostMemoryManager has a set of methods whose names match those of the Win32 virtual memory functions. When you implement a memory manager, the CLR will use the virtual memory methods provided by your implementation of IHostMemoryManager instead of calling those provided by Win32.
Most of the parameters to the VirtualAlloc, VirtualFree, VirtualQuery, and VirtualProtect methods on IHostMemoryManager map directly to those provided by the Win32 APIs of the same name. The one notable exception is the eCriticalLevel parameter to IHostMemoryManager::VirtualAlloc, which is unique to the virtual memory management functions provided by the CLR hosting API. Here's the definition of the VirtualAlloc method on IHostMemoryManager from mscoree.idl: interface IHostMemoryManager : IUnknown { // Other methods omitted... HRESULT VirtualAlloc([in] void* pAddress, [in] SIZE_T dwSize, [in] DWORD flAllocationType, [in] DWORD flProtect, [in] EMemoryCriticalLevel eCriticalLevel, [out] void** ppMem); } Hosts are free to deny the CLR's request for more memory by returning the E_OUTOFMEMORY HRESULT from their implementation of VirtualAlloc. However, depending on how critical the CLR's need for more memory is when it calls VirtualAlloc, it might not be able to complete certain operations if the request for more memory is denied. The consequences of denying a request for more memory are communicated to the host using eCriticalLevel. Typically, the failure to allocate memory causes the CLR to abort the specific task[1] it is executing at the time. In more extreme cases, the failure to obtain more memory can cause the CLR to unload the current application domain or even terminate the entire process. Each time the CLR calls VirtualAlloc, it passes in a value from the EMemoryCriticalLevel indicating whether a failure to obtain the requested memory will cause the task, application domain, or process to be terminated. Here's the definition of EMemoryCriticalLevel from mscoree.idl:
typedef enum { eTaskCritical = 0, eAppDomainCritical = 1, eProcessCritical = 2 } EMemoryCriticalLevel; Given that a host can use a memory manager to supply the CLR with implementations of VirtualAlloc and VirtualFree, it's relatively easy to see how SQL Server uses the CLR hosting API to make sure that it never exceeds the amount of memory it is configured to use. SQL Server implementation of IHostMemoryManager records both the sizes of all memory allocations that are made through VirtualAlloc and the amount of memory freed by each call to VirtualFree. The difference between the two values is the amount of virtual memory the CLR is using at any one time. This total, combined with the virtual memory allocated by SQL Server, is always kept under the amount that SQL Server is configured to use. Throughout this section, I haven't said anything about how a host's implementation of the virtual memory management methods on IHostMemoryManager should behave, other than to allocate (or deny) and free the memory that is requested by the CLR. With the exception of these basic requirements, the host is free to implement these methods any way it sees fit. This freedom is a very powerful aspect of the abstraction provided by the CLR hosting API. In some cases, a host might choose simply to delegate the calls to the virtual memory methods on IHostMemoryManager to their Win32 equivalents after doing any bookkeeping that is necessary. A host's implementation of these methods need not do that, however. If a host has specific requirements around the timing of memory allocations, the locations from which the memory comes, and so on, it can use the abstraction provided by the hosting APIs to hide these details from the CLR. The management of virtual memory is only one part of the overall picture, however. In the next section, you'll see how a host can use a memory manager to supply the CLR with the primitives it uses to manage memory allocated in heaps. Heap ManagementIn addition to the virtual-memory APIs described in the previous section, the CLR also relies on a set of functions that enable it to manage memory in heaps. A host provides the CLR with these functions through the IHostMalloc interface. As you can see from Table 13-2, IHostMalloc contains methods that correspond to heap management APIs provided by Win32.
The CLR obtains the IHostMalloc interface from the host by calling the CreateMalloc method on IHostMemoryManager. CreateMalloc takes a set of flags that identify the characteristics needed in the heap that is returned as shown in the following definition from mscoree.idl: interface IHostMemoryManager : IUnknown { HRESULT CreateMalloc([in] DWORD dwMallocType, [out] IHostMalloc **ppMalloc); // Other methods omitted... } The valid flags to CreateMalloc are represented by the MALLOC_TYPE enumeration: typedef enum { MALLOC_THREADSAFE = 0x1, MALLOC_EXECUTABLE = 0x2, } MALLOC_TYPE; The MALLOC_THREADSAFE flag indicates that the CLR must be able to safely allocate and free data in the heap from multiple threads simultaneously. The current version of the CLR always sets this value. The CLR sets the MALLOC_EXECUTABLE flag when it intends to store executable code in the heap. It sets this, for example, when it dynamically creates and stores the code it uses as part of the COM Interoperability layer. The MALLOC_EXECUTABLE flag exists so the host and the CLR can properly use the No Execute (NX) feature available on some processors today. Essentially, NX enables memory pages to be marked with a bit that prevents executable code from being stored and run from the page. By marking pages in this way, the potential for security vulnerabilities is reduced when a page is not explicitly intended to contain executable code. Specifically, NX helps mitigate the vulnerability whereby a malicious party writes code into random locations in memory and causes it to be executed. In the same way that all requests for virtual memory come through IHostMemoryManager, all requests to allocate memory from a heap come through IHostMalloc. If you are implementing a memory manager for the purposes of restricting the amount of memory the CLR can use, remember to account for the memory allocated through IHostMalloc when determining how much memory the CLR has requested. File MappingThe CLR uses the Win32 memory-mapped file APIs when loading and executing assemblies. Because the process of mapping a file requires address space, the CLR must keep the host informed of all memory allocated while mapping files. The methods used to communicate information about file mappings to the host are the NeedsVirtualAddressSpace, AcquiredVirtual-AddressSpace, and ReleasedVirtualAddressSpace methods on IHostMemoryManager. Here are the definitions of those methods from mscoree.idl: interface IHostMemoryManager : IUnknown { HRESULT NeedsVirtualAddressSpace( [in] LPVOID startAddress, [in] SIZE_T size ); HRESULT AcquiredVirtualAddressSpace( [in] LPVOID startAddress, [in] SIZE_T size ); HRESULT ReleasedVirtualAddressSpace( [in] LPVOID startAddress ); } The CLR maps files into memory using the Win32 MapViewOfFile API. All address space acquired by calling MapViewOfFile is reported to the host by calling AcquiredVirtualAddressSpace. If the host is using a memory manager to keep track of the amount of address space used by the CLR, it must include the address space reported through AcquiredVirtualAddressSpace in its totals. If the CLR's call to MapViewOfFile fails because of low address space conditions, the CLR tells the host that it needs additional address space by calling NeedsVirtualAddressSpace and passing in the start address and size of the address space it needs. If the host is able to make the address space available, it returns the S_OK HRESULT from NeedsVirtualAddressSpace. The CLR will then try to call MapViewOfFile again, assuming that the required address space is now available. After the CLR unmaps a file using the Win32 UnmapViewOfFile API, it notifies the host that the virtual address space used by the file mapping is now free by calling ReleasedVirtualAddressSpace. Reporting Memory Status to the CLROne of the heuristics the CLR uses to determine when to perform a garbage collection is the amount of memory pressure currently on the system. If memory pressure is high (signifying that very little memory is available), the CLR will do a garbage collection and return all the memory it can to the system. The CLR uses two Win32 APIs to determine the current memory load: GlobalMemoryStatus and the memory resource notification created with CreateMemoryResourceNotification. Hosts that implement a memory manager can provide replacements for these APIs so that a host's own impression of the current memory load can be used to influence when garbage collections occur. The GetMemoryLoad MethodThe equivalent of the Win32 GlobalMemoryStatus API is the GetMemoryLoad method on IHostMemoryManager. The CLR will call GetMemoryLoad periodically to determine the memory load on the system from the host's perspective. The host returns two values from GetMemoryLoad, as shown in the following definition from mscoree.idl: interface IHostMemoryManager : IUnknown { // Other methods omitted... HRESULT GetMemoryLoad([out] DWORD* pMemoryLoad, [out] SIZE_T *pAvailableBytes); } The first parameter, pMemoryLoad, is the percentage of physical memory that is currently in use. This parameter is equivalent to the dwMemoryLoad field of the MEMORYSTATUS structure returned from GlobalMemoryStatus. The pAvailableBytes parameter is the number of bytes that are currently available for the CLR to use. The exact behavior of the CLR in response to the values returned from GetMemoryLoad isn't defined and is likely to change between releases. That is, returning specific values doesn't guarantee that a specific amount of memory will be freed or even that a garbage collection will be done immediately. All that is guaranteed is that the CLR considers the values returned from GetMemoryLoad when determining the timing of the next garbage collection. The ICLRMemoryNotificationCallback InterfaceThe CLR calls the GetMemoryLoad method on IHostMemoryManager when it wants to determine the current memory load on the system. As a host, you have no control over when GetMemoryLoad is called. However, you can be more proactive about notifiying the CLR of the current memory status by calling the methods on an interface provided by the CLR called ICLRMemoryNotificationCallback. The process of reporting memory status through this callback is as follows. After the CLR obtains your memory manager by calling IHostControl::GetHostManager, it calls the RegisterMemoryNotificationCallback method on IHostMemoryManager, passing in an interface pointer of type ICLRMemoryNotificationCallback. The definition of RegisterMemoryNotificationCallback is shown here: interface IHostMemoryManager : IUnknown { // Other methods omitted... HRESULT RegisterMemoryNotificationCallback( [in] ICLRMemoryNotificationCallback * pCallback); } A host's implementation of RegisterMemoryNotificationCallback should save a copy of the ICLRMemoryNotificationCallback interface pointer it is given by the CLR. At any time, the host can call back through ICLRMemoryNotificationCallback to report memory status to the CLR. ICLRMemoryNotificationCallback has a single method called OnMemoryNotification as shown in the following definition from mscoree.idl: interface ICLRMemoryNotificationCallback : IUnknown { HRESULT OnMemoryNotification([in] EMemoryAvailable eMemoryAvailable); } Memory status is reported to the CLR by passing a value from the EMemoryAvailable enumeration: typedef enum { eMemoryAvailableLow = 1, eMemoryAvailableNeutral = 2, eMemoryAvailableHigh = 3 } EMemoryAvailable; The most useful value from EMemoryAvailable is the eMemoryAvailableLow. When this value is passed, the CLR will perform a garbage collection in an attempt to make more storage available to the system. The current version of the CLR doesn't take any action at all when it receives either eMemoryAvailableNeutral or eMemoryAvailableHigh from the host. |
|