What Does vm_area_struct Do in Linux? – Free Linux Device Driver Course

Linux Kernel VMA Internals
How the Kernel Tracks Every Memory Region of a Running Process
Free
Linux Kernel Course
~25 min
Read Time
Kernel 6.x
Covered

What You Will Learn

  • What a Virtual Memory Area (VMA) is and why the Linux kernel creates one
  • How vm_area_struct is organized in kernel 6.x (maple tree, not red-black tree)
  • What fields inside vm_area_struct mean for device driver authors
  • How to inspect VMAs of any process using /proc/PID/maps and /proc/PID/smaps
  • How VMAs relate to page fault handling and memory-mapped files
  • Practical kernel module code to walk a process VMA list
Prerequisites
Linux Process Model basics Virtual vs Physical Addresses Basic C programming How to build a kernel module

What is a Virtual Memory Area (VMA)?

When a Linux process runs, the operating system gives it the illusion of a large, private block of memory. This is the Virtual Address Space (VAS). But not every address inside this space is actually usable. The process only has permission to access certain ranges — for example, its code lives at one range, its heap at another, its stack at yet another, and each shared library it loads occupies its own range.

The kernel needs to keep precise track of every one of these ranges. That is exactly what a Virtual Memory Area does. A VMA is a kernel data structure that describes one contiguous, permission-uniform range of virtual addresses belonging to a process. If the process has N distinct mapped regions, the kernel maintains N VMA objects for it.

An important point that trips up beginners: VMAs exist only in user space. The kernel’s own virtual address space is managed differently — it does not use VMAs for itself. Only the segments that belong to a user-mode process are described by VMAs.

Process Virtual Address Space → VMA Objects
Process VAS
Stack region
[unmapped gap]
mmap / libs region
[unmapped gap]
Heap region
[unmapped gap]
BSS / Data
Text (code)
⟶
VMA Objects in Kernel
vm_area_struct #N → Stack
vm_area_struct #3 → libpthread.so
vm_area_struct #2 → libc.so
vm_area_struct #… → Heap
vm_area_struct #1 → BSS/Data
vm_area_struct #0 → Text
Stored in Maple Tree (kernel 6.1+)

How Many VMAs Does a Process Have?

The number of VMAs a process has equals the number of mapped segments in its user VAS. A typical modern application on Linux will have dozens to hundreds of VMAs. A GUI application that loads many shared libraries can easily have 200 or more.

You can count them yourself right now. Open a terminal and run:

# Count VMAs of any running process (replace PID with actual PID)
cat /proc/self/maps | wc -l

# See all VMAs of your shell process
cat /proc/$$/maps

Each line in /proc/PID/maps corresponds to exactly one VMA. The format is:

address-range       perms  offset  dev  inode  pathname
7f8a2c000000-7f8a2c200000 r--p 00000000 08:01 12345 /usr/lib/x86_64-linux-gnu/libc.so.6

The fields mean:

Field What it tells you
address-range Start and end virtual addresses of this VMA
perms r=read, w=write, x=execute, p=private, s=shared
offset Offset into the file (if file-backed), else 0
dev Major:minor of block device backing the file
inode Inode of the file (0 for anonymous mappings)
pathname File path, [heap], [stack], [vdso], or blank

Inside vm_area_struct: Key Fields for Kernel and Driver Developers

The kernel structure that represents one VMA is struct vm_area_struct, defined in include/linux/mm_types.h. It has evolved significantly across kernel versions. Let’s look at what matters for people writing kernel modules or device drivers today.

Here are the most important fields grouped by purpose:

vm_area_struct — Key Fields Grouped by Purpose
Address Boundaries
vm_start
vm_end
Start and end virtual addresses of this region
Permissions & Flags
vm_flags
vm_page_prot
Access rights, shared/private, growsdown, etc.
Backing Store
vm_file
vm_pgoff
File mapped (NULL for anonymous) + page offset
Operations
vm_ops
Function pointers: fault, open, close, mmap
Owning mm
vm_mm
Pointer back to the mm_struct of the process

