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:

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:

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:

On success the kernel either:

  1. Merges the new interval with an adjacent VMA that has the same permissions, or
  2. 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

Practice

  1. What component is responsible for translating virtual addresses to physical addresses at runtime?
  2. Which of the following statements about mm_users and mm_count in mm_struct is correct?
  3. When fork() creates a child process, which call chain allocates a new mm_struct for the child?
  4. Two threads T1 and T2 belong to the same process. Which of the following is true about their vm_area_struct objects?
  5. What does the VM_HUGETLB flag on a VMA indicate, and what is the main performance benefit?
  6. A kernel thread is scheduled. Its task_struct.mm is NULL. How does the kernel handle address-space state for this thread?
  7. Explain the difference between do_mmap() and mmap(). Under what conditions does do_mmap() create a new vm_area_struct versus merging with an existing one?
  8. A student claims: 'The VMA information is directly loaded into the MMU.' Is this correct? Explain why or why not.
  9. Which of the following correctly describes the boundary convention for a VMA's address range?
  10. Describe the sequence of events that frees a process's mm_struct when the process calls exit(). Why is it safe to free the struct even if a kernel thread has borrowed it via active_mm?