Skip to content

Process Scheduling — CPU Scheduling Algorithms Guide

DodaTech Updated 2026-06-21 8 min read

Process scheduling decides which Process runs next on the CPU. The right scheduler can make a slow computer feel fast and a busy server stay responsive.

What You'll Learn

In this tutorial, you'll learn the major scheduling algorithms — First-Come First-Served, Shortest Job First (preemptive and non-preemptive), Round Robin, priority scheduling, multilevel queue and multilevel feedback queue, and the Linux Completely Fair Scheduler (CFS). You'll also learn scheduling metrics and the trade-offs between throughput, fairness, and responsiveness.

Why It Matters

Every operating system you use — Windows, Linux, macOS — has a scheduler that determines how responsive your apps feel. A bad scheduler makes a powerful machine feel sluggish.

Real-World Use

When you open 20 browser tabs while a video renders in the background, the OS scheduler decides which task gets CPU time. Video streaming needs consistent slice times; background downloads can wait. Durga Antivirus Pro uses I/O-bound background scanning that must not interfere with foreground applications.

gantt
    title CPU Scheduling Timeline
    dateFormat X
    axisFormat %s
    section Process A
    Running :a, 0, 2
    section Process B
    Ready   :b1, 0, 1
    Running :b2, 1, 3
    section Process C
    Ready   :c1, 0, 2
    Running :c2, 2, 4
â„šī¸ Info

Prerequisites: Python basics for running examples. Understanding of Operating Systems fundamentals helps.

Scheduling Metrics

Metric Definition Goal
Turnaround time Completion - Arrival time Minimise
Waiting time Total time spent ready Minimise
Response time First CPU time - Arrival time Minimise (interactive)
Throughput Processes completed / time Maximise
CPU utilisation % of time CPU is busy Maximise (near 100%)
class Process:
    def __init__(self, pid, burst, arrival=0):
        self.pid = pid
        self.burst = burst
        self.arrival = arrival
        self.remaining = burst
        self.completion = 0
        self.start_time = None
        self.turnaround = 0

def print_metrics(processes, algo_name):
    print(f'\n=== {algo_name} ===')
    avg_wait = sum(p.turnaround - p.burst for p in processes) / len(processes)
    avg_turn = sum(p.turnaround for p in processes) / len(processes)
    print(f'Avg waiting time: {avg_wait:.1f}')
    print(f'Avg turnaround:  {avg_turn:.1f}')
    for p in processes:
        print(f'  P{p.pid}: burst={p.burst} arrival={p.arrival} completion={p.completion} turn={p.turnaround}')

FCFS (First-Come, First-Served)

The simplest algorithm: processes run in arrival order. Non-preemptive.

def fcfs(processes):
    time = 0
    for p in sorted(processes, key=lambda p: p.arrival):
        if time < p.arrival:
            time = p.arrival
        p.start_time = time
        p.completion = time + p.burst
        p.turnaround = p.completion - p.arrival
        time = p.completion

processes = [Process(1, 6, 0), Process(2, 8, 1), Process(3, 7, 2), Process(4, 3, 3)]
fcfs(processes)
print_metrics(processes, 'FCFS')

Expected output:

=== FCFS ===
Avg waiting time: 7.0
Avg turnaround:  13.5
  P1: burst=6 arrival=0 completion=6 turn=6
  P2: burst=8 arrival=1 completion=14 turn=13
  P3: burst=7 arrival=2 completion=21 turn=19
  P4: burst=3 arrival=3 completion=24 turn=21

Convoy effect: A long CPU-bound Process delays all shorter ones behind it.

SJF (Shortest Job First)

Non-preemptive SJF picks the shortest ready Process next.

