CPU Scheduling Fundamentals

Imagine a single CPU core, ten runnable processes, and no rule for whose turn it is next. Chaos ensues. Some processes hog the core for minutes while others starve. Interactive programs feel frozen even though the machine is technically "working." CPU scheduling is the operating system policy that decides which ready process runs next. It is the difference between a system that feels instant and one that feels broken. Every multiprogrammed operating system must solve this problem continuously. This applies to a phone juggling background apps or a data-center server juggling thousands of requests. Such systems must solve this problem thousands of times per second. The core insight that makes scheduling possible at all is the CPU–I/O burst cycle: real programs alternate between bursts of computation and bursts of waiting on I/O. This alternation is what creates the opportunity for multiprogramming. While one process waits on a disk read, the CPU need not sit idle. It can run another process instead. Scheduling is the art of exploiting this opportunity well. It means keeping the CPU busy, keeping response times low, and keeping the system fair. All this must happen while paying almost nothing in overhead for the decision itself. This article builds the vocabulary and machinery that every scheduling algorithm sits on top of. It covers the scheduler (the policy-maker), the dispatcher (the mechanism that carries out the decision), and the fundamental fork in the road between preemptive and non-preemptive scheduling. It assumes you already understand Context Switching and the Process State and the Lifecycle. Here we connect those mechanisms to the decisions that trigger them. By the end, you will understand exactly when the operating system is allowed to make a scheduling decision. You will know what it costs to switch from one process to another. You will also learn how to reason about a dispatcher's overhead as a real cost subtracted from useful CPU work. This is the foundation on which later scheduling algorithms are built and evaluated. These include First-Come First-Served, Shortest Job First, Round Robin, and priority-based schemes.

Key takeaways
  • CPU scheduling decides which ready process runs next; the dispatcher is the mechanism that carries out that decision.
  • Programs alternate CPU bursts and I/O bursts; this CPU–I/O burst cycle is what makes multiprogramming and scheduling meaningful.
  • Scheduling decisions happen only at four specific transition points in a process's lifecycle.
  • Non-preemptive scheduling lets a running process keep the CPU until it blocks or exits; preemptive scheduling can forcibly take the CPU away.
  • Dispatch latency — the time to switch contexts and jump to the new process — is pure overhead and must be minimized.
  • These concepts are prerequisite machinery for evaluating any concrete algorithm (FCFS, SJF, Round Robin, priority scheduling) by real performance metrics.
The core insight

CPU scheduling is the policy of choosing the next ready process, while the dispatcher is the mechanism that pays a small, unavoidable time cost to make that choice real.

The idea

CPU scheduling is the operating system's method for selecting, from among all processes in the ready queue, which one will be given the CPU next. It is a policy question — a decision about priorities and goals — layered on top of the mechanism question already answered by context switching: how do you actually save one process's state and load another's? The scheduler answers "who's next?" The dispatcher is the module that answers "how do we get them running?" — it performs the context switch, restores the memory-management state, and transfers control to the chosen process's next instruction, typically by executing a jump to the location stored in its saved program counter. The reason scheduling exists at all is the CPU burst / I/O burst cycle. Process execution consists of alternating segments: a burst of pure CPU computation, followed by a burst of I/O wait (disk, network, keyboard), followed by another CPU burst, and so on, until the process terminates. Empirically, CPU burst lengths follow a distribution with many short bursts and a few long ones (often modeled as roughly exponential in shape). This matters enormously: it means most processes are usually not using the CPU (they are blocked on I/O), so a scheduler that lets a CPU-bound process monopolize the core wastes the opportunity to run many short, I/O-bound processes that are ready and waiting. A scheduling decision must be made at exactly four points in a process's lifecycle, corresponding to specific state transitions: 1. Running → Waiting: the process issues an I/O request or waits on an event (e.g., a semaphore). 2. Running → Ready: an interrupt (typically a timer) forces the process to give up the CPU. 3. Waiting → Ready: an I/O completion or event makes a previously blocked process runnable again, and it may (in preemptive schemes) contend for the CPU immediately. 4. Terminated: the process finishes and releases the CPU voluntarily. Transitions 1 and 4 are unavoidable — the running process is leaving the CPU no matter what the scheduler decides. Scheduling here is non-preemptive (or cooperative): the OS only chooses a replacement, it never forces the current process out. Transitions 2 and 3 are where the scheduler has real teeth: it can forcibly interrupt a running process to give the CPU to someone else. Scheduling that uses transitions 2 and 3 is called preemptive. Almost all modern general-purpose operating systems (Linux, Windows, macOS, Android, iOS) use preemptive scheduling, because it is the only way to bound how long any single process can hold the CPU and guarantee reasonable response times for interactive and real-time work.

