select, poll, and epoll: What’s the real difference? A deep dive into I/O multiplexing
This article explains the concepts of synchronous vs. asynchronous and blocking vs. non‑blocking I/O, outlines the five Unix I/O models, and then compares select, poll, and epoll—including their internal mechanisms, performance trade‑offs, edge‑ and level‑triggered modes, and why epoll is considered the solution to the C10K problem.
Overview
Synchronization and asynchronization differ in whether the caller must wait for a result. Blocking suspends the calling thread until data is copied from kernel to user space; non‑blocking returns immediately and the caller must poll or be notified later.
Unix I/O Model Overview
An input operation consists of two phases: waiting for data to become ready and copying data from the kernel to the process. Linux provides five I/O models: blocking I/O, non‑blocking I/O, I/O multiplexing (select/poll), signal‑driven I/O (SIGIO), and asynchronous I/O (AIO).
Blocking I/O
The process is blocked until recvfrom copies data into the user buffer. Other processes can still run, so CPU is not consumed.
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);Non‑blocking I/O
After the system call returns an error code, the process continues and repeatedly polls to check whether the I/O has completed, which is inefficient because of extra system calls.
I/O Multiplexing (select/poll)
Select or poll waits for events on multiple sockets. The kernel checks all file descriptors, marks those that are ready, and returns the set to user space, where the application iterates to handle the ready sockets.
Both use a linear data structure, requiring O(n) traversal and copying of the descriptor set between user and kernel space, which becomes costly as the number of connections grows.
Select
Select copies the entire descriptor set to the kernel, the kernel traverses it, then copies the set back. It uses a fixed‑size bitmap limited by FD_SETSIZE (default 1024), so it cannot monitor descriptors above that value. Two copies and two traversals occur per call.
Poll
Poll replaces the bitmap with a dynamic array, removing the hard descriptor limit, but still performs linear traversal and copying, so its performance degrades similarly to select under high concurrency.
epoll
epoll solves the scalability problem by maintaining a red‑black tree in the kernel that tracks all monitored sockets. Adding or removing a socket via epoll_ctl() costs O(log n). When an event occurs, the kernel places the ready socket into a linked list, and epoll_wait() returns only those ready descriptors, eliminating full‑set traversal.
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...);
listen(s, ...);
int epfd = epoll_create(...);
epoll_ctl(epfd, ...); // add sockets to epfd
while (1) {
int n = epoll_wait(...);
for (each socket with data) {
// handle
}
}epoll also supports edge‑triggered (ET) and level‑triggered (LT) modes.
Edge‑triggered (ET)
When an event is reported, the server is awakened only once; the application must read all available data in that wake‑up, otherwise the kernel will not notify again, leading to possible empty‑poll loops if the socket is left in blocking mode.
Level‑triggered (LT)
The kernel continues to notify as long as data remains unread, reducing the risk of empty polls but potentially generating more wake‑ups.
In practice, ET combined with non‑blocking I/O yields higher efficiency because it reduces the number of epoll_wait() system calls.
Comparison
Blocking, non‑blocking, I/O multiplexing, signal‑driven, and asynchronous I/O are all synchronous at the data‑copy stage; only the waiting phase differs.
Select and poll share the same linear‑structure limitation, causing O(n) traversal and copying overhead that grows with connection count.
epoll uses a kernel‑side red‑black tree (O(log n)) and an event‑driven linked list, eliminating the need to copy the full descriptor set and scaling to the maximum number of file descriptors allowed by the process.
epoll supports both ET and LT; ET is generally more efficient but requires careful handling to avoid empty‑poll problems.
Conclusion
Traditional per‑connection thread or process models cannot handle tens of thousands of simultaneous clients due to scheduling and memory overhead. I/O multiplexing allows a single process to manage many connections. Among the three Linux APIs, select and poll suffer from linear‑structure inefficiencies, while epoll, with its red‑black‑tree tracking and event‑driven design, overcomes these limitations and is regarded as the key tool for solving the C10K problem.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
IT Services Circle
Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
