Vasu Patel logoVasu Patel
← Under the Hood: Operating Systems
All chapters in this track
  1. 01Chapter 1: Why OS Fundamentals Matter to Application Engineers
  2. 02Chapter 2: Processes, Threads, and the Process Lifecycle
  3. 03Chapter 3: The Memory Hierarchy: Registers to Disk
  4. 04Chapter 4: CPU Scheduling Algorithms
  5. 05Chapter 5: Memory Management I: Paging and Segmentation
  6. 06Chapter 6: Memory Management II: Virtual Memory and Page Replacement
  7. 07Chapter 7: File Systems: How Data Is Actually Stored and Retrieved
  8. 08Chapter 8: Interprocess Communication: Pipes, Shared Memory, Message Queues, Sockets
  9. 09Chapter 9: I/O Management and Interrupts
  10. 10Chapter 10: Deadlocks at the OS Level: Conditions, Prevention, Detection, Recovery
  11. 11Chapter 11: Virtualization and Containers: A Conceptual Introduction
  12. 12Chapter 12: When OS Abstractions Leak: Failure Modes Application Engineers Actually Hit
  13. 13Chapter 13: Walkthrough: What Happens When You Run a Program
  14. 14Chapter 14: Common OS Interview Questions: Paging, Page Faults, and Deadlock Scenarios
  15. 15Chapter 15: OS Concepts in System Design and Production Debugging

Chapter 9 of 15

Chapter 9: I/O Management and Interrupts

Why this matters

Chapter 3 established that disk and network I/O are orders of magnitude slower than memory access. This chapter explains the mechanism the OS uses to avoid wasting the CPU while waiting for something that slow — and understanding that mechanism directly explains why async/non-blocking I/O models (Node.js's event loop, Python's asyncio, and similar) exist and what problem they're actually solving at the OS level.

The core concept: interrupts versus polling

[BEGINNER] When a program requests I/O (read a file, receive network data), the underlying device operates far slower than the CPU. The naive approach — polling, where the CPU repeatedly checks "is the data ready yet?" in a tight loop — wastes enormous CPU time on a device that might take milliseconds (an eternity in CPU time) to respond.

The alternative, and what real hardware/OSes actually use: an interrupt — a signal the hardware device sends to the CPU when it needs attention (data has arrived, an operation completed), letting the CPU do other useful work in the meantime instead of spinning in a polling loop.

Polling model:                          Interrupt model:
CPU: "ready?" -> No                     CPU: dispatches I/O request, then runs OTHER work
CPU: "ready?" -> No                     [... CPU does useful work for other processes ...]
CPU: "ready?" -> No          vs.        Device: [DONE] -> raises an interrupt
CPU: "ready?" -> No                     CPU: pauses current work, runs the interrupt handler,
CPU: "ready?" -> Yes, finally!                 resumes the process that was waiting on the I/O

What happens when an interrupt fires, concretely:

1. Device signals the interrupt controller: "I'm done" (or "I have data").
2. CPU finishes its current instruction, then jumps to a predefined Interrupt Service
   Routine (ISR) — the CPU saves its current state first, so it can resume exactly
   where it left off afterward.
3. The ISR (kernel code) handles the immediate need — e.g., copies data from the
   device's buffer to a kernel buffer — as QUICKLY as possible, deferring any
   heavier processing (this split is often implemented as "top half / bottom half"
   or similar deferred-work mechanisms in real kernels).
