Loading learning content...
At the heart of every CPU scheduling system lies a fundamental question: When can the operating system take the CPU away from a running process?
The answer to this question divides all scheduling approaches into two categories with profoundly different characteristics:
Non-preemptive (Cooperative) scheduling: Once a process has the CPU, it keeps it until it voluntarily gives it up—by blocking on I/O, waiting for a resource, or terminating.
Preemptive scheduling: The operating system can forcibly take the CPU from a running process, typically via timer interrupts, to give it to another process.
This distinction isn't merely academic—it shapes user experience, system security, fairness guarantees, and the complexity of kernel design. Understanding this choice is essential for grasping why modern operating systems work the way they do.
By the end of this page, you will understand: (1) The precise definitions and mechanics of both scheduling types, (2) When scheduling decisions occur in each model, (3) The tradeoffs between responsiveness, throughput, and complexity, (4) Historical context and why preemption became dominant, and (5) Where non-preemptive scheduling is still used today.
In non-preemptive scheduling (also called cooperative scheduling), a process retains the CPU until it explicitly relinquishes control. The operating system cannot forcibly remove a running process; it must wait for the process to cooperate.
Formal definition:
In a non-preemptive system, scheduling decisions occur only at these points:
Crucially, scheduling does NOT occur:
1234567891011121314151617181920212223242526272829303132
// In a non-preemptive system, this loop runs forever// No timer interrupt will stop it// No higher-priority process can preempt it// Only way to stop: I/O, yield(), or termination #include <stdbool.h> void malicious_or_buggy_process() { // This would freeze the entire system on single-CPU non-preemptive OS while (true) { // Pure computation - no I/O, no yielding volatile int x = 0; x++; } // Never reaches here // Other processes never run // User cannot interact with system} // In a well-behaved non-preemptive system, processes must cooperate:void cooperative_process() { while (true) { // Do some work perform_computation(); // Explicitly yield to let others run yield(); // "I'm done for now, let someone else have a turn" // This requires TRUST that all processes yield regularly // One misbehaving process breaks the entire system }}Characteristics of non-preemptive scheduling:
Non-preemptive scheduling only works when all processes are trusted to yield regularly. In a general-purpose OS with arbitrary user programs, this trust is impossible to guarantee. A buggy program (e.g., infinite loop) or malicious software can freeze the entire system. This is why non-preemptive scheduling is no longer used for general-purpose operating systems.
In preemptive scheduling, the operating system can forcibly remove the CPU from a running process, even if the process hasn't completed or voluntarily yielded. This is typically accomplished through hardware timer interrupts.
Formal definition:
In a preemptive system, scheduling decisions can occur at:
The key mechanism: A hardware timer is programmed to generate an interrupt at regular intervals (typically 1-10 milliseconds). When the interrupt fires, the kernel's interrupt handler saves the current process's state and invokes the scheduler to decide what to run next.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// Conceptual illustration of how preemption works // === Hardware Timer Interrupt Flow === // The hardware timer generates an interrupt every N milliseconds// This causes the CPU to:// 1. Save current instruction pointer to stack// 2. Switch to kernel mode// 3. Jump to interrupt handler address void timer_interrupt_handler(struct cpu_state *regs) { // STEP 1: Save complete process state // The current process was executing arbitrary user code // We must save EVERYTHING: registers, flags, FPU state save_process_state(current_process, regs); // STEP 2: Acknowledge the interrupt acknowledge_timer_interrupt(); // STEP 3: Update accounting current_process->time_used += TIMER_QUANTUM; current_process->remaining_quantum--; // STEP 4: Check if preemption is needed if (current_process->remaining_quantum <= 0 || higher_priority_process_ready()) { // STEP 5: Invoke scheduler to pick next process struct process *next = schedule(); // STEP 6: Switch to new process if (next != current_process) { context_switch(current_process, next); // This function never returns in the normal sense // CPU is now running 'next' process } } // STEP 7: Return from interrupt // Restores user-mode and resumes execution return_from_interrupt();} // Key insight: The running process has NO CONTROL over this// It cannot prevent or delay the timer interrupt// The OS is in charge, not the processCharacteristics of preemptive scheduling:
The time quantum (or time slice) is the maximum amount of time a process can run before preemption. Typical values range from 1ms to 100ms. Shorter quanta improve responsiveness but increase context switch overhead. Longer quanta improve throughput but hurt interactive response. Most modern systems use adaptive quanta that vary based on process behavior.
To precisely understand the difference between preemptive and non-preemptive scheduling, let's enumerate all possible scheduling decision points and which type responds to each.
| Event | Non-preemptive | Preemptive | Description |
|---|---|---|---|
| Process terminates | ✓ Yes | ✓ Yes | Running process exits; must schedule next |
| Process blocks (I/O) | ✓ Yes | ✓ Yes | Running process can't continue; must schedule |
| Process yields voluntarily | ✓ Yes (if API exists) | ✓ Yes | Process explicitly releases CPU |
| Timer quantum expires | ✗ No | ✓ Yes | OS forcibly reclaims CPU |
| Higher-priority ready | ✗ No | ✓ Yes (if priority-based) | Preempt for urgent work |
| I/O completion (wakeup) | ✗ No (waits for yield) | ✓ Yes (can preempt) | Awakened process may preempt current |
| New process created | ✗ No | ✓ Optionally | New process may preempt if higher priority |
The critical difference illustrated:
Consider a scenario with Process A (CPU-bound, running) and Process B (I/O-bound, just completed I/O and now ready):
Non-preemptive system:
Time 0ms: A running, B waiting for I/O
Time 5ms: B's I/O completes, B moves to ready queue
Time 5ms: A continues running (B must wait)
Time 10ms: A continues running (B still waiting)
...
Time 500ms: A finally blocks on I/O
Time 500ms: NOW the scheduler runs, B gets CPU
B waited 495ms even though it was ready and A was just doing computation that could have been interrupted.
Preemptive system:
Time 0ms: A running, B waiting for I/O
Time 5ms: B's I/O completes, B moves to ready queue
Time 5ms: If B has higher priority, A is preempted immediately
OR waits until A's quantum expires
Time 15ms: Timer expires, scheduler runs
Time 15ms: B gets CPU (even if A wanted to continue)
B gets CPU within milliseconds, not seconds.
Some preemptive systems preempt immediately when a higher-priority process becomes ready (immediate preemption), while others wait until the next timer interrupt (deferred preemption). Immediate preemption provides better worst-case latency but requires more complex kernel code. Linux supports both modes depending on configuration.
Neither approach is universally superior—each represents a different balance of competing concerns. Understanding these tradeoffs informs system design decisions.
The overhead quantified:
Context switch overhead on modern systems:
With 1000 timer interrupts per second (1ms quantum) and 100 context switches per second:
For most workloads, this overhead is overwhelmingly justified by the benefits of fairness and responsiveness.
By the 1990s, preemptive scheduling won decisively for general-purpose operating systems. The 1-2% overhead is negligible compared to the benefits of a system that doesn't freeze when one program misbehaves. Every mainstream OS today (Windows, Linux, macOS, iOS, Android) uses preemptive scheduling.
The evolution from non-preemptive to preemptive scheduling mirrors the evolution of computing itself—from single-user batch systems to multi-user interactive systems.
Era 1: Early batch systems (1950s-1960s)
The first operating systems were non-preemptive by necessity:
Era 2: Multiprogramming (1960s-1970s)
To improve CPU utilization, multiprogramming was introduced:
Era 3: Time-sharing (1960s-1980s)
Interactive computing demanded preemption:
| System | Era | Type | Notes |
|---|---|---|---|
| IBM OS/360 (batch) | 1960s | Non-preemptive | Jobs ran to completion or block |
| CTSS | 1961 | Preemptive | First time-sharing system |
| Multics | 1960s-70s | Preemptive | Influenced Unix design |
| Classic Mac OS (1-9) | 1984-2001 | Cooperative | Last major cooperative consumer OS |
| Windows 3.x | 1990-1994 | Cooperative | 16-bit Windows applications |
| Windows 95/NT | 1993-1995 | Preemptive | Transition to preemptive |
| Mac OS X | 2001 | Preemptive | Replaced cooperative Classic Mac OS |
| Linux | 1991-present | Preemptive | Always preemptive |
The Classic Mac OS story:
Classic Mac OS (versions 1-9, 1984-2001) is notable as the last major consumer operating system to use cooperative scheduling. This choice had significant consequences:
Windows had a similar journey—Windows 3.x was cooperative for 16-bit programs, but Windows NT (1993) and Windows 95 (1995) introduced preemptive scheduling.
Cooperative scheduling persisted on personal computers longer than on servers because personal computers were initially single-user, single-task systems. When multitasking became standard, the transition to preemptive scheduling became essential. The stability improvement was dramatic enough that users happily accepted the slightly higher overhead.
While preemptive scheduling dominates general-purpose OSes, non-preemptive (cooperative) scheduling remains valuable in specific contexts where its advantages outweigh its risks.
User-space cooperative threading (green threads/fibers):
Many modern languages and runtimes implement cooperative scheduling within a preemptively scheduled process:
12345678910111213141516171819202122232425262728293031
# Python asyncio: Cooperative scheduling of coroutines# Within the async runtime, tasks yield cooperatively at 'await' points# But the OS can still preempt the entire Python process import asyncio async def task_a(): print("Task A starting") await asyncio.sleep(1) # Cooperative yield point print("Task A resuming") await asyncio.sleep(1) # Another yield point print("Task A done") async def task_b(): print("Task B starting") await asyncio.sleep(0.5) # Yields to other tasks print("Task B done") async def main(): # Both tasks run "concurrently" via cooperative scheduling await asyncio.gather(task_a(), task_b()) # This is cooperative scheduling:# - Tasks voluntarily yield at 'await'# - No timer-based preemption within the event loop# - A task that never awaits blocks all others # BUT the entire Python process is preemptively scheduled by the OS# So a misbehaving asyncio program doesn't freeze the system asyncio.run(main())Embedded and real-time systems:
Simple embedded systems often use cooperative scheduling:
Why it works in these contexts:
Modern systems often combine both approaches: the OS kernel uses preemptive scheduling to manage processes, while user-space runtimes (like Go, Node.js, or asyncio) use cooperative scheduling to manage lightweight tasks within a process. This gives the best of both worlds: system stability from OS preemption, plus efficiency from user-space cooperation.
Our discussion so far has focused on user-space preemption—can the OS preempt user processes? But there's another level: kernel preemption—can the OS preempt code running in kernel mode?
The distinction:
Why kernel preemption is harder:
Kernel code manipulates shared data structures (process tables, file systems, device state). If preempted mid-operation, another process might see inconsistent state:
123456789101112131415161718192021222324252627282930313233
// Problem: Kernel preemption during critical operation // Process A is in kernel mode, allocating a pagevoid allocate_page_frame() { struct page *page = get_free_page(); // Gets page 0x1000 // PREEMPTION HAPPENS HERE // Process B runs, also allocates a page // B gets the SAME page 0x1000 (still marked free!) page->owner = current_process; // A marks it as owned page->flags = PAGE_PRESENT; add_to_page_table(current_process, page); // Both A and B now think they own page 0x1000 // Data corruption guaranteed} // Solution 1: Non-preemptive kernel// System calls run to completion; no preemption during kernel mode// Simple but hurts latency // Solution 2: Preemptive kernel with locksvoid allocate_page_frame_safe() { spin_lock(&page_allocator_lock); // Disable preemption in critical section struct page *page = get_free_page(); page->owner = current_process; page->flags = PAGE_PRESENT; add_to_page_table(current_process, page); spin_unlock(&page_allocator_lock); // Re-enable preemption}Kernel preemption in major operating systems:
| OS | Kernel Preemptibility | Notes |
|---|---|---|
| Linux (default) | Preemptible | Can preempt kernel code except in critical sections |
| Linux PREEMPT_RT | Fully preemptible | Even spinlocks are preemptible for real-time |
| Windows NT | Preemptible | Kernel-mode APCs can preempt kernel code |
| macOS/XNU | Partly preemptible | Mach microkernel portions preemptible |
| FreeBSD | Optional | PREEMPTION kernel option |
Linux preemption models:
Kernel preemption primarily affects worst-case latency for high-priority tasks. Without kernel preemption, a high-priority process might wait for a long-running system call in another process to complete. With kernel preemption, the scheduler can intervene. This matters for audio/video processing, gaming, and real-time control systems.
The choice between preemptive and non-preemptive scheduling is foundational to operating system design. Let's consolidate the key insights:
What's next:
With the preemption question settled (modern systems are preemptive), we can now explore scheduling criteria—the metrics by which we evaluate scheduling algorithms. CPU utilization, throughput, turnaround time, waiting time, response time—each represents a different stakeholder's priorities, and no algorithm can optimize all simultaneously.
You now understand the fundamental distinction between preemptive and non-preemptive scheduling—why preemption exists, how it works mechanically (timer interrupts), what tradeoffs it involves, and where cooperative scheduling still has a role. This understanding is essential for appreciating why scheduling algorithms take the forms they do.