Chapter 13: Context Switching
- What a context switch is and why it is needed
- What state must be saved and restored during a switch
- How to write a context switch routine in assembly
- The difference between cooperative and preemptive switching
- How the timer interrupt triggers preemptive context switches
- How to implement switch_to() in our kernel
13.1 What is a Context Switch?
A context switch is the mechanism by which the kernel pauses one process and resumes another. It is the core of multitasking. Without context switching, the CPU would run one program until it finishes, blocking everything else.
During a context switch, the kernel saves the entire state of the currently running process (registers, stack pointer, program counter, page table base) and restores the saved state of another process. When the restored process resumes execution, it has no idea it was paused -- it looks the same as when it was stopped.
13.2 What State Must Be Saved?
The CPU has 31 general-purpose registers (x0-x30), the stack pointer (SP_EL0 for user, SP_EL1 for kernel), the program counter (ELR_EL1), and the processor state (SPSR_EL1). Additionally, for memory isolation, we must switch page tables (TTBR0_EL1).
| State | Saved Where |
|---|---|
| x0-x29 (general purpose) | Stack or process struct |
| x30 (link register) | Stack or process struct |
| SP_EL0 (user stack) | Saved on kernel stack during exception entry |
| SP_EL1 (kernel stack) | Per-process kernel stack pointer |
| ELR_EL1 (return address) | Saved in exception context |
| SPSR_EL1 (processor state) | Saved in exception context |
| TTBR0_EL1 (page table) | Per-process page table base |
| TPIDR_EL0 (thread pointer) | Per-process thread-local storage |
13.3 The Process Control Block
Each process has a Process Control Block (PCB) that stores its saved state:
#define MAX_PROCESSES 64
struct context {
uint64_t x19;
uint64_t x20;
uint64_t x21;
uint64_t x22;
uint64_t x23;
uint64_t x24;
uint64_t x25;
uint64_t x26;
uint64_t x27;
uint64_t x28;
uint64_t x29; /* frame pointer */
uint64_t x30; /* link register (return address) */
uint64_t sp; /* kernel stack pointer */
};
struct process {
uint64_t pid;
char state; /* 0=running, 1=runnable, 2=blocked, 3=zombie */
struct context context; /* saved registers */
uint64_t *page_table; /* TTBR0_EL1 value for this process */
uint64_t kernel_sp; /* kernel stack pointer */
uint64_t user_sp; /* user stack pointer (SP_EL0) */
};
Notice we only save x19-x30, not x0-x18. This is because the ARM64 calling convention designates x19-x30 as callee-saved: if a function uses them, it must save and restore them. The context switch function is itself a callee, so it follows the same convention. The calling function is responsible for saving x0-x18 (which happened when the exception handler saved context).
13.4 The switch_to Function
The switch_to function saves the current process's context and restores the next process's context. It is written in assembly because it directly manipulates the stack pointer and callee-saved registers:
.global switch_to
/* switch_to(struct context *prev, struct context *next) */
/* x0 = address of previous process's context struct */
/* x1 = address of next process's context struct */
switch_to:
/* Save callee-saved registers of current process */
stp x19, x20, [x0, #16 * 0]
stp x21, x22, [x0, #16 * 1]
stp x23, x24, [x0, #16 * 2]
stp x25, x26, [x0, #16 * 3]
stp x27, x28, [x0, #16 * 4]
stp x29, x30, [x0, #16 * 5]
/* Save the current stack pointer */
mov x2, sp
str x2, [x0, #16 * 6]
/* Restore callee-saved registers of next process */
ldp x19, x20, [x1, #16 * 0]
ldp x21, x22, [x1, #16 * 1]
ldp x23, x24, [x1, #16 * 2]
ldp x25, x26, [x1, #16 * 3]
ldp x27, x28, [x1, #16 * 4]
ldp x29, x30, [x1, #16 * 5]
/* Restore the next process's stack pointer */
ldr x2, [x1, #16 * 6]
mov sp, x2
/* Return to the next process (uses restored x30) */
ret
This function is called from the scheduler. After switch_to returns, the CPU is executing in the context of the next process, using its stack. The ret instruction returns to whatever address was saved in x30 for that process -- which is the point where the process was last paused.
13.5 Preemptive vs Cooperative Switching
Cooperative multitasking: processes voluntarily yield the CPU by calling a yield() system call. A process that never yields will run forever.
Preemptive multitasking: the kernel forces a context switch when the timer interrupt fires. The process does not need to cooperate. This is what modern OSes use.
Our kernel implements preemptive switching. The timer interrupt handler (Chapter 9) fires every 10 ms. The handler calls the scheduler, which decides which process to run next and calls switch_to.
/* Called from the timer interrupt handler */
void timer_tick(void) {
struct process *prev = current_process;
struct process *next = scheduler_pick_next();
if (prev != next) {
current_process = next;
switch_to(&prev->context, &next->context);
}
}
13.6 The Full Context Switch Flow
- Timer interrupt fires (EL0 -> EL1 via exception)
- CPU saves ELR_EL1, SPSR_EL1, and jumps to IRQ vector
- Exception handler saves x0-x29, x30, ELR, SPSR to the kernel stack
- Handler calls
irq_handler()which callstimer_tick() timer_tick()calls the scheduler, thenswitch_to()switch_to()saves callee-saved regs to prev process structswitch_to()restores callee-saved regs from next process structswitch_to()returns -- now running on the next process's kernel stack- Exception handler restores x0-x29, x30, ELR, SPSR from the (new) kernel stack
- ERET returns to the next process's user-space code at EL0
13.7 Our Implementation
Context switching in our kernel requires these components to work together:
- exception handler: saves/restores volatile state on kernel stack
- process struct: stores callee-saved state per process
- switch_to: assembly function that swaps callee-saved regs and SP
- scheduler: decides which process to run next
- timer handler: triggers the scheduling decision periodically
When a new process is created (Chapter 23), we initialize its context struct so that when switch_to first loads it, execution begins at a known function that sets up user space and returns to EL0.
13.8 Exercises
Exercise 1: Trace the Switch
Add a print statement before and after switch_to that prints the PID of the previous and next processes. Run with two processes and observe the pattern.
Exercise 2: Save All Registers
Modify the context struct and switch_to to also save and restore x0-x18. Measure the time difference with and without saving them.
13.9 Summary
Context switching is how the kernel implements multitasking. The switch_to function saves callee-saved registers and the stack pointer of the current process, then restores those of the next process. Combined with the timer interrupt for preemption and the scheduler for making decisions, this gives us a fully preemptive multitasking system.