Process Address Spaces
Now that you understand the mechanics of paging, let's talk about process
address spaces and how the Memory Manager defines and keeps track of them. As
I've stated, each process has a 4GB virtual address space. This space is divided
into two areas, in which the lower addresses map to data and code that are
private to the process, and the upper addresses map to system data and code that
are common to all processes, as Figure 3 shows. When the scheduler changes
execution from one process to another, the page directory register in the
process changes so that the process-private portion of the processor's mappings
is updated. The system mappings are kept global through common page tables that
every process' page directories point to.
The permissions that apply to pages in each region also reflect the split
in mappings. The PTEs of x86 and Alpha processors contain a permissions bit.
This bit specifies whether a page is accessible from a program executing in user
mode. If a process refers to a page that is not accessible from user mode, the
MMU generates a page fault, and the Memory Manager generates an access violation
for the process (which is caught by Dr. Watson). As the Memory Manager sets up
address spaces, it marks all system PTEs as accessible only from kernel mode.
Thus, the Executive subsystems, device drivers, hardware abstraction layer
(HAL), and Kernel can reference system memory, but user applications cannot
touch system memory. This restriction protects key OS data structures and makes
the system secure.
Figure 3 shows that on pre-Service Pack 3 (SP3) systems, the boundary
between the lower address space (user mode) and the upper address space (kernel
mode) is at 2GB. On SP3 and NT 4.0, Enterprise Edition, the boundary can be
moved with a switch on a boot.ini line (/3GB) so that user programs have access
to 3GB and system memory can access 1GB. This change enables memory-intensive
applications such as database servers to directly address more memory, thus
relying less on shuffling data to and from disk in a manner similar to paging.
Memory allocation in NT is a two-step process--virtual memory addresses are
reserved first, and committed second. The reservation process is simply a way NT
tells the Memory Manager to reserve a block of virtual memory pages to satisfy
other memory requests by the process. However, Memory Manager makes no changes
to a process at the time of a reservation because the reservation does not use
actual memory. When a process wants to use addresses it has reserved, it must commit them. Access to uncommitted reserved memory will typically result in an access
violation. When a process wants to commit addresses, the Memory Manager ensures
that the process has enough memory quota to do so. The Memory Manager also
checks to see that there is enough commit memory (physical memory plus the size
of all the paging files) for the commit request. There are many cases in which
an application will want to reserve a large block of its address space for a
particular purpose (keeping data in a contiguous block makes the data easy to
manage) but might not want to use all of the space. The application can specify
both reservation and commit in a single request to the Memory Manager with a
specific API, and this is the way most applications allocate memory.
One example of the Memory Manager's use of the single-request functionality
is in the management of a thread's call stack. The Memory Manager reserves a
range of memory (usually 1MB) in a process' address space for each thread
created in a process. However, the Memory Manager actually commits only one page
of each stack. As a thread uses the stack and touches reserved pages, the Memory
Manager commits the reserved pages, increasing the size of the stack.
When a process reserves memory, it must specify the amount of memory but
can also request a starting virtual address and specific protections to be
placed on the memory. NT uses bits in a PTE that the MMU defines to indicate
whether a page can be written to or read from, and whether code can be executed
in the page. NT sets the addresses that the API parameters specify. If the PTE
bits specify a page as read-only and a process attempts to write to the page,
the MMU generates a page fault, and NT's page-fault handler will flag an access
violation for the process. The system uses this protection to easily detect
errant programs that try to do things such as modify their own code image.
NT keeps track of the reserved or committed address ranges in a process'
address space by using a tree data structure, such as the example Figure 4
shows. Each node in the tree represents a range of pages that have identical
characteristics with respect to protection and commit state information. This
tree structure is a binary splay tree, which means that the depth of the
tree (the maximum number of links from the root to any leaf, or bottom, node) is
kept to a minimum. The tree's nodes are called Virtual Address Descriptors
(VADs), and the process' process control block stores the root of the tree. When
a process makes a memory reservation or commit request, the Memory Manager can
quickly determine where there are free spaces in the process address map, and
whether the request overlaps memory that is already reserved or committed.
Prev. page
1
2
[3]
4
next page