Device Drivers & the I/O Abstraction Layer

Building an Operating System Kernel: Concurrency, I/O & Synchronization

Chapter 6 · Device Drivers & the I/O Abstraction Layer

Every prior chapter's work happens entirely inside the CPU — fast, and always available. A real device is neither: reading from a disk takes real time, during which the CPU could be doing something else entirely. This chapter builds a real simulated device with genuine latency, measures the real cost of the naive way to wait for it, and finds a bug that could only ever surface once a process could genuinely block — something no earlier chapter, in either course, ever made possible.

A Real Device, a Real Driver Abstraction

class SimulatedDisk: # an operation takes a fixed number of real kernel steps, ticked # forward independently of which process is currently running def tick(self): if self.pending is None: return None self.pending['remaining'] -= 1 if self.pending['remaining'] <= 0: done = self.pending self.pending = None return done # completed EXACTLY on this real step return None

DiskDriver is the real abstraction layer: calling code never touches the disk's own raw latency counter — it only ever calls read_start() and lets the kernel dispatch a real interrupt on completion.

Finding 1: The Real, Measured Cost of Polling

Verified directly — 200 real steps to complete 100 real WORK steps
A waiter issues a disk read, then polls — checking, every one of its own turns, whether the operation is done yet. A worker does 100 real WORK steps. Under strict round-robin, the worker only gets every other real step (the waiter's own polling turns don't disappear — they just accomplish nothing), so it takes 200 real steps to finish 100 turns' worth of work.

Finding 2: A Real, Measured Speedup From Interrupt-Driven Blocking

Verified directly — the identical worker finishes in 111 steps, not 200 — a real 1.80x speedup
Same disk, same latency (90 ticks), same worker target (100 WORK steps). The only change: the waiter READ_DISK_BLOCKINGs instead of polling — transitioning to BLOCKED and leaving the ready-queue rotation entirely. During the disk's own 90-tick latency window, the worker is the sole ready process, getting every single real step uncontested. Result: 111 real steps instead of 200 — a measured 1.80x speedup, using the exact same scheduler and exact same amount of real work.

Finding 3: The Interrupt Correctly Targets the Right Process

Verified directly — a bystander that never touched the disk is never affected
reader_x issues a real disk read and blocks. bystander_y is a second, completely unrelated process doing its own real work — it never calls the disk driver at all. Only reader_x is ever transitioned to BLOCKED; only reader_x is woken by the real DISK_INTERRUPT once its own operation genuinely completes. The interrupt correctly identifies and wakes the specific process its completed operation belongs to — not just "whoever happens to be blocked."

Finding 4: A Real Bug — The Timer Handler Assumed the CPU Was Never Idle

Every prior chapter's own _on_timer() unconditionally re-adds the outgoing process to the ready queue. What happens the very first time the CPU is genuinely idle when the timer fires?

Verified directly — a real crash, on a scenario no earlier chapter could ever reach
The moment a process blocks (Finding 2's own mechanism), current_pcb becomes None for the first time in either course — Course 1 never had a way for a process to voluntarily give up the CPU, so this state was simply unreachable before this chapter. When a later timer interrupt fires after the disk interrupt has added a process back to the ready queue, the original _on_timer() tries scheduler.add(self.current_pcb) — but self.current_pcb is still None at that point in the function. Result: 'NoneType' object has no attribute 'state', a real crash.
The fix — the exact same guard context_switch_fixed() already uses
if self.current_pcb is not None: self.scheduler.add(self.current_pcb) — one line, mirroring the guard context_switch_fixed() already applies to its own old_pcb parameter. Verified directly: the identical scenario now completes cleanly, with the process correctly resuming and its own follow-up work running to completion.

Where This Connects

This chapter's findingWhat it connects to
The polling-vs-blocking measurementCourse 1 Chapter 8's own quantum-tradeoff measurement — a real, measured cost/benefit comparison, not an assumption
Real blocking on I/OCourse 1 Chapter 4's own ProcessState machine — reused directly; BLOCKED existed from the start, but this is the first chapter that ever genuinely uses it
A bug only reachable through a genuinely new stateoskernel1's own capstone pick_next() mismatch — both bugs are real integration gaps, invisible until a new combination of existing pieces is actually tried
The interrupt correctly targeting one processChapter 5's own MessageQueue — both rely on a real, tracked identity (a PID, a whole message) rather than an ambiguous shared signal

Hands-On Exercises

Exercise 1

Block a single process on a disk read with no other ready process in the entire system. Confirm the CPU genuinely goes idle rather than stalling, and that the process correctly resumes once the disk completes.

📄 View solution
Exercise 2

Have two different processes each issue their own real disk read, timed so the second only starts once the first has genuinely completed. Confirm both operations complete correctly and in the right order.

📄 View solution
Exercise 3

Start a second disk operation while the first is still genuinely pending. Report exactly what happens to the first operation, and explain why this is an honest, documented limitation rather than a bug this chapter fixes.

📄 View solution

Chapter 6 Quick Reference

  • SimulatedDisk: a real device with genuine latency, ticking forward independently of which process is running
  • DiskDriver: the real abstraction layer — calling code never touches raw device latency, only read_start()
  • Verified Finding 1: polling wastes the polling process's own real scheduled turns — measured, not assumed
  • Verified Finding 2: interrupt-driven blocking delivers a real, measured 1.80x speedup under identical conditions
  • Verified Finding 3: the disk interrupt correctly targets only the specific process its completed operation belongs to
  • Verified Finding 4 (bug): the timer handler crashed the first time the CPU was genuinely idle — a state no earlier chapter could ever reach — fixed with one guard clause
  • Golden rule: code that's worked correctly for five chapters can still hide a bug in a state combination nothing before ever actually exercised
  • Next chapter: Interrupt-Driven I/O vs. Polling — a deeper, direct measured comparison of both strategies