Linux

Paging & Context Switching

Every process on Linux believes it owns a private, contiguous chunk of memory starting near address zero, and believes it has the CPU to itself. Neither is true — paging is the illusion machinery for the first belief, and context switching is the illusion machinery for the second. Both are core kernel mechanisms with real, measurable costs.

Quick reference
What it is
How Linux gives every process its own private virtual address space (paging), and how the CPU is shared between processes and threads over time (context switching).
Mental model
Virtual memory is a lookup table sitting on top of real RAM; a context switch is the kernel hitting pause on one program and resume on another, saving and restoring just enough state that neither program can tell it happened.
Key numbers
A typical Linux timer tick fires every 1–4 ms (CONFIG_HZ 250–1000); switching between threads in the same process skips the TLB flush a process-to-process switch has to pay.
Reach for it when
Diagnosing a high context-switch count, an unexplained first-touch latency spike, or reasoning about real-time scheduling behavior.

Why virtual memory exists

Without virtual memory, every process would address physical RAM directly — one stray pointer write could corrupt the kernel or another program, and two programs both compiled to assume they own address 0x400000 couldn't coexist. Virtual memory solves both problems by giving every process its own virtual address space: an address a process uses is not a physical RAM address at all, but an index into a per-process translation the MMU (memory management unit) resolves on every access. This buys three things at once: isolation (a process literally cannot form a pointer into another process's memory), the illusion of contiguity (a process's memory looks flat and contiguous even though the physical frames backing it are scattered across RAM), and overcommit (a page of virtual address space can exist before any physical RAM is actually set aside for it — see page faults below).

Page tables and the TLB

Memory is managed in fixed-size chunks called pages (4 KiB is the common default on both x86 and ARM). A virtual address splits into a page number (which page) and an offset (where within that page). The page table is the per-process data structure mapping each virtual page number to the physical frame currently backing it — on real hardware this is a multi-level radix tree (4 levels on both x86-64 and ARM64) rather than one flat array, so a full address doesn't require one giant table per process. Each process has its own page table, and the CPU register pointing at the currently-active one (CR3 on x86, TTBR0/1 on ARM) is part of what makes a process a process, from the MMU's point of view.

Walking four levels of page table on every single memory access would be ruinously slow, so the CPU caches recent virtual→physical translations in the TLB (translation lookaside buffer). Most memory accesses hit the TLB and skip the table walk entirely; a TLB miss falls back to walking the page table in hardware (x86, ARM) to refill it.

A virtual address is checked against the TLB first; a hit resolves straight to a physical frame, a miss falls back to walking the page table Virtual address → physical address Virtual address page number + offset TLB cached translations Page table walked on miss Physical memory RAM frame TLB hit — fast path miss walk result refills TLB
The TLB is checked first because a full page-table walk is many memory accesses deep — a TLB hit turns translation into effectively free, a miss pays the walk once and caches the result.

Page faults

A page fault is the MMU telling the kernel "this virtual address has no valid translation right now — you handle it." That's not always an error. A minor fault means the page exists but isn't mapped into this process's page table yet — the common case for a freshly mmap()ed region, handled purely in memory with no disk I/O. A major fault means the data actually has to come from disk or swap, which is orders of magnitude slower. This is what makes demand paging possible: mmap() can reserve gigabytes of address space instantly because no physical frame is allocated until the process actually touches a given page.

void *p = mmap(NULL, 4096 * 1000, PROT_READ | PROT_WRITE,
               MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
// No physical page exists yet for any of this — the call only
// reserves virtual address space. Touching page 0 for the first
// time triggers a minor page fault; the kernel handles it by
// backing that one page with a real frame, then execution resumes
// as if the fault never happened.
p[0] = 1;

// Per-process fault counts, straight from the kernel:
// $ cat /proc/$$/stat | awk '{print "minflt="$10, "majflt="$12}'

What a context switch actually does

A context switch is the kernel suspending one thread of execution and resuming another on the same CPU core. To make the interruption invisible, it has to save exactly the state that execution depends on — general-purpose registers, the program counter, and the stack pointer — into that thread's kernel-side control block, then load the same set back in for whatever runs next. Switching between two threads of the same process stops there. Switching between two processes additionally means pointing the CPU's page-table base register at the new process's page table — which is also why a process switch is inherently more expensive than a thread switch (see cost below).

A timer interrupt causes the kernel to save process A's CPU state and load process B's, then B resumes running on the same CPU Anatomy of a context switch Process A running on CPU timer IRQ Kernel: switch_to() save A, load B Process B running on CPU PCB — Process A saved CPU state PCB — Process B restored CPU state save restore
Every register, the program counter, and the stack pointer round-trip through each process's control block on the way through the switch — that's the minimum state needed to make the interruption invisible.

What triggers a switch

Why context switches cost what they cost

The direct cost — saving and restoring registers — is genuinely cheap, on the order of tens to low hundreds of nanoseconds. The expensive part is indirect: switching page tables invalidates the TLB entries tied to the old process's address space (an ARM/x86 core generally can't tell which cached translations still apply to the new process, so it has to assume none do), and every subsequent memory access pays a TLB-miss walk until the cache repopulates. The CPU's data and instruction caches suffer the same fate for the same reason — the new process's working set has to evict the old one's cache lines one access at a time. This is exactly why switching between threads of the same process is cheaper than switching between processes: threads share one page table, so there's no TLB flush and far less cache churn to pay for.

# system-wide context switch rate — the "cs" column
vmstat 1

# per-process breakdown: voluntary (blocked on I/O) vs.
# involuntary (preempted by the scheduler)
pidstat -w -p <pid> 1
#   cswch/s   = voluntary
#   nvcswch/s = involuntary

# count context switches for one command's whole run
perf stat -e context-switches ./my_program

Real-world applications

Gotchas

Field notes