9.2. Virtual Address Spaces
The virtual address space of a process is the range of memory addresses that are presented to the process as its environment; some addresses are mapped to physical memory, some are not. A process's virtual address space skeleton is created by the kernel at the time the fork() system call creates the process. (See Section 2.7.) The virtual address layout within a process is set up by the dynamic linker and sometimes varies across different hardware platforms. As we can see in Figure 9.2, virtual address spaces are assembled from a series of memory mappings. Each process has at least four mappings:
Figure 9.2. Process Virtual Address Space
Figure 9.2 illustrates a process's virtual address space.
The figure shows how the /sbin/sh process has its executable mapped in near the bottom address, with the heap adjoining it, the stack at the top, and a hole between the heap and the stack. The heap grows upward as more memory is allocated through malloc(), and the stack grows downward as more frames are placed on the stack. Not all of the virtual address space within a process is mapped, and the process can legally access memory only within the areas with valid mappings; a process's attempt to access memory outside of the mappings causes a page fault. A more sophisticated process may have more mappings; those that make use of shared libraries or mapped files will have additional mappings between the heap and stack.
9.2.1. Sharing Executables and Libraries
The Solaris kernel supports sharing of memory, files, libraries, and executables. For example, the Solaris kernel shares libraries by dynamically mapping the library file into the address space during program startup. The libraries are mapped into the address space between the stack and the heap, at different positions on different platforms.
When a shared library object is mapped into a process's address space, it can be mapped shared so that all processes share the same physical memory pages. Executable text and data are shared in the same manner, by simply mapping the same executable file into every address space.
We see more about how mapping of files and sharing of memory occur when we explore the vnode segment driver, which is responsible for mapping files into address spaces.
9.2.2. Address Spaces on SPARC Systems
The process address space on SPARC systems varies across different SPARC platforms according to the MMU on that platform. SPARC has three different address space layouts:
The SPARC V7 systems use a shared address space between the kernel and process and use the processor's privilege levels to prevent user processes from accessing the kernel's address space. The kernel occupies the top virtual memory addresses, and the process occupies the lower memory addresses. This means that part of the virtual address space available to the process is consumed by the kernel, limiting the size of usable process virtual memory to between 3.5 and 3.75 Gbytes, depending on the size of the kernel's virtual address space. This also means that the kernel has a limited size, ranging between 128 and 512 Mbytes. The SPARC V7 combined 32-bit kernel and process address space is shown in Figure 9.3.
Figure 9.3. SPARC 32-Bit Shared Kernel/Process Address Space
The SPARC V9 (UltraSPARC, sun4u) microprocessor allows the kernel to operate in an address space separate from user processes, so the process can use almost all of the 32-bit address space (a tiny bit is reserved at the top for the Open Boot PROM) and also allows the kernel to have a similar, large address space. This design removes the 512-Mbyte limit for kernel address space, which was a major problem for large machines such as the older SPARCcenter 2000 machines. The process address space looks similar to the shared kernel/process address space, except that the kernel area is missing and the stack and libraries are moved to the top of memory.
The UltraSPARC processor also supports the SPARC V9 64-bit mode, which allows a process to have a virtual address space that spans 64 bits. The Ultra-SPARC-I and -II implementations, however, support only 44 bits of the address space, which means that there is a virtual address space hole in the middle of the address space. This area of memory creates a special type of UltraSPARC trap when accessed. Some future generations of SPARC V9 processors will not have the same hole in the address space. The UltraSPARC V9 32-bit and 64-bit address spaces are shown in Figure 9.4.
Figure 9.4. SPARC sun4u 32- and 64-Bit Process Address Space
On all SPARC platforms, the bottom of the virtual address space is not mapped. Null pointer references cause a segmentation fault rather than return spurious contents of whatever was at the bottom of the address space.
9.2.3. x86 and x64 Address Space Layout
The Intel x86 32-bit user address space also includes a mapping of the kernel. The main difference with the Intel address space is that the space is reserved at the top of the address space for the kernel and the stack is mapped underneath the executable binary, growing down toward the bottom. The x64 address space is a closer representation of the SPARC 64-bit address space. The x86 and x64 address spaces are shown in Figure 9.5.
Figure 9.5. x86/x64 Process Address Spaces
9.2.4. Growing the Heap
Process virtual memory for user data structures is allocated from the heap mapping, which resides above the executable data mapping. The heap starts out small and then grows as virtual memory is allocated. The heap grows in units of pages; it is simply a large area of virtual memory available for reading and writing. A single, large, virtual memory area is difficult to program to, so a general-purpose memory allocator manages the heap area; thus, arbitrarily sized memory objects can be allocated and freed. The general-purpose memory allocator is implemented with malloc() and related library calls.
A process grows its heap space by making the sbrk() system call. The sbrk() system call grows the heap mapping by the amount requested each time it is called. A user program does not need to call sbrk() directly because the malloc() library calls sbrk() when it needs more space to allocate from. The sbrk() system call is shown below.
void *sbrk(intptr_t incr);
The heap mapping is virtual memory, so requesting memory with malloc and sbrk does not allocate physical memory; it merely allocates the virtual address space. Only when the first reference is made to a page within the allocated virtual memory is physical memory allocated, one page at a time. The memory system transparently achieves this "zero fill on demand" allocation because a page fault occurs the first time a page is referenced in the heap, and the segment driver then recognizes the first memory access and simply creates a page at that location on-the-fly.
Memory pages are allocated to the process heap by zero-fill-on-demand and then remain in the heap mapping until the process exits or until they are stolen by the page scanner. Calls to the memory allocator free() function do not return physical memory to the free memory pool; free() simply marks the area within the heap space as free for later use. For this reason, the amount of physical memory allocated to a process typically grows, but unless there is a memory shortage, it will not shrink, even if free() has been called.
The heap can grow until it collides with the memory area occupied by the shared libraries. The maximum size of the heap depends on the platform virtual memory layout and differs on each platform. In addition, on 64-bit platforms, processes may execute in either 32- or 64-bit mode. As shown in Figure 9.4, the size of the heap can be much larger in processes executing in 64-bit mode. Table 9.1 shows the maximum heap sizes and the operating system requirements that affect the maximum size.
9.2.5. The Stack
The process stack is mapped into the address space with an initial allocation and then grows downward. The stack, like the heap, grows on demand, but no library grows the stack; instead, a different mechanism triggers this growth.
Initially, a single page is allocated for the stack, and as the process executes and calls functions, it pushes the program counter, arguments, and local variables onto the stack. When the stack grows larger than one page, the process causes a page fault, and the kernel notices that this is a stack-mapping page fault and grows the stack mapping.
188.8.131.52. Memory Mapped Files
The address space mapping architecture makes it easy for one or more processes to map the same file into their address space. When files are mapped into one or more processes, seg_vn mappings are created in each process that points to the same vnode. Each process has its own virtual memory mapping to the file, but they all share the same physical memory pages for the files. The first mapping to cause a page fault reads a page into physical memory, and then the second and subsequent mappings simply create a reference to the existing physical memory pageas attaching.
Figure 9.6 shows how two processes can map the same file. Each process creates its own mapping object, but both mappings point to the same file and are mapped to the same physical pages. Notice that the second process need not have all the pages attached to the mapping, even if both mappings map the same parts of the file. In this case, the second process would attach to these pages when they are referenced. A minor fault is used to describe this event. You can see minor faults by using vmstat.
Figure 9.6. Shared Mapped Files
Several options govern how a file is shared when it is mapped between two or more processes. These options control how changes are propagated across the shared file. For example, if one process wants to modify one of the pages mapped into the process, should the other process see exactly the same change or should the change remain private to the process that made the change? The options allow you to choose which behavior you desire. The options are those that can be passed to the protection and flags argument of mmap() when the file is mapped. The behavior for the different flags is listed in Table 9.2.
9.2.6. Using pmap to Look at Mappings
Use the pmap command to inspect the mappings for a process. One line of output is shown for each mapping, along with descriptive data.
sol9$ pmap 102905 102905: sh 00010000 192K r-x-- /usr/bin/ksh [ Text Mapping ] 00040000 8K rwx-- /usr/bin/ksh [ Data Mapping ] 00042000 40K rwx-- [ heap ] [ Heap] FF180000 664K r-x-- /usr/lib/libc.so.1 [ C Library Text ] FF236000 24K rwx-- /usr/lib/libc.so.1 [ C Library Data ] FF23C000 8K rwx-- /usr/lib/libc.so.1 [ C Library Data ctd... ] FF250000 8K rwx-- [ anon ] [ Misc anon mapping ] FF260000 16K r-x-- /usr/lib/en_US.ISO8859-1.so.2 [ Library mappings contunue..] FF272000 16K rwx-- /usr/lib/en_US.ISO8859-1.so.2 FF280000 560K r-x-- /usr/lib/libnsl.so.1 FF31C000 32K rwx-- /usr/lib/libnsl.so.1 FF324000 32K rwx-- /usr/lib/libnsl.so.1 FF340000 16K r-x-- /usr/lib/libc_psr.so.1 FF350000 16K r-x-- /usr/lib/libmp.so.2 FF364000 8K rwx-- /usr/lib/libmp.so.2 FF380000 40K r-x-- /usr/lib/libsocket.so.1 FF39A000 8K rwx-- /usr/lib/libsocket.so.1 FF3A0000 8K r-x-- /usr/lib/libdl.so.1 FF3B0000 8K rwx-- [ anon ] FF3C0000 152K r-x-- /usr/lib/ld.so.1 FF3F6000 8K rwx-- /usr/lib/ld.so.1 FFBFC000 16K rw--- [ stack ] [ Stack ] total 188
As shown in the example, the program's address space comprises several mappings. At the top is the program's binary, mapped as a read-only text mapping followed by a writable data mapping, continuing through to the process stack. Without any further options, pmap simply shows the starting address, the virtual address size, protection modes, and a description of each mapping. The columns are explained as follows: