Schedulingfcfsround robinpriority schedulingconvoy effectstarvation

Scheduling Simulator: FCFS, Round Robin, Priority

Run six processes on one to four cores under three textbook policies and watch the convoy effect, starvation and the quantum trade-off appear in the timeline — as an educational model, not a kernel.

Educational modelSimulated
▶ InteractiveInterview question
Progress

The problem

Three classic policies all "solve" the 100-tasks-8-cores problem. On the same six processes they produce wildly different waiting times, and two of them have a failure mode that can make a task wait forever. Which policy is bad at what, and why?

Progressive depth

The same mechanism at different altitudes — start where you are.

Three ways to pick

Run whoever came first; run everyone for a short slice in turn; run the most important one. Each has one signature failure: convoy, overhead, starvation.

What the simulator models — and what it does not

Educational model

The simulator has 1–4 CPUs and six processes P1–P6. Each process is a script: run for N ms, then sleep or wait on I/O for M ms, then run again, until its total work is done. Every process is in exactly one state — running (on a CPU), ready (runnable, waiting for a CPU), sleeping (timer wait) or waiting (blocked on I/O) — and the timeline shows which CPU ran which process in each millisecond tick. The numbers are ticks of a model, not measurements; a real kernel’s tick, its wakeup preemption rules and its cache effects are all absent by design.

The three policies are the textbook ones, chosen because each isolates one idea. FCFS (first-come, first-served): run the head of the queue until it blocks or finishes. Round Robin: run the head of the queue for at most one quantum, then move it to the tail. Priority: always run the runnable process with the highest priority, preempting a lower-priority one when a higher one becomes ready. No production general-purpose kernel uses any of these unmodified; they are the vocabulary in which real schedulers are described (see the Linux section of The Scheduling Problem).

Three metrics are computed per process and averaged: waiting time (ms spent in ready), turnaround time (arrival to completion) and response time (arrival to first run). Interactive systems care about response time; batch systems care about turnaround; both hate high variance more than a high mean.

FCFS and the convoy effect

Educational model

Put P1 (a 40 ms CPU burst) at the head of the queue on a single CPU, followed by P2–P6 that each want 1 ms of CPU before doing 10 ms of I/O. Under FCFS the five short processes wait 40 ms for P1, run for 1 ms each, go to I/O, and by the time they come back P1 — which also went to I/O and came back — is at the head again. The I/O device sits idle while everybody waits for the CPU, then the CPU sits idle while everybody waits for the device. This is the convoy effect: one long burst turns a set of independent I/O-bound processes into a convoy trailing behind it.

The response time numbers make it concrete: with the simulator on one CPU, average response time under FCFS is dominated by the longest burst in front of you, regardless of how little you need. Adding CPUs helps only until there are more long bursts than CPUs. The policy is not unfair in the accounting sense — everyone eventually runs — but it is blind to how much each process needs, and that blindness is what preemption fixes.

FCFS survives in real systems in one place: disk and network request queues, where reordering has its own costs. For CPU scheduling it is the baseline every other policy is measured against.

Round Robin and the quantum trade-off

Educational model

Round Robin fixes the convoy by preemption: P1 gets a quantum, then goes to the tail, and P2–P6 each get theirs. Response time drops from "the longest burst ahead of you" to "at most (n − 1) × quantum". Set the quantum to 4 ms in the simulator and the short processes respond within 20 ms instead of 40; set it to 1 ms and they respond within 5.

Now count the context switches. At a 1 ms quantum P1’s 40 ms burst is cut into 40 slices, each preceded by a switch; the simulator charges a configurable switch cost (default 0.1 ms, a model value — real switches are 1–5 µs direct plus cache effects, see Context Switching), so P1’s effective run time grows and everyone’s turnaround with it. At a 100 ms quantum Round Robin degenerates into FCFS. Somewhere in between is a quantum that keeps switch overhead under a few percent while bounding response time — which is why real kernels derive the slice from a latency target and the number of runnable tasks rather than fixing it.

Round Robin is fair in the strict sense: every runnable process gets 1/n of the CPU in every round. It is also completely priority-blind, which is a problem the moment one process matters more than another.

  • Response time ≤ (n − 1) × quantum on one CPU with n runnable processes.
  • Switch overhead fraction ≈ switch cost ÷ quantum; keep it under a few percent.
  • Quantum → ∞ gives FCFS; quantum → switch cost gives 50% overhead.

Priority scheduling and starvation

Educational model

Give P1 priority 1 (highest) and a script that runs 5 ms, sleeps 1 ms, repeats forever, and give P6 priority 6 with a 20 ms burst. Under strict priority scheduling P6 never runs: every time it is about to, P1 wakes up and preempts it. This is starvation, and it is not a corner case — any strict-priority scheduler with a busy high-priority task starves the rest. The simulator shows P6’s waiting time climbing without bound while the CPU reads 100% busy.

