Linux Virtual Memory Explained-Free Linux Device Drivers Tutorial

← PREV_LEC | NEXT_LEC →

Linux Virtual Memory Explained

Free Linux Kernel Development Course • Kernel Memory Management • Lecture 1

Chapter 11
Kernel 6.x Ready
Beginner Friendly
free embedded systems course
free linux development course
free linux device drivers course
free linux kernel development course
linux virtual memory

If you are searching for a free linux kernel development course that explains linux virtual memory the way real kernel engineers understand it, this lecture is where the Kernel Memory Management chapter begins. Every address your program touches — every pointer, every array, every stack variable — is a virtual address, never a real RAM location. Before we can talk about page allocators, kmalloc, or mmap() later in this chapter, we need to understand exactly what “virtual” means and how the kernel keeps user space and kernel space apart. This is the same core concept taught in any serious free linux device drivers course or free embedded systems course, updated here for modern 64-bit kernels.

What You Will Learn

  • Why Linux never lets a program touch physical RAM directly
  • How the MMU translates a virtual address into a physical address
  • The classic 32-bit 3G/1G kernel-user split and CONFIG_PAGE_OFFSET
  • How the modern 64-bit x86_64 address space is laid out
  • What pages, page frames, and page frame numbers (PFN) actually are
  • How to inspect virtual-to-physical mappings from a real kernel module

Prerequisites

  • Basic C programming knowledge
  • A Linux machine (native or VM) with kernel headers installed
  • Comfort compiling a simple “hello world” kernel module
  • No prior memory management knowledge required — we start from zero

Why Linux Virtual Memory Exists

Think of an apartment building with a shared mail room instead of individual street addresses. As a visitor, you never walk directly to “the third pipe on the second floor” of the building’s plumbing — you hand your letter to the mail room clerk with a resident’s name, and the clerk looks up which physical mailbox slot belongs to that name today. Tenants move in and out, mailbox slots get reassigned, and the building can even renovate the mailroom without anyone outside needing to know. The visitor only ever deals with a name; the clerk deals with the real slot.

Linux does the same thing with memory. Every process is handed its own private range of addresses, called virtual addresses. When the CPU executes an instruction that touches memory, a hardware component called the Memory Management Unit (MMU) intercepts that virtual address and looks up the matching real, physical RAM location. The process never sees the physical address at all — only the kernel and the MMU know that mapping.

Virtual Address Translation Flow
CPU issues virtual address
MMU + Page Table lookup
Physical frame in RAM

This one extra layer of indirection is what makes modern operating systems possible. It gives Linux three guarantees that every embedded and systems developer relies on daily:

Rule What It Means In Practice
No direct physical access A process cannot read or write RAM it was never mapped to — the CPU raises a fault instead
Unmapped access is trapped Touching an address with no page table entry triggers a page fault, handled by the kernel
Freed memory is unreachable Once a mapping is torn down, that virtual address may now belong to a completely different process
Same virtual address, different processes Two processes can both use virtual address 0x400000, backed by two completely different physical frames

A page fault is not always an error, by the way — the kernel deliberately uses “not yet mapped” as a trigger for lazy allocation, copy-on-write, and swapping. We will cover the page fault handler itself later in this chapter.

Kernel Space vs User Space Split

Every process address space is divided into two regions: the part the process itself is allowed to touch (user space), and the part reserved for the kernel (kernel space). The kernel’s portion is mapped into every single process, but it can only be accessed once the CPU switches into privileged (kernel) mode — normally through a system call, interrupt, or exception. This is why a system call does not require a full address-space switch: the kernel’s virtual mappings are already sitting right there in the same page tables.

Legacy 32-bit Split (3G/1G)

On classic 32-bit systems, the entire address space is 4 GB, and a build-time option called CONFIG_PAGE_OFFSET decides where the split happens. On mainline x86 the default is 0xC0000000, giving user space the lower 3 GB and the kernel the upper 1 GB — commonly called the 3G/1G split. Some ARM SoC families changed this default; for example several 32-bit i.MX6 configurations use 0x80000000 instead, giving a 2G/2G split.

32-bit Address Space (CONFIG_PAGE_OFFSET = 0xC0000000)
0xFFFFFFFF — Kernel space (1 GB)
0xC0000000 — CONFIG_PAGE_OFFSET
0x00000000 — User space (3 GB)

Modern 64-bit x86_64 Split

Most kernel development today happens on 64-bit hardware, so this course targets that layout. A 64-bit pointer is not fully used — current x86_64 CPUs only implement 48 bits of virtual addressing (57 bits on newer CPUs with 5-level paging, CONFIG_X86_5LEVEL). Instead of a simple midpoint split, the address space has three zones: a low canonical range for user space, a huge unusable “non-canonical” hole in the middle, and a high canonical range for the kernel.

64-bit x86_64 Address Space (4-level paging)
0xFFFFFFFFFFFFFFFF
Kernel space (starts at 0xffff800000000000)
Non-canonical hole (unusable, ~16M TB)
0x00007FFFFFFFFFFF
User space (128 TB, per process)
0x0000000000000000

