How CPU Context Switching Powers Multitasking: From Theory to Code
This article explains the fundamentals of CPU context switching, covering the hardware and kernel mechanisms, step‑by‑step saving and restoring of registers, scheduling algorithms, different types of switches, performance costs, optimization techniques, and includes a complete C++ simulation example.
CPU Context
A CPU context is the collection of register values, the program counter, stack pointer and other processor state that must be preserved so that a task can resume execution exactly where it left off.
Context‑switch procedure
Save current context
The operating system copies the running task’s registers, program counter and related state into the task’s Process Control Block (PCB) or Thread Control Block (TCB). This is analogous to noting the current progress before switching to another job.
Scheduler selects the next task
The scheduler applies an algorithm (e.g., First‑Come‑First‑Served, Round‑Robin, priority‑based) to pick a ready task from the run‑queue. The chosen task’s PCB contains the saved context that will be restored.
Load new context
The OS loads the saved registers and program counter from the target PCB back into the CPU, allowing execution to continue from the exact point where the task was previously paused.
Types of context switches
Process context switch
Switches between separate address spaces (user and kernel mode). The full user‑mode state, kernel‑mode state and often memory‑management structures must be saved and restored.
Thread context switch
Threads share most resources of a process, so only the thread‑private stack and registers need to be saved and restored, making this switch cheaper than a full process switch.
Interrupt context switch
Triggered by hardware devices. The CPU saves a minimal kernel‑mode context, runs the interrupt handler, then restores the previously running task. This switch is fast because it does not involve user‑mode state.
Overhead and optimisation
Sources of overhead
Saving and restoring registers and the program counter requires memory accesses.
Scheduler decision time varies with algorithm complexity.
Cache invalidation, TLB flushes and page‑table updates may accompany a switch.
Optimisation strategies
Hardware improvements such as larger caches and more CPU cores reduce contention.
Choosing scheduling algorithms that match workload characteristics (e.g., I/O‑bound vs. CPU‑bound).
Minimising the amount of state saved for thread switches and using shared memory to avoid unnecessary copies.
Adopting lightweight concurrency primitives like coroutines that perform user‑mode switches without kernel involvement.
Practical C++ example (simplified scheduler)
#include <iostream>
#include <string>
#include <vector>
#include <cstdint>
// Simplified CPU registers used for context
struct CPUContext {
uint64_t rax; // general‑purpose register
uint64_t rbx; // general‑purpose register
uint64_t pc; // program counter (next instruction address)
uint64_t rsp; // stack pointer
};
// Process Control Block (PCB) storing a task’s context and metadata
struct PCB {
int pid;
std::string name;
CPUContext context;
bool is_running;
PCB(int id, const std::string& n) : pid(id), name(n), is_running(false) {
context = {0, 0, 0, 0};
}
};
class Scheduler {
std::vector<PCB> processes;
PCB* current_running = nullptr;
public:
void add_process(const PCB& proc) {
processes.push_back(proc);
std::cout << "Added process: PID=" << proc.pid << ", name=" << proc.name << std::endl;
}
void save_context() {
if (!current_running) {
std::cout << "No running process to save" << std::endl;
return;
}
// Simulated register values
current_running->context.rax = 0x12345678;
current_running->context.rbx = 0x87654321;
current_running->context.pc = 0x00401000;
current_running->context.rsp = 0x7ffeef00;
std::cout << "Saved context for PID=" << current_running->pid << std::endl;
current_running->is_running = false;
}
void restore_context(int target_pid) {
PCB* target = nullptr;
for (auto& p : processes) {
if (p.pid == target_pid) { target = &p; break; }
}
if (!target) {
std::cerr << "Process " << target_pid << " not found" << std::endl;
return;
}
std::cout << "Restoring context for PID=" << target->pid << std::endl;
current_running = target;
current_running->is_running = true;
}
void context_switch(int target_pid) {
std::cout << "--- Context switch start ---" << std::endl;
save_context();
restore_context(target_pid);
std::cout << "--- Context switch complete ---" << std::endl;
}
};
int main() {
Scheduler os;
os.add_process(PCB(1, "Task A (video playback)"));
os.add_process(PCB(2, "Task B (file download)"));
os.restore_context(1); // start Task A
os.context_switch(2); // switch to Task B
return 0;
}Advanced example with explicit CPU model
#include <iostream>
#include <string>
#include <vector>
#include <cstdint>
#include <iomanip>
// Core registers (simplified)
struct CPURegisters {
uint64_t rax; // temporary data
uint64_t rbx; // temporary data
uint64_t pc; // next instruction address
uint64_t rsp; // stack pointer
};
// PCB storing task metadata and saved context
struct PCB {
int pid;
std::string task_name;
CPURegisters saved_context;
bool is_ready;
PCB(int id, const std::string& name) : pid(id), task_name(name), is_ready(true) {
if (name.find("video") != std::string::npos) {
saved_context = {0x11223344, 0x55667788, 0x00401200, 0x7ffeef50};
} else if (name.find("download") != std::string::npos) {
saved_context = {0xaabbccdd, 0xeeff0011, 0x00402400, 0x7ffeef80};
} else {
saved_context = {0, 0, 0, 0};
}
}
};
// Physical CPU (holds current register state)
struct CPU {
CPURegisters regs;
void print_current_state(const std::string& tip) const {
std::cout << "
[CPU state] " << tip << "
";
std::cout << "rax: 0x" << std::hex << std::setw(8) << std::setfill('0') << regs.rax << std::dec << "
";
std::cout << "rbx: 0x" << std::hex << std::setw(8) << std::setfill('0') << regs.rbx << std::dec << "
";
std::cout << "pc : 0x" << std::hex << std::setw(8) << std::setfill('0') << regs.pc << " (next instruction)" << std::dec << "
";
std::cout << "rsp: 0x" << std::hex << std::setw(8) << std::setfill('0') << regs.rsp << " (stack top)" << std::dec << "
";
}
};
class Scheduler {
std::vector<PCB> task_list;
CPU& cpu;
public:
Scheduler(CPU& cpu_hw) : cpu(cpu_hw) {}
void add_task(const PCB& task) {
task_list.push_back(task);
std::cout << "Scheduler: added task [" << task.pid << "] " << task.task_name << std::endl;
}
PCB* select_next_task(int target_pid) {
for (auto& t : task_list) {
if (t.pid == target_pid && t.is_ready) {
std::cout << "Scheduler: selected task [" << t.pid << "] " << t.task_name << std::endl;
return &t;
}
}
std::cerr << "Scheduler: task " << target_pid << " not found or not ready" << std::endl;
return nullptr;
}
void load_task_context(PCB* new_task) {
if (!new_task) return;
std::cout << "
--- Loading context for task [" << new_task->pid << "] ---
";
cpu.regs = new_task->saved_context;
cpu.print_current_state("after loading task " + std::to_string(new_task->pid));
std::cout << "Task [" << new_task->pid << "] ready to run" << std::endl;
}
void schedule_task(int target_pid) {
PCB* next = select_next_task(target_pid);
load_task_context(next);
}
};
int main() {
CPU physical_cpu; // registers start at 0
physical_cpu.print_current_state("initial state (no task)
");
Scheduler os_scheduler(physical_cpu);
os_scheduler.add_task(PCB(1, "video playback task"));
os_scheduler.add_task(PCB(2, "file download task"));
os_scheduler.schedule_task(2); // load and run the download task
return 0;
}Monitoring context switches
Linux tools such as vmstat and pidstat report the number of voluntary and involuntary switches per second (cs, cswch/s, nvcswch/s). High values indicate frequent switching, which may be caused by many short‑lived tasks, I/O‑bound workloads, or sub‑optimal scheduling policies.
Deepin Linux
Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
