select() System Call – Introduction

 

select() System Call – Introduction
Chapter 63 – Alternative I/O Models | The Linux Programming Interface
Topic
I/O Multiplexing
Level
Intermediate
Part
1 of 3

What is I/O Multiplexing?

Imagine you have 10 open file descriptors — sockets, pipes, files — and you need to know which one has data ready to read without blocking forever on any one of them. That is exactly the problem I/O multiplexing solves.

The select() system call is one of the oldest and most widely supported ways to monitor multiple file descriptors at the same time. You tell the kernel: “watch these descriptors and wake me up when at least one of them is ready for reading, writing, or has an exceptional condition.” The kernel does the waiting; your program stays in control.

This is used everywhere: web servers, chat programs, terminal emulators, network daemons — any place where one thread needs to handle multiple I/O channels without blocking.

Key Terms in This Page

select() fd_set FD_ZERO FD_SET FD_CLR FD_ISSET timeval nfds FD_SETSIZE timeout

select() Function Signature

The select() call lives in <sys/select.h>. Here is its signature:

#include <sys/select.h>

int select(int nfds,
           fd_set *readfds,
           fd_set *writefds,
           fd_set *exceptfds,
           struct timeval *timeout);

/* Returns: number of ready file descriptors,
            0 on timeout, -1 on error */

Let’s understand each argument one by one.

Parameter Type What it means
nfds int Highest file descriptor number in any set, plus one. Tells kernel how many descriptors to scan.
readfds fd_set * Set of descriptors to watch for incoming data (read ready). Can be NULL.
writefds fd_set * Set of descriptors to watch for write readiness (space in send buffer). Can be NULL.
exceptfds fd_set * Set of descriptors to watch for exceptional conditions (e.g. TCP out-of-band data). Can be NULL.
timeout struct timeval * How long to wait. NULL means wait forever. Zero fields mean poll and return immediately.

fd_set and the Four Macros

An fd_set is just a fixed-size bitset. Each bit position corresponds to one file descriptor. The kernel defines the type; you never access bits directly. Instead, you use four macros:

FD_ZERO(&set)
Clears all bits. Always call this before using a set. Forgetting this causes random behavior.
FD_SET(fd, &set)
Sets bit for fd. Adds that descriptor to the set you want to watch.
FD_CLR(fd, &set)
Clears bit for fd. Removes that descriptor from the set.
FD_ISSET(fd, &set)
Returns nonzero if bit for fd is set. Use this AFTER select() returns to check which fds are ready.

The maximum number of file descriptors you can put in a set is FD_SETSIZE, which is typically 1024 on Linux. Trying to add a descriptor with a number ≥ FD_SETSIZE is undefined behavior — it can corrupt memory or silently do nothing.

#include <sys/select.h>
#include <stdio.h>

int main(void)
{
    fd_set readfds;

    /* Step 1: always zero out the set first */
    FD_ZERO(&readfds);

    /* Step 2: add the descriptors you want to watch */
    FD_SET(0, &readfds);   /* stdin */
    FD_SET(5, &readfds);   /* some open socket */

    /* nfds must be highest fd + 1 */
    int nfds = 5 + 1;       /* = 6 */

    /* Step 3: call select() ... (see next section) */

    /* Step 4: after select() returns, check who is ready */
    if (FD_ISSET(0, &readfds))
        printf("stdin is ready to read\n");
    if (FD_ISSET(5, &readfds))
        printf("fd 5 is ready to read\n");

    return 0;
}

Why nfds = highest_fd + 1? The kernel loops from 0 to nfds-1 checking each bit. Passing the exact highest descriptor plus one tells it exactly where to stop — not more, not less.

The timeval Structure – Controlling the Timeout

The timeout argument controls how long select() will wait. It uses the struct timeval type:

struct timeval {
    time_t      tv_sec;   /* seconds (whole part) */
    suseconds_t tv_usec;  /* microseconds (fractional part) */
};

Three Timeout Modes
timeout = NULL
Block forever. select() waits until at least one descriptor becomes ready or a signal arrives. Use this when you have nothing else to do while waiting.
tv_sec > 0 or tv_usec > 0
Wait up to N seconds/microseconds. If no descriptor becomes ready within the time limit, select() returns 0. The program can then do other work or retry.
tv_sec=0 tv_usec=0
Poll – return immediately. select() checks all descriptors right now and returns instantly. Useful for checking without committing to any wait.
struct timeval timeout;

/* Example: wait up to 5 seconds 500000 microseconds (5.5 s) */
timeout.tv_sec  = 5;
timeout.tv_usec = 500000;

int ready = select(nfds, &readfds, NULL, NULL, &timeout);

if (ready > 0)
    printf("ready: %d descriptor(s)\n", ready);
else if (ready == 0)
    printf("timed out – no activity\n");
else
    perror("select");

