Linux Virtual Memory Areas Explained-free Linux device drivers tutorial

Linux Virtual Memory Areas Explained

Free Linux Kernel Development Course — Kernel Memory Management, Lecture 3

Every running process on Linux is made of separate chunks of memory — code, data, heap, stack, and shared libraries — and the kernel needs a clean way to track each one. That tracking unit is called a virtual memory area (VMA), represented in the kernel by struct vm_area_struct. In this lecture of our free Linux kernel development course, we open up how a modern 6.x kernel stores, searches, and manages VMAs for every process, and why the old red-black tree design you may have read about in older books no longer exists in current kernels.

This lecture continues our free Linux device drivers and embedded Linux course series on kernel memory management. If you are new here, catch up using the PREV_LEC link above before continuing.

virtual memory area
vm_area_struct
free linux kernel development course
free embedded linux course
free linux device drivers course
maple tree linux kernel
find_vma

What You Will Learn

What a virtual memory area is
mm_struct and the maple tree
vm_area_struct key fields
Reading /proc/pid/maps
find_vma() and the VMA iterator
Address translation basics

Prerequisites

You should be comfortable with C pointers, have basic familiarity with writing and loading a Linux kernel module, and have completed the earlier lectures in this Kernel Memory Management chapter (use the PREV_LEC link above). A Linux machine or VM running kernel 6.x with headers installed is recommended for the hands-on examples.

What Is a Virtual Memory Area in Linux?

From a process’s point of view, its entire address space looks flat and contiguous. In reality the kernel divides that address space into distinct regions — one for the program’s executable code, one for initialized data, one for the heap, one for the stack, and one for every file or anonymous mapping created with mmap(). Each such region is a virtual memory area, and every VMA is virtually contiguous even though the physical pages backing it may be scattered anywhere in RAM.

Every VMA carries a start address, an end address, a set of permission and behaviour flags (read, write, execute, shared, growable), and, if it maps a file, a pointer back to that file. A process can have anywhere from a handful of VMAs to several thousand, depending on how many libraries and mappings it uses.

How mm_struct Tracks Every Virtual Memory Area

Each process’s memory is described by struct mm_struct, reachable from task_struct->mm. Older kernels stored VMAs two ways at once: an augmented red-black tree for fast range lookups, plus a doubly linked list threaded through vm_next and vm_prev for sequential walking. Kernel 6.1 removed both of those in favour of a single, unified maple tree stored in mm_struct->mm_mt.

mm_struct field Purpose
mm_mt Maple tree holding every VMA of the process (replaces the old rbtree + linked list, since kernel 6.1)
mmap_base Base address for memory-mapped file and anonymous regions
pgd Pointer to the process’s page global directory
mmap_lock Reader/writer lock protecting the address space (renamed from the older mmap_sem)
start_code / end_code Bounds of the program’s text (code) segment
start_data / end_data Bounds of the initialized data segment
start_brk / brk Bounds of the heap, adjusted by brk()
start_stack Top of the process stack
map_count Number of VMAs currently mapped for this process
Process Memory Layout
task_struct
Process descriptor
mm_struct
Memory descriptor
mm_mt (maple tree)
Stack VMA (grows down)
Memory mapping VMAs (mmap, shared libraries)
Heap VMA (grows up)
BSS & Data VMAs
Text (code) VMA

struct vm_area_struct: Key Fields

Every node in the maple tree is a struct vm_area_struct describing one virtual memory area.

Field Meaning
vm_start First virtual address inside the VMA
vm_end First virtual address just outside the VMA
vm_mm Back-pointer to the owning mm_struct
vm_flags Permission and behaviour flags: VM_READ, VM_WRITE, VM_EXEC, VM_SHARED, VM_GROWSDOWN
vm_page_prot Page protection bits handed to the page table entries
vm_file struct file pointer if this VMA is a file-backed mapping, NULL for anonymous memory
vm_ops vm_operations_struct with fault/open/close callbacks used by drivers implementing mmap

Modernization note: the vm_next and vm_prev pointers that older references describe do not exist in current kernels. To move between neighbouring VMAs today you use the VMA iterator API (vma_next(), vma_prev(), for_each_vma()) built on top of the maple tree.

Reading Virtual Memory Areas from /proc/pid/maps

The simplest way to see every VMA of a running process is to read /proc/<pid>/maps. Let’s build a tiny original program that creates one anonymous mapping and one file-backed mapping, then inspect its own memory layout.

// ep_mmap_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

int main(void)
{
    /* Anonymous private mapping - a fresh VMA with no backing file */
    void *anon = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                       MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (anon == MAP_FAILED) {
        perror("mmap anon");
        return 1;
    }

    /* File-backed mapping of this very binary - a second VMA */
    int fd = open("/proc/self/exe", O_RDONLY);
    void *file_map = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0);

    printf("Anonymous VMA at   : %p\n", anon);
    printf("File-backed VMA at : %p\n", file_map);
    printf("My PID is %d - now check /proc/%d/maps\n", getpid(), getpid());

    pause();  /* keep the process alive so we can inspect it */
    return 0;
}
$ gcc -o ep_mmap_demo ep_mmap_demo.c
$ ./ep_mmap_demo &
Anonymous VMA at   : 0x7f2a1c3d0000
File-backed VMA at : 0x7f2a1c3ce000
My PID is 24831 - now check /proc/24831/maps

