The Real-World Problem
Imagine you are writing a chat server. One hundred users are connected. Each user might send a message at any point in time. With normal blocking read(), your program would freeze waiting for one user and miss everyone else. This is not a bug — it is how blocking I/O works by design.
Alternative I/O models exist to answer one question: how can a single process efficiently monitor many file descriptors at the same time?
The Blocking I/O Problem
With standard blocking I/O, a server using one thread per client looks like this:
The Alternative — Monitor All at Once
Alternative I/O models let one thread ask the kernel: “tell me which of these 10,000 fds is ready”. Then the program only does I/O on ready fds.
Important Point — These APIs Don’t Do I/O
select(), poll(), and epoll do NOT read or write data. They only tell you which file descriptors are ready. After they return, you still call read() or write() yourself.
Think of them as a waiting room notification system — they say “your fd is ready”, and then you go do the actual work.
The Four Alternative I/O Models — Quick Comparison
| Model | How It Works | Performance at Scale | POSIX Standard? |
|---|---|---|---|
| select() | Pass fd set every call; kernel scans it | Poor — O(N) scan, limited to 1024 fds | Yes (POSIX) |
| poll() | Pass array every call; no fd limit | Medium — still O(N) scan | Yes (POSIX) |
| Signal-Driven I/O | Kernel sends SIGIO when fd is ready | Good — kernel remembers fds | Mostly (SUSv3) |
| epoll | Register once; kernel tracks changes | Best — O(1) for readiness | Linux only |
What Does “Ready” Mean?
- Data available in socket receive buffer
- EOF reached (read returns 0)
- A new connection waiting on listening socket
- Error condition pending
- Space available in socket send buffer
- Pipe write end has room
- Nonblocking write would not block
- Error condition pending
The C10K Problem
In the late 1990s, Dan Kegel wrote a famous article asking: can a single web server handle 10,000 simultaneous clients? That is the “C10K problem” (C = connections, 10K = 10,000).
The answer requires using one of the alternative I/O models in this chapter — because thread-per-client simply does not scale. Today we deal with C100K and C1M (million connections), and epoll is the foundation that makes it possible.
Basic Flow — How Any of These Models Works
/* General pattern used by all alternative I/O models */
1. Setup phase:
- Open/create your file descriptors (sockets, pipes, files)
- Register them with select/poll/epoll (or enable signal-driven I/O)
2. Event loop:
while (1) {
/* Block here until at least one fd is ready */
ready_count = select() or poll() or epoll_wait();
/* Check which fds are ready */
for each ready fd {
if (fd is readable)
read(fd, buf, sizeof(buf)); /* This will NOT block */
if (fd is writable)
write(fd, data, len); /* This will NOT block */
}
}
3. Important rule:
Once the monitoring API says fd is ready, I/O on that fd
will NOT block (assuming no race with another thread).
