IO multiplexing enables a single thread to monitor multiple file descriptors (FDs) and perform IO only when those descriptors become ready (readable, writable, or exceptional). It eliminates the traditional blocking IO model’s one-thread-per-connection overhead and avoids wasted cycles of pure non‑blocking IO. Linux provides three primary multiplexing mechanisms—select, poll, and epoll—which form the basis of Java NIO's Selector, Redis, Nginx, and similar high‑performance systems.
File Descriptors and IO Readiness
In Linux, everything is a file. Network sockets, disk files, and pipes are each represented by an integer file descriptor (FD). Standard input is FD 0, standard output FD 1, standard error FD 2, and newly created sockets or files start from FD 3 onwards.
An IO operation proceeds in two phases. For a read socket:
- Data preparation – The kernel reads data from the NIC or disk into a kernel buffer (potentially blocking).
- Data copy – The kernel copies data from the kernel buffer to the user buffer (quick).
An FD is "ready" when the first phase completes, allowing a non‑blocking read or write. Multiplexing centers on monitoring this ready state across many FDs.
select
select is the earliest multiplexing interface, standardized by POSIX. Internally, the kernel scans the FD set provided by the user and checks readiness.
How It Works
- The application fills a
fd_setbitmask with the FDs to moniter and callsselectwith a timeout. - The kernel iterates over every bit in the
fd_set, checking readiness. - If no FD is ready, the process is suspended until data arrives or the timeout expires.
- When readiness occurs, the kernel marks the ready FDs directly in the
fd_set, returns the total count of ready FDs, and the user process must again iterate through all FDs to find the ready ones.
Key Constraints
fd_setis a fixed‑size bitmask, limited to 1024 FDs by default.- Every call triggers a full traversal of the FD set (O(n) complexity) and requires the user to rebuild and recopy the FD set into the kernel.
- Ready FDs are identified by manual user‑space iteration.
API Sketch
#include <sys/select.h>
int select(int max_fd_plus_one, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
// Helper macros
FD_ZERO(fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_CLR(int fd, fd_set *set);
poll
poll removes the fixed FD limit by using a dynamical sized array, though the core scanning logic remains the same.
How It Works
The application passes an array of struct pollfd, each element specifying an FD and the events of interest (e.g., POLLIN, POLLOUT). The kernel traverses this array, sets the revents field on each ready FD, and returns the total number of ready descriptors. The application then scans the array to extract ready entries.
API Sketch
#include <poll.h>
struct pollfd {
int fd;
short events; // requested events
short revents; // returned ready events
};
int poll(struct pollfd *fds, nfds_t nfds, int timeout_millis);
Improvements and Remaining Issues
- No built‑in FD count ceiling (limited only by system memory).
eventsandreventsare separate, avoiding the need to rebuild the entire set on every call.- Still requires O(n) kernel traversal and user‑space scanning; repeated user‑to‑kernel copying persists.
epoll
Introduced in Linux 2.6, epoll addresses the scalability bottlenecks of select/poll through an event‑driven, O(1) architecture. Most JVM NIO implementations, Nginx, and Redis rely on it.
Core Design
| Component | Purpose |
|---|---|
| Red‑black tree | Stores all registered FDs and their event subscriptions. Insertion and deletion are O(log n). |
| Ready list | A linked list where the kernel places FDs that have become ready. |
| Callback mechanism | A callback is attached to each FD when registered; the driver invokes it as soon as data arrives, moving the FD to the ready list. |
Detailed Flow
epoll_create
Creates an epoll instance: a red‑black tree (for all watched FDs), a ready list (for ready FDs), and a wait queue (for sleeping application threads).epoll_ctl
Adds, modifies, or removes an FD from the red‑black tree. When adding, the kernel registers a driver‑level callback that enqueues the FD onto the ready list once data becomes available.epoll_wait
Checks the ready list. If it is non‑empty, the kernel directly returns the ready FDs and their events. If empty, the calling thread is placed on the wait queue until a callback fires or the timeout expires.- Processing
The application handles only the returned ready FDs. Already‑registered FDs remain in the tree and do not need re‑registration.
API Sketch
#include <sys/epoll.h>
int epoll_create(int size_hint);
int epoll_ctl(int epfd, int operation, int fd, struct epoll_event *ev);
int epoll_wait(int epfd, struct epoll_event *out_events,
int max_events, int timeout_millis);
// Event structure
struct epoll_event {
uint32_t events; // EPOLLIN, EPOLLOUT, ...
epoll_data_t data; // fd or user pointer
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
Trigger Modes
- Level‑triggered (LT, default) –
epoll_waitcontinues to report an FD as long as it remains ready. - Edge‑triggered (ET) – Reports the FD exactly once when its state transitions from not‑ready to ready. This demands that the application drains all data immediately and is often paired with non‑blocking I/O for higher efficiency.
Side‑by‑Side Comparison
| Dimension | select | poll | epoll |
|---|---|---|---|
| Max FDs | 1024 (configurable) | Limited by system memory | Limited by system file limit |
| Time complexity | O(n) – scan all | O(n) – scan all | O(1) – only ready FDs |
| Storage structure | fd_set bitmask |
struct pollfd array |
Red‑black tree + ready list |
| Ready discovery | Application scans all FDs | Application scans all FDs | Kernel returns ready list |
| Data copy overhead | Full set recopied each call | Full array recopied each call | Only on registration; reused |
| Trigger modes | Level‑triggered | Level‑triggered | LT + Edge‑triggered |
| Ideal workload | Very few FDs, cross‑platform | Moderate FDs, cross‑platform | Tens of thousands+ FDs, Linux |
When to Use Each
- select – Cross‑platform code, embedded systems, or legacy environments where FD counts stay under a thousand.
- poll – Medium‑scale workloads that exceed 1024 FDs or need macOS/BSD support (where epoll is unavailable).
- epoll – High‑performance Linux servers handling massive connections: instant messengers, load balancers, message brokers, and any scenario requiring minimal latency at scale.
Java NIO and epoll
java.nio.channels.Selector delegates to the best available system call:
- Linux → epoll (since JDK 1.5)
- Windows → IOCP
- macOS/BSD → kqueue
- Fallback to poll/select on older kernels
The mapping is direct:
Selector.open()⇒epoll_createchannel.register(sel, ops)⇒epoll_ctlselector.select()⇒epoll_waitselector.selectedKeys()⇒ iterating the ready list returned by epoll