Context Switching

A CPU can only execute one instruction stream at a time. Yet a modern laptop juggles hundreds of processes. How does an operating system create the illusion that everything runs simultaneously? The answer is context switching. This is the mechanism that pauses one execution flow, saves everything needed to resume it later, and loads the saved state of another. This is the physical machinery underneath the abstract idea of the Process State and the Lifecycle. It is the act that actually moves a process from "running" to "waiting" or "ready" and back. Context switching matters because it is the hidden tax on every multiprogrammed system. Every timer interrupt, every I/O block, every thread yield potentially triggers one. If switches are slow or too frequent, the CPU spends more time bookkeeping than computing. System designers call this overhead or "thrashing" the CPU's own state. Understanding the exact mechanics of a switch matters greatly. What gets saved, where, and why it is safe to resume from that point — these questions separate a superficial notion of "multitasking" from a working mental model. That model explains how an OS kernel actually multiplexes hardware. The big picture: a context switch is a carefully choreographed sequence of privileged operations. These operations must preserve the illusion that a process never noticed it was interrupted. From the process's point of view, execution simply continues from the next instruction. It is as if no time passed and no other program ever touched the CPU. Achieving that illusion correctly, and achieving it fast, is a central engineering problem in kernel design. This article builds the concept from first principles: what "context" precisely means, the step-by-step mechanism of switching it, the data structures involved, and the true cost in cycles. It also explains why that cost sets a hard limit on how finely an OS can slice CPU time. It leads directly into CPU Scheduling Fundamentals, since every scheduling decision has to pay the context-switch bill.

Key takeaways
  • A context switch saves the full execution state of a running process/thread and restores that of another, using the The Process Control Block (PCB) as the save location.
  • It is triggered by interrupts, system calls, or explicit yields — never by a process choosing to switch itself out arbitrarily.
  • The switch has two conceptually separate steps: switching context (registers, PC, stack pointer) and switching address space (page tables), the latter being far more expensive.
  • Cost is measured in microseconds but paid thousands of times per second; it is pure overhead, contributing zero useful work.
  • Thread switches within one process are cheaper than process switches because the address space (and its cache/TLB footprint) stays the same.
  • The switch cost directly bounds scheduling granularity, which CPU Scheduling Fundamentals and Scheduling Performance Metrics must account for.
The core insight

A context switch is the kernel's atomic act of saving one execution state into a PCB and loading another, trading CPU cycles for the illusion of concurrency.

The idea

The context of a running process is everything the CPU needs to resume it later exactly where it left off. This includes the program counter (next instruction to execute), the CPU registers (general purpose, stack pointer, status/flags register), and, for a full process (as opposed to a thread), the memory-management state — page table base register, segment registers, and open file descriptor tables. A context switch is the operation of saving this entire bundle for the currently running execution unit and loading the previously saved bundle of another one, so the CPU can resume it. It is essential to separate two things that are often conflated. A thread switch changes only the CPU-register context (registers, program counter, stack pointer) because threads of the same process share an address space. A process switch additionally changes the memory context — it must reload the page table base register (and, on architectures without tagged TLBs, flush the Translation Lookaside Buffer). This is why switching between two threads of the same process is meaningfully cheaper than switching between two unrelated processes: the address-space-related cost is the dominant, and avoidable, part of the bill. Context switches are always mediated by the kernel and occur only at well-defined trigger points: a hardware interrupt (timer, I/O completion), an internal trap/exception (page fault, illegal instruction), or a system call that blocks (e.g., a `read()` waiting on disk) or explicitly yields the CPU. A process cannot switch itself out of the CPU; it can only request the kernel to do so, directly or indirectly, because the save/restore machinery touches privileged CPU state that user code is not permitted to manipulate. This is the essential link to the Threads and Concurrency model: preemption is what makes concurrent scheduling possible even for CPU-bound code that never voluntarily yields.

How it works

A context switch is not a single instruction — it is a sequence of operations spanning hardware and kernel software, executed with interrupts disabled to guarantee atomicity. We can decompose it into five stages.