Address Fields: vm_start and vm_end

These are unsigned long values giving the virtual address range this VMA covers. The range is half-open: [vm_start, vm_end). The size of a VMA in bytes is simply vm_end - vm_start, and it is always a multiple of the page size (4096 bytes on x86_64).

/* How to compute VMA size in a kernel module */
unsigned long vma_size = vma->vm_end - vma->vm_start;
unsigned long page_count = vma_size >> PAGE_SHIFT;  /* divide by page size */

vm_flags: The Most Used Field in Driver Development

This field is a bitmask of flags that control the behavior of the VMA. Device driver developers encounter it most often when implementing a custom mmap() handler. Some commonly checked flags:

Flag Meaning
VM_READ Region is readable
VM_WRITE Region is writable
VM_EXEC Region is executable
VM_SHARED Mapping is shared (not copy-on-write)
VM_IO Mapping of device I/O space
VM_PFNMAP PFN-based mapping, no struct page involved
VM_DONTEXPAND mremap() cannot expand this VMA
VM_DONTDUMP Exclude from core dumps

vm_ops: The VMA Operations Table

This is a pointer to a struct vm_operations_struct — a table of function pointers that the kernel calls for events related to this VMA. If you write a character device driver that supports mmap(), you will fill in this structure. The most important callback is .fault, which gets called every time a page fault occurs within this VMA (i.e., when the process accesses a page that is not yet in physical memory).

/* Typical vm_ops setup in a device driver */
static const struct vm_operations_struct my_driver_vm_ops = {
    .open  = my_vma_open,
    .close = my_vma_close,
    .fault = my_vma_fault,   /* handle page faults for this region */
};

/* In your driver's mmap() implementation */
static int my_driver_mmap(struct file *filp, struct vm_area_struct *vma)
{
    vma->vm_ops      = &my_driver_vm_ops;
    vma->vm_flags   |= VM_IO | VM_PFNMAP | VM_DONTEXPAND | VM_DONTDUMP;
    vma->vm_private_data = filp->private_data;  /* pass driver context */
    return 0;
}

How VMAs Are Stored in Kernel 6.1+ (Maple Tree)

This is where the free Linux kernel development course content gets really interesting for anyone targeting modern kernels. In older kernels (up to ~5.x), VMAs were stored using a red-black tree plus a linked list. Starting from kernel 6.1, Linus Torvalds merged the Maple Tree data structure as the primary storage for VMAs.

The Maple Tree is a cache-friendly, RCU-safe B-tree variant. It replaced both the red-black tree and the linked list with a single, more efficient structure. This was a major internal change but is transparent to user space.

VMA Storage: Old Kernel vs Kernel 6.1+
Kernel ≤ 5.x (Old)
mm_struct
├── mmap (linked list head)
→ vma1 → vma2 → vma3 …
└── mm_rb (red-black tree root)
For O(log N) lookup
Two separate structures to maintain in sync
Kernel 6.1+ (New)
mm_struct
└── mm_mt (maple tree root)
O(log N) lookup
RCU-safe iteration
Cache-line friendly
Range queries built-in
Single structure handles all VMA operations

What this means practically when you write kernel code that iterates VMAs:

/* Kernel 6.1+ way to iterate all VMAs of a process */
#include <linux/mm.h>
#include <linux/maple_tree.h>

/* vma_iter_t is the modern VMA iterator */
static void print_all_vmas(struct mm_struct *mm)
{
    struct vm_area_struct *vma;
    VMA_ITERATOR(vmi, mm, 0);   /* initialise iterator at address 0 */

    mmap_read_lock(mm);         /* take the mmap read lock */

    for_each_vma(vmi, vma) {
        pr_info("VMA: 0x%lx - 0x%lx  flags=0x%lx\n",
                vma->vm_start, vma->vm_end, vma->vm_flags);
    }

    mmap_read_unlock(mm);
}
⚠️ Compatibility Note: If you see older tutorials using find_vma() in a loop or directly walking mm->mmap, that code will not compile on kernel 6.1+ without changes. Always check the kernel version your module targets and use the VMA iterator API for kernel ≥ 6.1.

