Alternative I/O Models I/O Multiplexing | select() System Call

 

Alternative I/O Models – Part 2
Chapter 63 | I/O Multiplexing | select() System Call
63.2
Section
1024
FD_SETSIZE limit
TLPI
Source

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

I/O Multiplexing select() fd_set FD_ZERO FD_SET FD_CLR FD_ISSET FD_SETSIZE readfds writefds exceptfds struct timeval nfds

1. What is I/O Multiplexing?

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.

Diagram: Without multiplexing vs With multiplexing
Without Multiplexing
Thread 1 → blocked on fd 0
Thread 2 → blocked on fd 1
Thread 3 → blocked on fd 2
One thread per fd — resource waste
With Multiplexing (select)
Single Thread
select(fd0, fd1, fd2, …)
Returns when ANY fd is ready
One thread handles all fds efficiently

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.

2. The select() System Call — Signature and Arguments
#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

3. The fd_set Type and FD_* Macros

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? */

Diagram: fd_set as a bitmask (simplified)
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.

Important gotcha: select() modifies the fd_set structures on return — they now contain only the ready fds. So if you call select() in a loop, you must reinitialize the fd_sets with FD_ZERO + FD_SET before each call. This is a common bug.

4. What Does “Exceptional Condition” Actually Mean?

Many beginners think exceptfds is for errors. It is not. “Exceptional condition” on Linux means only two specific things:

Condition 1: Pseudoterminal packet mode
A state change on a pseudoterminal slave whose master is in packet mode (used for terminal multiplexers like screen/tmux internals)
Condition 2: Out-of-band data on TCP socket
TCP urgent data (OOB) arrives on a stream socket. This is a rarely used TCP feature (the URG flag).

In most real programs, you pass NULL for exceptfds unless you specifically need one of the above.

5. The nfds Argument — Why “highest + 1”?

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);

Diagram: How nfds limits kernel scanning
fd numbers 0 1 15 16 1023
Kernel scans? YES — scanned (nfds=16) SKIP — not scanned (saves time)

6. Complete Working Example — select() Echo Server

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;
}
Compile and test:
gcc -o server server.c
./server &
echo "hello" | nc localhost 8080

7. The Critical select() Loop Pattern

The most common mistake with select() is forgetting to reinitialize the fd_sets. Here is the correct pattern broken into steps:

Diagram: select() Event Loop Flow
Start event loop
Step 1 (Every iteration!)
FD_ZERO(&readfds)
FD_SET(all fds)
Step 2
select(nfds+1, &readfds, …)
Blocks until fd ready
Step 3
FD_ISSET(fd, &readfds)
to find which fds are ready
Step 4
Handle each ready fd
(read/write/accept)
↑ Loop back to Step 1

8. Limitations of select()
Limit 1: FD_SETSIZE = 1024
Cannot monitor more than 1024 fds. Modern servers handle millions of connections — select() cannot scale.
Limit 2: O(n) scan every call
Kernel scans all fds from 0 to nfds-1 every call. With 1000 fds, it checks 1000 bits even if only 1 is ready.
Limit 3: fd_sets modified on return
Must reinitialize all three fd_sets before every call. Easy to forget, causes subtle bugs.
Limit 4: User/kernel copy overhead
The entire fd_set is copied from user space to kernel space on every call — expensive for large fd sets.

For all these reasons, Linux provides epoll as the scalable alternative. epoll uses O(1) notification regardless of total fd count.

Interview Questions & Answers
Q1: What does I/O multiplexing mean and which system calls provide it?
I/O multiplexing means monitoring multiple file descriptors in a single blocking call and returning when any of them become ready. In Linux, select() and poll() provide this. select() was from BSD, poll() from System V. Both are POSIX-required. epoll() is the modern Linux-specific scalable replacement.
Q2: Why must you reinitialize fd_sets before every select() call?
select() modifies the fd_set structures in place. On return, they contain only the fds that are currently ready, not all the fds you originally put in. If you reuse them without reinitializing, you will only watch the fds that were ready last time — you lose track of all other fds.
Q3: What is FD_SETSIZE and why is it a problem?
FD_SETSIZE is the maximum number of file descriptors that fit in an fd_set. On Linux it is 1024. This limits select() to monitoring at most 1024 fds. You cannot change this easily in glibc without modifying headers. For large-scale servers, use epoll which has no such limit.
Q4: What does the nfds argument to select() do?
nfds tells the kernel the upper bound of fd numbers to check. It must be set to one greater than the highest fd number in any of the three fd_sets. The kernel only scans fd bits 0 to nfds-1, which avoids checking all 1024 bits when your highest fd is small. Setting it wrong (too low) can cause fds to be missed.
Q5: What does exceptfds monitor in select()?
It monitors for exceptional conditions, not errors. On Linux this means: (1) a state change on a pseudoterminal slave in packet mode, or (2) out-of-band (urgent) data received on a TCP stream socket. Most programs pass NULL for exceptfds because these situations are rare.
Q6: How do you use select() with a timeout?
Pass a pointer to a struct timeval with tv_sec (seconds) and tv_usec (microseconds). Set both to 0 for a non-blocking poll (returns immediately). Pass NULL for no timeout (blocks forever). On return 0 means timeout expired, positive means fds became ready, -1 means error.
Q7: What are the main disadvantages of select() compared to epoll?
Four main disadvantages: (1) Hard limit of 1024 fds (FD_SETSIZE). (2) O(n) scan cost — kernel checks every fd up to nfds each call even if only one is ready. (3) fd_sets are value-result and must be rebuilt before every call. (4) Full fd_set bitmask copied from user to kernel space on every call. epoll avoids all of these — it uses an event-driven model with O(1) per-event cost.
Q8: Write code to monitor stdin and a socket fd with select()
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).

← Part 1: Notification Models EmbeddedPathashala Home

Leave a Reply

Your email address will not be published. Required fields are marked *