⏱️ Reading time: 11 min

Right now, the browser, the code editor, and the terminal you have open believe they have access to all the RAM on your machine, even though combined they may request far more than what’s physically installed. That’s not a calculation error: it’s virtual memory, the mechanism that sustains that illusion without one program colliding with another.

📑 En este artículo
  1. TL;DR
  2. What Is Virtual Memory?
  3. Why Paging Matters
  4. How Paging Works, Step by Step
  5. Practical Examples and How to Get Started
  6. Real-World Use Cases
  7. Common Mistakes and Best Practices
  8. Comparison with Other Memory Management Approaches
  9. Going Deeper: Inside the Kernel
  10. Frequently Asked Questions
    1. Does Virtual Memory Slow Down the Computer?
    2. What’s the Difference Between Paging and Swap Memory?
    3. How Do I See How Much Virtual Memory a Process Uses on Linux?
    4. What Is a TLB Miss and Why Does It Matter?
    5. Do Embedded Systems Use Demand Paging?
    6. What Happens If Available Memory Runs Out on Linux?
  11. References

The translation happens in hardware, thousands of times per second, with a special cache that speeds it up. That piece, the TLB, determines whether your program runs fast or crawls.

TL;DR

  • The virtual address space translates logical addresses to physical ones through page tables.
  • The TLB caches those translations to avoid consulting the page table on every memory access.
  • A page fault occurs when the data isn’t in RAM and the kernel has to fetch it from disk.
  • `vmstat` and `/proc//smaps` show live how Linux distributes memory pages.
  • Each process has its own virtual address space, isolated from the rest by design.

What Is Virtual Memory?

Virtual memory is a memory management technique that assigns each process its own address space, separate from the real physical RAM. The operating system and the MMU translate those addresses into physical addresses through page tables, which isolates processes from one another.

The basic unit of this translation is the page: a fixed-size block, typically 4 KB on x86-64, that the system moves, protects, and replaces as a whole. Each page table entry maps a virtual page to a specific physical frame, or marks that page as not yet present in RAM.

Current x86-64 processors can address up to 48 bits of virtual space per process. Foto de Andrey Matveev en Unsplash

Why Paging Matters

Without this scheme, every program would need to know exactly which physical RAM address it will run at, and two processes could never use the same address range without stepping on each other. Paging solves this by giving each process the illusion of its own address space that starts at zero.

It also enables security isolation: one process can’t read or write another’s memory because it simply has no page table entries pointing there. And it allows something counterintuitive, overcommit, where the system promises more memory than physically exists, betting that not all processes will use it at the same time.

How Paging Works, Step by Step

When a process accesses an address, the CPU first checks the TLB (Translation Lookaside Buffer), a small, very fast cache of recent translations. If the translation is there (TLB hit), the physical address comes out almost for free. If it isn’t (TLB miss), the hardware walks the entire page table, a process called a page table walk that can cost hundreds of clock cycles.

If the page table indicates the page isn’t in RAM, a page fault occurs. There are two types: the minor fault, where the kernel only needs to assign a free physical frame and update the table (for example, on the first write after a `malloc`), and the major fault, where the content also has to be brought in from disk or the swap area, which is much slower.

flowchart TD
    A["Process virtual address"] --> B["MMU checks the TLB"]
    B -->|"Hit"| C["Physical address in RAM"]
    B -->|"Miss"| D["Walks the page table"]
    D --> E{"Page present?"}
    E -->|"Yes"| C
    E -->|"No"| F["Page fault: kernel intervenes"]
    F --> G["Loads the page from disk or swap"]
    G --> C

When the fault is major, the kernel suspends the process, requests the page from the storage device, and resumes it once the data is ready. That sequence is what turns a simple memory access into a full input/output operation.

sequenceDiagram
    participant P as Process
    participant M as MMU
    participant K as Kernel
    participant D as DiskSwap
    P->>M: accesses virtual address
    M->>M: checks TLB and page table
    M-->>K: page missing, triggers page fault
    K->>D: requests the missing page
    D-->>K: returns the page content
    K-->>P: updates the table and resumes

