What You Will Learn
- What a Virtual Memory Area (VMA) is and why the Linux kernel creates one
- How
vm_area_structis organized in kernel 6.x (maple tree, not red-black tree) - What fields inside
vm_area_structmean for device driver authors - How to inspect VMAs of any process using
/proc/PID/mapsand/proc/PID/smaps - How VMAs relate to page fault handling and memory-mapped files
- Practical kernel module code to walk a process VMA list
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.
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_end
vm_page_prot
vm_pgoff
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.
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);
}
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:
read(fd, buf, n) on /proc/PID/mapsvm_area_struct in mm_mt (maple tree)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:
- Look up which VMA contains the faulting address using the maple tree (O(log N) search).
- If no VMA contains the address → segmentation fault (SIGSEGV) is sent to the process.
- If a VMA is found, check if the access type (read/write/exec) is permitted by
vm_flags. - If permitted, call
vm_ops->fault()(for file-backed or driver VMAs) or the anonymous page handler. - 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
The linked list mm->mmap was removed. Using it causes a compile error. Always use VMA_ITERATOR and for_each_vma().
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.
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.
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 thatvma->vm_end - vma->vm_startdoes not exceed your device’s mappable region size before proceeding. - Set
VM_IOandVM_DONTDUMPon device memory VMAs so they are excluded from core dumps and handled correctly by the memory subsystem. - If your driver does not support
mremap(), setVM_DONTEXPANDto prevent the kernel from silently expanding the mapping. - Use
vma->vm_private_datato pass per-mapping state from your driver into the VMA operations callbacks.
Key Takeaways
Frequently Asked Questions
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.
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.
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).
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.
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.
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.
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.