Stage 1 — Trap into kernel mode. A hardware interrupt or software trap forces the CPU to switch to kernel/supervisor mode and jump to a fixed interrupt-handler address. The hardware itself typically saves a minimal frame (program counter, status register, sometimes the stack pointer) onto the kernel stack, because this much must happen before any software runs.

Stage 2 — Save the full context of the current process. The interrupt handler (or a scheduler routine it calls) saves the remaining general-purpose registers into the outgoing process's PCB, extending the minimal hardware-saved frame into a complete snapshot.

Stage 3 — Kernel-mode bookkeeping and scheduling decision. The kernel updates the process state field in the PCB (e.g., `RUNNING → READY` or `RUNNING → WAITING`), inserts the PCB into the appropriate scheduler queue, and invokes the scheduler to pick the next process to run. (Which policy it uses is the subject of CPU Scheduling Fundamentals; the switch mechanism itself is policy-agnostic.)

Stage 4 — Restore the context of the chosen process. The kernel loads the general-purpose registers from the incoming process's PCB, and — if it is a different address space — reloads the page-table base register (e.g., `CR3` on x86, or the ASID/page-table pointer on ARM), invalidating stale TLB entries as required.

Stage 5 — Return from trap. A special return-from-interrupt instruction restores the program counter and status register from the (now newly loaded) saved frame and drops back to user mode, resuming execution of the new process exactly at its saved instruction. ``` // Executed with interrupts disabled — must be atomic w.r.t. further interrupts function CONTEXT_SWITCH(old_pcb, new_pcb): // --- Stage 2: save outgoing context --- old_pcb.pc <- CPU.program_counter old_pcb.regs <- CPU.general_registers old_pcb.sp <- CPU.stack_pointer old_pcb.status_reg <- CPU.flags // --- Stage 3: bookkeeping --- old_pcb.state <- READY or WAITING // caller decides which enqueue(old_pcb, ready_queue_or_wait_queue) new_pcb <- scheduler_pick_next() // policy defined elsewhere new_pcb.state <- RUNNING // --- Stage 4: restore incoming context --- if old_pcb.address_space != new_pcb.address_space: CPU.page_table_base <- new_pcb.page_table_ptr flush_or_tag_TLB(new_pcb.asid) CPU.general_registers <- new_pcb.regs CPU.stack_pointer <- new_pcb.sp CPU.flags <- new_pcb.status_reg CPU.program_counter <- new_pcb.pc // committed on IRET/ERET return // hardware "return-from-interrupt" resumes new_pcb in user mode ``` The pseudocode makes explicit the invariant that a correct implementation must preserve: every bit of mutable CPU state that a running program can observe must be saved before it is overwritten, and restored before control returns to user mode. Miss one register, and the resumed process silently corrupts its own computation — a bug class that is notoriously hard to reproduce because it depends on exactly which process happened to run next.

Figure
Anatomy of a context switch between two processes
CPU PC, SP, regs, flags, PTBR PCB: Process A state: RUNNING→READY pc: 0x4012 sp: 0x7ffee0 regs: {r0..r7} flags: Z=0,C=1 page_table_ptr: 0x9000 PCB: Process B state: READY→RUNNING pc: 0x2088 sp: 0x7ffab4 regs: {r0..r7} flags: Z=1,C=0 page_table_ptr: 0xA400 ① SAVE ② LOAD Ready Queue ← A B now RUNNING Interrupt / trap fires here

Complexity & correctness