Practical Examples and How to Get Started

To try this live, you need a Linux machine with gcc installed (sudo apt install build-essential) and the GNU time package to measure page faults (sudo apt install time), different from bash’s builtin.

First, a general overview with free:

$ free -h
               total        used        free      shared  buff/cache   available
Mem:            15Gi       4.2Gi       1.1Gi       412Mi        10Gi        10Gi
Swap:          2.0Gi          0B       2.0Gi

The available column is the one that matters: it’s how much memory the kernel can hand to a new process without starting to use swap. With vmstat you can see the movement in real time:

$ vmstat 1 3
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0      0 1123456  98234 9876543    0    0     3    12  410  620  8  2 89  1  0

The si and so columns (swap in / swap out) being zero is a good sign: the system isn’t swapping pages to disk. Now, a program that forces page faults on purpose:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    size_t size = 100 * 1024 * 1024; // 100 MB
    char *buf = malloc(size);
    for (size_t i = 0; i < size; i += 4096) {
        buf[i] = 1; // touches a new page each time
    }
    printf("Done: %zu pages touched\n", size / 4096);
    free(buf);
    return 0;
}

This program reserves 100 MB with malloc, but the system doesn’t assign physical RAM yet. Only when the loop writes to the first byte of each 4 KB page does the kernel resolve a minor fault and assign a real frame. Compile and measure it like this:

$ gcc -O0 -o paginas paginas.c
$ /usr/bin/time -v ./paginas
Done: 25600 pages touched
...
    Minor (reclaiming a frame) page faults: 25607
    Major (requiring I/O) page faults: 0

The number of minor faults will be around 25,600 pages (100 MB divided by 4 KB), plus a handful extra for the binary itself and the process stack. Major faults at zero confirm everything was resolved in RAM, without touching disk.

A TLB miss can cost hundreds of clock cycles compared to the few a TLB hit takes. Foto de William Warby en Unsplash

Real-World Use Cases

Containers (Docker, containerd) don’t create a physical copy of the host’s RAM: they use cgroups to cap how many pages a group of processes can hold, while each process’s virtual address space is still managed by the host’s shared kernel.

Databases like PostgreSQL or SQLite use mmap to map entire files into the process’s virtual address space, letting the operating system’s own paging mechanism decide which blocks of the file to keep in RAM at any given time, instead of reinventing that caching in the application.

In the cloud, small instances with little RAM often use zram (compressed in-memory swap) instead of swapping to disk, because compressing and decompressing on the CPU turns out to be faster than waiting on a read from a network disk.

Common Mistakes and Best Practices

A common mistake is confusing virtual memory with swap: swap is just one of the possible destinations for a page that doesn’t fit in RAM, but the paging mechanism exists even when swap is completely disabled.

Another mistake is assuming that reserving virtual memory (a large malloc) consumes RAM immediately. On Linux, that’s not the case: real consumption only shows up once you write to those pages, which is why VmSize and VmRSS in /proc/<pid>/status can differ by orders of magnitude.

💡 Tip: if your program has very scattered memory access patterns and you’re worried about latency, measure first with perf stat -e dTLB-load-misses before assuming the bottleneck is in the algorithm.
⚠️ Watch out: Linux’s overcommit allows reserving more memory than physically exists; if several processes claim it all at the same time, the OOM killer starts killing processes without warning.

Comparison with Other Memory Management Approaches

StrategyWhen It’s UsedAdvantageLimitation
Demand pagingGeneral-purpose Linux, Windows, macOSOnly loads what’s used, allows overcommitThrashing if RAM runs short
SegmentationLegacy x86 systems in real/protected modeMaps complete logical units (code, data, stack)External fragmentation, nearly abandoned
Flat, unpaged memoryMicrocontrollers and embedded RTOSNo translation overhead, predictable latencyNo isolation: a bug corrupts everything
Huge pages / THPDatabases and VMs with large heapsFewer TLB entries, fewer TLB missesMay reserve more RAM than needed

Going Deeper: Inside the Kernel

