Understanding Kernel Virtual Memory-Free Embedded Linux Course

PREV_LEC  |  NEXT_LEC
Free Embedded Linux Course · Chapter 11

Understanding Kernel Virtual Memory

Lecture 1 of the Managing Memory chapter — how Linux splits address space between user processes and the kernel, and why it matters on embedded targets.

Topics Covered
virtual memory MMU & page tables user space vs kernel space copy-on-write free embedded systems course free linux kernel development course

What You Will Learn

This lecture is part of a free embedded systems course and opens the Managing Memory chapter of our free linux kernel development course. By the end of this lecture you will understand why every Linux process sees a private virtual address space instead of touching physical RAM directly, how that space is divided between user space and kernel space, the different states a virtual page can be in, and how to inspect this layout yourself on a running embedded Linux board using a small original kernel module.

Prerequisites

You should be comfortable writing and loading a basic loadable kernel module (insmod/rmmod), reading dmesg output, and cross-compiling for an ARM or ARM64 embedded target. If you are new to module basics, work through our device driver fundamentals lectures first before continuing here.

Why Linux Needs Virtual Memory

Every process running under Linux — whether it’s a tiny sensor daemon on a microcontroller-class SoC or a full desktop browser — believes it owns the entire address space, starting at address zero and running up to the architecture’s maximum address. That belief is a carefully maintained illusion. In reality, the physical RAM on your board is a shared, finite resource, and it’s the job of virtual memory to hand each process its own private view of memory while the kernel quietly maps that view onto real, physical pages behind the scenes.

This matters enormously on embedded Linux systems, where RAM might be measured in tens or a few hundred megabytes rather than gigabytes. Understanding exactly how that memory is carved up — and what “using memory” actually means at the kernel level — is the foundation for everything else in this chapter, including detecting leaks and surviving out-of-memory conditions later on.

The MMU and Page Tables

The hardware component responsible for this illusion is the Memory Management Unit (MMU), built into essentially every application-class processor that runs Linux. The MMU divides the virtual address space into fixed-size chunks called pages — 4 KiB is by far the most common page size across ARM, ARM64, and x86 targets, though some architectures support larger page sizes for specific workloads. The kernel maintains page tables that describe how each virtual page maps to a physical page frame. When a CPU core accesses a virtual address, the MMU walks these tables (with help from a translation lookaside buffer, or TLB, which caches recent translations) to find the matching physical address.

Virtual Address Translation Path
CPU issues virtual address | v TLB lookup (cached translation?) | | HIT MISS | | v v physical addr MMU walks page tables | v physical addr (or page fault)

Splitting the Address Space: User Space vs Kernel Space

Linux divides every process’s virtual address space into two regions: user space, which belongs exclusively to that one process, and kernel space, which is identical no matter which process is currently running. This split exists so the kernel’s own code and data structures are always reachable through a simple, unchanging set of addresses, while every process is still sandboxed away from every other process.

On a classic 32-bit embedded ARM build, this split is controlled by a kernel configuration value historically referred to as the kernel/user memory split — commonly arranged as three-quarters of the address space for user space and one quarter for the kernel, though this ratio is configurable and some embedded configurations use a different split to give user space more room. On modern 64-bit ARM64 or x86_64 systems the story is different: the usable virtual address space is enormous (typically 48 or more bits wide), so kernel and user regions occupy separate, very widely spaced ranges rather than dividing a cramped 4 GiB space, and canonical-address rules keep the two from ever colliding.

Key Point

Kernel space is shared and identical for every process; user space is private and re-created per process. A process can never read another process’s user-space memory directly — but every process shares the same kernel mappings.

32-bit vs 64-bit Address Space

AspectTypical 32-bit Embedded ARMTypical 64-bit ARM64 / x86_64
Total virtual space per process4 GiB256 TiB or more (architecture-dependent)
User/kernel splitFixed ratio via a build-time config optionSeparate, widely spaced canonical ranges
Pressure on address spaceReal constraint — large mappings compete for roomEffectively unlimited for typical embedded workloads
Common onCost-sensitive MCU-class SoCs, older ARMv7 boardsModern ARMv8/ARMv9 SoCs, most new embedded designs

The States a Virtual Page Can Be In

Not every virtual page is backed by memory at every moment. A given page of a process’s virtual address space is always in one of a small number of states:

  • Unmapped — no translation exists; touching it raises a page fault and, for a normal user-space access, delivers SIGSEGV.
  • Privately mapped — backed by a physical page that belongs to this process alone (typical of heap and stack memory).
  • Shared — backed by a physical page visible to more than one process, such as a shared library’s code pages or a POSIX shared memory segment.
  • Copy-on-write (COW) — mapped read-only and shared, with a flag telling the kernel to transparently duplicate the page and remap it privately the moment either side attempts to write.
  • Kernel-owned — mapped to physical memory that belongs to the kernel itself, such as page-table pages or driver-owned buffers.