Time complexity. A context switch is O(1) in the number of processes: it saves and restores a fixed-size register set (independent of how many other processes exist), so the direct save/restore work does not grow with system load. The scheduling decision that picks the next process, however, may cost more depending on the scheduler's data structure — O(1) for a simple ready queue, O(log n) for a priority heap, O(1) amortized for a red-black-tree-based scheduler with cached leftmost pointer (as in CFS-style designs). The switch mechanism itself is therefore cheap and constant; what varies is the policy layer riding on top of it, which is the concern of CPU Scheduling Fundamentals. The real cost, though, is not the register save/restore — it is indirect cost, and this is where honesty about "complexity" must go beyond asymptotic notation: 1. Mode-switch overhead: trapping into the kernel and back, typically hundreds of cycles. 2. TLB flush / reload: if the new process has a different address space and the hardware lacks tagged TLBs (ASIDs), every TLB entry must be invalidated, and the first memory accesses of the new process all take TLB-miss penalties — page-table walks costing tens to hundreds of cycles each. 3. Cache pollution: the outgoing process's working set in L1/L2/L3 cache is evicted by the incoming process's accesses, so the new process runs at "cold cache" speed for a while — this can dominate total cost, often being larger than the raw register-save time by an order of magnitude. 4. Pipeline/branch-predictor state loss: modern superscalar CPUs lose speculative state and branch history across a switch, incurring further stalls. A widely used empirical figure: raw register save/restore takes on the order of 1–5 microseconds on commodity hardware, but the effective cost including cache/TLB re-warming can be several times higher, and grows with the memory footprint of the processes involved. This is why the space complexity matters too: the PCB itself is O(1) additional space per process (a fixed-size record), but the transient cost of "cold" caches is proportional to the working set size being displaced — not a fixed constant.

Correctness argument. The invariant that must hold before and after any switch is: > State-preservation invariant. For any process P, the CPU-visible state (registers, PC, flags, and — for process switches — address mapping) recorded in P's PCB at the moment it is switched out is exactly sufficient to resume P's execution as if it had never stopped. The proof sketch is by construction: the hardware trap mechanism guarantees the PC and flags at the exact instruction boundary are captured atomically (this is a hardware contract — traps occur only at instruction boundaries, never mid-instruction), and the kernel save routine is written to touch every remaining architecturally-visible register exactly once, with interrupts disabled so no nested interrupt can corrupt the save in progress. Because the save is total (covers the full visible state) and atomic (no partial overwrite is observable), replaying it exactly reverses it: restoring the saved values before returning to user mode is bit-for-bit equivalent to never having left. The only way this invariant breaks is an implementation bug — e.g., forgetting a callee-saved register, or a race where an interrupt is re-enabled before the restore completes — both are classic sources of real kernel bugs, not flaws in the concept itself.

Tradeoffs. Faster switching (smaller saved context, e.g., switching only among threads of one process) buys responsiveness but the OS must still eventually pay full process-switch cost when scheduling across security/address-space boundaries. Reducing switch frequency (longer time slices) amortizes overhead but increases response latency for interactive workloads — a tension that CPU Scheduling Fundamentals resolves differently for throughput-oriented versus latency-oriented policies.

Worked example

Consider two processes, P1 (a text editor waiting on keyboard input) and P2 (a CPU-bound compiler), both runnable on a single-core machine with a 10 ms timer tick. Trace the events:

Time (ms)EventActionRegister/PCB state after
0P1 runningexecutes user code`PC=0x4010, SP=0x7ffe00 (P1)`
3P1 calls `read()` on keyboard, no data yettrap → kernel; P1 blocksP1.PCB: state=WAITING, pc=0x4018 saved
3scheduler picks P2 (only runnable process)context switch executesCPU regs loaded from P2.PCB
3–13P2 running (compiling)executes user code`PC` advancing in P2
13timer interrupt firestrap → kernel; P2 was RUNNINGP2.PCB: state=READY, pc=current saved
13scheduler re-picks P2 (P1 still WAITING, no other work)context switch (same process — degenerate case, some kernels skip full switch)P2 resumes as RUNNING
13–20P2 continues
20keyboard interrupt: data arrives for P1P1.PCB: state=READY, enqueuedP2 keeps running (no preemption needed yet)
23next timer tick; scheduler now sees P1 READY with higher priority (interactive)context switch: save P2, restore P1P1.PCB: pc=0x4018 restored, regs restored
23P1 resumes `read()` return path exactly at saved PCexecutes as if never interrupted`PC=0x4018 (P1)`

