Back to the map

Module 01 · Machine basics · Lesson 4 of 4

I/O and interrupts

How a CPU talks to disks and network cards that are millions of times slower: polling, interrupts and DMA. What happens when a packet arrives, why blocking I/O wastes threads, and how this leads to event loops, epoll and io_uring.

CoreShould-knowMid 12 minVideo Updated Nov 2026
Video walkthrough · 2:26 · narrated, with captions

Why it matters

A server spends most of its life waiting for I/O: for a request to arrive on the network, for a disk read, for a reply from a database. How the machine handles that waiting decides how many requests a server can juggle at once and how much CPU it burns doing nothing. Thread-per-request versus event loops, the C10K problem, why Node.js and Go scale the way they do, and why a busy network card can saturate a core all come down to the mechanisms in this lesson.

The idea

Devices (network cards, SSDs, keyboards) work on their own schedule, and they are slow next to the CPU: a nanosecond-scale core may wait microseconds to milliseconds for a device. There are three ways for the CPU to find out a device has data, and modern systems use all three.

MechanismHow it worksGood forCost
PollingThe CPU repeatedly checks a device status registerVery high event rates where something is almost always readyBurns a core spinning when nothing is happening
InterruptsThe device signals the CPU, which pauses its current work to run a handlerInfrequent or unpredictable eventsEach interrupt costs a context switch into the kernel; at very high rates the overhead dominates
Direct memory access (DMA)The device copies data to or from memory itself, then interrupts once when doneBulk transfers: disk blocks, network packetsSetup per transfer; the CPU is free during the copy

What happens when a packet arrives

Network cardRAMring bufferCPU core1 DMA copy2 interruptKernel handlerack, schedule the rest3Network stack → socket4 wake the waiting process
The network card writes the packet into memory by DMA, then raises an interrupt. The kernel's handler does the minimum and defers the rest, and the waiting process is woken.

Interrupt handlers do as little as possible (acknowledge the device, note what arrived) and defer the heavy work, such as running the packet through the TCP/IP stack, to a later, interruptible phase (softirqs and kernel threads on Linux). Interrupts can be masked briefly and are routed to specific cores; spreading network interrupts across cores (receive-side scaling) is how one machine handles millions of packets per second.

From the device to your program

Your program never touches devices directly. It makes a system call (read, write, recv, send), which traps into the kernel, and the kernel's driver talks to the device. If the data is not ready, the classic blocking call puts the thread to sleep until an interrupt eventually wakes it. That is simple to program, and it means one thread per in-flight operation: ten thousand concurrent connections need ten thousand threads, each with its own stack and scheduler overhead.

  1. 1

    Blocking I/O, one thread per connection

    Easy to write; memory and context switches limit it to thousands of connections.
  2. 2

    Non-blocking I/O with readiness notification

    Sockets never block; one thread asks the kernel 'which of my 50,000 sockets are ready?' with select, poll, or the scalable epoll (Linux) and kqueue (BSD, macOS), and handles only those. This is the event loop inside Node.js, nginx, Netty and Redis.
  3. 3

    Completion-based I/O

    The program submits operations and is told when they finish, with batching to avoid a system call per operation: io_uring on Linux, IOCP on Windows. It cuts system-call overhead further and covers files as well as sockets.
  4. 4

    Runtimes that hide it

    Go's goroutines and Java's virtual threads let you write blocking-style code while the runtime multiplexes them onto an event loop underneath.

When interrupts become the bottleneck

At extreme packet rates, one interrupt per packet would consume a core on interrupt handling alone. Network cards and drivers therefore coalesce interrupts (one per batch or per time window) and switch to polling under load (Linux NAPI), then back to interrupts when traffic drops. The most demanding systems bypass the kernel entirely (DPDK, AF_XDP), dedicating cores to polling the card directly, trading CPU for latency and throughput.

Trade-offs

Interrupts are efficient when events are rare and wasteful when they are constant; polling is the reverse. Blocking I/O is simple and caps concurrency; event-driven I/O scales to huge connection counts at the cost of callback-heavy or async code, which is why modern runtimes offer lightweight threads to hide it. Kernel bypass buys the lowest latency by giving up the kernel's protections and tools.

In an interview

Connect it to concurrency choices: "Each server holds 100,000 mostly idle WebSocket connections, so we need an event-driven runtime using epoll rather than a thread per connection; with thread-per-connection we would spend gigabytes on stacks and most of our CPU on context switches." For high-throughput networking: "At several million packets per second we spread receive queues across cores with RSS and rely on interrupt coalescing."

Check yourself

A server holds 50,000 concurrent, mostly idle connections using one blocking thread per connection. Memory use is huge and CPU is high even though little data flows. What is the standard fix?

I/O and interrupts · Backplane