Important Linux behavior: On Linux, after select() returns, the timeout structure is updated to show how much time was left when the call returned. This is not portable — most other UNIX systems leave it unchanged. If you use select() in a loop, always re-initialize the timeval structure before each call.

/* Portable loop pattern – always reinitialize timeout */
while (1) {
    struct timeval tv;
    tv.tv_sec  = 3;
    tv.tv_usec = 0;

    fd_set rfds;
    FD_ZERO(&rfds);
    FD_SET(sockfd, &rfds);

    int n = select(sockfd + 1, &rfds, NULL, NULL, &tv);
    if (n < 0) { perror("select"); break; }
    if (n == 0) { printf("timeout, retrying\n"); continue; }
    if (FD_ISSET(sockfd, &rfds)) handle_incoming();
}

The FD_SETSIZE Limit and What It Means

FD_SETSIZE is typically 1024 on Linux. This means select() can only watch file descriptors numbered 0 through 1023. In practice, a server that opens more than 1024 simultaneous connections cannot use select() safely for all of them.

fd_set visualized as a bit array
fd 0
SET
fd 1
0
fd 2
0
fd 3
SET
fd 4
0
fd 5
SET

fd 1023
0
Each bit = one file descriptor. FD_SET turns a bit on; FD_CLR turns it off; FD_ISSET checks it.
/* Safe guard: always check before adding to set */
if (new_fd >= FD_SETSIZE) {
    fprintf(stderr, "fd %d exceeds FD_SETSIZE (%d)\n",
            new_fd, FD_SETSIZE);
    close(new_fd);
    return -1;
}
FD_SET(new_fd, &readfds);

For servers needing thousands of connections, use poll() or epoll() instead. They have no such fixed limit.

Historic Trick: Using select() as a Subsecond Sleep

Before nanosleep() existed on older UNIX systems, programmers used select() to sleep for less than one second. You pass nfds=0, all three sets as NULL, and put the desired sleep time in timeout. select() with no descriptors to watch just sleeps for the timeout duration.

/* Sleep for 250 milliseconds – old-school portable trick */
struct timeval snooze;
snooze.tv_sec  = 0;
snooze.tv_usec = 250000;   /* 250 ms */
select(0, NULL, NULL, NULL, &snooze);

/* Modern equivalent – use nanosleep() instead */
struct timespec ts = { .tv_sec = 0, .tv_nsec = 250000000 };
nanosleep(&ts, NULL);

This trick still works today, though nanosleep() is the correct modern approach.

Interview Questions – select() Basics
Q1. What is the purpose of the nfds argument in select()? How do you calculate it?

nfds tells the kernel how many file descriptors to scan, starting from 0. You calculate it as the highest file descriptor number you added to any of the three sets, plus one. For example, if the highest fd you added is 7, nfds must be 8. Passing a value that is too low means select() will miss some descriptors silently.

Q2. Why must you call FD_ZERO() before using an fd_set?

An fd_set is a local variable and its contents are undefined until initialized. Uninitialized bits could look like set bits, causing select() to monitor random file descriptors. FD_ZERO() clears all bits to zero so you start with a clean, empty set.

Q3. What happens to the fd_sets after select() returns?

select() modifies the fd_set arguments in place. After it returns, only the bits for descriptors that are actually ready remain set. Any descriptor that was not ready has its bit cleared. This means if you call select() in a loop, you must rebuild the sets from scratch before every call — you cannot reuse the returned sets as input.

Q4. What does it mean to pass timeout as NULL vs. a zeroed timeval?

NULL means block indefinitely — select() will not return until a descriptor is ready or a signal arrives. A timeval with both fields set to zero is a poll — select() checks all descriptors immediately and returns at once, even if none are ready. These are opposite behaviors despite looking similar.

Q5. What is FD_SETSIZE and why does it matter for high-performance servers?

FD_SETSIZE is the maximum number of file descriptors an fd_set can hold, typically 1024 on Linux. A server managing more than 1024 simultaneous connections cannot use select() because descriptors with numbers at or above 1024 cannot be safely added. For such workloads, poll() or epoll() are used instead since they have no fixed descriptor limit.

Q6. Is the timeval timeout modified by select() on Linux? Is this portable?

Yes, on Linux, select() updates the timeval to reflect how much time remained when the call returned. This is not portable — most other UNIX implementations leave timeval unchanged. Portable code must reinitialize the timeval structure before every select() call inside a loop.

Q7. Can the same file descriptor appear in more than one of the three fd_sets?

Yes. You can add the same fd to readfds, writefds, and exceptfds simultaneously. After select() returns, if that fd is ready for more than one type of event, it will be marked in each set independently. The total count returned by select() will count it once for each set in which it appears ready.

Continue Learning

Next: understand what select() returns and how blocking and signals interact with it.

Part 2: Return Values & Blocking → EmbeddedPathashala Home

Leave a Reply

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