How /proc/PID/maps Works Under the Hood

When you run cat /proc/self/maps, here is the exact sequence of events inside the kernel:

cat /proc/self/maps → Kernel Execution Flow
User space: read(fd, buf, n) on /proc/PID/maps
↓
Kernel: process switches to kernel mode
↓
VFS routes to procfs read handler
↓
procfs iterates every vm_area_struct in mm_mt (maple tree)
↓
Formats each VMA as one text line → copies to user buffer
↓
cat displays output on terminal

For more detailed VMA information including RSS, PSS, and swap usage per region, use /proc/PID/smaps instead. It outputs the same VMA list but with extra statistics per region.

# Quick way to see memory breakdown per VMA
cat /proc/$$/smaps | grep -E "^[0-9a-f]|^Size|^Rss|^Pss"

VMAs and Page Fault Handling

One of the most important roles of the VMA structure is to serve as the authoritative source of information when the kernel handles a page fault. A page fault is a hardware exception raised by the CPU whenever a program accesses a virtual address that has no valid physical page backing it at that moment.

When a page fault occurs, the kernel’s fault handler does this:

  1. Look up which VMA contains the faulting address using the maple tree (O(log N) search).
  2. If no VMA contains the address → segmentation fault (SIGSEGV) is sent to the process.
  3. If a VMA is found, check if the access type (read/write/exec) is permitted by vm_flags.
  4. If permitted, call vm_ops->fault() (for file-backed or driver VMAs) or the anonymous page handler.
  5. A physical page frame is allocated and wired in → fault is resolved → process continues.

This is why the VMA is so central to the kernel’s memory management subsystem. Every memory access by a user process that involves an unmapped page goes through VMA lookup. Modern kernels handle millions of these per second on a busy system.

Practical Kernel Module: Walking VMAs of a Process

Let’s put everything together with a working kernel module that prints the VMA layout of the current process. This is useful both as a learning tool and as a foundation for real driver work.

/* vma_walker.c - Print VMA layout of the current process
 * Tested on Linux kernel 6.1 and above
 * Build: make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
 */

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

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Walk and print VMAs of current process");

static int __init vma_walker_init(void)
{
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma;
    int count = 0;
    VMA_ITERATOR(vmi, mm, 0);

    pr_info("vma_walker: Process [%s] PID %d VMA dump:\n",
            current->comm, current->pid);

    pr_info("%-20s %-20s %-10s %s\n",
            "Start", "End", "Size(KB)", "Flags");

    mmap_read_lock(mm);

    for_each_vma(vmi, vma) {
        unsigned long size_kb = (vma->vm_end - vma->vm_start) >> 10;
        pr_info("0x%016lx 0x%016lx %-10lu %c%c%c%c\n",
                vma->vm_start,
                vma->vm_end,
                size_kb,
                (vma->vm_flags & VM_READ)   ? 'r' : '-',
                (vma->vm_flags & VM_WRITE)  ? 'w' : '-',
                (vma->vm_flags & VM_EXEC)   ? 'x' : '-',
                (vma->vm_flags & VM_SHARED) ? 's' : 'p');
        count++;
    }

    mmap_read_unlock(mm);

    pr_info("vma_walker: Total VMAs: %d\n", count);
    return 0;
}

static void __exit vma_walker_exit(void)
{
    pr_info("vma_walker: module unloaded\n");
}

module_init(vma_walker_init);
module_exit(vma_walker_exit);
# Minimal Makefile to build this module
obj-m += vma_walker.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
# Load the module and check output
sudo insmod vma_walker.ko
dmesg | tail -50
sudo rmmod vma_walker

Common Mistakes When Working with VMAs

