On this page
Context Switch
Coverage: context_switch() full path → switch_mm (page tables/TLB/PCID) → switch_to (registers/FPU) → lazy TLB → Switching overhead analysis Kernel versions: 2.6 ~ 6.x, with emphasis on PCID/ASID and FPU optimizations in 5.x+
Overview
Context switching is the "execution side" of the scheduler: the scheduler decides "who runs," while context switching handles "how to switch." Each switch requires the kernel to save the CPU register state of the current process, restore the registers of the next process, and handle address space switching (page tables, TLB) and FPU/SIMD context.
The typical overhead of a context switch is between 1 and 5 μs, depending on:
- Whether the address space changes (process switch vs. thread switch)
- Whether the TLB needs flushing (PCID/ASID can reduce this)
- Whether the FPU is dirty (requiring save/restore of the XSAVE area)
- Cache locality (a freshly woken process may still have data in the cache)
Entry Point: context_switch()
// kernel/sched/core.c
// Called by schedule() → __schedule()
static __always_inline struct rq *
Key Insight: After context_switch() calls switch_to(), execution is already in next. switch_to does not return (from the C perspective); it passes control to the return point of switch_to where the next process was previously switched out. When prev is scheduled again, it resumes execution from the return point of switch_to to execute finish_task_switch().
switch_mm: Address Space Switching
This is the most expensive part of context switching, especially when crossing address spaces.
x86-64 Implementation
// arch/x86/mm/tlb.c
void
PCID: Avoiding TLB Flushes
// x86 PCID (Process-Context Identifier):
// One 12-bit ASID (Address Space Identifier) per address space
// Attaching ASID when writing CR3 → Entries for another ASID in the TLB won't hit
// And don't need to be flushed
//
// Without PCID: Every write to CR3 → Flushes all non-global TLB entries → High cache miss rate
// With PCID: Switching address spaces → Only writes CR3 + new ASID → TLB retains old entries
// (Old entries have old ASID, won't cause false hits)
//
// The kernel allocates and recycles ASIDs via per-CPU ASID bitmaps
Lazy TLB
// When switching kernel threads (mm == NULL):
// enter_lazy_tlb(prev->active_mm, next)
// → next->active_mm = prev->active_mm
// → Does not write CR3 (continues using prev's page tables)
// → Kernel address space is shared, no need to switch
//
// This is why kernel thread switching is particularly fast—no TLB flush overhead
ARM64 Equivalent Implementation
// ARM64 uses ASID (8/16 bit) instead of PCID
// TTBR0_EL1: User space page tables (different for each process)
// TTBR1_EL1: Kernel space page tables (shared by all processes)
//
// switch_mm(): Writes TTBR0_EL1 + ASID
// → No extra instructions needed to flush TLB (ASID distinguishes hardware-side)
//
// VMID (Virtual Machine ID):
// Virtualization extension, managed by hypervisor
// TLB tag for stage-2 page tables
switch_to: Register Context
// arch/x86/entry/entry_64.S
// switch_to() is a macro that expands to:
//
// 1. Save prev's general-purpose registers to the kernel stack
// 2. Save prev's RIP/RSP to task_struct.thread
// 3. Restore next's RIP/RSP
// 4. Restore next's general-purpose registers
// 5. Jump to next's RIP
thread_struct — Saved Hardware Context
// arch/x86/include/asm/processor.h
;
FPU Lazy Switching
// arch/x86/kernel/fpu/core.c
// FPU switching strategy under modern x86 (XSAVE/XSAVES):
// 1. Each process has an independent fpstate (XSAVE area, up to ~10KB for AMX)
// 2. "Lazy" handling during switch:
// - FPU state is not automatically saved/restored in switch_to()
// - Sets TS (Task Switched) or XFD bit
// - New process uses FPU for the first time → #NM exception → Kernel performs XRSTOR
//
// 3. Why? The XSAVE area is large (AMX can be several KB), and many processes never use FPU
// Lazy saving avoids meaningless data movement
// Exceptions to lazy saving:
// - If the next process explicitly needs FPU (checked via __fpu_state_active)
// - And the FPU is dirty → Must perform XSAVE/XSAVES in switch_to
Architectural Differences
| x86-64 | ARM64 | RISC-V | |
|---|---|---|---|
| Switch Function | __switch_to_asm() (Assembly) | cpu_switch_to() (Assembly) | __switch_to() (Assembly) |
| FPU | XSAVE/XRSTOR (~10KB) | FPSIMD/SVE (Lazy) | FPU (Lazy) |
| Address Space | Write CR3 + PCID | Write TTBR0 + ASID | Write SATP + ASID |
| Barrier | Explicit membar | isb after TTBR write | sfence.vma |
TLB Shootdown
When modifying global page table entries (e.g., munmap), all other CPUs need to be notified to flush their TLBs:
// kernel/smp.c → arch/x86/mm/tlb.c
void
The overhead of multi-core TLB shootdowns is significant (IPI + waiting), so the kernel implements various optimizations:
- Lazy TLB mode: If the target CPU is in a kernel thread (not running user space), flushing can be deferred
- Batching: Multiple TLB operations are merged into a single IPI
- Self-snoop: Some hardware can automatically detect page table changes, eliminating the need for software IPIs
Switching Overhead Analysis
Typical numbers (x86-64, ~3GHz):
| Scenario | Latency | Reason |
|---|---|---|
| Thread Switch (Same Process) | ~0.5-1 μs | No CR3 write, FPU usually lazy |
| Process Switch (With PCID) | ~1-3 μs | Write CR3, but TLB retained |
| Process Switch (No PCID) | ~3-5 μs | Write CR3 + full non-global TLB cold start |
| Cross NUMA | +1-3 μs | New CPU cache cold, needs remote data fetch |
Measurement Methods
# 1. perf: Latency histogram
# 2. bpftrace: Directly measure schedule() latency
# 3. /proc/<pid>/sched
|
# nr_voluntary_switches: Process actively yields (block/sleep/yield)
# nr_involuntary_switches: Preempted due to time slice expiration
# High involuntary → CPU-intensive; High voluntary → IO-intensive
References and Further Reading
- Kernel Documentation:
Documentation/x86/tlb.rst,Documentation/arch/x86/topology.rst - LWN:
- "PCID and the TLB" (lwn.net/Articles/729754/)
- "Lazy FPU state restoration" (lwn.net/Articles/675957/)
- Source Files:
kernel/sched/core.c— context_switch()arch/x86/mm/tlb.c— switch_mm_irqs_off()arch/x86/kernel/process_64.c— __switch_to()arch/x86/entry/entry_64.S— switch_to at the assembly levelarch/x86/kernel/fpu/core.c— FPU lazy switching
Keywords: context_switch, switch_mm, switch_to, PCID, ASID, TLB shootdown, lazy TLB, FPU lazy restore, XSAVE