Skip to content

11. Context Switching - The Heart of Multitasking

We've built a solid foundation with drivers, graphics, and memory management. Now comes the magic: Multitasking. How can a single CPU core run multiple programs "at the same time"?

The answer is Context Switching. The OS saves the state (context) of the current task, loads the state of another task, and resumes it.

The CPU Context

The "context" of a running program consists mainly of its CPU registers and Stack.

On ARM64, the calling convention (AAPCS64) defines which registers must be preserved by a function call (Callee-saved registers). When we switch tasks, we only need to save these:

  • x19 - x28: General purpose registers
  • x29 (FP): Frame Pointer
  • x30 (LR): Link Register (Return address)
  • sp: Stack Pointer

The other registers (x0-x18) are "Caller-saved", meaning a task expects them to be overwritten if it calls a function (like schedule()), so we don't need to save them explicitly for cooperative multitasking.

Process Control Block (PCB)

We need a data structure to track each process.

struct cpu_context {
    uint64_t x19;
    uint64_t x20;
    // ... x21-x28 ...
    uint64_t fp;  // x29
    uint64_t sp;
    uint64_t pc;  // x30 (lr)
};

struct process {
    struct cpu_context context;
    long state;
    long pid;
    void* stack_page;
};

The Switch Function (cpu_switch_to)

This is the most critical piece of assembly code in the OS. It takes two pointers: prev (where to save current state) and next (where to load new state).

.global cpu_switch_to
cpu_switch_to:
    /* x0 = prev, x1 = next */

    /* 1. Save current context to 'prev' */
    stp x19, x20, [x0, #0]
    stp x21, x22, [x0, #16]
    /* ... save others ... */
    stp x29, x30, [x0, #80]

    mov x9, sp
    str x9, [x0, #96]

    /* 2. Load new context from 'next' */
    ldp x19, x20, [x1, #0]
    /* ... load others ... */
    ldp x29, x30, [x1, #80]

    ldr x9, [x1, #96]
    mov sp, x9

    /* 3. Jump to new task (Return) */
    ret

When ret executes, it jumps to the address stored in x30 (LR). For a newly created task, we artificially set LR to the function entry point.

Cooperative Multitasking

In this article, we implement Cooperative Multitasking. Tasks must voluntarily yield the CPU by calling schedule().

1
2
3
4
5
6
void task_a(void) {
    while (1) {
        print("Task A");
        schedule(); // Yield
    }
}

In the next article, we'll use the System Timer to interrupt tasks automatically (Preemptive Multitasking).

Building and Testing

1. Build

1
2
3
cd simpian-os/build
cmake -DBUILD_EXAMPLES=ON ..
make

2. Deploy

Copy multitasking_demo.img to SD card (rename to kernel8.img).

3. Expected Output

Multitasking Demo
=================

Process subsystem initialized.
Creating tasks...
Created Task A
Created Task B
Starting scheduler...

Kernel Task running...
Task A running...
Task B running...
Kernel Task running...
Task A running...
...

You will see the tasks alternating execution!

What's Next?

We have multiple tasks running! But they have to be "nice" and yield the CPU. In the next article, we will implement a Scheduler that uses the timer interrupt to forcibly switch tasks, creating a true time-sharing system.