The Waiting Game
In the standard Unix model, I/O is synchronous (blocking). When your code calls read(fd, buf, size), your process stops executing. The kernel suspends it, context-switches to another process, and wakes your process up only when the data is ready. Your process was blocked — it consumed no CPU, but it also made no progress.
For a simple script reading one file, this is fine. For a web server handling 10,000 simultaneous connections, it is catastrophic. If each connection blocks for 10ms waiting for a database read, a server with 100 threads can handle only 10,000 requests per second — and that is assuming perfect conditions. Real workloads are bursty.
The solution is asynchronous I/O: submit an I/O request, continue doing other work, and receive a notification when the I/O completes. This decouples the number of concurrent connections from the number of threads, which is the foundation of high-performance servers.
The Three Generations
Linux has three generations of async I/O, each solving the limitations of the previous:
select/poll(1980s–1990s): Polling-based. O(N) per check.epoll(Linux 2.6, 2002): Event-driven. O(1) per event.io_uring(Linux 5.1, 2019): Shared-memory ring buffers. Near-zero syscall overhead.
The following concepts cover each generation in detail.