Preemptive Multitasking

Subpage of Operating Systems

Diving deep into the design and structure of Operating Systems.

while (1) {
    thread = choose_next_thread();   // scheduling policy
    switch_to(thread);               // context switch
    // The CPU now runs the thread until it yields or is interrupted
}

In the loop above, the OS runs only when switch_to() is called. If a thread never calls switch_to(), the OS never gets the CPU back.

In early systems (e.g., Windows 3.1, classic Mac OS), the solution was cooperative multitasking (what you call “yielding”). The thread must voluntarily give up the CPU:

  • Explicit yield – The thread calls a function like thread_yield() or sched_yield() (POSIX). This is your “yield function available through POSIX”.
  • Blocking operations – If the thread performs I/O (e.g., read() from disk or network), the OS automatically switches to another thread because the current thread must wait. This is also a form of yielding.

Problem with cooperative multitasking: A buggy or malicious thread that never yields (e.g., an infinite loop without I/O) can monopolise the CPU indefinitely. The system becomes unresponsive. Modern general-purpose OSes do not rely on cooperation; they use preemptive multitasking via interrupts.

Preemptive multitasking 

A CPU-bound thread (e.g., heavy numerical computation) never blocks. In a cooperative system, it would indeed run forever unless it explicitly yields. In preemptive systems, we use external events (interrupts).

The timer interrupt (also called the system tick) is generated by a programmable interval timer (PIT) or a more modern high-resolution timer. It fires at a fixed frequency, typically 100 Hz to 1000 Hz (10 ms to 1 ms intervals).

When the timer interrupt occurs:

  • The CPU saves the current thread’s state (registers, PC).
  • The kernel’s timer interrupt handler runs.
  • The handler increments a system clock counter.
  • Then it calls the scheduler (scheduler_tick() or similar).
  • The scheduler checks if the current thread has exceeded its time quantum (e.g., 10 ms). If yes, it marks the thread as ready and chooses another thread from the ready queue.
  • The kernel performs a context switch to the new thread.

This mechanism guarantees that no thread can monopolise the CPU indefinitely. Even a CPU-bound infinite loop will be preempted after its time slice expires. The OS regains control without any cooperation from the thread.

This is preemptive multitasking. It is the foundation of all modern general-purpose operating systems (Linux, Windows, macOS, BSD, etc.). The infinite loop from Part 2 now runs not because threads yield, but because a hardware timer repeatedly forces the CPU back into the scheduler.

An example

Scenario: A CPU-bound thread running on a single-core system with a 10 ms timer interrupt.

  1. Thread A is currently running. It’s doing a tight loop: while(1) { a = b + c; }. It never calls yield(), never does I/O.
  2. The hardware timer counts down from 10 ms. When it reaches zero, it sends an interrupt to the interrupt controller, which delivers it to the CPU.
  3. CPU pipeline is flushed (stops executing Thread A’s instructions). The CPU saves the minimal state (program counter = the next instruction in Thread A) and jumps to the kernel’s timer interrupt handler (entering kernel mode).
  4. The interrupt handler runs. It notices that this is a timer interrupt, calls scheduler_tick().
  5. The scheduler looks at Thread A’s TCB: it has used its time quantum. The scheduler moves Thread A from RUNNING to READY and inserts it at the back of the ready queue (for a Round-Robin policy).
  6. The scheduler picks the next thread from the ready queue – say Thread B.
  7. The kernel switches to Thread B: it saves Thread A’s full context (registers, stack pointer, etc.) into Thread A’s TCB, then loads Thread B’s saved context from its TCB.
  8. The CPU returns to user mode and starts executing Thread B from where it left off.
  9. Thread B runs until the next timer interrupt (or until it blocks on I/O), at which point the cycle repeats.

If Thread B blocks on disk I/O:

  1. Thread B calls read(). This is a system call, so it enters kernel mode voluntarily.
  2. The kernel initiates the I/O, then moves Thread B from RUNNING to WAITING (blocked queue for that disk).
  3. The kernel immediately calls the scheduler without waiting for the next timer interrupt. This is an immediate context switch.
  4. The scheduler picks Thread A (which is ready) and switches to it. Thread A runs again.
  5. Later, the disk completes the I/O and raises an interrupt. The disk interrupt handler runs, moves Thread B from WAITING to READY, and puts it back in the ready queue. The next timer interrupt (or a subsequent scheduling point) will resume Thread B.

/ Continue

Follow the technical trail.

Use the dense notes as the source material, then move through the guided route, writing, or project proof when you want a cleaner entry point.