$ cat /proc/24831/maps
55f2a1a3b000-55f2a1a3c000 r-xp 00000000 08:02 1311247 /home/user/ep_mmap_demo
55f2a1c5c000-55f2a1c5d000 r--p 00000000 08:02 1311247 /home/user/ep_mmap_demo
55f2a1c5d000-55f2a1c5e000 rw-p 00001000 08:02 1311247 /home/user/ep_mmap_demo
7f2a1c3ce000-7f2a1c3cf000 r--p 00000000 08:02 1311247 /home/user/ep_mmap_demo
7f2a1c3d0000-7f2a1c3d1000 rw-p 00000000 00:00 0
7fffb2f9e000-7fffb2fbf000 rw-p 00000000 00:00 0          [stack]
7fffb2fd0000-7fffb2fd3000 r--p 00000000 00:00 0          [vvar]
7fffb2fd3000-7fffb2fd5000 r-xp 00000000 00:00 0          [vdso]

Each line is one virtual memory area. The fields, in order, are: address range, permissions (r/w/x plus p for private or s for shared), file offset, device major:minor, inode, and pathname. Notice the anonymous mapping line has no filename and device/inode 0.

Field What it tells you
Permissions r, w, x show read/write/execute; a bare – means that access is denied
p / s Whether writes are private (copy-on-write) or shared with other mappers
offset Byte offset into the file where this VMA’s mapping begins
[heap] / [stack] / [vdso] Special kernel-provided regions instead of a real file path

Finding a Virtual Memory Area with find_vma()

Kernel code frequently needs to answer one question: “which VMA, if any, covers this virtual address?” That is exactly what find_vma() does, declared in linux/mm.h. Internally it now walks the maple tree instead of the old rbtree, but the calling convention for driver and module authors is unchanged.

/* Look up the VMA covering (or the first VMA after) addr */
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr);

/* Look up the VMA that exactly contains addr, or NULL */
struct vm_area_struct *vma_lookup(struct mm_struct *mm, unsigned long addr);

Here is an original, minimal kernel module that walks every VMA belonging to the current process and prints a short summary to the kernel log using the modern VMA iterator instead of the removed vm_next pointer.

// ep_vma_walk.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/mm.h>

static int __init ep_vma_walk_init(void)
{
    struct vm_area_struct *vma;
    struct mm_struct *mm = current->mm;
    unsigned long count = 0;

    if (!mm)
        return 0;

    mmap_read_lock(mm);

    /* for_each_vma() drives the maple-tree based VMA iterator */
    VMA_ITERATOR(vmi, mm, 0);
    for_each_vma(vmi, vma) {
        pr_info("ep_vma_walk: VMA %#lx-%#lx flags=%pGv\n",
                vma->vm_start, vma->vm_end, &vma->vm_flags);
        count++;
    }

    mmap_read_unlock(mm);
    pr_info("ep_vma_walk: process %s has %lu virtual memory areas\n",
            current->comm, count);
    return 0;
}

static void __exit ep_vma_walk_exit(void)
{
    pr_info("ep_vma_walk: unloaded\n");
}

