Managing Virtual Memory
Randy Kath
Microsoft Developer Network Technology Group
Created: January 20, 1993
Abstract
Determining which function or set of functions to use for managing memory in your application is difficult without a solid understanding of how each group of functions works and the overall impact they each have on the operating system. In an effort to simplify these decisions, this technical article focuses on the virtual memory management functions: which ones are available, how they are used, and how their use affects the operating system. The following topics are discussed in this article:
- Reserving, committing, and freeing virtual memory
- Changing protection on pages of virtual memory
- Locking pages of virtual memory
- Querying a process's virtual memory
A sample application called ProcessWalker accompanies this technical article on the Microsoft Developer Network CD. This sample application is useful for exploring the virtual address space of a process. It also employs the use of virtual memory functions for implementing a linked list structure.
Introduction
This is one of three related technical articles—"Managing Virtual Memory," "Managing Memory-Mapped Files," and "Managing Heap Memory"—that explain how to manage memory in applications for Windows. In each article, this introduction identifies the basic memory components in the Windows programming model and indicates which article to reference for specific areas of interest.
The first version of the Microsoft Windows operating system introduced a method of managing dynamic memory based on a single global heap, which all applications and the system share, and multiple, private local heaps, one for each application. Local and global memory management functions were also provided, offering extended features for this new memory management system. More recently, the Microsoft C run-time (CRT) libraries were modified to include capabilities for managing these heaps in Windows using native CRT functions such as malloc and free. Consequently, developers are now left with a choice—learn the new application programming interface (API) provided as part of Windows or stick to the portable, and typically familiar, CRT functions for managing memory in applications written for Windows.
The Windows API offers three groups of functions for managing memory in applications: memory-mapped file functions, heap memory functions, and virtual memory functions.
Figure 1. The Windows API provides different levels of memory management for versatility in application programming.
In all, six sets of memory management functions exist in Windows, as shown in Figure 1, all of which were designed to be used independently of one another. So, which set of functions should you use? The answer to this question depends greatly on two things: the type of memory management you want and how the functions relevant to it are implemented in the operating system. In other words, are you building a large database application where you plan to manipulate subsets of a large memory structure? Or maybe you're planning some simple dynamic memory structures, such as linked lists or binary trees? In both cases, you need to know which functions offer the features best suited to your intention and exactly how much of a resource hit occurs when using each function.
Table 1 categorizes the memory management function groups and indicates which of the three technical articles in this series describes each group's behavior. Each technical article emphasizes the impact these functions have on the system by describing the behavior of the system in response to using the functions.
Table 1. Memory Management Functions
Memory set | System resource affected | Related technical article |
---|---|---|
Virtual memory functions | A process' virtual address space System pagefile System memory Hard disk space |
"Managing Virtual Memory" |
Memory-mapped file functions | A process's virtual address space System pagefile Standard file I/O System memory Hard disk space |
"Managing Memory-Mapped Files" |
Heap memory functions | A process's virtual address space System memory Process heap resource structure |
"Managing Heap Memory" |
Global heap memory functions | A process's heap resource structure | "Managing Heap Memory" |
Local heap memory functions | A process's heap resource structure | "Managing Heap Memory" |
C run-time reference library | A process's heap resource structure | "Managing Heap Memory" |
Windows Memory System Overview
Windows employs a page-based virtual memory system that uses linear addressing. Internally, the system manages all memory in segments called pages. Each page of physical memory is backed by either a pagefile for volatile pages of memory or a disk file for read-only memory pages. There can be as many as 16 separate pagefiles at a time. Code, resources, and other read-only data are backed directly by the files from which they originated.
Windows NT provides an independent, 2 gigabyte (GB) user address space for each application (process) in the system. To the application, it appears that there is 2 GB of memory available, regardless of the amount of physical memory that is actually available. When an application requests more memory than is available, Windows NT satisfies the request by paging noncritical pages of memory—from this and/or other processes—to a pagefile and freeing those physical pages of memory. Conceptually, the global heap no longer exists in Windows NT. Instead, each process has a private 32-bit address space from which all of the memory for the process is allocated—including code, resources, data, DLLs (dynamic-link libraries), and dynamic memory. Realistically, the system is still limited by whatever hardware resources are available, but the management of available resources is performed independently of the applications in the system.
Virtual Memory
Windows NT makes a distinction between memory and address space. Each process is attributed 2 GB of user address space no matter how much physical memory is actually available for the process. Also, all processes use the same range of linear 32-bit addresses ranging from 0000000016-7FFFFFFF16, regardless of what memory is available. Windows NT takes care of paging memory to and from disk at appropriate times so that each process is sure to be able to address the memory it needs. Although two processes may attempt to access memory at the same virtual address simultaneously, the Windows NT virtual memory manager actually represents these two memory locations at different physical locations where neither is likely to coincide with the original virtual address. This is virtual memory.
Because of virtual memory, an application is able to manage its own address space without having to consider the impact on other processes in the system. The memory manager in Windows NT is responsible for seeing that all applications have enough physical memory to operate effectively at any given moment. Applications for the Windows NT operating system do not have to be concerned with sharing system memory with other applications as they did in Windows version 3.1 or earlier. Yet even with their own address space, applications still have the ability to share memory with other applications.
One benefit of distinguishing between memory and address space is the capability it provides to applications for loading extremely large files into memory. Instead of having to read a large file into memory, Windows NT provides support for the application to reserve the range of addresses that the file needs. Then, sections of the file can be viewed (physically read into memory) as needed. The same can be done for large allocations of dynamic memory through virtual memory support.
In previous versions of Windows, an application had to allocate memory before being able to manipulate the addresses in that memory. In Windows NT, the address space of each process is already allocated; whether there is any memory associated with the addresses in the address space is a different issue. The virtual memory management functions provide low-level support for independently managing both the addresses and memory of a process.
The key virtual memory functions are:
- VirtualAlloc and VirtualFree
- VirtualLock and VirtualUnlock
- VirtualQuery or VirtualQueryEx
- VirtualProtect or VirtualProtectEx
Each function is grouped with its counterpart if it has one. Memory is allocated using VirtualAlloc and, once allocated, must be freed with VirtualFree. Similarly, pages that have been locked with VirtualLock must be unlocked with VirtualUnlock when no longer needed. VirtualQuery and VirtualProtect have no counterparts, but they both have complementary functions (indicated by the Ex extension on the function names) that allow them to be used on processes other than the calling process, if the calling process has the appropriate privilege to do so. These functions are explained below in their appropriate context.
Free, Reserved, and Committed Virtual Memory
Every address in a process can be thought of as either free, reserved, or committed at any given time. A process begins with all addresses free, meaning they are free to be committed to memory or reserved for future use. Before any free address may be used, it must first be allocated as reserved or committed. Attempting to access an address that is either reserved or free generates an access violation exception.
The entire 2 GB of addresses in a process are either free for use, reserved for future use, or committed to specific memory (in use). Figure 2 represents a hypothetical process consisting of free, reserved, and committed addresses.
Figure 2. A process's 2 GB of virtual address space is divided into regions of free, reserved, and committed memory locations.
Reserved Addresses
When reserving addresses in a process, no pages of physical memory are committed, and perhaps more importantly, no space is reserved in the pagefile for backing the memory. Also, reserving a range of addresses is no guarantee that at a later time there will be physical memory available to commit to those addresses. Rather, it is simply saving a specific free address range until needed, protecting the addresses from other allocation requests. Without this type of protection, routine operations such as loading a DLL or resource could occupy specific addresses and jeopardize their availability for later use.
Reserving addresses is a quick operation, completely independent of the size of the address range being reserved. Whether reserving a 1 GB or a 4K range of addresses, the function is relatively speedy. This is not surprising considering that no resources are allocated during the operation. The function merely makes an entry into the process's virtual address descriptor (VAD) tree.
To reserve a range of addresses, invoke the VirtualAlloc function as shown in the following code fragment:
/* Reserve a 10 MB range of addresses */
lpBase = VirtualAlloc (NULL,
10485760,
MEM_RESERVE,
PAGE_NOACCESS);
As shown here, a value of NULL used for the first parameter, lpAddress, directs the function to reserve the range of addresses at whichever location is most convenient. Alternatively, a specific address could have been passed indicating a precise starting address for the reserved range. Either way, the return value to this function indicates the address at the beginning of the reserved range of addresses, unless the function is unable to complete the request. Then, the return value for the VirtualAlloc function is an error-status value.
The second parameter indicates the range of addresses the function should allocate. This value can be anywhere from one page to 2 GB in size, but VirtualAlloc is actually constrained to a smaller range than that. The minimum size that can be reserved is 64K, and the maximum that can be reserved is the largest contiguous range of free addresses in the process. Requesting one page of reserved addresses results in a 64K address range. Conversely, requesting 2 GB will certainly fail because it is not possible to have that much address space free at any given time. (Remember that the act of loading an application consumes part of the initial 2 GB address space.)
Note
Windows NT builds a safeguard into every process's address space. Both the upper and lower 65,536 bytes of each process are permanently reserved by the system. These portions of the address space are reserved to trap stray pointers—pointers that attempt to address memory in the range 0000000016-0000FFFF16 or 7FFF000016-7FFFFFFF16. Not coincidentally, it is easy to detect pointers in this range by simply ignoring the lower four nibbles (the rightmost two bytes) in these addresses. Essentially, a pointer is invalid if the upper four nibbles are 000016 or 7FFF16; all other values represent valid addresses.
The final two parameters in the VirtualAlloc function, dwAllocationType and dwProtect, are used to determine how to allocate the addresses and the protection to associate with them. Addresses can be allocated as either type MEM_COMMIT or MEM_RESERVE. PAGE_READONLY, PAGE_READWRITE, and PAGE_NOACCESS are the three protections that can be applied to virtual memory. Reserved addresses are always PAGE_NOACCESS, a default enforced by the system no matter what value is passed to the function. Committed pages can be either read-only, read-write, or no-access.
Committed Memory
To use reserved addresses, memory must first be committed to the addresses. Committing memory to addresses is similar to reserving it—call VirtualAlloc with the dwAllocation parameter equal to MEM_COMMIT. At this point, resources become committed to addresses. Memory can be committed as little as one page at a time. The maximum amount of memory that can be committed is based solely on the maximum range of contiguous free or reserved addresses (but not a combination of both), regardless of the amount of physical memory available to the system.
When memory is committed, physical pages of memory are allocated and space is reserved in a pagefile. That is, pages of committed memory always exist as either physical pages of memory or as pages that have been paged to the pagefile on disk. It is also possible that, while committing a chunk of memory, part or all of that memory will not reside in physical memory initially. Some pages of memory reside initially in the pagefile until accessed. Once pages of memory are committed, the virtual memory manager treats them like all other pages of memory in the system.
In the Windows NT virtual memory system, page tables are used to access physical pages of memory. Each page table is itself a page of memory, like committed pages. Occasionally, when committing memory, additional pages must be allocated for page tables at the same time. So a request to commit a page of memory can require one page commitment for a page table, one page for the requested page, and two pages of space in the pagefile to back each of these pages. Consequently, the time it takes VirtualAlloc to complete a memory-commit request varies widely, depending on the state of the system and the size of the request.
The following example demonstrates how to commit a specific page of reserved addresses from the previous example to a page of memory.
/* Commit memory for 3rd page of addresses. */
lpPage3 = VirtualAlloc (lpBase + (2 * 4096),
4096,
MEM_COMMIT,
PAGE_READWRITE);
Notice that instead of specifying NULL for lpAddress, a specific address is given to indicate exactly which page of reserved addresses becomes committed to memory. Also, this page of memory is initially given PAGE_READWRITE protection instead of PAGE_NOACCESS as in the previous example. The return address from the function is the virtual address of the first pages of committed addresses.
Freeing Virtual Memory
Once addresses have been allocated as either reserved or committed, VirtualFree is the only way to release them—that is, return them to free addresses. VirtualFree can also be used to decommit committed pages and, at the same time, return the addresses to reserved status. When decommitting addresses, all physical memory and pagefile space associated with the addresses is released. The following example demonstrates how to decommit the page of memory committed in the previous example.
/* Decommit memory for 3rd page of addresses. */
VirtualFree (lpBase + (2 * 4096),
4096,
MEM_DECOMMIT,
PAGE_NOACCESS);
Only addresses that are committed can be decommitted. This is important to remember when you need to decommit a large range of addresses. Say, for example, you have a range of addresses where several subsets of the addresses are committed and others are reserved. The only way to make the entire range reserved is to independently decommit each subset of committed addresses one by one. Attempting to decommit the entire range of addresses will fail because reserved addresses cannot be decommitted.
Conversely, the same range of addresses can be freed in one fell swoop. It doesn't matter what the state of an address is when the address is freed. The following example demonstrates freeing the 10 MB range of addresses reserved in the first example.
/* Free entire 10 MB range of addresses. */
VirtualFree (lpBase,
10485760,
MEM_RELEASE,
PAGE_NOACCESS);
Changing Protection on Pages of Virtual Memory
Use the VirtualProtect function as a method for changing the protection on committed pages of memory. An application can, for example, commit a page of addresses as PAGE_READWRITE and immediately fill the page with data. Then, the protection on the page could be changed to PAGE_READONLY, effectively protecting the data from being overwritten by any thread in the process. The following example uses the VirtualProtect function to make an inaccessible page available.
/* Change page protection to read/write. */
VirtualProtect (lpStack + 4096,
4096,
PAGE_READWRITE,
lpdwOldProt);
Consider the following as a context for using this function. A data-buffering application receives a varying flow of data. Depending on specific hardware configurations and other software applications competing for CPU time, the flow of data may at times exceed the capability of the process. To prevent this from happening, the application designs a memory system that initially commits some pages of memory for a buffer. The application then protects the upper page of memory with PAGE_NOACCESS protection so that any attempt to access this memory generates an exception. The application also surrounds this code with an exception handler to handle access violations.
When an access violation exception occurs, the application is able to determine that the buffer is approaching its upper limit. It responds by changing the protection on the page to PAGE_READWRITE, allowing the buffer to receive any additional data and continue uninterrupted. At the same time, the application spawns another thread to slow the data flow until the buffer is back down to a reasonable operating range. When things are back to normal, the upper page is returned to PAGE_NOACCESS and the additional thread goes away. This scenario describes how combining page protection and exception handling can be used to provide unique memory management opportunities.
Locking Pages of Virtual Memory
Processes in Windows NT have a minimal set of pages called a working set that, in order for the process to run properly, must be present in memory when running. Windows NT assigns a default number of pages to a process at startup and gradually tunes that number to achieve a balanced optimum performance among all active processes in the system. When a process is running (actually, when the threads of a process are running), Windows NT works hard at making sure that the process has its working set of pages resident in physical memory at all times.
Processes in Windows NT are granted subtle influence into this system behavior with the VirtualLock and VirtualUnlock functions. Essentially, a process can establish specific pages to lock into its working set. However, this does not give the process free reign over its working set. It cannot affect the number of pages that make up its working set (the system adjusts the working set for each process routinely), and it cannot control when the working set is in memory and when it is not. The maximum number of pages that can be locked into a process's working set at one time is limited to 32. An application could do more harm than good by locking pages of committed memory into the working set because doing so may force other critical pages in the process to become replaced. In that case, the pages could become paged to disk, causing page faults to occur whenever they were accessed. Then the process would spend much of its CPU allotment just paging critical pages in and out of memory.
Below is an example that locks a range of addresses into memory when the process is running.
/* Lock critical addresses into memory. */
VirtualLock (lpCriticalData, 1024);
Notice the range of addresses being locked into memory in this example is less than one page. It is not necessary for the entire range to be in a single page of memory. The net result is that the entire page of memory containing the data for the addresses, not just the data for the addresses indicated, is locked into memory. If the data straddles a page boundary, both pages are locked.
Querying a Process's Virtual Memory
Given a process's 2 GB of address space, managing the entire range of addresses would be difficult without the ability to query address information. Because the addresses themselves are represented independent of the memory that may or may not be committed to them, querying them is simply a matter of accessing the data structure that maintains their state. In Windows NT, this structure is the virtual address descriptor tree mentioned earlier. Windows exposes the capability of "walking the VAD structure" in the VirtualQuery and VirtualQueryEx functions. Again, the Ex suffix indicates which function can be called from one process to query another—if the calling process has the security privilege necessary to perform this function. The following example is extracted from the ProcessWalker sample:
/* Query next region of memory in child process. */
VirtualQueryEx (hChildProcess,
lpMem,
lpList,
sizeof (MEMORY_BASIC_INFORMATION));
The ProcessWalker application's primary function is to walk a process's address space, identifying each of its distinct address regions and representing specific state information about each region. It does this by enumerating each region one at a time from the bottom of the process to the top. lpMem is used to indicate the location of each region. Initially it is set to 0, and after returning from each query of a new region, it is incremented by the size of the region it queried. This process is repeated until lpMem reaches the upper system reserved area.
lpList is a pointer to a MEMORY_BASIC_INFORMATION structure to be filled in by the VirtualQueryEx function. When the function returns, this structure represents information about the region queried. The structure has the following members:
typedef struct _MEMORY_BASIC_INFORMATION { /* mbi */
PVOID BaseAddress; /* Base address of region */
PVOID AllocationBase; /* Allocation base address */
DWORD AllocationProtect; /* Initial access protection */
DWORD RegionSize; /* Size in bytes of region */
DWORD State; /* Committed, reserved, free */
DWORD Protect; /* Current access protection */
DWORD Type; /* Type of pages */
} MEMORY_BASIC_INFORMATION;
The VirtualQuery function returns this state information for any contiguous address region. The function determines the lower bound of the region and the size of the region, along with the exact state of the addresses in the region. The address it uses to determine the region can be any address in the region. So, if you wish to determine how much stack space has been committed at any given time, follow these steps:
Get the thread context for the thread in question.
Call the VirtualQuery function, supplying the address of the stack pointer in the thread context information as the lpMem parameter in the function.
The query returns the size of the committed memory and the address of the base of the stack in the MEMORY_BASIC_INFORMATION structure in the form of the RegionSize and BaseAddress, respectively.
Regions of memory, as defined by VirtualQuery, are a contiguous range of addresses whose protection, type, and base allocation are the same. The type and protection values are described earlier in this technical article. The base allocation is the lpAddress parameter value that is used when the entire region of memory was first allocated via the VirtualAlloc function. It is represented in the MEMORY_BASIC_INFORMATION structure as the AllocationBase field.
When free addresses become either reserved or committed, their base allocation is determined at that time. A region of memory is not static by any means. Once a single page in a region of reserved addresses becomes committed, the region is broken into one or more reserved regions and one committed region. This continues as pages of memory change state. Similarly, when one of several PAGE_READWRITE committed pages is changed to PAGE_READONLY protection, the region is broken into multiple, smaller regions.
Conclusion
The virtual memory management functions in Windows offer direct management of virtual memory in Windows NT. Each process's 2 GB user address space is divided into regions of memory that are either reserved, committed, or free virtual addresses. A region is defined as a contiguous range of addresses in which the protection, type, and base allocation of each address is the same. Within each region are one or more pages of addresses that also carry protection and pagelock flag status bits.
The virtual memory management functions provide capabilities for applications to alter the state of pages in the virtual address space. An application can change the type of memory from committed to reserved or change the protection from PAGE_READWRITE to PAGE_READONLY to prevent access to a region of addresses. An application can lock a page into the working set for a process to minimize paging for a critical page of memory. The virtual memory functions are considered low-level functions, meaning they are relatively fast but they lack many high-level features.