Case Study 2: How Operating Systems Use Queues

Process Scheduling and I/O Queues

Right now, your computer is running dozens — perhaps hundreds — of processes simultaneously: a web browser with multiple tabs, a music player, a code editor, system services, background updates. But most computers have only a handful of CPU cores. How does your operating system create the illusion of everything running at once? The answer lies in one of the oldest and most important applications of the queue data structure: process scheduling.


The Problem: One CPU, Many Processes

Imagine a restaurant with a single chef. Orders arrive from multiple tables, each requiring different preparation times. The chef can only cook one dish at a time. How should the restaurant decide which order to prepare next?

This is exactly the problem an operating system faces. The CPU is the chef. Processes (running programs) are the orders. The operating system is the manager who decides what gets cooked when.

The simplest approach: First-Come, First-Served (FCFS) — process tasks in the order they arrive. This is a queue.


Simulating FCFS Scheduling

Let's simulate a basic process scheduler using our Queue class:

from collections import deque


class Queue:
    """A First-In, First-Out (FIFO) data structure."""

    def __init__(self):
        self._items = deque()

    def enqueue(self, item):
        self._items.append(item)

    def dequeue(self):
        if self.is_empty():
            raise IndexError("dequeue from empty queue")
        return self._items.popleft()

    def is_empty(self):
        return len(self._items) == 0

    def size(self):
        return len(self._items)


class Process:
    """Represents a process waiting to use the CPU."""

    def __init__(self, name: str, burst_time: int, arrival_time: int = 0):
        self.name = name
        self.burst_time = burst_time       # CPU time needed (ms)
        self.arrival_time = arrival_time   # when the process arrived
        self.start_time: int | None = None
        self.finish_time: int | None = None

    @property
    def wait_time(self) -> int | None:
        if self.start_time is None:
            return None
        return self.start_time - self.arrival_time

    @property
    def turnaround_time(self) -> int | None:
        if self.finish_time is None:
            return None
        return self.finish_time - self.arrival_time

    def __repr__(self) -> str:
        return f"Process('{self.name}', burst={self.burst_time}ms)"


def fcfs_schedule(processes: list[Process]) -> None:
    """Simulate First-Come, First-Served scheduling."""
    ready_queue = Queue()
    for p in processes:
        ready_queue.enqueue(p)

    current_time = 0
    print(f"{'Process':<12} {'Arrival':>8} {'Burst':>6} "
          f"{'Start':>6} {'Finish':>7} {'Wait':>6} {'Turnaround':>11}")
    print("-" * 62)

    while not ready_queue.is_empty():
        process = ready_queue.dequeue()

        # If the process arrived after current_time, CPU was idle
        if process.arrival_time > current_time:
            current_time = process.arrival_time

        process.start_time = current_time
        process.finish_time = current_time + process.burst_time
        current_time = process.finish_time

        print(f"{process.name:<12} {process.arrival_time:>8} "
              f"{process.burst_time:>6} {process.start_time:>6} "
              f"{process.finish_time:>7} {process.wait_time:>6} "
              f"{process.turnaround_time:>11}")

    # Summary statistics
    total_wait = sum(p.wait_time for p in processes)
    total_turnaround = sum(p.turnaround_time for p in processes)
    n = len(processes)
    print(f"\nAverage wait time:       {total_wait / n:.1f}ms")
    print(f"Average turnaround time: {total_turnaround / n:.1f}ms")


# Simulate!
processes = [
    Process("Chrome",    10, arrival_time=0),
    Process("VS Code",    4, arrival_time=1),
    Process("Spotify",    2, arrival_time=2),
    Process("Terminal",   1, arrival_time=3),
    Process("Slack",      5, arrival_time=4),
]

print("=== First-Come, First-Served (FCFS) Scheduling ===\n")
fcfs_schedule(processes)

# Expected output:
# === First-Come, First-Served (FCFS) Scheduling ===
#
# Process       Arrival  Burst  Start  Finish   Wait  Turnaround
# --------------------------------------------------------------
# Chrome              0     10      0      10      0          10
# VS Code             1      4     10      14      9          13
# Spotify             2      2     14      16     12          14
# Terminal            3      1     16      17     13          14
# Slack               4      5     17      22     13          18
#
# Average wait time:       9.4ms
# Average turnaround time: 13.8ms

The Problem with FCFS

Look at the results above. Chrome takes 10ms and goes first. Every other process waits 10ms just because Chrome arrived first. Terminal only needs 1ms of CPU time but waits 13ms. This is called the convoy effect — short processes get stuck behind long ones.