module_init(ep_vma_walk_init);
module_exit(ep_vma_walk_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala demo: walk current process VMAs");
$ sudo insmod ep_vma_walk.ko
$ dmesg | tail -6
[  812.221101] ep_vma_walk: VMA 0x559a12b3b000-0x559a12b3c000 flags=read|exec
[  812.221104] ep_vma_walk: VMA 0x559a12d5c000-0x559a12d5d000 flags=read
[  812.221106] ep_vma_walk: VMA 0x559a12d5d000-0x559a12d5e000 flags=read|write
[  812.221108] ep_vma_walk: VMA 0x7f11a0000000-0x7f11a0021000 flags=read|write
[  812.221110] ep_vma_walk: process insmod has 14 virtual memory areas
$ sudo rmmod ep_vma_walk

Note that current->mm belongs to the process running insmod at the moment the module initializes, which is why you see the loader’s own VMAs here.

Address Translation and the MMU: A Quick Preview

A virtual memory area only tells the kernel that an address range is valid and what permissions apply to it — it does not by itself put any data in physical RAM. Every time the CPU touches memory, the Memory Management Unit (MMU) must translate the virtual address into a physical one using the process’s page tables, and it consults the VMA to check permissions along the way. If a page is accessed that falls outside every VMA, or that violates the VMA’s permissions, the MMU raises a page fault and the kernel responds (often by killing the process with a segmentation fault). We will open up the page table walk itself — PGD, P4D, PUD, PMD, and PTE levels — in the next lecture of this free Linux kernel development course.

Old rbtree Design vs Modern Maple Tree Design

Aspect Pre-6.1 kernels Kernel 6.1 and later
Data structure Augmented red-black tree + separate linked list + VMA cache Single maple tree (mm_mt)
Sequential walk vma->vm_next / vma->vm_prev VMA iterator: for_each_vma(), vma_next(), vma_prev()
Range lookup find_vma() walks the rbtree find_vma() / vma_lookup() walk the maple tree
Locking model mmap_sem mmap_lock (renamed) with ongoing RCU-mode work for reduced contention
Cache behaviour Full VMA loaded into cache while walking rbtree nodes Maple tree’s higher branching factor means fewer cache misses per lookup

Real-World Use Cases

  • Debugging memory corruption or unexpected memory growth by reading /proc/pid/maps or using pmap on a running process
  • Writing character and platform drivers that implement an mmap() file operation, which allocates a VMA for user space to directly access device memory
  • Investigating out-of-memory (OOM) situations by checking how many VMAs and how much virtual address space a process holds
  • Security auditing: checking for writable-and-executable VMAs, which many hardened systems forbid

Common Mistakes and Troubleshooting

Mistake Why it happens / fix
Referencing vma->vm_next in new code That field was removed with the maple tree conversion; use the VMA iterator API instead
Walking VMAs without holding mmap_lock The maple tree can change under you; always take mmap_read_lock() or mmap_write_lock() first
Assuming VMAs are physically contiguous Only the virtual address range is contiguous; backing physical pages can be scattered
Confusing find_vma() with vma_lookup() find_vma() can return a VMA that starts after addr; vma_lookup() only returns a VMA that truly contains addr

Best Practices

  • Always take the appropriate mmap_lock before walking or modifying a process’s VMAs
  • Use vma_lookup() when you specifically need a VMA that contains an address, not just the nearest one
  • Prefer the VMA iterator helpers over any private tree-walking logic
  • When implementing mmap() in a driver, set vm_ops and vm_page_prot carefully to avoid granting unintended access

Performance and Security Considerations

Performance: the maple tree’s larger branching factor (10 for internal nodes, 16 for leaves) means shorter trees and fewer cache-line misses than the old rbtree, and dropping the separate linked list removes extra pointer chasing during lookups.

Security: permission flags on a virtual memory area (VM_READ, VM_WRITE, VM_EXEC) are what stand between a process and arbitrary memory access. Guard pages between the stack and neighbouring mappings, along with address space layout randomization (ASLR) of mmap_base, both rely directly on correct VMA management.

Summary / Key Takeaways

  • A virtual memory area (VMA) describes one contiguous, permission-bounded region of a process’s address space
  • struct vm_area_struct holds vm_start, vm_end, vm_flags, and an optional vm_file
  • Since kernel 6.1, all VMAs of a process live in a single maple tree (mm_mt), replacing the old rbtree plus linked list
  • vm_next/vm_prev are gone; use the VMA iterator API (for_each_vma, vma_next, vma_prev)
  • find_vma() and vma_lookup() remain the standard ways to locate a VMA by address
  • /proc/pid/maps is the fastest way to inspect a process’s VMAs from user space

Conclusion

Virtual memory areas are the bridge between a process’s simple view of memory and the kernel’s much more complex bookkeeping underneath. Understanding how vm_area_struct is organized, and how the maple tree replaced the older rbtree and linked-list combination in kernel 6.1, will make it far easier to read modern mm/ source code, debug driver mmap() implementations, and follow the page-table-level address translation we cover next. Keep this lecture as reference as you continue through this free Linux kernel development and embedded Linux course.

Frequently Asked Questions

What is a virtual memory area in the Linux kernel?

A virtual memory area is a contiguous, permission-bounded region of a process’s virtual address space, represented in the kernel by struct vm_area_struct.

Does the Linux kernel still use a red-black tree for VMAs?

No. Kernel 6.1 replaced the rbtree, the VMA cache, and the linked list with a single maple tree stored in mm_struct->mm_mt.

What happened to vma->vm_next and vm_prev?

Those fields were removed. Sequential access to neighbouring VMAs now goes through the VMA iterator API, such as for_each_vma() and vma_next().

What is the difference between find_vma() and vma_lookup()?

find_vma() returns the first VMA whose end address is beyond the given address, which may start after it. vma_lookup() only returns a VMA that actually contains the given address, or NULL.

How can I see the virtual memory areas of a running process?

Read /proc/<pid>/maps, or run the pmap command against the process ID.

Why did the kernel move to a maple tree for VMAs?

The maple tree has a higher branching factor than an rbtree, giving shorter trees and fewer CPU cache misses, and it removes the need for a separate linked list, reducing mmap_lock contention.

Is this free Linux kernel development course updated for modern kernels?

Yes. Every lecture in this series, including this one, is verified against current mainline kernel behaviour rather than reproducing outdated book content.

 

Continue the Free Linux Kernel Development Course

Next up: page table structure — PGD, P4D, PUD, PMD, and PTE levels on modern Linux.

Leave a Reply

Your email address will not be published. Required fields are marked *