How it works

The scheduling subsystem is really two cooperating pieces with very different jobs and very different frequencies of execution.

The scheduler (short-term / CPU scheduler) runs whenever a scheduling decision is triggered — at the four transition points above. Its job is purely to select a process from the ready queue according to some policy (FCFS order, priority, remaining time, etc. — the specific algorithms build on this article). It does not touch hardware registers or memory maps; it just returns "run process P next."

The dispatcher takes that decision and makes it physically true. It must: 1. Save the context (registers, program counter, stack pointer) of the process being removed from the CPU, if it is not already saved by an earlier trap/interrupt handler. 2. Switch the memory-management context if needed (page tables, TLB flush or ASID reload). 3. Switch to kernel/user mode as appropriate. 4. Restore the context of the newly chosen process. 5. Jump to the resumed location (its saved program counter), returning control to user mode. The time this entire sequence takes is called dispatch latency. It is pure overhead: no user-visible work happens during a dispatch, and this time is deducted from the CPU's useful throughput. A well-engineered dispatcher tries to make this cost as small and as predictable as possible, because in real-time systems the worst-case dispatch latency is itself a correctness constraint. ``` // Simplified scheduling + dispatch cycle, executed on every // scheduling-relevant event (timer interrupt, I/O completion, // blocking call, process exit). function on_scheduling_event(event): current = running_process() switch event.type: case TIMER_INTERRUPT: if scheduling_policy.is_preemptive(): ready_queue.enqueue(current) current.state = READY else: return // ignore tick, keep running case IO_REQUEST or BLOCKING_WAIT: current.state = WAITING wait_queue[event.device].enqueue(current) case IO_COMPLETION(proc): proc.state = READY ready_queue.enqueue(proc) if scheduling_policy.is_preemptive() and priority(proc) > priority(current): ready_queue.enqueue(current) // preempt now current.state = READY else: return // let current keep running case PROCESS_EXIT: current.state = TERMINATED free_resources(current) // A new decision is needed whenever the CPU is free // (exit) or the current process was just made non-running. if current.state != RUNNING: next = scheduler.select(ready_queue) // policy plugged in here dispatch(current, next)

function dispatch(prev, next): t_start = clock() save_context(prev) // registers, PC, SP -> PCB if prev.address_space != next.address_space: switch_memory_map(next) // page table base, flush/reload TLB restore_context(next) // PCB -> registers, PC, SP next.state = RUNNING t_end = clock() dispatch_latency = t_end - t_start // pure overhead, minimize this resume_execution(next) // jump to next.PC ``` The pseudocode makes the preemptive/non-preemptive fork explicit: on a `TIMER_INTERRUPT`, a non-preemptive policy simply returns and lets the current process keep running (the interrupt only updates accounting, e.g. incrementing a burst counter); a preemptive policy forces the process back into the ready queue. Similarly, on `IO_COMPLETION`, a preemptive policy can immediately steal the CPU back from a lower-priority running process, while a non-preemptive policy always lets the current process finish its burst uninterrupted.

Figure
The four scheduling decision points and the CPU–I/O burst cycle
Ready Running Waiting Terminated ① scheduler dispatch ② I/O or event wait (non-preemptive point) ③ I/O completion (may preempt) ④ timer preemption process exits (non-preemptive point) CPU–I/O burst cycle (one process over time): CPU burst I/O burst CPU I/O burst long CPU burst I/O exit Most bursts are short (I/O-bound work); scheduling exploits this mix to keep the CPU busy.

Complexity & correctness