4. The CPU restores its saved state and resumes whatever it was doing before
   the interrupt — which might be the SAME process that was waiting on that I/O
   (now moved from Waiting back to Ready, per Chapter 2's process lifecycle),
   or a completely different process the scheduler was running in the meantime.

This is the literal mechanism behind Chapter 2's process lifecycle diagram: a process moves from Running to Waiting when it issues a blocking I/O call, and an interrupt (signaling that I/O completed) is precisely what moves it from Waiting back to Ready, letting the scheduler give it the CPU again.

Blocking vs. non-blocking I/O, and why async runtimes exist

[SENIOR INSIGHT] A blocking I/O call (a normal read() on a socket with no data yet available) puts the calling thread into the Waiting state until the interrupt-driven completion happens — the thread does nothing else in the meantime. This is fine for one connection, but a server handling ten thousand concurrent connections using one blocking thread per connection needs ten thousand threads, each mostly idle in the Waiting state, paying real memory and context-switching overhead (Chapter 2) for essentially no work.

Non-blocking I/O lets a single thread check "is this ready?" across many I/O sources without blocking on any one of them, using an OS-provided mechanism to efficiently wait on many file descriptors at once rather than the naive polling loop this chapter opened by rejecting:

#include <sys/epoll.h>

int epfd = epoll_create1(0);
// ... register many socket file descriptors with epoll_ctl ...

struct epoll_event events[64];
int n = epoll_wait(epfd, events, 64, -1); // blocks efficiently until ANY registered fd is ready
// The kernel — not a user-space polling loop — tracks readiness across all
// registered file descriptors and wakes this call the moment any of them are ready.
for (int i = 0; i < n; i++) {
    // handle whichever specific connections are actually ready — no wasted checks
}

[IN PRACTICE] This is precisely the OS-level mechanism (epoll on Linux; kqueue on BSD/macOS; IOCP on Windows) underneath every high-concurrency async runtime — Node.js's event loop, Python's asyncio, Nginx's worker model. A single OS thread can efficiently manage tens of thousands of concurrent connections this way, because the kernel handles readiness notification for all of them via the same interrupt-driven mechanism this chapter describes, rather than the application needing one thread per connection.

DMA: keeping the CPU out of the data path entirely

[BEGINNER] For large data transfers (disk reads, network packets), having the CPU personally copy every byte between a device and memory would waste CPU cycles on pure data movement. Direct Memory Access (DMA) lets a dedicated DMA controller move data directly between a device and memory, with the CPU only involved at the start (initiating the transfer) and the end (receiving the completion interrupt) — freeing the CPU to do other work for the entire duration of the actual transfer.

Common mistakes

  1. Building a polling loop for I/O readiness in application code when an OS-provided mechanism (epoll/kqueue/async runtime primitives) already solves this efficiently — a hand-rolled polling loop wastes exactly the CPU cycles interrupts and epoll-style mechanisms exist to avoid.
  2. Assuming "async" is a purely language-level/library feature with no OS grounding — async runtimes are a user-space abstraction built directly on top of OS-level non-blocking I/O and readiness-notification mechanisms (epoll and equivalents); understanding this connection explains why async I/O scales the way it does.
  3. Using one thread per connection at very high concurrency without recognizing the real cost (Chapter 2's context-switch and memory overhead, multiplied by thread count) — this is exactly the problem non-blocking I/O and event-driven architectures exist to solve at scale.
  4. Not distinguishing CPU-bound work from I/O-bound work when choosing a concurrency model. Async/event-driven models excel at I/O-bound workloads (many waiting connections) but don't help — and can even hurt — genuinely CPU-bound work, which needs actual parallel execution across cores instead.

Checklist: reasoning about I/O and concurrency models

  1. When a service needs to handle very high connection concurrency, favor a non-blocking/event-driven model (or a runtime built on one) over one-thread-per-connection, specifically because of Chapter 2's per-thread overhead multiplied at scale.
  2. Distinguish I/O-bound problems (async/event-driven models help a lot) from CPU-bound problems (they don't help; need actual parallelism across cores) before choosing a concurrency strategy.
  3. Never hand-roll a busy-polling loop for I/O readiness — use the platform's efficient readiness-notification mechanism (epoll, kqueue, or the async runtime built on them) instead.
  4. Remember that "async" in application code is a user-space abstraction over real OS mechanisms (non-blocking I/O, interrupts, epoll-style multiplexing) — this connection is worth being able to state explicitly, especially in an interview.

Discussion / interview questions

  1. "Why do interrupts exist instead of just polling a device to check if it's ready?" Strong answer: Polling wastes CPU cycles repeatedly checking a device that operates far slower than the CPU; interrupts let the CPU do other useful work and be notified only when the device actually needs attention, which is a vastly more efficient use of CPU time.

  2. "How does epoll (or a similar mechanism) let one thread handle tens of thousands of concurrent connections?" Strong answer: The kernel tracks readiness across all registered file descriptors and wakes the calling thread only when at least one is actually ready, avoiding both a one-thread-per-connection model's overhead and a naive polling loop's wasted CPU cycles — this is the OS-level mechanism underneath async runtimes like Node.js's event loop.

  3. "What's the relationship between a language's 'async/await' feature and the operating system?" Strong answer: Async/await is a user-space, language-level abstraction built on top of real OS mechanisms — non-blocking I/O and kernel-level readiness notification (epoll/kqueue/IOCP) — not an independent, OS-agnostic concept; the runtime uses those OS facilities under the hood to know when to resume a suspended async task.

  4. "Would an async/event-driven concurrency model help a CPU-bound workload (like video encoding)? Why or why not?" Strong answer: No — async models solve the problem of many threads sitting idle waiting on I/O; a CPU-bound workload has no idle waiting to eliminate, it needs genuine parallel execution across multiple CPU cores, which is a fundamentally different problem (and a different set of tools — process/thread-level parallelism, not event loops).

Chapter summary / cheat sheet

  • Interrupts let the CPU avoid wasting cycles polling slow devices — a device signals completion, and the CPU is notified and resumes exactly where it left off, via an interrupt handler.
  • This interrupt mechanism is the literal implementation of Chapter 2's Waiting-to-Ready process transition when blocking I/O completes.
  • Non-blocking I/O with kernel-level readiness notification (epoll/kqueue/IOCP) lets one thread efficiently manage many concurrent I/O sources — the real OS mechanism underneath high-concurrency async runtimes.
  • DMA lets large data transfers happen directly between devices and memory without the CPU personally copying every byte, freeing the CPU during the transfer.
  • Async/event-driven models solve I/O-bound concurrency problems specifically; they don't help (and aren't meant to help) genuinely CPU-bound workloads, which need real parallelism instead.