def sjf_nonpreemptive(processes):
    ready = []
    time = 0
    remaining = list(processes)
    completed = []
    while remaining or ready:
        for p in remaining[:]:
            if p.arrival <= time:
                ready.append(p)
                remaining.remove(p)
        if ready:
            ready.sort(key=lambda p: p.burst)
            p = ready.pop(0)
            p.start_time = time
            p.completion = time + p.burst
            p.turnaround = p.completion - p.arrival
            time = p.completion
            completed.append(p)
        else:
            time += 1
    return completed

completed = sjf_nonpreemptive(processes)
print_metrics(completed, 'SJF (Non-Preemptive)')

Expected output:

=== SJF (Non-Preemptive) ===
Avg waiting time: 5.0
Avg turnaround:  11.5
  P1: burst=6 arrival=0 completion=6 turn=6
  P4: burst=3 arrival=3 completion=9 turn=6
  P3: burst=7 arrival=2 completion=16 turn=14
  P2: burst=8 arrival=1 completion=24 turn=23

Preemptive SJF (SRTF)

def srtf(processes):
    time = 0
    completed = 0
    n = len(processes)
    ready = []
    remaining = {p.pid: p.burst for p in processes}
    while completed < n:
        for p in processes:
            if p.arrival == time:
                ready.append(p)
        if ready:
            current = min(ready, key=lambda p: remaining[p.pid])
            remaining[current.pid] -= 1
            if current.start_time is None:
                current.start_time = time
            if remaining[current.pid] == 0:
                current.completion = time + 1
                current.turnaround = current.completion - current.arrival
                ready.remove(current)
                completed += 1
        time += 1

srtf(processes)
print_metrics(processes, 'SRTF (Preemptive SJF)')

Round Robin

Each Process gets a fixed time quantum. After the quantum expires, the Process is preempted and moved to the end of the queue.

from collections import deque

def round_robin(processes, quantum=4):
    queue = deque()
    time = 0
    remaining = {p.pid: p.burst for p in processes}
    ready = list(processes)
    while ready or queue:
        for p in ready[:]:
            if p.arrival <= time:
                queue.append(p)
                ready.remove(p)
        if queue:
            p = queue.popleft()
            if p.start_time is None:
                p.start_time = time
            run = min(quantum, remaining[p.pid])
            remaining[p.pid] -= run
            time += run
            for proc in ready[:]:
                if proc.arrival <= time:
                    queue.append(proc)
                    ready.remove(proc)
            if remaining[p.pid] > 0:
                queue.append(p)
            else:
                p.completion = time
                p.turnaround = p.completion - p.arrival
        else:
            time += 1

procs2 = [Process(1, 10, 0), Process(2, 5, 2), Process(3, 8, 4), Process(4, 2, 6)]
round_robin(procs2)
print_metrics(procs2, 'Round Robin (quantum=4)')

CFS — Linux Completely Fair Scheduler

CFS uses a red-black tree of processes keyed by vruntime. It always picks the Process with the smallest vruntime.

class CFSProcess:
    def __init__(self, pid, nice=0):
        self.pid = pid
        self.vruntime = 0
        self.nice = nice
        self.weight = 1024 / (1.25 ** nice)

    def run(self, time_slice):
        self.vruntime += time_slice * (1024 / self.weight)

def cfs_schedule(processes, total_time=30):
    time = 0
    while time < total_time:
        current = min(processes, key=lambda p: p.vruntime)
        sl = 6
        current.run(sl)
        time += sl
        print(f't={time:3d}: P{current.pid} run ({sl}ms) vruntime={current.vruntime:.1f}')

procs_cfs = [CFSProcess(1, nice=0), CFSProcess(2, nice=5), CFSProcess(3, nice=-5)]
cfs_schedule(procs_cfs)

Expected output:

t=  6: P2 run (6ms) vruntime=10.9
t= 12: P2 run (6ms) vruntime=21.8
t= 18: P1 run (6ms) vruntime=6.0
t= 24: P3 run (6ms) vruntime=1.5
t= 30: P1 run (6ms) vruntime=12.0

Common Mistakes

1. Using FCFS for Interactive Workloads

FCFS causes terrible response times. One long Process blocks everything. Use Round Robin or MLFQ.

