Loading learning content...
The scheduler is the heart of any operating system—the component that decides which process runs when, for how long, and on which CPU. It directly impacts system responsiveness, throughput, fairness, and power consumption.
Building a scheduler from scratch forces you to confront fundamental questions: How do you balance interactive responsiveness with batch throughput? How do you prevent starvation? How do you handle priority inversion?
This project simulates process scheduling in user-space, implementing multiple algorithms and comparing their behavior under different workloads.
By the end of this page, you will understand scheduler architecture, implement FCFS, SJF, Round Robin, Priority, and MLFQ schedulers, measure performance metrics, and understand the tradeoffs each algorithm makes.
A scheduler simulation consists of several core components:
The simulation can run in discrete time (event-driven) or continuous time (tick-based). We'll use tick-based for simplicity.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
#ifndef SCHEDULER_TYPES_H#define SCHEDULER_TYPES_H #include <stdbool.h> typedef enum { PROCESS_NEW, PROCESS_READY, PROCESS_RUNNING, PROCESS_WAITING, PROCESS_TERMINATED} ProcessState; typedef struct { int pid; // Process ID char name[32]; // Process name int arrival_time; // When process arrives int burst_time; // Total CPU time needed int remaining_time; // CPU time remaining int priority; // Priority (lower = higher priority) ProcessState state; // Current state // Timing metrics int start_time; // First time process ran (-1 if never) int finish_time; // When process completed int waiting_time; // Total time in ready queue int turnaround_time; // finish - arrival int response_time; // start - arrival} Process; typedef struct { Process **processes; // Array of process pointers int size; // Current number of processes int capacity; // Maximum capacity} ProcessQueue; typedef struct { int total_processes; int current_time; double avg_turnaround; double avg_waiting; double avg_response; double cpu_utilization; double throughput;} SchedulerMetrics; #endif| Metric | Definition | Importance |
|---|---|---|
| Turnaround Time | Completion time − Arrival time | Overall efficiency |
| Waiting Time | Time spent in ready queue | Queue efficiency |
| Response Time | First run − Arrival time | Interactive responsiveness |
| Throughput | Processes completed per unit time | System productivity |
| CPU Utilization | % of time CPU is busy | Resource efficiency |
Different scheduling algorithms require different queue implementations:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
#include <stdlib.h>#include <string.h>#include "scheduler_types.h" /** * Create a new process queue with given capacity. */ProcessQueue *queue_create(int capacity) { ProcessQueue *q = malloc(sizeof(ProcessQueue)); q->processes = malloc(sizeof(Process *) * capacity); q->size = 0; q->capacity = capacity; return q;} /** * Add process to the queue (FIFO behavior). */void queue_enqueue(ProcessQueue *q, Process *p) { if (q->size >= q->capacity) { q->capacity *= 2; q->processes = realloc(q->processes, sizeof(Process *) * q->capacity); } q->processes[q->size++] = p;} /** * Remove and return the first process (FIFO). */Process *queue_dequeue(ProcessQueue *q) { if (q->size == 0) return NULL; Process *p = q->processes[0]; // Shift remaining elements memmove(q->processes, q->processes + 1, sizeof(Process *) * (q->size - 1)); q->size--; return p;} /** * Remove and return process with shortest remaining time. * Used for SJF and SRTF. */Process *queue_dequeue_shortest(ProcessQueue *q) { if (q->size == 0) return NULL; int min_idx = 0; for (int i = 1; i < q->size; i++) { if (q->processes[i]->remaining_time < q->processes[min_idx]->remaining_time) { min_idx = i; } } Process *p = q->processes[min_idx]; // Remove by shifting memmove(q->processes + min_idx, q->processes + min_idx + 1, sizeof(Process *) * (q->size - min_idx - 1)); q->size--; return p;} /** * Remove and return highest priority process. * Lower priority number = higher priority. */Process *queue_dequeue_priority(ProcessQueue *q) { if (q->size == 0) return NULL; int best_idx = 0; for (int i = 1; i < q->size; i++) { if (q->processes[i]->priority < q->processes[best_idx]->priority) { best_idx = i; } } Process *p = q->processes[best_idx]; memmove(q->processes + best_idx, q->processes + best_idx + 1, sizeof(Process *) * (q->size - best_idx - 1)); q->size--; return p;}Let's implement the core scheduling algorithms. Each has distinct characteristics and tradeoffs.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
#include <stdio.h>#include <limits.h>#include "scheduler_types.h"#include "ready_queue.h" /** * First-Come, First-Served (FCFS) * Non-preemptive, simple FIFO ordering. */void scheduler_fcfs(Process *processes[], int n) { ProcessQueue *ready = queue_create(n); int time = 0; int completed = 0; int next_arrival = 0; while (completed < n) { // Add newly arrived processes while (next_arrival < n && processes[next_arrival]->arrival_time <= time) { queue_enqueue(ready, processes[next_arrival]); processes[next_arrival]->state = PROCESS_READY; next_arrival++; } if (ready->size == 0) { time++; // CPU idle continue; } // Select first process in queue Process *current = queue_dequeue(ready); current->state = PROCESS_RUNNING; if (current->start_time == -1) { current->start_time = time; current->response_time = time - current->arrival_time; } // Run to completion (non-preemptive) time += current->remaining_time; current->remaining_time = 0; current->finish_time = time; current->state = PROCESS_TERMINATED; current->turnaround_time = time - current->arrival_time; current->waiting_time = current->turnaround_time - current->burst_time; completed++; } queue_destroy(ready);} /** * Round Robin with configurable time quantum. * Preemptive, fair time sharing. */void scheduler_round_robin(Process *processes[], int n, int time_quantum) { ProcessQueue *ready = queue_create(n); int time = 0; int completed = 0; int next_arrival = 0; while (completed < n) { // Add newly arrived processes while (next_arrival < n && processes[next_arrival]->arrival_time <= time) { queue_enqueue(ready, processes[next_arrival]); processes[next_arrival]->state = PROCESS_READY; next_arrival++; } if (ready->size == 0) { time++; continue; } Process *current = queue_dequeue(ready); current->state = PROCESS_RUNNING; if (current->start_time == -1) { current->start_time = time; current->response_time = time - current->arrival_time; } // Execute for quantum or until completion int exec_time = (current->remaining_time < time_quantum) ? current->remaining_time : time_quantum; time += exec_time; current->remaining_time -= exec_time; // Check for newly arrived during execution while (next_arrival < n && processes[next_arrival]->arrival_time <= time) { queue_enqueue(ready, processes[next_arrival]); processes[next_arrival]->state = PROCESS_READY; next_arrival++; } if (current->remaining_time == 0) { current->finish_time = time; current->state = PROCESS_TERMINATED; current->turnaround_time = time - current->arrival_time; current->waiting_time = current->turnaround_time - current->burst_time; completed++; } else { // Preempted - return to ready queue current->state = PROCESS_READY; queue_enqueue(ready, current); } } queue_destroy(ready);}| Algorithm | Preemptive | Best For | Weakness |
|---|---|---|---|
| FCFS | No | Simple batch systems | Convoy effect |
| SJF | No | Minimizing avg wait | Starvation of long jobs |
| SRTF | Yes | Optimal avg wait | High overhead, starvation |
| Round Robin | Yes | Interactive systems | High context switches |
| Priority | Both | Differentiated service | Priority inversion, starvation |
| MLFQ | Yes | General purpose | Complex tuning required |
MLFQ is the gold standard of general-purpose scheduling—it adapts to process behavior without prior knowledge. The key rules:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253
#define NUM_QUEUES 4#define BOOST_INTERVAL 100 // Time units between priority boosts typedef struct { ProcessQueue *queues[NUM_QUEUES]; int time_quanta[NUM_QUEUES]; // Different quantum per level int boost_timer;} MLFQ; MLFQ *mlfq_create(void) { MLFQ *mlfq = malloc(sizeof(MLFQ)); for (int i = 0; i < NUM_QUEUES; i++) { mlfq->queues[i] = queue_create(64); // Quantum increases with lower priority mlfq->time_quanta[i] = (1 << i) * 10; // 10, 20, 40, 80 } mlfq->boost_timer = 0; return mlfq;} void mlfq_boost(MLFQ *mlfq) { // Move all processes to highest priority queue for (int i = 1; i < NUM_QUEUES; i++) { while (mlfq->queues[i]->size > 0) { Process *p = queue_dequeue(mlfq->queues[i]); p->priority = 0; // Reset to highest queue_enqueue(mlfq->queues[0], p); } }} Process *mlfq_select(MLFQ *mlfq, int *quantum_out) { // Select from highest priority non-empty queue for (int i = 0; i < NUM_QUEUES; i++) { if (mlfq->queues[i]->size > 0) { *quantum_out = mlfq->time_quanta[i]; return queue_dequeue(mlfq->queues[i]); } } return NULL;} void mlfq_requeue(MLFQ *mlfq, Process *p, bool used_full_quantum) { int new_priority = p->priority; if (used_full_quantum && new_priority < NUM_QUEUES - 1) { new_priority++; // Demote } // If yielded early (I/O), keep same priority p->priority = new_priority; queue_enqueue(mlfq->queues[new_priority], p);}A clever process could game MLFQ by yielding just before its quantum expires, staying at high priority. Modern MLFQ implementations track total CPU usage at each level, demoting based on accumulated time, not just per-quantum behavior.
Extend your scheduler with: multi-core support (load balancing), real-time scheduling (EDF, RMS), I/O wait simulation, varying workload generators, and visualization of queue states over time.