Modernization note: This 47-bit user / 47-bit kernel layout, documented today under Documentation/arch/x86/x86_64/mm.rst in the mainline kernel tree, replaces the old Documentation/x86/x86_64/mm.txt path. On CPUs that support 5-level page tables, the same document defines an even larger layout using 56-bit user space and 56-bit kernel space, automatically selected at boot depending on hardware support and available physical memory.

Pages, Page Frames, and the PFN

The MMU does not translate memory one byte at a time — that would be far too slow. Instead, virtual memory is divided into fixed-size chunks. A chunk of virtual memory is called a page; the matching chunk of physical memory it is mapped onto is called a page frame. On x86 and ARM the standard page size is 4 KB (4096 bytes).

Architecture Default Page Size Notes
x86 / x86_64 4 KB Fixed; huge pages (2 MB / 1 GB) available as an optimization
ARM (32-bit) 4 KB Standard across mainstream distros
ARM64 4 KB, 16 KB, or 64 KB Selectable at kernel build time via CONFIG_ARM64_4K_PAGES / 16K / 64K

Every page frame is identified by a Page Frame Number (PFN) — simply the physical address divided by the page size. The kernel keeps a struct page for every physical frame in the system, and two macros convert between a page structure and its PFN: page_to_pfn() and pfn_to_page(). The structure that actually stores the virtual-to-physical mapping used by the MMU is called a page table; we will explore its internal structure (PGD, P4D, PUD, PMD, PTE) in the next lecture.

Hands-On: Inspecting Virtual Memory From User Space

Before writing any kernel code, you can already observe virtual memory behavior from a normal shell. Try these commands on any Linux machine:

$ getconf PAGE_SIZE
4096

$ cat /proc/self/maps | head -5
55f2a1a3d000-55f2a1a3f000 r--p 00000000 08:01 1234567 /usr/bin/cat
55f2a1a3f000-55f2a1a44000 r-xp 00002000 08:01 1234567 /usr/bin/cat
55f2a1a44000-55f2a1a47000 r--p 00007000 08:01 1234567 /usr/bin/cat
7f0e2c000000-7f0e2c021000 rw-p 00000000 00:00 0        [heap]
7ffe4b1a0000-7ffe4b1c1000 rw-p 00000000 00:00 0        [stack]

Every row in /proc/self/maps is a range of virtual addresses belonging to that process — the physical frames backing them can be scattered anywhere in RAM, and you would never know it from this output alone. That is exactly the abstraction this lecture has been describing.

Kernel-Side Example: Printing a Virtual-to-Physical Mapping

Now let’s confirm the same concept from inside the kernel. The following original demo module, ep_vm_demo, allocates a small kernel buffer and prints its virtual address, its physical address, and its PFN, using virt_to_phys() and virt_to_pfn() (the modern kernel 6.x helper; on older trees this was written manually as virt_to_page(addr) >> PAGE_SHIFT).

// ep_vm_demo.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/mm.h>

static void *ep_buf;

static int __init ep_vm_demo_init(void)
{
	phys_addr_t phys;
	unsigned long pfn;

	ep_buf = kmalloc(PAGE_SIZE, GFP_KERNEL);
	if (!ep_buf)
		return -ENOMEM;

	phys = virt_to_phys(ep_buf);
	pfn  = virt_to_pfn(ep_buf);

	pr_info("ep_vm_demo: virtual  addr = %px\n", ep_buf);
	pr_info("ep_vm_demo: physical addr = %pa\n", &phys);
	pr_info("ep_vm_demo: page frame number (PFN) = %lu\n", pfn);
	pr_info("ep_vm_demo: PAGE_SIZE = %lu bytes\n", PAGE_SIZE);

	return 0;
}

static void __exit ep_vm_demo_exit(void)
{
	kfree(ep_buf);
	pr_info("ep_vm_demo: unloaded, buffer freed\n");
}