It's like being at the grocery store with one item, stuck behind someone with a full cart. Frustrating — and inefficient.


Round Robin: Fair Time Sharing

Most modern operating systems use a variation of Round Robin (RR) scheduling. Each process gets a fixed time slice (called a quantum or time slice). If a process doesn't finish in its quantum, it goes back to the end of the queue.

def round_robin_schedule(processes: list[Process],
                         quantum: int) -> None:
    """Simulate Round Robin scheduling with a given time quantum."""
    ready_queue = Queue()
    remaining: dict[str, int] = {}

    for p in processes:
        ready_queue.enqueue(p)
        remaining[p.name] = p.burst_time

    current_time = 0
    execution_log: list[tuple[str, int, int]] = []
    completed: dict[str, int] = {}

    while not ready_queue.is_empty():
        process = ready_queue.dequeue()

        if process.arrival_time > current_time:
            current_time = process.arrival_time

        if process.start_time is None:
            process.start_time = current_time

        # Execute for at most `quantum` ms
        run_time = min(quantum, remaining[process.name])
        execution_log.append((process.name, current_time,
                              current_time + run_time))
        current_time += run_time
        remaining[process.name] -= run_time

        if remaining[process.name] == 0:
            # Process is done
            process.finish_time = current_time
            completed[process.name] = current_time
        else:
            # Process needs more time — back to the queue
            ready_queue.enqueue(process)

    # Print execution timeline
    print(f"Time quantum: {quantum}ms\n")
    print("Execution timeline:")
    for name, start, end in execution_log:
        bar = "#" * (end - start)
        print(f"  [{start:>3}-{end:>3}] {name:<12} {bar}")

    # Print summary
    print(f"\n{'Process':<12} {'Burst':>6} {'Finish':>7} "
          f"{'Wait':>6} {'Turnaround':>11}")
    print("-" * 48)

    total_wait = 0
    total_turnaround = 0
    for p in processes:
        wait = p.finish_time - p.arrival_time - p.burst_time
        turnaround = p.finish_time - p.arrival_time
        total_wait += wait
        total_turnaround += turnaround
        print(f"{p.name:<12} {p.burst_time:>6} {p.finish_time:>7} "
              f"{wait:>6} {turnaround:>11}")

    n = len(processes)
    print(f"\nAverage wait time:       {total_wait / n:.1f}ms")
    print(f"Average turnaround time: {total_turnaround / n:.1f}ms")


# Reset processes
processes_rr = [
    Process("Chrome",    10, arrival_time=0),
    Process("VS Code",    4, arrival_time=0),
    Process("Spotify",    2, arrival_time=0),
    Process("Terminal",   1, arrival_time=0),
    Process("Slack",      5, arrival_time=0),
]

print("\n=== Round Robin Scheduling ===\n")
round_robin_schedule(processes_rr, quantum=3)

# Expected output:
# === Round Robin Scheduling ===
#
# Time quantum: 3ms
#
# Execution timeline:
#   [  0-  3] Chrome       ###
#   [  3-  6] VS Code      ###
#   [  6-  8] Spotify      ##
#   [  8-  9] Terminal     #
#   [  9- 12] Slack        ###
#   [ 12- 15] Chrome       ###
#   [ 15- 16] VS Code      #
#   [ 16- 18] Slack        ##
#   [ 18- 22] Chrome       ####
#
# Process       Burst  Finish   Wait  Turnaround
# ------------------------------------------------
# Chrome           10      22     12          22
# VS Code           4      16     12          16
# Spotify           2       8      6           8
# Terminal          1       9      8           9
# Slack             5      18     13          18
#
# Average wait time:       10.2ms
# Average turnaround time: 14.6ms

Comparing the Two Approaches

Metric FCFS Round Robin (q=3)
Avg wait time 9.4ms 10.2ms
Avg turnaround time 13.8ms 14.6ms
Fairness Low — long jobs block short ones High — everyone gets regular turns
Responsiveness Low — short jobs may wait a long time High — every process runs within one quantum

Round Robin's average times are slightly worse in this example, but that misses the point. In an interactive system (like your desktop), responsiveness matters more than average completion time. You want Spotify to keep playing music, your terminal to respond to keystrokes, and your browser to stay interactive — all at the same time. Round Robin achieves this by giving each process a fair share of CPU time.


I/O Queues: The Other Kind of Waiting