Two things this trace makes concrete. First, at t=3ms, the switch is triggered by a blocking system call, not a timer — context switches are not solely a scheduling-tick phenomenon. Second, at t=23ms, P1 resumes at exactly `0x4018`, the instruction right after the trapping `read()` call, with its stack pointer and registers bit-identical to what they were at t=3ms — this is the state-preservation invariant holding in practice, twenty milliseconds and one full compiler run later. If P1 and P2 shared the same address space (i.e., were threads, not processes), the switch at t=23ms would skip the page-table-base reload and TLB invalidation entirely — the same register save/restore logic runs, but stage 4's address-space branch is skipped, saving the TLB-refill penalty. This is precisely why thread-based concurrency inside one process is cheaper than process-based concurrency, even though the trap-and-restore skeleton is identical.

Common pitfalls & traps

PitfallWhy it happensConsequence
Forgetting to save a callee-saved or status registerAssuming "general purpose registers" covers flags/condition codes tooResumed process silently gets wrong branch outcomes or arithmetic flags
Re-enabling interrupts before restore completesSloppy interrupt-enable placement in handlerA nested interrupt can overwrite in-flight registers — a race condition corrupting state
Treating thread switch and process switch as equally expensiveNot distinguishing address-space reload from register reloadOverestimating threading cost, underestimating process-switch cost in capacity planning
Ignoring TLB/cache warm-up cost in "switch time" benchmarksMeasuring only the save/restore instructionsUnderestimates real overhead by 5–10x under heavy multiprogramming
Assuming a process can switch itself outConfusing "yield" (a request) with "switch" (a kernel action)Misunderstanding why user code cannot preempt itself without a syscall/trap
Off-by-one on when state transitions to READY vs RUNNINGUpdating the PCB state field before vs after the save completesA process could be scheduled again before its saved context is actually consistent

Pioneers & history

Context switching as a concept emerged directly from the shift away from single-program batch computing toward multiprogramming, where the whole point was to keep the CPU busy while one job waited on slow I/O.

Origins · 1 minEdsger W. Dijkstra and how this was discovered
EW
Edsger W. Dijkstra
1930–2002 · Multiprogramming systems at Eindhoven

Dijkstra's work on the THE multiprogramming system formalized the idea of processes with well-defined states and the synchronization primitives needed to switch safely between them, laying conceptual groundwork that every kernel's context-switch routine still rests on.

History
1961
— Compatible Time-Sharing System (CTSS) at MIT demonstrates practical multiplexing of one CPU among interactive users.
1968
— Dijkstra publishes work on the THE operating system, formalizing cooperating sequential processes and process states.
1969
— Unix begins development, embedding the process/PCB abstraction and preemptive scheduling as core kernel primitives.
1991
— Linux kernel introduces its own scheduler and context-switch path, iterated repeatedly for lower latency.
2003
— Hardware TLB tagging (ASIDs) becomes mainstream in commodity CPUs, reducing the address-space-reload cost of process switches.

Where this matters

