Software Dev Notes

Home › Systems Programming

How Virtual Memory Works in Modern Operating Systems

Virtual memory gives each process its own address space, backed by pages and page tables. Learn page faults, the TLB and why it protects programs.

2026-10-19 · 6 min read

Every program running on a modern computer believes it owns a large, private block of memory that starts at a low address and runs upward. Two programs can use the same address and see different values, and a crash in one rarely corrupts the other. That illusion is virtual memory, one of the most important ideas in operating system design. The physical RAM chips are shared, but each process sees its own address space.

This article explains how addresses are translated, what pages and page tables do, why the translation cache matters for speed, and what happens when a program touches memory that is not present. The examples use the common 4 KB page size of x86-64 Linux systems, and note where other platforms differ.

Two kinds of address

A virtual address is the number a program uses in its instructions, pointers and data structures. A physical address identifies a real location on the memory bus. The processor's memory management unit, or MMU, translates each virtual address into a physical one before the memory access happens.

The program never needs to know where its data sits in RAM. The operating system can move data, share it between processes or keep it on disk, and the program continues to use the same virtual address without any change.

Pages: the unit of mapping

Virtual memory works in fixed-size blocks called pages. On x86-64 Linux the usual page size is 4 KB, which is 4,096 bytes. Apple Silicon Macs use 16 KB pages. The operating system maps each virtual page to a physical frame of the same size, so translation works on whole pages rather than individual bytes.

A 48-bit virtual address on x86-64 contains a page number and an offset within the page. The offset is the low 12 bits for a 4 KB page, so it can address 4,096 bytes. The remaining high bits select the page, and the page table maps that page number to a physical frame number. The offset passes through unchanged.

Part of the virtual address Bits (4 KB pages) Role
Page number Upper bits Looked up in the page table
Page offset Lower 12 bits Copied unchanged to the physical address

The page table

The mapping from pages to frames is stored in page tables. A single flat table covering the full 48-bit space would be enormous, so x86-64 uses a multilevel structure with four levels. Each level is a table of entries that point to the next level, and only the parts of the address space that a program actually uses need tables allocated.

Each entry carries flags as well as the frame number. Typical flags say whether the page is present in RAM, whether it is writable, whether user code may access it, and whether it may be executed. The hardware checks these flags on every access. A write to a read-only page, or an attempt to run code from a data page on a system that enforces the no-execute bit, fails before any damage occurs.

Each process has its own page table, which is why the same virtual address can refer to different physical memory in two processes. The operating system switches the active table when it switches between processes.

The TLB keeps translation fast

Walking four levels of tables on every memory access would be too slow. Processors therefore keep a translation lookaside buffer, or TLB, a small cache of recent virtual-to-physical translations. A hit returns the frame number almost immediately. A miss forces a page table walk, which costs many memory accesses in the worst case.

Programs that touch many distinct pages in a short time can overwhelm the TLB even when their total memory is modest. Scanning a large array with a stride of one page, for example, causes a TLB miss on nearly every access. Keeping related data in the same page reduces that cost, and it is one reason why contiguous arrays often beat linked structures in practice.

Page faults: the hardware asks for help

A page fault happens when the MMU finds that the requested page is not usable. The page may not be mapped yet, it may have been swapped out to disk, or the access may violate the page's permissions. In each case the processor transfers control to the operating system's page fault handler.

The handler decides what to do based on the reason. For a valid page that has not been loaded yet, it allocates a frame, fills it with data from the file or with zeros, updates the page table and returns. The faulting instruction then runs again and succeeds. This is called demand paging, and it means a program can reserve a large region without using physical memory until it actually touches the pages.

For an illegal access, such as writing through a null pointer, the handler sends a signal to the process. On Unix-like systems that signal is usually SIGSEGV, which terminates the program unless it handles the signal.

Copy-on-write and shared memory

Virtual memory makes some efficient tricks possible. When a process calls fork on Unix-like systems, the child initially shares all of the parent's pages. Both page tables point to the same frames, and the pages are marked read-only. The first write by either process triggers a page fault, and the kernel copies just that page before allowing the write. Most pages are never written, so the copy cost is small.

Shared libraries use the same mechanism. The code of a library can be mapped into many processes at once, and each process sees it at its own virtual address. Only one physical copy exists in RAM, which saves memory across the system.

Swapping and paging to disk

When physical memory runs short, the operating system can evict pages that are not recently used. A clean page that matches a file can simply be dropped and reloaded later from that file. A modified page must be written to a swap area or swap file before its frame is reused. Later access causes a page fault that reads the page back in.

Swapping is much slower than RAM access, often by several orders of magnitude depending on the storage device. A system that constantly evicts and reloads pages can slow to a crawl, a condition called thrashing. Monitoring page fault rates and swap use is a practical way to detect memory pressure before users notice it.

Why this design matters to programmers

Knowing how virtual memory works explains several everyday behaviors. A large allocation can succeed even on a machine with little free RAM, because pages are committed only when touched. A pointer that is valid in one process means nothing in another. A crash from an invalid access is a protection feature working as designed.

It also suggests performance habits. Locality, meaning access to nearby data over a short period, helps both the CPU cache and the TLB. Avoiding unnecessary touches of large regions keeps the working set small, which keeps page faults rare.

Key takeaways

Virtual memory gives each process a private address space by translating virtual addresses into physical frames through page tables. The unit of translation is the page, commonly 4 KB on x86-64 Linux and 16 KB on Apple Silicon Macs.

The TLB makes translation fast, page faults let the operating system load, share and protect memory on demand, and copy-on-write and swapping build efficient features on top of the same mechanism. Understanding these pieces helps you read crashes, reason about performance and write code that behaves well under memory pressure.

More in Systems Programming

Systems

How a C Program Goes From Source File to Executable

A C program passes through preprocessing, compilation, assembly and linking before it runs. Learn each stage, the files produced and the gcc commands.

6 min read