❌ Mistake 1: Walking mm->mmap directly on kernel 6.1+

The linked list mm->mmap was removed. Using it causes a compile error. Always use VMA_ITERATOR and for_each_vma().

❌ Mistake 2: Accessing VMAs without the mmap lock

The VMA data structures are protected by mm->mmap_lock (an rwsem). Reading VMAs requires at minimum mmap_read_lock(mm). Forgetting this causes data races that are very hard to debug.

❌ Mistake 3: Assuming vm_end is the last valid address

vm_end is one byte past the last valid address, following the C half-open range convention [vm_start, vm_end). The last accessible byte is at vm_end - 1.

❌ Mistake 4: Setting vm_flags with assignment instead of helpers

In kernel 6.3+, vm_flags became a read-only view for safety. Use vm_flags_set(vma, VM_IO) and vm_flags_clear(vma, VM_WRITE) helper functions instead of direct assignment.

Best Practices for VMA Handling in Device Drivers

  • Always take the appropriate lock before accessing VMA fields. Use read lock for inspection, write lock for modification.
  • Use find_vma(mm, addr) to look up a single VMA by address — it uses the maple tree and is fast.
  • In your driver’s mmap() callback, always validate that vma->vm_end - vma->vm_start does not exceed your device’s mappable region size before proceeding.
  • Set VM_IO and VM_DONTDUMP on device memory VMAs so they are excluded from core dumps and handled correctly by the memory subsystem.
  • If your driver does not support mremap(), set VM_DONTEXPAND to prevent the kernel from silently expanding the mapping.
  • Use vma->vm_private_data to pass per-mapping state from your driver into the VMA operations callbacks.

Key Takeaways

VMAs describe user-space memory regions only
One VMA per contiguous mapped segment
Kernel 6.1+ uses Maple Tree (not red-black tree)
Use VMA_ITERATOR for modern kernel code
Always hold mmap_lock when reading VMAs
vm_ops.fault handles page faults for your driver
/proc/PID/maps shows all VMAs from user space

Frequently Asked Questions

Q: Does the kernel itself have VMAs?

No. VMAs are exclusively for user-space segments. The kernel manages its own virtual address space through a completely different set of mechanisms (kernel logical addresses, vmalloc region, modules space, etc.), none of which use vm_area_struct.

Q: If I call mmap() twice, do I always get two VMAs?

Not necessarily. The kernel may merge adjacent or overlapping VMAs if they have identical flags, permissions, and backing store. This is called VMA merging. It keeps the VMA count manageable and reduces memory overhead. You can see this by mapping the same anonymous region twice with the same flags — the kernel may combine them.

Q: What is the maximum number of VMAs a process can have?

On a default Linux system the limit is controlled by /proc/sys/vm/max_map_count, which defaults to 65530. This means a single process can have at most 65530 VMAs. You can increase this limit for applications that need it (for example, applications using many huge numbers of shared memory segments).

Q: Is the Maple Tree available on all architectures?

Yes. The Maple Tree is architecture-independent code in the Linux kernel. It is the default VMA storage mechanism on all supported architectures starting from kernel 6.1.

Q: How does fork() interact with VMAs?

When a process calls fork(), the kernel duplicates all VMA structures of the parent into the child’s mm_struct. This is a copy of the metadata, not of the actual memory pages. The actual pages are shared between parent and child using Copy-On-Write (COW) until either one writes to them.

Q: Can a kernel module read the VMA list of any arbitrary process?

Yes, but carefully. You need to obtain a reference to the target process’s mm_struct using get_task_mm(task), then use the VMA iterator, and finally release the reference with mmput(mm). Accessing the mm of a different process requires proper reference counting to avoid use-after-free.

Q: What happens to VMAs when a process exits?

The kernel’s exit path calls exit_mm() which calls mmput(). When the reference count of the mm_struct reaches zero, __mmput() tears down all VMAs, frees page tables, and releases the physical pages. VMAs are destroyed in address order.

Leave a Reply

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