12. Scheduler - Preemptive Multitasking¶
In the previous article, we implemented Cooperative Multitasking where tasks voluntarily yield the CPU. But what if a task misbehaves and never yields? The system would hang.
The solution is Preemptive Multitasking: The OS forcibly switches tasks at regular intervals using interrupts.
How Timer Interrupts Work on Raspberry Pi 4¶
Before implementing the scheduler, we need to understand how interrupts are delivered to the CPU.
The GICv2 Interrupt Controller¶
Raspberry Pi 4 uses ARM Generic Interrupt Controller version 2 (GICv2), which is significantly different from earlier Pi models.
The GICv2 has two main components:
1. Distributor (GICD) - Receives interrupt signals from peripherals and timers - Filters, prioritizes, and routes interrupts - Routes to the appropriate CPU Interface
2. CPU Interface (GICC) - One per CPU core - Receives routed interrupts from Distributor - Delivers interrupts to the processor - Provides interrupt acknowledgment and EOI (End of Interrupt)
Memory-Mapped Addresses¶
On Raspberry Pi 4:
- Distributor Base: 0xff841000
- CPU Interface Base: 0xff842000
Initializing the Timer Interrupt¶
To receive timer interrupts, we must configure:
-
Distributor (GICD):
- Enable PPI 27 (ARM Virtual Timer) with
GICD_ISENABLER - Set priority with
GICD_PRIORITYR - Set interrupt group (secure/non-secure) with
GICD_IGROUPR - Enable Distributor with
GICD_CTLR
- Enable PPI 27 (ARM Virtual Timer) with
-
CPU Interface (GICC):
- Set interrupt priority mask with
GICC_PMR - Enable CPU Interface with
GICC_CTLR
- Set interrupt priority mask with
-
Processor:
- Enable IRQ in DAIF register
- Set up exception vector table
Note: ITARGETSR (target processor) is only needed for SPI (Shared Peripheral Interrupts). PPI 27 is a private timer, automatically routed to the local CPU core.
PPI 27: The Virtual Timer¶
We use PPI 27 (Private Peripheral Interrupt 27), which is the ARM Generic Timer virtual counter. This timer: - Is per-CPU (each core has its own) - Can be programmed to fire at specific intervals - Fires even while CPU is in deep sleep
Handling Interrupts¶
When an interrupt fires:
- IRQ Vector (in
vectors.S) saves all registers to apt_regsstructure - IRQ Handler reads
GICC_IARto get the interrupt ID - Dispatch based on ID (PPI 27 = timer, etc.)
- Context Switch modifies
pt_regsto switch to next task - EOI write interrupt ID to
GICC_EOIRto acknowledge - Restore loads registers from
pt_regsanderetreturns to new task
IRQ Context Switching
You cannot call a normal schedule() function from an IRQ handler. The IRQ handler has its own stack and saved registers. To switch tasks from an IRQ, you must:
- Save all registers (not just callee-saved) to
pt_regs - Copy
pt_regsto the current task's PCB - Copy the next task's saved
pt_regsback - Return via
eretwhich restores PC and PSTATE
The Concept of Preemption¶
Preemption means the OS can interrupt a running task and switch to another, even if the task doesn't want to yield.
This is achieved by:
1. Timer Interrupt: Fires at regular intervals (e.g., every second).
2. Scheduler Hook: The interrupt handler calls schedule().
3. Context Switch: The scheduler saves the current task's state and loads another.
Implementation¶
1. The pt_regs Structure¶
First, we need a structure to hold the complete CPU state during an interrupt:
Each process stores this in its PCB:
2. IRQ Handler Assembly¶
The IRQ handler must build pt_regs on the stack and pass it to C:
3. schedule_from_irq¶
This is the key function that switches tasks by modifying pt_regs:
Critical: pstate Value
When starting a new task, pstate must be 0x345 (not 0x3C5):
0x3C5= EL1h + IRQ masked → Task runs but no more interrupts!0x345= EL1h + IRQ enabled → Timer keeps firing, preemption works
The difference is bit 7 (I bit): 0 = IRQ enabled, 1 = IRQ masked.
4. Preemption Control¶
But there's a problem: What if the scheduler itself gets interrupted while switching tasks? This would corrupt the process state.
We need a way to disable preemption during critical sections:
5. The Demo¶
In examples/12_scheduler_demo.c, we create tasks that never call schedule():
Building and Testing¶
1. Build¶
2. Deploy¶
Copy scheduler_demo.img to SD card (rename to kernel8.img).
3. Expected Output¶
Tasks switch every second automatically - true preemptive multitasking!
Time Slicing¶
Currently, our timer fires at 1 Hz (once per second), which is very slow. In a real OS, you'd want 100 Hz or 1000 Hz for smooth multitasking.
To change the frequency, modify timer_interrupt_init() in kernel/interrupts.c:
What's Next?¶
We now have a true time-sharing operating system! Multiple tasks run concurrently, and the OS ensures fair CPU time.
The next logical steps are: - Synchronization Primitives (Mutexes, Semaphores) to coordinate between tasks. - System Calls to allow user programs to request OS services. - File Systems to persist data.
Congratulations - you've built the core of a real operating system!