I/O Multiplexing solves the CPU waste problem of tight polling loops. Instead of spinning and calling read() over and over, your process calls select() or poll() and sleeps until at least one file descriptor becomes ready. The kernel wakes your process only when something interesting happens. No wasted CPU. No missed events.
These are the oldest and most portable alternative I/O APIs in the UNIX world. Every serious network programmer needs to understand them.
The word multiplexing means handling many things through a single channel. In I/O multiplexing, a single system call monitors many file descriptors at once and tells you which ones are ready.
You give select()/poll() a list of file descriptors and what events you care about (read ready? write ready? error?)
The kernel monitors all FDs. Your process uses zero CPU. You can set a timeout so you wake up after N seconds even if nothing is ready.
When at least one FD is ready, the kernel wakes your process and tells you which ones are ready.
You call read()/write() only on the FDs that are confirmed ready. No blocking. No EAGAIN. Then go back to step 1.
select() is the oldest I/O multiplexing API. It has been part of UNIX since BSD 4.2. Every UNIX system supports it.
#include <sys/select.h>
#include <sys/time.h>
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
/*
* Returns: number of ready FDs on success
* 0 if timeout expired (no FD became ready)
* -1 on error (check errno)
*/
| Parameter | Type | What it Means |
|---|---|---|
| nfds | int | Highest FD number in your sets + 1. e.g. if you watch FD 5 and FD 7, nfds = 8 |
| readfds | fd_set * | Set of FDs to watch for readability (data available to read) |
| writefds | fd_set * | Set of FDs to watch for writability (space in send buffer) |
| exceptfds | fd_set * | Set of FDs to watch for exceptional conditions (e.g. out-of-band TCP data) |
| timeout | struct timeval * | Max time to wait. NULL = wait forever. {0,0} = return immediately (poll) |
You manipulate fd_set structures using four macros:
fd_set readfds;
FD_ZERO(&readfds); /* Clear all bits in the set (always do this first!) */
FD_SET(fd, &readfds); /* Add fd to the set */
FD_CLR(fd, &readfds); /* Remove fd from the set */
FD_ISSET(fd, &readfds); /* Test if fd is set (use after select() returns) */
fd_set is a fixed-size bitmask. Its size is defined by FD_SETSIZE, which is typically 1024 on Linux. This means select() can only watch file descriptors with numbers 0 to 1023. If your server opens more than 1024 file descriptors (common in high-load servers), select() simply cannot monitor the higher ones. This is one of the main reasons select() does not scale.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
/*
* Monitor stdin (fd=0) and a pipe read end simultaneously.
* Whichever has data first gets processed.
* Demonstrates the select() pattern.
*/
int main() {
int pipefd[2];
char buf[256];
ssize_t n;
fd_set readfds;
struct timeval timeout;
int ret, maxfd;
/* Create a pipe */
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
/* Write something into the pipe (simulating data from a client) */
write(pipefd[1], "pipe data!", 10);
maxfd = pipefd[0]; /* stdin=0, pipe read end. pipefd[0] is higher */
printf("Waiting for data on stdin or pipe...\n");
/* select() loop */
while (1) {
/*
* IMPORTANT: Rebuild fd_set before EVERY call to select().
* select() modifies the sets to show only ready FDs.
*/
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds); /* watch stdin */
FD_SET(pipefd[0], &readfds); /* watch pipe read end */
/* Timeout: wait up to 5 seconds */
timeout.tv_sec = 5;
timeout.tv_usec = 0;
ret = select(maxfd + 1, &readfds, NULL, NULL, &timeout);
if (ret == -1) {
perror("select");
break;
} else if (ret == 0) {
/* Timeout expired. No FD became ready in 5 seconds. */
printf("Timeout! No data arrived in 5 seconds.\n");
break;
}
/* ret > 0: at least one FD is ready */
/* Check stdin */
if (FD_ISSET(STDIN_FILENO, &readfds)) {
n = read(STDIN_FILENO, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("stdin: got '%s'\n", buf);
} else if (n == 0) {
printf("stdin: EOF\n");
break;
}
}
/* Check pipe */
if (FD_ISSET(pipefd[0], &readfds)) {
n = read(pipefd[0], buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("pipe: got '%s'\n", buf);
break; /* pipe data consumed, exit loop */
}
}
}
close(pipefd[0]);
close(pipefd[1]);
return 0;
}
Compile: gcc -o select_demo select_demo.c && ./select_demo
poll() was designed to fix some of the awkward parts of select(). Instead of three separate fd_sets, poll() takes an array of struct pollfd structures. Each structure describes one file descriptor and what events you want to watch.
#include <poll.h>
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
/*
* Returns: number of ready FDs on success
* 0 if timeout expired
* -1 on error
*/
struct pollfd {
int fd; /* File descriptor to watch */
short events; /* Events we are interested in (input bitmask) */
short revents; /* Events that actually occurred (output bitmask) */
};
| Flag | In events? | In revents? | Meaning |
|---|---|---|---|
| POLLIN | โ Yes | โ Yes | Data available to read |
| POLLOUT | โ Yes | โ Yes | Space available to write (without blocking) |
| POLLPRI | โ Yes | โ Yes | Urgent/out-of-band data available |
| POLLERR | โ No | โ Yes | Error condition on FD |
| POLLHUP | โ No | โ Yes | Hangup (e.g. pipe write end closed) |
| POLLNVAL | โ No | โ Yes | Invalid fd (not open) |
| Value | Behavior |
|---|---|
| -1 | Block indefinitely until an event occurs |
| 0 | Return immediately (non-blocking check) |
| N (positive) | Wait at most N milliseconds |
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
/*
* Simple poll() example: monitor stdin and a pipe simultaneously.
* poll() is cleaner than select() - no fd_set rebuilding needed,
* no FD_SETSIZE limit.
*/
int main() {
int pipefd[2];
char buf[256];
ssize_t n;
int ret;
/* Create a pipe and put some data in it */
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
write(pipefd[1], "Hello from pipe!", 16);
/*
* Set up the pollfd array.
* One entry per FD we want to monitor.
* Unlike select(), no need to track the highest FD number.
*/
struct pollfd fds[2];
fds[0].fd = STDIN_FILENO; /* stdin */
fds[0].events = POLLIN; /* watch for readable */
fds[0].revents = 0; /* kernel fills this */
fds[1].fd = pipefd[0]; /* pipe read end */
fds[1].events = POLLIN;
fds[1].revents = 0;
printf("Monitoring stdin and pipe with poll()...\n");
/*
* poll() loop.
* Unlike select(), poll() does NOT modify the events field,
* only the revents field. So you do NOT need to rebuild fds[]
* before each call (though you should reset revents to 0).
*/
while (1) {
/* Reset revents before each call */
fds[0].revents = 0;
fds[1].revents = 0;
/* Wait up to 5000ms (5 seconds) */
ret = poll(fds, 2, 5000);
if (ret == -1) {
perror("poll");
break;
}
if (ret == 0) {
printf("Timeout: no events in 5 seconds\n");
break;
}
/* Check stdin */
if (fds[0].revents & POLLIN) {
n = read(STDIN_FILENO, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("stdin ready: got '%s'\n", buf);
} else if (n == 0) {
printf("stdin: EOF\n");
}
}
if (fds[0].revents & POLLERR) {
printf("stdin: error condition\n");
}
/* Check pipe */
if (fds[1].revents & POLLIN) {
n = read(pipefd[0], buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("pipe ready: got '%s'\n", buf);
break;
}
}
if (fds[1].revents & POLLHUP) {
printf("pipe: write end closed (HUP)\n");
break;
}
}
close(pipefd[0]);
close(pipefd[1]);
return 0;
}
| Feature | select() | poll() |
|---|---|---|
| Portability | โญ Excellent (POSIX, every UNIX) | โ Good (POSIX) |
| Max FD limit | 1024 (FD_SETSIZE) | No hard limit |
| Rebuild sets each call? | Yes (modifies input sets) | No (only revents changes) |
| Data structure | fd_set bitmask | Array of struct pollfd |
| Timeout precision | Microseconds (struct timeval) | Milliseconds (int) |
| Event granularity | Read/Write/Except only | Many event types (POLLIN, POLLHUP, etc.) |
| Performance (many FDs) | Poor (O(n) scan) | Moderate (O(n) scan) |
| Windows support | โ Yes (Winsock) | โ No (not available on Windows) |
Both select() and poll() have an O(n) scalability problem. Even if only one file descriptor out of a thousand is ready, the kernel still has to scan through all n FDs to find which one is ready. Your application then also has to scan through all the returned results to find the ready ones. As n grows to thousands, this becomes very slow.
With 10,000 FDs and only 1 ready, 9,999 FDs are checked needlessly on every call
Additionally, each call to poll()/select() requires copying the entire FD list from user space into kernel space. With thousands of FDs this copying overhead becomes significant. This is why epoll was invented. epoll registers interest once and only notifies you about the FDs that actually become ready.
It is the highest file descriptor number in any of your fd_sets, plus one. For example if you are watching FD 3 and FD 7, you pass nfds = 8. The kernel uses this to know how many bits to scan in the bitmask. Passing too small a value means some FDs won’t be monitored. Passing a too-large value is wasteful but not incorrect.
select() modifies the fd_set in place. When it returns, the fd_set only contains the FDs that are currently ready, not the original set of FDs you wanted to monitor. So on the next iteration of your loop, if you pass the modified set, some FDs will be missing. You must call FD_ZERO and re-add all your FDs each time. poll() does not have this problem because it uses a separate revents field.
FD_SETSIZE is a compile-time constant (typically 1024 on Linux) that defines the maximum number of file descriptors that an fd_set can hold. select() can only monitor FDs with numbers 0 to FD_SETSIZE-1. High-performance servers that open thousands of connections cannot use select() because their FD numbers exceed this limit. poll() and epoll have no such restriction.
poll() returns 0. This is not an error. It means no file descriptor became ready within the specified timeout period. You can use this to perform periodic housekeeping work, like checking connection health or logging statistics.
events is an input field that you set before calling poll(). It tells the kernel what events you are interested in for that FD. revents is an output field that the kernel fills in when poll() returns. It tells you which events actually occurred. You should check revents after poll() returns to decide what action to take. Always reset revents to 0 before calling poll() again.
Both have O(n) complexity. The kernel must scan through every monitored FD to find which are ready, even if only one is. Additionally, the entire list of FDs is copied from user space to kernel space on every call. With 10,000 FDs, this is a lot of work even if only one FD becomes ready. epoll avoids this by using an event-driven model where the kernel only returns the FDs that are actually ready.
Set its fd field to -1. The kernel ignores entries with fd = -1 and sets revents to 0 for them. This is useful when you want to temporarily disable monitoring of an FD without resizing the array, and re-enable it later by setting fd back to the actual descriptor value.
POLLHUP means the remote end has closed the connection or a pipe’s write end was closed. It is a normal shutdown event. POLLERR means an error condition occurred on the file descriptor, like a socket error. Both can appear in revents simultaneously. After POLLHUP on a socket you should close that FD. After POLLERR you should call getsockopt with SO_ERROR to find out what error occurred.