Every general-purpose operating system kernel — Linux, Windows NT, macOS/XNU, the BSDs — implements a context-switch routine as one of its most performance-scrutinized pieces of assembly and C code, because it runs millions of times per second across a datacenter fleet. Real-time operating systems (FreeRTOS, VxWorks) are judged partly on their worst-case context-switch latency, a headline number in embedded and aerospace systems where missing a deadline is a safety failure. Hypervisors (KVM, Xen, Hyper-V) perform an even heavier-weight version — the VM exit/entry — which saves and restores not just process context but entire virtual-CPU state, making context-switch cost a first-order concern in cloud infrastructure billing and density planning. Modern language runtimes with user-level "green threads" or coroutines (Go's goroutines, Erlang processes) exist specifically to dodge kernel context-switch overhead by implementing cheap, user-space stack switches — a direct engineering response to the cost analyzed in this article. Understanding this mechanism is the necessary foundation for CPU Scheduling Fundamentals, where scheduling policies must be designed around the fact that every decision to switch costs real, non-negotiable time.

Ask the tutor: How does hardware support like tagged TLBs (ASIDs) actually change the steps of the context-switch pseudocode above?

Does a context switch happen only when the scheduler's timer fires?

No. A timer interrupt is one trigger, but far from the only one. Any event that traps into the kernel can lead to a context switch: a blocking system call (like `read()` waiting on disk or network data), a page fault requiring disk I/O, a hardware exception, or an explicit yield from user code. The unifying feature is that control enters the kernel, and the kernel's scheduler then decides whether the currently running process should keep the CPU or be replaced. In fact, most context switches in an I/O-heavy workload are triggered by blocking calls, not timer preemption, since a process waiting on disk has nothing useful to do and voluntarily gives up the CPU well before its time slice expires.

Is a context switch the same thing as a mode switch (user mode to kernel mode)?

No, though they are related and often confused. A mode switch changes the CPU's privilege level (user to kernel or back) without necessarily changing which process is running — a simple system call that doesn't block, like `getpid()`, causes a mode switch into the kernel and back to the same process, no context switch involved. A context switch, by contrast, always changes which process/thread is executing next, and always involves at least one mode switch as a sub-step (you must enter kernel mode to perform the save/restore). So every context switch includes a mode switch, but not every mode switch results in a context switch.

Why do PCBs need to store the page table pointer if register save/restore is the "core" of a context switch?

Because for a full process switch, the address space is part of the execution context just as much as the registers are. If the kernel restored registers but left the old process's page table active, the new process's virtual addresses would resolve to the wrong physical memory — a fatal correctness bug, not just a performance issue. The PCB stores the page-table base pointer (and ASID, if the hardware supports tagged TLBs) precisely so that stage 4 of the switch can re-point the CPU's memory-management unit at the correct address space before any instruction of the new process executes.

Can context-switch overhead ever be eliminated entirely?

Not eliminated, but it can be minimized or amortized. Techniques include tagged TLBs (ASIDs) that avoid full TLB flushes on process switches, larger time slices that amortize fixed overhead over more useful work (at the cost of responsiveness), and user-space threading models (green threads, coroutines, goroutines) that switch execution contexts entirely in user space without trapping into the kernel at all. The last approach doesn't eliminate the concept of saving/restoring registers — it just moves it out of the kernel, making each switch cheaper because there's no mode switch or TLB implication, only at the cost of losing true preemptive, kernel-enforced scheduling guarantees.

Why is cache pollution often a bigger cost than the register save/restore itself?

Register save/restore touches a small, fixed number of CPU registers — a handful of memory writes and reads, taking at most a few dozen cycles. But a running process's working set — the code and data it actively touches — occupies the L1/L2/L3 caches, potentially megabytes of state built up over milliseconds of execution. When a different process runs next, its own memory accesses evict the old process's cached data. When the original process resumes, it experiences a burst of cache misses reloading data it previously had cached "for free." This re-warming cost, multiplied across every level of the memory hierarchy, routinely dwarfs the microsecond-scale cost of the register transfer itself.

Does switching between two threads of the same process still require disabling interrupts?

Yes. Even though thread switches skip the address-space-reload step, the underlying register save/restore is still a critical section: if an interrupt fires mid-save, it could invoke another switch on top of a half-saved register set, corrupting both threads' state. The safety argument for atomicity applies equally whether the two execution contexts share an address space or not — what differs is only the extra work of stage 4 (page table reload), not the need for atomic register handling in stages 2 and 4's register portion.

How does this relate to what comes next in scheduling?

This article covers the mechanism — how a switch is performed and what it costs. CPU Scheduling Fundamentals covers the policy — which process should be chosen to run next, and when a switch should be triggered at all (preemptive vs. cooperative). Scheduling Performance Metrics then quantifies how scheduling choices, combined with unavoidable context-switch overhead, translate into measurable outcomes like turnaround time, waiting time, and CPU utilization. The mechanism you've learned here is the fixed cost that every scheduling policy must budget for; none of those higher-level policies can be evaluated honestly without accounting for it.

Before this

Where this leads