The standard fix is aging: raise the effective priority of a process the longer it waits, so that eventually even P6 outranks a freshly woken P1 for one slice. The simulator’s aging toggle adds one priority level per 10 ms of waiting; watch P6 finally get its slice, then drop back. Windows implements exactly this idea as a periodic pass that boosts threads that have not run for several seconds; Linux’s fair-share accounting makes explicit aging unnecessary because a task that has not run has a large lag and is always eligible.

A subtler failure is priority inversion: a low-priority process holds a lock the high-priority one needs, and a medium-priority process preempts the low one — so the high-priority task waits on the medium one. This is what hung the Mars Pathfinder lander in 1997; the fix (priority inheritance: the lock holder temporarily takes the waiter’s priority) is implemented in pthread_mutexattr_setprotocol and Linux PI-futex. See Mutexes and Deadlocks.

The three policies side by side

Educational model

The comparison below is the point of the simulator. No single policy wins every column, which is why real schedulers combine ideas: preemption from Round Robin, weight from priority, and an accounting rule (fair share, aging, or boosts) that prevents starvation.

Textbook policies on the simulator’s six processes (educational model)
PropertyFCFSRound RobinPriority (strict)
PreemptiveNoYes (timer)Yes (on higher-priority wakeup)
Response timeBounded by longest burst ahead≤ (n − 1) × quantumExcellent for high priority, unbounded for low
Convoy effectYesNoOnly among equal priorities
StarvationNoNoYes, without aging
Switch overheadMinimalRises as quantum shrinksDepends on wakeup pattern
Knows task importanceNoNoYes
Where it survivesDevice request queuesTime-sharing among equal priority (SCHED_RR)Real-time classes, with care

Key points

  • The simulator is an educational model: ticks, not measurements; three textbook policies, not a kernel.
  • FCFS: no preemption, so one long CPU burst creates a convoy of I/O-bound processes waiting behind it.
  • Round Robin: preemption bounds response time to (n − 1) × quantum; a shorter quantum lowers latency and raises switch overhead.
  • Strict priority: perfect for the top, starvation for the bottom; aging or fair-share accounting is the fix.
  • Priority inversion is a real-world consequence of priority scheduling plus locks; priority inheritance is the fix.
  • Real schedulers combine preemption, weights and an anti-starvation rule; no textbook policy is used unmodified.

Why does this exist?

Mechanisms are answers to constraints. Open each question before reading the answer.

Why preempt at all if every process eventually finishes?

Because "eventually" is measured in the longest burst ahead of you. Interactive work needs a bound on response time that does not depend on what other processes are doing, and only preemption provides one.

Why not a tiny quantum for perfect responsiveness?

Every switch costs direct time and, worse, evicts the previous task’s cache and TLB state. Below a few hundred microseconds the overhead dominates and throughput collapses for everyone, including the interactive task.

Why does priority need aging?

A strict order between processes plus a busy high-priority process is a proof that the low one never runs. Aging converts "never" into "later" by making waiting itself raise priority.

Scheduler simulator

Scheduler simulator
Six processes, one to four cores, three policies. Each tick is one scheduler decision; the Gantt strip is what each core actually ran.
Cores
Algorithm
Educational model
core 0
P1running
CPU-bound · prio 3 · bursts cpu11 io2 cpu7
waited 0 · 11 left
P2new
I/O-bound · prio 2 · bursts cpu1 io3 cpu1 io3 cpu2
waited 0 · not arrived
P3new
interactive · prio 1 · bursts cpu2 io2 cpu3 io2 cpu2
waited 0 · not arrived
P4new
mixed · prio 4 · bursts cpu4 io1 cpu3
waited 0 · not arrived
P5new
I/O-bound · prio 2 · bursts cpu1 io2 cpu1 io2 cpu1 io2 cpu2
waited 0 · not arrived
P6new
batch · prio 5 · bursts cpu4 io1 cpu5
waited 0 · not arrived
Ready queue (head → tail)
— empty —
Blocked on I/O
Avg wait (all processes)
0.0 ticks
Avg turnaround (finished)
— (0 finished)
Context switches
0
CPU utilisation0%

A model, not a real OS scheduler: Linux CFS/EEVDF use virtual runtime and weights, not fixed queues; ticks here are abstract, real slices are ~1–10 ms.

1/49 · tick 0

How it fails

What the failure looks like from inside real software.

  • A nightly batch job scheduled at normal priority on a request-serving host: requests queue behind its slices (convoy in practice) — run it under nice 19 or a separate cgroup.
  • A thread pool sized at 10× the core count for CPU-bound work: the ready queue is always long and the quantum trade-off is paid on every request.
  • A "watchdog" thread at real-time priority that polls in a tight loop: starves the workers it is supposed to watch.
  • Priority inversion: a high-priority audio thread stalls because a low-priority thread holding the shared lock was preempted by a medium-priority one; the symptom is intermittent glitches with no CPU saturation.