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.

IT Services Circle
IT Services Circle
IT Services Circle
select, poll, and epoll: What’s the real difference? A deep dive into I/O multiplexing

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

LinuxI/O multiplexingepollpollselectnon-blocking I/Oedge-triggeredlevel-triggered
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.