Process scheduling isn't the only place operating systems use queues. I/O queues manage access to shared hardware resources:

  • Disk I/O queue: When multiple processes want to read from or write to the hard drive, requests are queued. The disk controller processes them one at a time (or optimizes their order to minimize seek time).

  • Network I/O queue: Outgoing network packets are queued in a buffer. If packets arrive faster than the network can send them, the queue fills up and eventually drops packets — this is what causes "network congestion."

  • Print queue: When you print from multiple applications, the print spooler queues documents and sends them to the printer one at a time. This is why you sometimes see "3 documents waiting" in your print dialog.

Here's a simplified I/O queue simulation:

class IORequest:
    """Represents a request to read/write a file."""

    def __init__(self, process: str, operation: str,
                 filename: str, size_kb: int):
        self.process = process
        self.operation = operation   # "read" or "write"
        self.filename = filename
        self.size_kb = size_kb

    def __repr__(self) -> str:
        return (f"{self.process}: {self.operation} "
                f"'{self.filename}' ({self.size_kb}KB)")


def simulate_io_queue(requests: list[IORequest],
                      bandwidth_kb_per_ms: int = 10) -> None:
    """Simulate an I/O queue processing disk requests."""
    io_queue = Queue()
    for req in requests:
        io_queue.enqueue(req)

    current_time = 0
    print(f"Disk bandwidth: {bandwidth_kb_per_ms} KB/ms\n")
    print(f"{'Time':>6}  {'Process':<12} {'Op':<6} "
          f"{'File':<20} {'Size':>6} {'Duration':>9}")
    print("-" * 67)

    while not io_queue.is_empty():
        req = io_queue.dequeue()
        duration = req.size_kb // bandwidth_kb_per_ms
        if duration == 0:
            duration = 1   # minimum 1ms

        print(f"{current_time:>5}ms  {req.process:<12} "
              f"{req.operation:<6} {req.filename:<20} "
              f"{req.size_kb:>5}KB  {duration:>7}ms")
        current_time += duration

    print(f"\nTotal I/O time: {current_time}ms")


# Simulate
io_requests = [
    IORequest("Chrome",   "read",  "cache/page.html",   50),
    IORequest("VS Code",  "write", "project/main.py",   12),
    IORequest("Spotify",  "read",  "music/song.mp3",   4000),
    IORequest("Terminal", "read",  "logs/system.log",    200),
    IORequest("Slack",    "write", "data/messages.db",   80),
]

print("=== Disk I/O Queue Simulation ===\n")
simulate_io_queue(io_requests)

# Expected output:
# === Disk I/O Queue Simulation ===
#
# Disk bandwidth: 10 KB/ms
#
#   Time  Process      Op     File                  Size  Duration
# -------------------------------------------------------------------
#    0ms  Chrome       read   cache/page.html        50KB       5ms
#    5ms  VS Code      write  project/main.py        12KB       1ms
#    6ms  Spotify      read   music/song.mp3       4000KB     400ms
#  406ms  Terminal     read   logs/system.log       200KB      20ms
#  426ms  Slack        write  data/messages.db       80KB       8ms
#
# Total I/O time: 434ms

Notice the convoy effect again: Spotify's large music file blocks Terminal and Slack, even though their requests are small. Real operating systems use more sophisticated algorithms (like I/O scheduling with priority levels) to mitigate this.


Multiple Queues: Priority Levels

Modern operating systems don't use a single queue. They use multiple priority queues. System-critical processes (the kernel, device drivers) get higher priority than user applications. Within each priority level, Round Robin ensures fairness.

class MultiLevelScheduler:
    """Simplified multi-level priority scheduling."""

    def __init__(self, quantum: int = 3):
        self.high_priority = Queue()     # system processes
        self.medium_priority = Queue()   # interactive apps
        self.low_priority = Queue()      # background tasks
        self.quantum = quantum

    def add_process(self, name: str, burst: int,
                    priority: str) -> None:
        process = {"name": name, "burst": burst,
                   "remaining": burst, "priority": priority}
        if priority == "high":
            self.high_priority.enqueue(process)
        elif priority == "medium":
            self.medium_priority.enqueue(process)
        else:
            self.low_priority.enqueue(process)
        print(f"  Added '{name}' ({burst}ms, {priority} priority)")

    def _get_next_queue(self) -> Queue | None:
        """Return the highest-priority non-empty queue."""
        if not self.high_priority.is_empty():
            return self.high_priority
        if not self.medium_priority.is_empty():
            return self.medium_priority
        if not self.low_priority.is_empty():
            return self.low_priority
        return None

    def run(self) -> None:
        """Execute all processes by priority."""
        current_time = 0
        print(f"\nExecution (quantum={self.quantum}ms):")

        while True:
            queue = self._get_next_queue()
            if queue is None:
                break

            process = queue.dequeue()
            run_time = min(self.quantum, process["remaining"])
            print(f"  [{current_time:>3}-{current_time + run_time:>3}] "
                  f"{process['name']:<15} "
                  f"({process['priority']})")
            current_time += run_time
            process["remaining"] -= run_time

            if process["remaining"] > 0:
                # Put it back in its priority queue
                queue.enqueue(process)

        print(f"\nAll processes complete at {current_time}ms")


