Process Scheduling â CPU Scheduling Algorithms Guide
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
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
Related Concepts
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
topon 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