Process Address Space
Why This Matters
Every process running on Linux believes it owns the entire memory system. This illusion—virtual memory—is one of the most important abstractions the kernel provides. It enables process isolation, memory overcommit, memory-mapped files, and copy-on-write during fork(). Understanding how the kernel represents and manipulates address spaces is essential for writing kernel code that touches memory management, debugging segfaults, and reasoning about process lifecycle.
Page Tables and the MMU
Linux enables paging very early in the boot process. From that point on, every memory address the CPU generates is a virtual address. The hardware Memory Management Unit (MMU)—typically integrated into the CPU—translates each virtual address to a physical address automatically using the page tables that the kernel sets up.
Key points:
- The kernel maintains one set of page tables per process.
- The MMU performs translations in hardware; the kernel only needs to set the tables up correctly.
- A TLB (Translation Look-aside Buffer) caches recent translations to avoid walking the full page-table tree on every access.
In 64-bit Linux on x86-64, the virtual address space is divided into a low user half and a high kernel half. The mapping is documented at Documentation/x86/x86_64/mm.rst in the kernel source.
The address space is sparsely populated: only certain regions (called VMAs) are valid. Any access outside those regions triggers a page fault that the kernel converts into a segmentation fault for the process.
Address Space: The Big Picture
A process's address space is the set of virtual addresses it is permitted to access. From the process's perspective it looks like a flat, contiguous range (32-bit or 64-bit), but in reality it is carved up into distinct regions with different contents and permissions:
| Region | Contents |
|---|---|
.text |
Executable code (mapped from the ELF file) |
.data |
Initialized global/static variables |
.bss |
Uninitialized variables (zero-mapped) |
| Stack | User-mode call stack (zero-mapped, grows down) |
| Shared libs | Each .so gets its own .text/.data/.bss regions |
mmap regions |
Memory-mapped files, shared memory, malloc anonymous mappings |
The kernel enforces per-region permissions (read/write/execute). An access that violates permissions results in a segfault (SIGSEGV).
Memory Descriptor: mm_struct
The kernel represents a process's entire address space with a single object: struct mm_struct. Every task_struct has an mm field pointing to its memory descriptor.
Reference counting
mm_struct uses two counters:
| Field | Meaning |
|---|---|
mm_users |
Number of user-space threads sharing this address space |
mm_count |
Total reference count: +1 when mm_users > 0, +1 when the kernel is actively using it |
The struct is freed only when mm_count reaches zero.
Lifecycle: fork()
When fork() is called, copy_mm() creates a copy of the parent's memory descriptor for the child:
fork()
└─ copy_mm()
└─ dup_mm()
└─ allocate_mm() ← allocates from a slab cache
The child gets its own mm_struct but initially shares the same physical pages (copy-on-write).
Threads share mm_struct
Threads are created with clone(CLONE_VM, …). The CLONE_VM flag tells the kernel not to call allocate_mm()—instead both threads' task_struct.mm fields point at the same mm_struct. This is how all threads in a process share the same address space.
Teardown: exit()
When a process exits:
do_exit()
└─ exit_mm()
└─ mmput() ← decrements mm_users; frees when it hits zero
Kernel threads and active_mm
Kernel threads have no user-space address space, so task_struct.mm == NULL. However, they still need access to the kernel mappings. The solution: when the scheduler picks a kernel thread, it borrows the previously loaded mm_struct and stores a pointer in task_struct.active_mm. This is safe because all processes share the same kernel address-space mappings.
Virtual Memory Areas (VMAs): vm_area_struct
Within an mm_struct, each contiguous region of valid virtual memory is described by a struct vm_area_struct. You can see them in /proc/<pid>/maps—each line is one VMA.
Boundaries
A VMA covers the half-open interval [vm_start, vm_end). Its size in bytes is vm_end - vm_start.
Each VMA belongs to exactly one mm_struct. Two processes mapping the same file each get their own vm_area_struct. Two threads sharing an mm_struct share all of its vm_area_struct objects.
VMA Flags
The vm_flags field controls permissions and behavior:
| Flag | Meaning |
|---|---|
VM_READ |
Pages are readable |
VM_WRITE |
Pages are writable |
VM_EXEC |
Pages are executable |
VM_SEQ_READ |
Hint: sequential access (increases read-ahead window) |
VM_RAND_READ |
Hint: random access (decreases read-ahead window) |
VM_HUGETLB |
Region uses huge pages (2 MB or 1 GB on x86-64) |
VM_SEQ_READ and VM_RAND_READ are set via the madvise() system call. VM_HUGETLB reduces TLB pressure by covering more address space per entry.
Typical combinations:
- Executable code:
VM_READ | VM_EXEC - Stack:
VM_READ | VM_WRITE
VMA Operations: vm_ops
vm_area_struct.vm_ops is a pointer to a struct vm_operations_struct, a table of function pointers (open, close, fault, page-level operations, etc.) that implement VMA-type-specific behavior—essentially a vtable for VMAs.
Manipulating Address Intervals
Creating a mapping: do_mmap()
do_mmap() adds a new linear address interval to the process address space. Its parameters include:
prot— access permissions (PROT_READ,PROT_WRITE,PROT_EXEC)flags— mapping options (e.g.,MAP_PRIVATE,MAP_SHARED,MAP_ANONYMOUS)
On success the kernel either:
- Merges the new interval with an adjacent VMA that has the same permissions, or
- Creates a new
vm_area_struct.
It returns a pointer to the start of the mapped area. On failure it returns a negative error code.
do_mmap() is the internal implementation behind the mmap2() system call exposed to user space.
Removing a mapping: do_munmap()
do_munmap() removes a linear address interval from the address space. It is the implementation behind the munmap() system call. It may need to split an existing VMA if only part of it is being removed.
Key Takeaways
- The MMU translates every CPU-generated virtual address to a physical address using kernel-maintained page tables; the TLB caches recent translations.
- Each process has exactly one
mm_structdescribing its address space; threads share the samemm_structviaCLONE_VM. mm_userscounts user-space sharers;mm_countis the total reference count protecting the struct from premature freeing.- Each valid region of the address space is a VMA (
vm_area_struct) covering[vm_start, vm_end)with specific permission flags. - Kernel threads have
mm == NULLand borrow the previously loaded address space viaactive_mm—safe because the kernel mapping is identical in all address spaces. do_mmap()/do_munmap()are the internal functions (behindmmap()/munmap()) that add and remove VMAs.