# Demonstrate
print("=== Multi-Level Priority Scheduling ===\n")
scheduler = MultiLevelScheduler(quantum=2)
scheduler.add_process("Kernel Timer", 3, "high")
scheduler.add_process("USB Driver", 2, "high")
scheduler.add_process("Chrome", 6, "medium")
scheduler.add_process("VS Code", 4, "medium")
scheduler.add_process("Backup", 8, "low")
scheduler.add_process("Indexer", 5, "low")
scheduler.run()

# Expected output:
# === Multi-Level Priority Scheduling ===
#
#   Added 'Kernel Timer' (3ms, high priority)
#   Added 'USB Driver' (2ms, high priority)
#   Added 'Chrome' (6ms, medium priority)
#   Added 'VS Code' (4ms, medium priority)
#   Added 'Backup' (8ms, low priority)
#   Added 'Indexer' (5ms, low priority)
#
# Execution (quantum=2ms):
#   [  0-  2] Kernel Timer    (high)
#   [  2-  4] USB Driver      (high)
#   [  4-  5] Kernel Timer    (high)
#   [  5-  7] Chrome          (medium)
#   [  7-  9] VS Code         (medium)
#   [  9- 11] Chrome          (medium)
#   [ 11- 13] VS Code         (medium)
#   [ 13- 15] Chrome          (medium)
#   [ 15- 17] Backup          (low)
#   [ 17- 19] Indexer         (low)
#   [ 19- 21] Backup          (low)
#   [ 21- 23] Indexer         (low)
#   [ 23- 25] Backup          (low)
#   [ 25- 26] Indexer         (low)
#   [ 26- 28] Backup          (low)
#
# All processes complete at 28ms

Notice the pattern: all high-priority processes finish before any medium-priority process runs, and all medium-priority processes finish before any low-priority process runs. This is why system-critical operations (like responding to a keystroke or processing a network packet) feel instant even when your computer is heavily loaded with background tasks.


What We Learned

  1. Queues are fundamental to operating systems. Process scheduling, I/O management, and network buffering all rely on FIFO queues.

  2. FCFS is simple but unfair. Long processes block short ones (the convoy effect). This is acceptable for batch processing but terrible for interactive systems.

  3. Round Robin uses a queue to achieve fairness. By cycling through processes in queue order and giving each a fixed time slice, every process gets regular CPU time.

  4. Multiple priority queues let the OS serve critical operations first while still being fair within each priority level.

  5. The same data structure (queue) appears at multiple levels of the same system — CPU scheduling, disk I/O, network buffers, and print spooling all use queues, sometimes with different policies layered on top.


Connections to This Course

  • Chapter 5 (Loops): The scheduling loop (while not queue.is_empty()) is a while loop driven by a queue's state — a pattern you first encountered in Chapter 5.
  • Chapter 14 (OOP): The Process and MultiLevelScheduler classes encapsulate state and behavior, just as you learned in Chapter 14.
  • Chapter 17 (Algorithms): The choice between FCFS and Round Robin is an algorithm design decision with measurable performance trade-offs — the same kind of thinking you applied in Chapter 17.

Try It Yourself

  1. Modify the Round Robin scheduler to support processes arriving at different times (not all at time 0). You'll need to check arrival times before adding processes to the ready queue.

  2. Implement Shortest Job First (SJF) scheduling: always process the job with the smallest burst time next. This minimizes average wait time but requires knowing burst times in advance. (Hint: you'll need a priority queue — sort by burst time.)

  3. Research multilevel feedback queues — the algorithm actually used by most modern operating systems. A process that uses its entire quantum gets moved to a lower-priority queue, while a process that finishes early stays at a higher priority. This automatically rewards interactive processes.

  4. Add "starvation detection" to the multi-level scheduler: if a low-priority process has been waiting for more than a threshold time, temporarily boost its priority.