On x86-64, the page table isn’t a single flat structure: it’s a hierarchy of up to four levels (PGD, PUD, PMD, PTE), where each level narrows the search space before reaching the final physical frame. The CR3 register always points to the top level.

flowchart TD
    A["CR3: top-level PGD table"] --> B["PUD"]
    B --> C["PMD"]
    C --> D["PTE"]
    D --> E["Physical page frame"]
    subgraph "x86-64 4-level translation"
    A
    B
    C
    D
    end

On multi-core systems, when the kernel changes a page table entry it has to invalidate the corresponding entry in each core’s TLB, an operation called a TLB shootdown. It’s expensive because it interrupts the other cores, so the kernel batches it whenever it can.

Huge pages (2 MB or 1 GB instead of 4 KB) reduce how many entries the TLB needs to cover the same amount of RAM, at the cost of wasting memory if a large page ends up half empty. To confirm whether Transparent Huge Pages are active on your system:

$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never

The value in brackets indicates the active mode; always in brackets means the kernel tries to use huge pages automatically for any large allocation.

The other mechanism worth understanding is copy-on-write: when a process calls fork(), the child doesn’t copy the parent’s RAM, it only copies the page tables and marks them as read-only. The actual copy of a page happens, page by page, only if either process modifies it afterward.

Your next step: compile this article’s example program, run /usr/bin/time -v on it, and compare the minor page faults it reports against the buffer size divided by 4 KB.

📬 Get new articles by email

We only email about big articles (1-2 a month).

Frequently Asked Questions

Does Virtual Memory Slow Down the Computer?

Not on its own. Virtual memory adds an address translation step, but the TLB makes it nearly free in the common case. Slowdowns appear when there are no free pages left and the kernel starts swapping to disk, a scenario called thrashing.

What’s the Difference Between Paging and Swap Memory?

Paging is the address translation mechanism; swap is just one possible destination for pages that don’t fit in RAM. A system can page without having any swap configured at all: it simply won’t be able to free pages to disk when physical memory runs short.

How Do I See How Much Virtual Memory a Process Uses on Linux?

With cat /proc/<pid>/status, checking the VmSize field for reserved memory or VmRSS for what actually occupies physical pages. The difference between the two is usually huge, because reserving virtual address space doesn’t consume RAM until you write to it.

What Is a TLB Miss and Why Does It Matter?

It’s when the MMU can’t find the translation in its cache and has to walk the entire page table, which can take hundreds of clock cycles. Programs with very scattered memory access patterns suffer more TLB misses than those that access memory sequentially.

Do Embedded Systems Use Demand Paging?

Most don’t. A microcontroller running an RTOS usually works with flat physical memory because its RAM is small, fixed, and known in advance, so the overhead of translating addresses isn’t justified. Embedded Linux, for example in routers, does maintain full paging.

What Happens If Available Memory Runs Out on Linux?

If overcommit allowed reserving more than what exists and real demand exceeds available RAM and swap, the kernel triggers the OOM killer, which picks and kills processes based on a score (oom_score) until it frees up memory.

References

  • docs.kernel.org: official Linux kernel documentation on memory management concepts.
  • man7.org: complete reference for the fields in /proc/[pid]/status and /proc/[pid]/smaps.
  • en.wikipedia.org: history and fundamentals of virtual memory since systems from the 1960s.
  • pages.cs.wisc.edu: Operating Systems Three Easy Pieces, chapters on paging and multilevel tables.

📱 Enjoy this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day. @programacion

Featured image: Foto de Luan Gjokaj en Unsplash

Did it work for you? Got a different error? Say so below: questions get answered and help the next reader.

Leave a comment
Categories: ProgrammingTutorials

Andrés Morales

Developer and AI researcher. Writes about language models, frameworks, developer tooling, and open source releases. Covers ML papers, the tech startup ecosystem, and programming trends.

0 Comments

Leave a Reply

Avatar placeholder

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

You can include code inside <code>…</code> or, for several lines, <pre><code>…</code></pre>.

This site uses Akismet to reduce spam. Learn how your comment data is processed.