What You Will Learn
Before epoll existed, select() was the standard way to monitor multiple file descriptors in one call. Understanding select() deeply is important for embedded systems, legacy code, and interviews. This tutorial covers I/O multiplexing concepts, the select() system call, fd_set manipulation, and real working code examples.
Key Terms
Think of a call center operator who handles many phone lines. Instead of dedicating one person per line (one thread per connection), one person watches all lines and picks up whichever one becomes active. That is I/O multiplexing.
In Linux, I/O multiplexing means calling a single system call that watches multiple file descriptors at once and returns when any of them becomes ready for I/O. This lets one thread handle many connections without blocking on any one of them.
What can you monitor with select()/poll()? Regular files, terminals, pseudoterminals, pipes, FIFOs, sockets, and some character devices.
History: select() came with BSD sockets and was the first widely used multiplexing call. poll() appeared later in System V. Both are required by the POSIX standard (SUSv3) today. On Linux, epoll is the modern preferred alternative for large-scale use.
#include <sys/time.h> /* for portability */
#include <sys/select.h>
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
/* Returns: number of ready fds, 0 on timeout, -1 on error */
Five arguments — let’s understand each one:
| Argument | Type | Purpose |
|---|---|---|
| nfds | int | One more than the highest fd number in any set. Helps kernel scan efficiently. |
| readfds | fd_set * | Set of fds to watch for readable data (input available) |
| writefds | fd_set * | Set of fds to watch for writable space (output possible) |
| exceptfds | fd_set * | Set of fds to watch for exceptional conditions (OOB data, pty packet mode) |
| timeout | struct timeval * | Max time to wait. NULL = block forever, {0,0} = return immediately (poll) |
The struct timeval:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
// Block up to 5 seconds
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
// Return immediately (non-blocking poll)
timeout.tv_sec = 0;
timeout.tv_usec = 0;
// Block forever (no timeout)
select(nfds, &readfds, NULL, NULL, NULL); // timeout = NULL
An fd_set is essentially a bitmask — one bit per file descriptor. You do not manipulate the bits directly. Instead, four macros do all the work:
#include <sys/select.h>
void FD_ZERO(fd_set *fdset); /* Clear all bits — initialize the set */
void FD_SET(int fd, fd_set *fdset); /* Set bit for fd — add fd to set */
void FD_CLR(int fd, fd_set *fdset); /* Clear bit for fd — remove fd from set */
int FD_ISSET(int fd, fd_set *fdset); /* Check if fd bit is set — is fd in set? */
| Bit position (fd#) | 0 | 1 | 2 | 3 | 4 | … |
| After FD_ZERO | 0 | 0 | 0 | 0 | 0 | … |
| After FD_SET(0) + FD_SET(3) | 1 | 0 | 0 | 1 | 0 | … |
| After FD_CLR(0) | 0 | 0 | 0 | 1 | 0 | … |
FD_SETSIZE limit: On Linux, FD_SETSIZE = 1024. This means you cannot monitor more than 1024 file descriptors with select(). This is a fundamental scalability limitation. For servers with thousands of connections, use epoll instead.
Many beginners think exceptfds is for errors. It is not. “Exceptional condition” on Linux means only two specific things:
In most real programs, you pass NULL for exceptfds unless you specifically need one of the above.
The nfds argument tells the kernel the upper bound of fd numbers to scan. The kernel uses it to avoid checking every bit up to FD_SETSIZE (1024). Setting it correctly makes select() more efficient.
// Correct: nfds = highest fd + 1
int fd1 = 3, fd2 = 7, fd3 = 15;
int nfds = fd3 + 1; // = 16
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd1, &readfds);
FD_SET(fd2, &readfds);
FD_SET(fd3, &readfds);
// Kernel only scans bits 0..15, not all 1024
select(nfds, &readfds, NULL, NULL, NULL);
| fd numbers | 0 | 1 | … | 15 | 16 | … | 1023 |
| Kernel scans? | YES — scanned (nfds=16) | SKIP — not scanned (saves time) | |||||
This example shows a simple TCP server that uses select() to handle multiple clients without threads. It is the classic pattern for level-triggered I/O multiplexing:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#define PORT 8080
#define MAX_CLIENTS 30
#define BUF_SIZE 1024
int main(void) {
int listen_fd, conn_fd;
int client_fds[MAX_CLIENTS];
fd_set readfds;
struct sockaddr_in addr;
char buf[BUF_SIZE];
int max_fd, i, n;
/* Initialize client fd array */
for (i = 0; i < MAX_CLIENTS; i++)
client_fds[i] = -1;
/* Create listening socket */
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) { perror("socket"); exit(1); }
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind"); exit(1);
}
if (listen(listen_fd, 10) < 0) {
perror("listen"); exit(1);
}
printf("Server listening on port %d\n", PORT);
while (1) {
/* STEP 1: Always reinitialize fd_sets before each select() call
because select() MODIFIES the sets on return */
FD_ZERO(&readfds);
/* Add listening socket to set */
FD_SET(listen_fd, &readfds);
max_fd = listen_fd;
/* Add all active client sockets to set */
for (i = 0; i < MAX_CLIENTS; i++) {
if (client_fds[i] > 0) {
FD_SET(client_fds[i], &readfds);
if (client_fds[i] > max_fd)
max_fd = client_fds[i];
}
}
/* STEP 2: Wait for any fd to become readable.
nfds = max_fd + 1 (one greater than highest fd) */
int ready = select(max_fd + 1, &readfds, NULL, NULL, NULL);
if (ready < 0) {
if (errno == EINTR) continue; /* interrupted by signal */
perror("select"); exit(1);
}
/* STEP 3: Check if new connection is pending */
if (FD_ISSET(listen_fd, &readfds)) {
conn_fd = accept(listen_fd, NULL, NULL);
if (conn_fd < 0) {
perror("accept");
} else {
printf("New client connected: fd=%d\n", conn_fd);
/* Add to client array */
for (i = 0; i < MAX_CLIENTS; i++) {
if (client_fds[i] == -1) {
client_fds[i] = conn_fd;
break;
}
}
if (i == MAX_CLIENTS) {
printf("Max clients reached, rejecting\n");
close(conn_fd);
}
}
}
/* STEP 4: Check each client fd for incoming data */
for (i = 0; i < MAX_CLIENTS; i++) {
int fd = client_fds[i];
if (fd < 0) continue;
if (FD_ISSET(fd, &readfds)) {
n = read(fd, buf, sizeof(buf) - 1);
if (n <= 0) {
/* n==0: client disconnected; n<0: error */
printf("Client fd=%d disconnected\n", fd);
close(fd);
client_fds[i] = -1;
} else {
buf[n] = '\0';
printf("From fd=%d: %s", fd, buf);
/* Echo back */
write(fd, buf, n);
}
}
}
}
close(listen_fd);
return 0;
}
gcc -o server server.c./server &echo "hello" | nc localhost 8080The most common mistake with select() is forgetting to reinitialize the fd_sets. Here is the correct pattern broken into steps:
FD_SET(all fds)
Blocks until fd ready
to find which fds are ready
(read/write/accept)
For all these reasons, Linux provides epoll as the scalable alternative. epoll uses O(1) notification regardless of total fd count.
fd_set readfds;
int sock_fd = 5; /* assume socket already created */
int max_fd = sock_fd + 1; /* sock_fd is highest */
char buf[256];
while (1) {
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds); /* fd 0 */
FD_SET(sock_fd, &readfds); /* fd 5 */
int ready = select(max_fd, &readfds, NULL, NULL, NULL);
if (ready < 0) { perror("select"); break; }
if (FD_ISSET(STDIN_FILENO, &readfds)) {
int n = read(STDIN_FILENO, buf, sizeof(buf));
if (n > 0) printf("stdin: %.*s\n", n, buf);
}
if (FD_ISSET(sock_fd, &readfds)) {
int n = read(sock_fd, buf, sizeof(buf));
if (n > 0) printf("socket: %.*s\n", n, buf);
}
}
What’s Next?
You now understand both notification models and the select() API. The next step is poll() (simpler API, same level-triggered model) and then epoll (the scalable Linux-specific solution).