Time complexity of the scheduling decision itself. The scheduler's job — selecting a process from the ready queue — has a cost that depends entirely on the queue's data structure and the policy, not on some inherent property of "scheduling." A naive linear scan of an unsorted ready list to find, say, the highest-priority process costs O(n)O(n) where nn is the number of ready processes. A ready queue implemented as a balanced binary heap keyed by priority or virtual runtime (as in Linux's Completely Fair Scheduler, which uses a red-black tree) reduces selection and re-insertion to O(log⁡n)O(\log n). A plain FIFO queue (used for round-robin or FCFS) gives O(1)O(1) enqueue/dequeue. This article deliberately does not commit to one policy — the complexity is a property of the algorithm layered on top, covered in later scheduling articles — but the queue mechanics discussed here bound what is achievable.

Time cost of the dispatcher. Dispatch latency has a fixed hardware/software floor: saving and restoring registers is O(1)O(1) (a constant number of registers), but switching address spaces can cost more — a full TLB flush followed by cold-cache refills can cost hundreds to thousands of cycles depending on architecture, and this cost is not asymptotic in nn, it's a constant-but-nontrivial overhead per switch. This is why operating systems care about context-switch frequency, not just the existence of switches: preemptive scheduling with a very short timer quantum increases fairness and responsiveness but multiplies this fixed per-switch cost, directly reducing CPU throughput available for actual process work. If a quantum of length qq requires dispatch overhead dd, the fraction of CPU time wasted on switching is at least

dq+d,\frac{d}{q + d},

which shows the fundamental tradeoff honestly: shrinking qq to improve responsiveness necessarily grows this wasted fraction, since dd (dominated by hardware costs) cannot shrink at will. Real systems choose q≫dq \gg d by one to two orders of magnitude (e.g., dd on the order of microseconds, qq on the order of milliseconds) to keep this overhead under roughly 1%.

Correctness argument. The state machine's correctness rests on one invariant: at every instant, at most one process per CPU core is in the RUNNING state, and every process is in exactly one of {Ready, Running, Waiting, Terminated} (plus New). The scheduling event handler above preserves this invariant because every transition explicitly moves a process out of RUNNING before another can enter it — `dispatch(prev, next)` only ever runs after `prev`'s state has already been updated away from RUNNING in the calling code, and `next.state = RUNNING` is set exactly once per dispatch call. The four-decision-point enumeration is exhaustive precisely because these correspond to the only edges leaving the RUNNING state in the process lifecycle graph (to Waiting, to Ready via preemption, to Terminated) plus the one edge that can trigger a need for a new decision (Waiting → Ready, when the newly-ready process might outrank the currently running one). No other event can legally change who occupies the CPU, so checking these four points is both necessary and sufficient for a correct scheduler implementation.

Tradeoff honesty. Non-preemptive scheduling has zero risk of race conditions on shared kernel data structures modified by the running process itself (nobody can interrupt you mid-update), which is why very old and very simple embedded schedulers use it — but it gives no guarantee on response time: one runaway CPU-bound process can freeze the whole system. Preemptive scheduling fixes responsiveness but reintroduces exactly the concurrency hazards that context switching and synchronization primitives (locks, disabling interrupts during critical kernel sections) must then guard against. There is no free lunch: preemption buys fairness and responsiveness at the cost of more frequent dispatch overhead and more delicate kernel synchronization.

Worked example

Consider three processes arriving with the CPU bursts and I/O bursts below, on a system where dispatch latency is d=0.1d = 0.1 ms. We trace what happens under a non-preemptive policy versus a preemptive one, using simple "run until you'd naturally yield vs. force a switch on timer tick" behavior to make the preemption/non-preemption distinction concrete (this is not yet a full algorithm like SJF or Round Robin — those are covered separately — this is purely to see the four decision points fire).

ProcessArrivalCPU burst 1I/O burstCPU burst 2
P10 ms5 ms4 ms2 ms
P21 ms3 ms——
P32 ms4 ms——

Non-preemptive trace (each process, once dispatched, runs its full burst uninterrupted until it blocks on I/O or terminates):

Time (ms)EventDecision point firedAction
0P1 arrives, ready queue empty → dispatchedRunning→Waiting will end this burstP1 runs
5P1's CPU burst 1 ends, issues I/O② Running→WaitingP1 → wait queue; scheduler picks from {P2, P3} (both ready) → dispatch P2
5.1dispatch overhead paid—P2 begins running at 5.1
8.1P2 finishes (3 ms burst, no I/O)④ terminateP2 exits; scheduler picks P3
8.2dispatch overhead paid—P3 begins running
9P1's I/O completes③ Waiting→ReadyP1 becomes ready but cannot preempt P3 (non-preemptive)
12.2P3 finishes (4 ms burst)④ terminatescheduler picks P1 (only ready process)
12.3dispatch overhead paid—P1 runs burst 2
14.3P1 finishes (2 ms burst)④ terminatedone

Total elapsed time: 14.3 ms, with two dispatch overheads paid (0.2 ms total) — because only two involuntary switches after the first ready arrival were needed once each process is running until it naturally yields the CPU. Note P1 waited from 9 ms to 12.2 ms (3.2 ms) even though it was ready, purely because non-preemptive scheduling refused to interrupt P3.

Preemptive trace (same workload, but here we allow the scheduler to preempt at I/O-completion time if it chooses — assume a policy that always favors a newly-ready process with a shorter remaining burst, which is legal to apply because the system is preemptive, foreshadowing later algorithms):

Time (ms)EventDecision pointAction
0P1 dispatched—P1 runs
5P1 blocks on I/O②dispatch P2 (only ready process)
5.1P2 runs——
8.1P2 finishes④dispatch P3
8.2P3 runs——
9P1's I/O completes, P1 ready③P1's remaining burst (2 ms) < P3's remaining burst (3.2 ms left) → preempt P3
9.1dispatch overhead paid—P1 runs burst 2
11.1P1 finishes④dispatch P3 (resumed)
11.2P3 resumes——
14.4P3 finishes (3.2 ms remaining + overhead)④done

Total elapsed: 14.4 ms with three dispatch overheads paid (0.3 ms total) — preemption cost more total dispatch overhead here, but P1 finished at 11.1 ms instead of 14.3 ms, a large win in turnaround time for that process, at the cost of P3 being delayed. This is exactly the tradeoff the complexity section predicted: preemption trades some throughput (extra switches) for better responsiveness on select processes — the concrete algorithms that decide whom to favor (Shortest Job First, Round Robin, priority scheduling) are the natural next topic once this cost/benefit shape is understood.

Common pitfalls & traps

PitfallWhy it happensConsequence
Confusing the scheduler with the dispatcherBoth feel like "the scheduling code"Blaming policy bugs on mechanism, or vice versa; miscounting overhead
Assuming all four decision points allow preemptionForgetting that Running→Waiting and Terminate are forced, not chosenBelieving a non-preemptive system can still "interrupt" a blocking process
Ignoring dispatch latency in comparisonsTreating context switches as free in back-of-envelope mathWildly overestimating the benefit of a very short time quantum
Treating burst lengths as fixed/known in advanceTextbook examples give exact burst timesForgetting real schedulers must estimate future bursts (e.g., exponential averaging)
Ignoring races introduced by preemptionAssuming shared kernel structures are always safeCorruption of ready queues or PCBs without proper locking/disabled interrupts during critical sections
Off-by-one in "who preempts whom" at a tieTwo processes with equal priority/remaining time arrive simultaneouslyNondeterministic behavior unless a tie-breaking rule (e.g., FCFS order) is defined

Pioneers & history

Origins · 1 minEdsger W. Dijkstra and how this was discovered
EW
Edsger W. Dijkstra
1930–2002 · Eindhoven University of Technology / Burroughs Corporation

Dijkstra's work on cooperating sequential processes and synchronization primitives in the 1960s laid the theoretical groundwork for reasoning correctly about preemptive multiprogramming — without his semaphore concept, safely preempting processes that share kernel data structures would have remained an unsolved hazard.

History
1954
— Batch processing systems (e.g., GM-NAA I/O) run one job at a time, no scheduling decision needed within a job.
1961
— Compatible Time-Sharing System (CTSS) at MIT introduces time-slicing, an early preemptive round-robin-like mechanism.
1965
— Dijkstra formalizes semaphores and cooperating processes, enabling safe preemptive multiprogramming.
1969
— Unix's early process model establishes clean fork/exec semantics that later schedulers (including CFS) still build on.
2007
— Linux replaces the O(1) scheduler with the Completely Fair Scheduler, using a red-black tree keyed by virtual runtime.

Where this matters

Every general-purpose operating system kernel — Linux, Windows NT, macOS/XNU, the various BSDs — implements exactly this scheduler/dispatcher split, and every one of them is preemptive at the process/thread level for this reason. Real-time operating systems (VxWorks, QNX, FreeRTOS) obsess over minimizing and bounding dispatch latency, because a real-time task missing its deadline due to unpredictable switch overhead is a system failure, not just a slowdown. Hypervisors (KVM, Xen, VMware ESXi) run a second layer of scheduling on top — scheduling entire virtual CPUs the same way a kernel schedules processes — making these same four decision points and the preemptive/non-preemptive distinction recur one level up the stack. Mobile operating systems (Android, iOS) tune preemption aggressiveness and quantum length specifically to keep UI threads responsive even while background work runs, a direct application of the responsiveness/throughput tradeoff derived above. Understanding this foundation is what makes it possible to evaluate concrete policies — covered next under Scheduling Performance Metrics and First-Come First-Served Scheduling — on a common, honest basis.

Ask the tutor: How does the Linux Completely Fair Scheduler decide which ready process to dispatch, and how does its red-black tree keep that decision at O(log n)?

What is the difference between the scheduler and the dispatcher?

The scheduler is a policy decision-maker: given the set of ready processes, it decides which one should run next, using whatever algorithm (FCFS, priority, shortest remaining time, etc.) the system implements. The dispatcher is the mechanism that carries out that decision: it performs the actual context switch — saving the outgoing process's registers and program counter, switching memory-management state if needed, and restoring the incoming process's saved state — then transfers control to it. The scheduler answers "who," the dispatcher answers "how." They are usually implemented as separate kernel modules, and the time the dispatcher takes (dispatch latency) is counted as pure overhead separate from the scheduler's decision-making cost.

Why do programs alternate between CPU bursts and I/O bursts at all?

Almost every real program needs to both compute and interact with the outside world — reading files, waiting on network responses, writing to a display, or waiting on a semaphore held by another process. Each of these interactions forces the process to stop computing and wait, which is exactly a CPU burst ending and an I/O (or event) burst beginning. This alternation is not an artifact of scheduling; it is a property of how real software is structured. Scheduling exists specifically to exploit it: while one process is in its I/O burst and unable to use the CPU, another process's CPU burst can run, keeping the processor busy instead of idle.

Can a non-preemptive scheduler ever achieve good response time for interactive processes?

Only by accident, and not reliably. In a purely non-preemptive scheme, once a long CPU-bound process starts running, it keeps the CPU until it voluntarily blocks or terminates — there is no mechanism to interrupt it even if an interactive process becomes ready and urgently needs a quick response. This is why almost all interactive, general-purpose, and real-time operating systems use preemptive scheduling: it allows the OS to bound the maximum wait time any ready process can experience, at the cost of extra context-switch overhead and more careful kernel synchronization.

Is a shorter time quantum always better for a preemptive round-robin-style system?

No — this is one of the most common misconceptions. A shorter quantum improves fairness and responsiveness (each process gets the CPU back sooner), but every quantum boundary triggers a dispatch, and dispatch latency is a roughly fixed overhead per switch. As derived in the complexity section, the wasted fraction of CPU time is at least d/(q+d), where d is dispatch latency and q is the quantum. As q shrinks toward d, this wasted fraction approaches 50% or more, meaning the system spends nearly as much time switching as computing. Real systems pick q large enough (often 1–2 orders of magnitude bigger than d) that overhead stays negligible while responsiveness remains acceptable.

Do all four scheduling decision points apply on multi-core systems?

The four transition points (block, preempt, unblock, exit) are properties of a single process's lifecycle and apply per core in the same way, but multi-core systems add extra decisions: which core should run a given ready process (load balancing), and whether to migrate a process between cores. These add complexity beyond the single-core model presented here — such as cache-affinity considerations, since migrating a process to a different core discards its cache locality — but the fundamental four decision points, and the scheduler/dispatcher split, still hold per core as the base mechanism that multi-core load-balancing policies build on top of.

Why is dispatch latency sometimes larger than "just saving a few registers" suggests?

Saving and restoring CPU registers is genuinely fast and roughly constant-time. The larger cost usually comes from address-space switching: if the incoming process uses a different page table than the outgoing one, the CPU may need to flush or partially invalidate the Translation Lookaside Buffer (TLB), and subsequent memory accesses suffer cache and TLB misses until the working set "warms up" again. Some architectures mitigate this with tagged TLBs (address-space identifiers) that avoid a full flush, but the cold-cache effect after a switch remains a real cost that can dominate the raw register-save time by one or two orders of magnitude.

How does this relate to algorithms like FCFS or Round Robin that come next?

This article establishes the shared vocabulary and mechanism — the burst cycle, the four decision points, preemptive versus non-preemptive, and dispatch overhead — that any concrete scheduling policy is built and evaluated on. First-Come First-Served is the simplest non-preemptive policy (run ready processes in arrival order, uninterrupted). Round Robin is a canonical preemptive policy (fixed time quantum, forced rotation). Once you understand why a scheduling decision only happens at these four points, and why dispatch cost is a real, boundable overhead, you're equipped to evaluate any such policy honestly using the metrics — turnaround time, waiting time, response time, throughput — covered under Scheduling Performance Metrics.

Before this

Where this leads