module_init(ep_vm_demo_init);
module_exit(ep_vm_demo_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Demo: virtual to physical address translation");

Build and load it like any other out-of-tree module:

$ make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
$ sudo insmod ep_vm_demo.ko
$ dmesg | tail -5
[ 1123.552011] ep_vm_demo: virtual  addr = 000000004f9a2c11
[ 1123.552013] ep_vm_demo: physical addr = 0x10a3f4000
[ 1123.552014] ep_vm_demo: page frame number (PFN) = 1082868
[ 1123.552015] ep_vm_demo: PAGE_SIZE = 4096 bytes
$ sudo rmmod ep_vm_demo

Expected output explanation: the virtual address printed with %px is the pointer kmalloc() handed to the driver; the physical address is where that same buffer actually sits in RAM; and multiplying the PFN by PAGE_SIZE (1082868 × 4096) should land you back on that same physical address, confirming the frame lookup is consistent.

Real-World Use Cases

  • Device drivers use virt_to_phys()/phys_to_virt() constantly when handing buffer addresses to DMA-capable hardware
  • Debugging tools like crash and kdump walk page tables to reconstruct memory state after a kernel panic
  • Containers rely on separate virtual address spaces to isolate processes without needing separate physical RAM per container
  • Embedded Linux BSPs often tune CONFIG_PAGE_OFFSET or page size choice to fit a specific SoC’s memory map

Common Mistakes and Troubleshooting

Mistake Why It Happens Fix
Passing a virtual address straight to hardware Hardware DMA engines only understand physical (or bus) addresses Always convert with virt_to_phys() / dma_map_single()
Assuming vmalloc() memory is physically contiguous vmalloc() only guarantees virtual contiguity Use kmalloc()/get_free_pages() when physical contiguity is required
Dereferencing a freed kernel pointer The virtual mapping may now point to a different frame Set pointers to NULL after kfree() and enable KASAN in test builds

Best Practices

  • Never hardcode CONFIG_PAGE_OFFSET-style addresses in driver code — always use kernel helper macros
  • Prefer devm_-managed and resource-managed allocation APIs to avoid manual free bugs
  • Enable CONFIG_DEBUG_VIRTUAL during development to catch invalid virt_to_phys() usage

Performance and Security Considerations

Performance: every virtual-to-physical translation ideally hits the Translation Lookaside Buffer (TLB), a small cache inside the CPU. A TLB miss forces a full page table walk, which is far slower — this is one reason huge pages (2 MB/1 GB) exist, since fewer, larger entries mean fewer misses.

Security: the non-canonical hole between user and kernel space, combined with Kernel Address Space Layout Randomization (KASLR), makes it much harder for an attacker to guess kernel addresses even if they can trigger a memory bug in a driver.

Summary and Key Takeaways

  • Every address in Linux is virtual; the MMU and page tables translate it to a physical frame
  • 32-bit systems use a fixed split defined by CONFIG_PAGE_OFFSET (commonly 3G/1G)
  • 64-bit x86_64 systems use a 47-bit (or 56-bit) canonical split with a large unusable hole in between
  • Memory is managed in fixed-size pages (usually 4 KB), each mapped to a physical page frame identified by a PFN
  • virt_to_phys() and virt_to_pfn() are the standard kernel 6.x helpers for translation inside drivers

Conclusion

Virtual memory is the foundation everything else in this chapter builds on. Once the difference between a virtual address, a physical address, a page, and a page frame is clear, concepts like the page allocator, the slab allocator, kmalloc, ioremap, and mmap() all become far easier to follow — because they are simply different ways of creating or managing these same virtual-to-physical mappings. In the next lecture, we go one level deeper into the page table structure itself: PGD, P4D, PUD, PMD, and PTE, and how a real address walk happens step by step.

Frequently Asked Questions

What is virtual memory in the Linux kernel?

Virtual memory is an abstraction where every process gets its own private range of addresses that do not point directly to physical RAM. The MMU and kernel page tables translate each virtual address to a real physical frame on every memory access.

Why does Linux separate kernel space and user space?

Separating them lets the kernel enforce privilege checks — user code cannot read or write kernel memory directly — while still keeping kernel mappings present in every process for fast, low-overhead system calls.

What is CONFIG_PAGE_OFFSET?

It is a kernel build-time configuration option that defines where the kernel address range begins in a 32-bit process’s address space. The default on mainline x86 is 0xC0000000, but SoC vendors like some 32-bit i.MX6 configurations override it.

Does the 3G/1G split still apply on 64-bit systems?

No. 64-bit x86_64 kernels use a completely different, much larger layout: a 47-bit (or 56-bit on newer CPUs) canonical user range, a large non-canonical hole, and a 47-bit (or 56-bit) kernel range, documented in Documentation/arch/x86/x86_64/mm.rst.

What is a page frame number (PFN)?

A PFN identifies a specific physical page frame — it is simply the physical address of that frame divided by the page size. The kernel converts between a struct page and its PFN using page_to_pfn() and pfn_to_page().

What is the default page size on Linux?

4 KB (4096 bytes) on x86 and standard ARM systems. ARM64 kernels can also be configured for 16 KB or 64 KB pages at build time.

How do I get the physical address of a kernel buffer in a driver?

Use the virt_to_phys() helper (or virt_to_pfn() to get just the frame number) as shown in the ep_vm_demo example in this lecture, rather than computing offsets manually.

What causes a page fault?

A page fault happens when the CPU accesses a virtual address that has no valid page table entry yet, or one it does not have permission to use. The kernel’s fault handler decides whether to map memory on demand or deliver a segmentation fault.

Is this course free?

Yes. This lecture is part of EmbeddedPathashala’s free Linux kernel development course covering device drivers, memory management, and embedded Linux topics updated for kernel 6.x.

Continue the Free Linux Kernel Development Course

Next up: page tables (PGD/P4D/PUD/PMD/PTE) and a real address translation walk-through.

← PREV_LEC | NEXT_LEC →

 

Leave a Reply

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