Copy-on-write deserves a closer look because it’s one of the cleverest tricks in the whole scheme. When fork() creates a child process, the kernel does not immediately duplicate the parent’s entire memory footprint. Instead, both processes are pointed at the same physical pages, marked read-only. Only when one of them actually writes to a page does the kernel trap that write, allocate a fresh physical page, copy the data across, and update that process’s page table — the other process’s mapping is untouched. This is why fork() followed by execve(), the classic pattern behind every shell command you run, is cheap even for a process with a large memory footprint.

Hands-On: Inspecting Address Space Layout

Rather than reusing any book’s demo code, let’s build a small original kernel module, ep_vmlayout, that reports the running kernel’s key address-space boundaries the moment it loads. This gives you a live, board-specific view instead of a static textbook diagram.

// ep_vmlayout.c — reports virtual address space boundaries on load
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/mm.h>
#include <linux/sched.h>

static int __init ep_vmlayout_init(void)
{
    pr_info("ep_vmlayout: TASK_SIZE          = 0x%lx\n", TASK_SIZE);
    pr_info("ep_vmlayout: PAGE_SIZE           = %lu bytes\n", PAGE_SIZE);
    pr_info("ep_vmlayout: PAGE_OFFSET         = 0x%lx\n", PAGE_OFFSET);
    if (current->mm) {
        pr_info("ep_vmlayout: current mm start_code = 0x%lx\n",
                current->mm->start_code);
        pr_info("ep_vmlayout: current mm start_stack = 0x%lx\n",
                current->mm->start_stack);
    }
    return 0;
}

static void __exit ep_vmlayout_exit(void)
{
    pr_info("ep_vmlayout: unloaded\n");
}

