A logical CPU executes one instruction stream at a time, yet a laptop may keep a browser responsive while compiling code, playing music, and running a database.
The operating system creates this illusion of simultaneous progress by repeatedly changing which thread owns each CPU. The mechanism that makes each suspended computation continue from exactly where it stopped is a context switch.
A context switch saves the execution state of one thread and restores the saved state of another so the CPU can resume the second thread.
Context switching is essential for multitasking. It is also not free. The kernel performs bookkeeping during the switch, and the incoming thread may find that useful CPU cache and translation state have grown cold.
At any instant, a CPU contains the immediate state of the thread it is executing. That state includes values such as:
Collectively, these values answer a practical question:
If this thread stops now, what must be preserved so it can later continue as though nothing happened?
The kernel also tracks scheduling state, accounting information, the thread's kernel stack, and references to resources such as its address space. Some of this information lives directly in the task's kernel bookkeeping; some is saved on its kernel stack or in architecture-specific structures.
A context switch does not normally copy the process's heap, executable code, or files. Those are longer-lived resources. The switch changes which saved execution context is active on the CPU.
There is only one set of real registers. A context switch is the work of emptying them into one saved area and filling them from another.
Only one of these contexts is active on a particular logical CPU at a given instant.
Consider one CPU running thread A while thread B is ready to run.
Conceptually, the transition proceeds as follows:
The exact sequence depends on the processor architecture and kernel implementation. For example, entering the kernel may already have saved part of the outgoing register state before the scheduler runs. The important guarantee is that every user-visible value required for correct continuation is preserved.
When A is chosen again, it does not restart its function. Its instruction pointer, stack, and registers are restored, so it continues after the point at which it stopped.
Programs are organized as processes, but schedulers work with independently executable threads.
A single-threaded process contributes one schedulable thread. A multithreaded process contributes several. On Linux, each of these schedulable entities is represented as a task.
This leads to precise terminology:
The phrase process context switch is commonly used when the outgoing and incoming threads belong to different processes. That case can require more memory-management work, but the fundamental scheduler operation still switches execution between threads.
Context switches happen when the current thread stops owning a CPU and another runnable thread is selected.
There are two broad categories.
A voluntary switch occurs when the running thread gives up the CPU because it cannot or does not want to continue immediately.
Common causes include:
For example, if a server performs a blocking read and no data is available, keeping that server on the CPU would accomplish nothing. The kernel marks it as waiting and runs something else.
The wakeup makes the server eligible to run again. A later scheduler decision restores its context.
An involuntary switch occurs when the thread remains capable of running but the kernel takes the CPU away.
This can happen because:
The outgoing thread normally remains runnable. It did not request a blocking operation; it is waiting for another turn on a CPU.
The words voluntary and involuntary describe why the task stopped running. They do not imply that one category is good and the other is necessarily bad.
A context switch often accompanies a state transition, but the two ideas are not identical.
When a running thread blocks, it moves from running to waiting, and the scheduler usually switches to another runnable thread.
When a running thread is preempted, it moves from running to ready. It remains eligible to execute, but a different thread becomes active.
When a waiting thread's event completes, it moves from waiting to ready. The wakeup alone does not prove that a context switch occurs immediately. The current thread may continue, or the newly runnable thread may be selected, depending on the scheduler's decision.
Similarly, if the only runnable thread voluntarily yields, the kernel may have no different thread to select. A scheduling decision does not always result in a switch to a different task.
A mode switch changes the CPU's privilege level, usually between user mode and kernel mode. A context switch changes the executing thread.
Compare a system call that completes immediately with a blocking read:
| Stage | Immediate operation | Blocking operation |
|---|---|---|
| Start | User mode, thread A | User mode, thread A |
| Entry | A system call | A blocking read |
| In the kernel | Kernel mode, thread A | Kernel mode, thread A |
| What happens next | The result is ready | A must wait |
| End | User mode, thread A | The kernel switches A to B, and thread B resumes |
In the first path, the CPU entered the kernel and returned to user mode while thread A remained current. This is a mode switch without a task context switch. The second path includes both a mode transition and a context switch.
Hardware interrupts also transfer control to the kernel, but an interrupt does not automatically mean another thread will run. The kernel may handle the event and return to the interrupted thread. A context switch happens only if scheduling selects a different thread.
This distinction matters when interpreting measurements: counting system calls or interrupts is not the same as counting context switches.
The outgoing and incoming threads determine how much surrounding state can remain useful.
Threads in one process share an address space. A switch between them must change the active register values, stack pointer, instruction pointer, and thread-specific state, but it can keep the same process memory map active.
Each thread still has a different user stack and execution history. Shared memory does not mean shared register state.
Different processes normally use different virtual address spaces. The kernel must activate the incoming process's memory-management context in addition to changing the execution context.
Modern processors provide address-space tags, often called ASIDs or PCIDs, that can allow translation entries from multiple address spaces to coexist. Consequently, a process switch does not necessarily flush every translation from the translation lookaside buffer.
The exact cost varies by processor and operating system. It is safer to say that switching address spaces can require additional work and can reduce translation locality—not that every process switch always empties the entire TLB.
| Switch | Execution state changes? | Address-space change normally required? |
|---|---|---|
| Between threads in one process | Yes | No |
| Between threads in different processes | Yes | Yes |
Both switches can disturb CPU-local state even when their direct kernel work differs.
During a context switch, the CPU is executing kernel bookkeeping rather than the application work that either thread exists to perform.
Direct work may include:
This cost is real, but it is only part of the performance story. A raw count of context switches does not reveal how expensive each one was, and there is no universal duration that applies to all machines and workloads.
The cost depends on the processor, kernel, security mitigations, relationship between the two tasks, and state that must be changed.
The larger cost can appear after the switch.
While thread A runs, it builds a useful working set in CPU structures:
When thread B runs, it uses those same finite hardware resources. B's working set can displace parts of A's. When A eventually resumes, it may suffer more cache and TLB misses while its useful state becomes warm again.
The operating system does not normally erase the CPU caches on every context switch. Cache contents are simply reused and replaced according to normal hardware behavior. This distinction is important: the penalty comes from interference and lost locality, not from a mandatory full cache flush.
A switch between threads that touch the same shared data may preserve more useful cache state than a switch between unrelated, memory-intensive processes. Conversely, two threads in one process can still have entirely different working sets and interfere heavily.
Loading simulation...
A context switch occurs on a logical CPU: one task stops running there and another starts.
A thread may later resume on a different CPU. This is a CPU migration. For example, a thread that previously ran on CPU 0 may later resume on CPU 3.
The new CPU may not have A's recently used data in its local caches. Some cache levels may be shared across cores, while others are private, so the effect depends on the hardware topology.
Schedulers balance two competing goals:
CPU affinity can restrict where a thread runs and sometimes improve locality, but it can also prevent useful load balancing. It should be applied only after measurement shows that migration is part of a real bottleneck.
Suppose a request-handling thread is preempted:
Thread A stops, a context switch runs, B and C run for a while, another context switch runs, and A resumes. Only the two switches are kernel work.
The context-switch operations themselves occupy only the small transition regions. The time during which B and C run is off-CPU time for A.
For latency-sensitive services, the scheduling delay before A runs again may matter more than the mechanical cost of saving and restoring registers.
This distinction prevents a common diagnostic mistake:
Good performance analysis asks both how often a task switches and why it remains off CPU.
Linux exposes cumulative switch counts for each task.
For a single-threaded process or the thread-group leader:
Example fields look like:
In a multithreaded process, inspect a particular thread through:
Do not assume /proc/PID/status is a sum across every thread. Per-thread inspection is often necessary to find the worker producing the switches.
The pidstat utility can report rates over time:
Its cswch/s column reports voluntary switches per second, while nvcswch/s reports involuntary switches per second. Adding thread-level reporting can expose imbalance hidden by a process-wide view:
For one command, Linux perf can count switches and CPU migrations:
Availability and permissions vary by system. These tools count events; they do not by themselves explain which resource, lock, or competing task caused each switch.
Linux also exposes context-switch counts through getrusage(). The following program compares a CPU-bound workload with a workload that repeatedly blocks for a short sleep. Run this experiment on Linux; other Unix-like kernels may classify these counters differently.
Compile and run both modes:
The exact counts vary between runs and machines. The blocking mode should normally produce roughly one voluntary switch for each sleep. The CPU mode should produce few voluntary switches; its involuntary count depends on system load and scheduler competition.
The experiment demonstrates an important interpretation rule: many voluntary switches can simply mean that a program intentionally spends much of its time waiting rather than wasting CPU.
In a multithreaded Linux program, RUSAGE_SELF aggregates usage across the process's threads. The Linux-specific RUSAGE_THREAD option can measure only the calling thread.
A high switch rate is a clue, not a diagnosis.
For an I/O server, frequent voluntary switches may be entirely appropriate. For a CPU-bound service, a large number of involuntary switches can indicate that too many runnable threads are competing for the available CPUs.
Common sources of harmful switching include:
The consequences can include lower throughput, colder caches, more scheduling delay, and worse tail latency.
The corrective action depends on the cause. Useful options include right-sizing CPU-bound worker pools, reducing lock contention, batching tiny units of work, and avoiding unnecessary thread creation. I/O-heavy workloads may reasonably use more concurrency than the CPU count because many workers are asleep, but the right amount must be measured under realistic load.
Trying to reduce all context switches is the wrong goal. Removing a blocking wait by busy-spinning, for example, can reduce voluntary switches while wasting an entire CPU. The goal is efficient progress and predictable latency.
Context-switch metrics need surrounding evidence.
A process that has run for months can have a huge cumulative count without any current problem. Measure the change over a known interval or use a rate-reporting tool.
A rise in voluntary switches points toward blocking, sleeping, or synchronization. A rise in involuntary switches points more strongly toward CPU competition or preemption.
These are directions for investigation, not proofs. The application and workload determine what is normal.
One overloaded worker can disappear inside process-wide totals. Per-thread counts reveal whether switching is evenly distributed.
Combine switch counts with CPU utilization, runnable-queue pressure, migrations, and request-latency measurements. A switch rate that looks large in isolation may have no meaningful effect on throughput or latency.
Debug logging, tracing, and profilers can introduce additional scheduling activity. Compare like-for-like runs and repeat measurements before drawing conclusions.
5 quizzes