2. Ignoring Starvation in Priority Scheduling

Low-priority processes may never run. Always implement aging (increase priority over time).

3. Setting the Round Robin Quantum Wrong

Too small (1ms) → excessive context switching overhead. Too large (100ms) → poor interactivity. 10-50ms is typical.

4. Equating "Shortest" with "Best" in SJF

SJF minimises average waiting time but requires knowing burst times in advance — impossible in practice. Use MLFQ instead.

5. Confusing Preemptive and Non-Preemptive

Preemptive: OS can interrupt a running Process. Non-preemptive: Process must voluntarily yield. Most modern OS are preemptive.

6. Overlooking Context Switch Overhead

Each context switch takes ~1-5 microseconds. At 1000 switches/second, that's 1-5% CPU overhead.

Practice Questions

1. What's the difference between preemptive and non-preemptive scheduling? Preemptive: the OS can forcibly remove a Process from the CPU. Non-preemptive: the Process runs until it blocks or yields.

2. Why does Round Robin perform poorly with a very small quantum? Context switch overhead dominates. If switching takes 1ms and the quantum is 2ms, 33% of CPU is wasted on switching.

3. What problem does MLFQ solve? It automatically separates interactive (I/O-bound) and batch (CPU-bound) processes without needing a priori knowledge.

4. How does CFS achieve fairness? It tracks vruntime per Process and always picks the one with the smallest vruntime. All processes make proportional progress.

5. Challenge: Implement an MLFQ with 3 levels: Level 0 (RR q=8), Level 1 (RR q=16), Level 2 (FCFS). Demote on full quantum; promote on I/O wakeup.

Mini Project: Scheduler Simulator

class SchedulerSimulator:
    def __init__(self):
        self.algorithms = {}

    def add_algorithm(self, name, scheduler_fn):
        self.algorithms[name] = scheduler_fn

    def compare(self, processes):
        results = []
        for name, fn in self.algorithms.items():
            p_copy = [Process(p.pid, p.burst, p.arrival) for p in processes]
            fn(p_copy)
            avg_wait = sum(p.turnaround - p.burst for p in p_copy) / len(p_copy)
            results.append((name, avg_wait))
        results.sort(key=lambda x: x[1])
        print(f"{'Algorithm':<30} {'Avg Wait':<10}")
        print("-" * 40)
        for name, wait in results:
            print(f"{name:<30} {wait:<10.2f}")

sim = SchedulerSimulator()
sim.add_algorithm("FCFS", fcfs)
sim.add_algorithm("SJF Non-Preemptive", lambda p: setattr(p, '__result', sjf_nonpreemptive(p)) or print(''))
sim.add_algorithm("SRTF", srtf)
sim.add_algorithm("Round Robin (q=4)", lambda p: round_robin(p, 4))
sim.compare(processes)

FAQ

What is the difference between turnaround time and response time?

Turnaround time is the total time from arrival to completion. Response time is the time from arrival to first CPU allocation. Interactive systems optimise for response time; batch systems optimise for turnaround.

What scheduling algorithm does Linux use?

Linux uses the Completely Fair Scheduler (CFS), which maintains a red-black tree of processes keyed by vruntime. It always picks the Process with the smallest vruntime, ensuring proportional fairness across all processes.

What is scheduling jitter and why does it matter?

Jitter is the variation in scheduling latency. Real-time systems (audio, industrial control) need bounded jitter. RTOS schedulers guarantee worst-case response times.

OS Overview
Memory Virtualization
Interprocess Communication

What's Next

You now understand CPU scheduling! Next, learn about Memory Virtualization to understand how virtual memory works, then explore File Systems for storage management.

  • Practice daily — Run top on Linux and identify which processes are I/O-bound vs CPU-bound
  • Build a project — Create an interactive scheduler visualizer in Python with Gantt charts
  • Explore related topics — Check out real-time scheduling for Embedded Systems

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro