ARM64 OS Handbook
🔍

Chapter 22: Schedulers

What You Will Learn in This Chapter
  • What a scheduler is and why the OS needs one
  • Scheduling policies: FIFO, Round Robin, Priority, O(1), CFS
  • The timer interrupt and preemption mechanism
  • How the scheduler picks the next process to run
  • How to implement a simple round-robin scheduler
  • Our kernel scheduler implementation

22.1 What is a Scheduler?

The scheduler is the component of the kernel that decides which process runs next on the CPU. On a single-core system, only one process can run at a time. The scheduler divides CPU time among all processes, giving each one a fair share.

The scheduler runs in two situations:

  • Preemptive scheduling: triggered by the timer interrupt (typically every 1-10 ms). The interrupt handler calls the scheduler, which may switch to a different process.
  • Voluntary scheduling: a process calls yield() or blocks on I/O, voluntarily giving up the CPU.

22.2 Scheduling Policies

PolicyDescriptionComplexity
First-In-First-OutRun each process until it blocks or exitsO(1)
Round RobinEach process runs for a fixed time slice, then cycles to the nextO(1) enqueue, O(n) pick
Priority-basedHigher-priority processes run before lower-priority onesO(1) with priority queues
Multi-Level FeedbackMultiple queues with different priorities; processes move between queuesO(1)
O(1) SchedulerTwo priority arrays (active/expired), O(1) selectionO(1)
Completely Fair SchedulerRed-black tree of processes sorted by virtual runtimeO(log n)

22.3 Round Robin Scheduler

A round-robin scheduler is the simplest practical scheduler. Each process gets a fixed time slice (quantum). When the timer interrupt fires, the kernel switches to the next process in the ready queue.

/* Ready queue (circular linked list of runnable processes) */
struct pcb *ready_queue = NULL;
int process_count = 0;

/* Add a process to the ready queue */
void scheduler_enqueue(struct pcb *pcb) {
    if (!ready_queue) {
        ready_queue = pcb;
        pcb->next_ready = pcb;
    } else {
        pcb->next_ready = ready_queue->next_ready;
        ready_queue->next_ready = pcb;
    }
    process_count++;
}

/* Remove a process from the ready queue */
void scheduler_dequeue(struct pcb *pcb) {
    /* Find predecessor and link around pcb */
    struct pcb *prev = ready_queue;
    while (prev->next_ready != pcb) prev = prev->next_ready;
    prev->next_ready = pcb->next_ready;
    if (pcb == ready_queue) {
        ready_queue = (pcb == pcb->next_ready) ? NULL : pcb->next_ready;
    }
    pcb->next_ready = NULL;
    process_count--;
}

/* Pick the next process to run */
struct pcb *scheduler_pick_next(void) {
    if (!ready_queue) return NULL;
    ready_queue = ready_queue->next_ready;  /* advance to next */
    return ready_queue;
}

22.4 Timer Interrupt and Preemption

The timer interrupt (Chapter 12) is the heartbeat of the scheduler. Every time the system timer fires (e.g., every 1 ms), the interrupt handler calls the scheduler.

/* Timer interrupt handler (called from interrupt dispatcher) */
void timer_tick_handler(void) {
    struct pcb *current = get_current_process();

    /* Check if current process has exhausted its quantum */
    current->ticks_remaining--;
    if (current->ticks_remaining > 0 && !current->need_resched) {
        return;  /* Quantum not exhausted, let it keep running */
    }

    /* Reset quantum */
    current->ticks_remaining = TIME_SLICE_TICKS;
    current->need_resched = 0;

    /* Signal that a reschedule is needed on return from interrupt */
    need_reschedule = 1;
}

/* Return from interrupt: check if we should reschedule */
void irq_return(void) {
    if (need_reschedule) {
        need_reschedule = 0;
        struct pcb *next = scheduler_pick_next();
        if (next && next != get_current_process()) {
            switch_to_process(next);
        }
    }
    /* ERET to the process */
    asm volatile("eret");
}

22.5 Process States

The scheduler tracks four process states:

enum process_state {
    PROCESS_RUNNING,     /* Currently executing on CPU */
    PROCESS_READY,       /* Could run, waiting for CPU */
    PROCESS_BLOCKED,     /* Waiting for I/O or a lock */
    PROCESS_ZOMBIE       /* Exited, waiting for parent to collect exit code */
};

Only READY processes are in the scheduler's ready queue. BLOCKED processes are moved to a wait queue. When the blocking condition resolves (e.g., I/O completes, lock acquired), the process is moved back to READY and re-enqueued.

22.6 Context Switch Flow

The full context switch sequence (building on Chapter 13):

/* Complete context switch with scheduler involvement */
void switch_to_process(struct pcb *next) {
    struct pcb *prev = get_current_process();

    /* Update state */
    prev->state = PROCESS_READY;
    next->state = PROCESS_RUNNING;

    /* Switch page tables */
    switch_page_table(next->page_table);

    /* Switch to next process's stack and restore registers */
    switch_context(&prev->context, &next->context);
}

22.7 Our Implementation

Our kernel uses a multi-level feedback queue with 3 priority levels:

  • Priority 0 (highest): interactive processes (short time slice, 2 ms)
  • Priority 1: normal processes (default time slice, 10 ms)
  • Priority 2 (lowest): batch processes (long time slice, 50 ms)

Each priority level has its own round-robin queue. A process that uses its full time slice without blocking is demoted to a lower priority. A process that blocks frequently (I/O-bound) is promoted to a higher priority. This ensures interactive processes get responsive scheduling while CPU-bound processes don't starve interactive ones.

The scheduler is initialized in scheduler_init(), which sets up the timer interrupt at 1 ms interval and creates the idle process (a process that runs when nothing else is ready). The idle process halts the CPU with WFI to save power.

22.8 Exercises

Exercise 1: Scheduler Simulation

Simulate a round-robin scheduler with 3 processes of lengths 10, 20, 30 time units and a quantum of 5. Show the order of execution and completion times.

Exercise 2: Priority Boost

Implement priority aging: every 100 ms, increase the priority of all ready processes by one level (to prevent starvation).

22.9 Summary

The scheduler determines which process runs next and for how long. Round-robin scheduling is simple and fair, giving each process an equal time slice. The timer interrupt triggers preemptive scheduling, allowing the kernel to switch between processes transparently. Our kernel uses a multi-level feedback queue with priority promotion/demotion to balance responsiveness and throughput. The idle process runs when no other process is ready, using WFI to conserve power.