module_init(ep_vmlayout_init);
module_exit(ep_vmlayout_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala: reports virtual address space layout");
# Makefile
obj-m += ep_vmlayout.o

all:
	make -C $(KDIR) M=$(PWD) modules

clean:
	make -C $(KDIR) M=$(PWD) clean
$ make -C ~/linux-kernel M=$(pwd) modules
$ sudo insmod ep_vmlayout.ko
$ dmesg | tail -6
[  812.101223] ep_vmlayout: TASK_SIZE          = 0x0000ffffffffffff
[  812.101229] ep_vmlayout: PAGE_SIZE           = 4096 bytes
[  812.101232] ep_vmlayout: PAGE_OFFSET         = 0xffff800000000000
[  812.101235] ep_vmlayout: current mm start_code = 0x0000aaaaaaaa0000
[  812.101238] ep_vmlayout: current mm start_stack = 0x0000fffff2345670
$ sudo rmmod ep_vmlayout

The exact numbers you see will vary with architecture, kernel configuration, and even ASLR — that’s expected. What matters is the technique: whenever you need ground truth about how memory is laid out on a specific board and kernel build, a tiny module like this beats guessing from a book.

Advantages and Trade-offs of Virtual Memory

AdvantageWhy It Matters on Embedded Linux
Fault isolationAn invalid access raises SIGSEGV instead of silently corrupting another process or the kernel
Process isolationEach process runs in its own sandbox, which is essential once you’re running more than a single statically-linked application
Efficient sharingShared libraries and copy-on-write pages mean multiple processes reuse the same physical RAM for common code
Optional swapPhysical memory can be logically extended with swap, though this is rare and often avoided on flash-based embedded storage
Trade-off to remember: virtual memory makes it genuinely hard to state a process’s real memory budget up front, the default allocation strategy over-commits (more on this later in the chapter), and page-fault handling introduces latency that matters if you’re chasing hard real-time deadlines.

Common Mistakes and Troubleshooting

  • Confusing virtual size with physical usage. A process can reserve a huge virtual range (for example, a large mmap()) without ever touching most of it, so virtual size alone tells you very little about real RAM pressure.
  • Assuming a fixed kernel/user split on every board. That split is a build-time configuration choice on 32-bit systems and effectively irrelevant on 64-bit systems — always check the actual kernel configuration for the target rather than assuming book defaults.
  • Forgetting that kernel space is not demand-paged. Every kernel allocation is backed by real physical memory the moment it’s made — there’s no equivalent of a lazily-faulted-in kernel page the way there is in user space.

Best Practices

  • Always confirm address-space assumptions on the actual target kernel and configuration rather than relying on generic figures.
  • Use a minimal diagnostic module (like ep_vmlayout above) early in bring-up to record a board’s real memory layout for later comparison.
  • When reviewing driver code, remember that kernel-space mappings for MMIO registers are a distinct category from ordinary allocated memory — we’ll cover this in a later lecture.
Interview Questions

Q: What happens at the hardware level when a process accesses an unmapped virtual page?
The MMU cannot resolve a translation, it raises a page fault, and the kernel’s fault handler determines whether this is a legitimate lazy allocation (e.g., stack growth) or an invalid access — in the latter case, delivering SIGSEGV.

Q: Why is copy-on-write important for fork() performance?
Without COW, every fork() would require copying the entire parent address space up front. COW defers the actual copy until a write occurs, which is rare given most forked processes immediately call execve().

Q: Is kernel memory ever swapped out?
No — kernel memory is not demand-paged and is never discarded or paged out; it remains resident for as long as it’s allocated.

Summary and Key Takeaways

  • Virtual memory gives every process an isolated, private view of address space while sharing finite physical RAM efficiently.
  • The MMU and page tables perform the translation from virtual to physical addresses, aided by the TLB.
  • Address space is split into user space (private per process) and kernel space (shared, identical for all processes).
  • Virtual pages exist in distinct states — unmapped, private, shared, copy-on-write, and kernel-owned.
  • Kernel memory is never demand-paged or swapped, unlike most user-space memory.

Conclusion

Virtual memory is the quiet foundation underneath every Linux process, and it’s especially consequential on RAM-constrained embedded boards where every megabyte counts. With the user/kernel split and page states clear, the next lecture in this free linux device drivers course moves on to exactly how kernel-space memory is consumed in practice — the slab allocator, vmalloc(), module memory, and reading real numbers out of /proc/meminfo.

FAQ

What is the difference between virtual memory and physical memory?

Physical memory is the actual RAM chips on your board. Virtual memory is the address space the CPU and MMU present to software; it’s translated to physical memory through page tables and may not correspond 1:1 with real RAM at any given moment.

Does every embedded Linux board use virtual memory?

Any board running full Linux (as opposed to a No-MMU configuration or a bare-metal RTOS) uses virtual memory, since it depends on an MMU. Some very low-end microcontroller Linux builds run MMU-less, which is a distinct topic outside this lecture’s scope.

Why is the page size almost always 4 KiB?

4 KiB balances translation efficiency against page-table memory overhead and matches what nearly all mainstream MMUs support natively; some architectures also offer huge pages for specific high-throughput use cases.

What triggers a copy-on-write page copy?

A write attempt to a page that is mapped read-only and marked copy-on-write. The kernel traps the write, allocates and copies a private physical page, updates the page table, and then allows the write to complete.

Can a process see another process’s kernel-space mappings?

Kernel space is identical across all processes, but ordinary user-space code cannot read or write it directly — access is mediated entirely through system calls.

Is this course free?

Yes — this lecture is part of EmbeddedPathashala’s free embedded systems course covering the full path from toolchains and bootloaders through kernel internals and device drivers.

{ “@context”: “https://schema.org”, “@type”: “FAQPage”, “mainEntity”: [ {“@type”: “Question”, “name”: “What is the difference between virtual memory and physical memory?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “Physical memory is the actual RAM chips on your board. Virtual memory is the address space the CPU and MMU present to software; it’s translated to physical memory through page tables and may not correspond 1:1 with real RAM at any given moment.”}}, {“@type”: “Question”, “name”: “Does every embedded Linux board use virtual memory?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “Any board running full Linux uses virtual memory, since it depends on an MMU. Some very low-end microcontroller Linux builds run MMU-less, which is a distinct topic.”}}, {“@type”: “Question”, “name”: “Why is the page size almost always 4 KiB?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “4 KiB balances translation efficiency against page-table memory overhead and matches what nearly all mainstream MMUs support natively; some architectures also offer huge pages for specific use cases.”}}, {“@type”: “Question”, “name”: “What triggers a copy-on-write page copy?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “A write attempt to a page that is mapped read-only and marked copy-on-write. The kernel traps the write, allocates and copies a private physical page, updates the page table, and allows the write to complete.”}}, {“@type”: “Question”, “name”: “Can a process see another process’s kernel-space mappings?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “Kernel space is identical across all processes, but ordinary user-space code cannot read or write it directly — access is mediated entirely through system calls.”}}, {“@type”: “Question”, “name”: “Is this course free?”, “acceptedAnswer”: {“@type”: “Answer”, “text”: “Yes — this lecture is part of EmbeddedPathashala’s free embedded systems course covering the full path from toolchains and bootloaders through kernel internals and device drivers.”}} ] }

Continue the Managing Memory Chapter

Next up: how kernel-space memory is actually consumed — the slab allocator, vmalloc, module memory, and reading real numbers from /proc/meminfo.

PREV_LEC  |  NEXT_LEC

1 Comment

Leave a Reply

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