select() – Complete Example Program

 

select() – Complete Example Program
Chapter 63 – Alternative I/O Models | The Linux Programming Interface
Topic
t_select.c Walkthrough
Level
Intermediate
Part
3 of 3

Putting It All Together

The best way to understand select() is to read a complete working program. The TLPI book provides t_select.c — a command-line tool that lets you watch any set of file descriptors with any timeout you choose, right from the shell. By running it with different arguments you can directly observe how select() behaves in each scenario.

This page walks through the complete program section by section, then shows you three shell session examples with detailed explanations of the output.

Key Concepts Demonstrated

Command-line fd monitoring sscanf argument parsing FD_ZERO / FD_SET loop nfds tracking NULL vs finite timeout Remaining timeout display Shell session demo

Program Design – What t_select Does

The program takes command-line arguments in this format:

./t_select {timeout|-} fd-num[rw]...

Examples:
  ./t_select 10 0r          # watch fd 0 for reading, timeout 10s
  ./t_select 0 0r           # poll fd 0 for reading, return immediately
  ./t_select - 0r 1w        # watch fd 0 read + fd 1 write, no timeout
  ./t_select 5 0r 1w 2r     # watch three fds, timeout 5s

The first argument is the timeout in seconds, or - to mean “block forever”. Remaining arguments are descriptor numbers followed by the letters r (watch for read) and/or w (watch for write). You can combine them: 0rw means watch fd 0 for both reading and writing.

Parse
argv[1]
timeout or –
Loop over
argv[2..]
parse “Nrw”
FD_SET each
fd into
readfds/writefds
Track max fd
to compute
nfds
Call
select()
Print which
fds are ready
& remaining time

The Complete t_select.c Program – Annotated

Here is the complete program with comments explaining every important decision:

#include <sys/time.h>
#include <sys/select.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

/* Print usage instructions and exit */
static void usageError(const char *progName)
{
    fprintf(stderr, "Usage: %s {timeout|-} fd-num[rw]...\n", progName);
    fprintf(stderr, "  - means infinite timeout\n");
    fprintf(stderr, "  r = monitor for read\n");
    fprintf(stderr, "  w = monitor for write\n\n");
    fprintf(stderr, "  e.g.: %s - 0rw 1w\n", progName);
    exit(EXIT_FAILURE);
}

int main(int argc, char *argv[])
{
    fd_set readfds, writefds;
    int ready, nfds, fd, numRead, j;
    struct timeval timeout;
    struct timeval *pto;          /* pointer to timeout – may be NULL */
    char buf[10];                 /* holds "r", "w", or "rw" */

    if (argc < 2 || strcmp(argv[1], "--help") == 0)
        usageError(argv[0]);

    /* -------------------------------------------------------
     * Step 1: Parse the timeout argument (argv[1])
     * "-" means block forever → pass NULL to select()
     * A number means wait that many seconds
     * ------------------------------------------------------- */
    if (strcmp(argv[1], "-") == 0) {
        pto = NULL;                   /* NULL = infinite block */
    } else {
        pto = &timeout;
        timeout.tv_sec  = atoi(argv[1]);   /* seconds */
        timeout.tv_usec = 0;               /* no sub-second precision here */
    }

    /* -------------------------------------------------------
     * Step 2: Build the fd_sets from remaining arguments
     * Each argument looks like "3rw", "0r", "5w", etc.
     * sscanf splits the number and the letter(s) apart.
     * ------------------------------------------------------- */
    nfds = 0;
    FD_ZERO(&readfds);
    FD_ZERO(&writefds);

    for (j = 2; j < argc; j++) {
        /* Parse "Nrw" into fd (integer) and buf ("r", "w", "rw") */
        numRead = sscanf(argv[j], "%d%2[rw]", &fd, buf);
        if (numRead != 2) {
            fprintf(stderr, "bad argument: %s\n", argv[j]);
            usageError(argv[0]);
        }

        /* Safety: reject descriptors that exceed fd_set capacity */
        if (fd >= FD_SETSIZE) {
            fprintf(stderr, "fd %d exceeds FD_SETSIZE (%d)\n",
                    fd, FD_SETSIZE);
            exit(EXIT_FAILURE);
        }

        /* Track the highest fd so we can compute nfds correctly */
        if (fd >= nfds)
            nfds = fd + 1;

        /* Add to readfds if 'r' was specified */
        if (strchr(buf, 'r') != NULL)
            FD_SET(fd, &readfds);

        /* Add to writefds if 'w' was specified */
        if (strchr(buf, 'w') != NULL)
            FD_SET(fd, &writefds);
    }

    /* -------------------------------------------------------
     * Step 3: Call select()
     * We don't watch for exceptional events (exceptfds = NULL)
     * ------------------------------------------------------- */
    ready = select(nfds, &readfds, &writefds, NULL, pto);

    if (ready == -1) {
        perror("select");
        exit(EXIT_FAILURE);
    }

    /* -------------------------------------------------------
     * Step 4: Display results
     * Print the total ready count, then scan each fd to see
     * which ones are ready and for which event type.
     * ------------------------------------------------------- */
    printf("ready = %d\n", ready);

    for (fd = 0; fd < nfds; fd++) {
        printf("  %d: %s%s\n", fd,
               FD_ISSET(fd, &readfds)  ? "r" : "",
               FD_ISSET(fd, &writefds) ? "w" : "");
    }

    /* On Linux: print remaining time if we used a finite timeout */
    if (pto != NULL)
        printf("timeout after select(): %ld.%03ld s\n",
               (long) timeout.tv_sec,
               (long) timeout.tv_usec / 1000);

    return 0;
}

Build and install:

gcc -o t_select t_select.c

Shell Session 1 – Wait for Input on stdin with 10-Second Timeout

We watch file descriptor 0 (stdin) for reading with a 10-second timeout. We press Enter after about 2 seconds.

$ ./t_select 10 0r

(User presses Enter after ~2 seconds)

ready = 1
  0: r
timeout after select(): 8.003 s
$
$

1
ready = 1 — select() found exactly one descriptor ready. This matches our expectation since we only watched one fd.
2
0: r — fd 0 (stdin) is ready for reading. The ‘r’ confirms it was in the readfds set and is now ready.
3
timeout after select(): 8.003 s — On Linux, select() updated the timeval to show ~8 seconds remained of the original 10. We pressed Enter at about 2 seconds in, so 8 remained. This is Linux-specific behavior.
4
Two shell prompts ($) — The program printed ready info and exited without reading stdin. The newline (Enter key) that made fd 0 ready was still in the terminal input buffer. The shell read that newline and printed an extra prompt.

Shell Session 2 – Immediate Poll with Zero Timeout

We pass timeout = 0. select() does not block at all; it simply checks if fd 0 is readable right now and returns instantly.

$ ./t_select 0 0r
ready = 0
timeout after select(): 0.000 s
$

1
ready = 0 — No descriptor became ready. With timeout of 0, select() returned immediately without waiting. At that instant, stdin had no data pending.
2
timeout after select(): 0.000 s — Zero time remained because we started with zero. This is a polling scenario — check and return immediately.

Use case: Polling is useful when you want to check for I/O without committing to any wait — for example, draining an event queue between frame renders in a game loop.

Shell Session 3 – Infinite Wait, Watching Two Descriptors

We pass - as timeout (block forever) and watch both fd 0 for reading and fd 1 for writing. select() returns immediately.

$ ./t_select - 0r 1w
ready = 1
  0:
  1: w
$

1
ready = 1 — Only one descriptor is ready (fd 1). Even though we watched two fds, only one event is currently available.
2
0: (empty) — fd 0 (stdin) is NOT ready for reading right now. No data is available on the terminal. The FD_ISSET check returned false.
3
1: w — fd 1 (stdout) IS ready for writing. This makes complete sense: the terminal output buffer almost always has space, so stdout is virtually always writable. select() returned the moment it found this.
4
No timeout line — Because we passed - (NULL timeout pointer), pto is NULL. The program skips the timeout printout since there was no timeout to report.

Important insight: stdout is almost always writable. If you add fd 1 to writefds, select() will almost never block on it. This is why most programs only watch sockets for writability — to detect when the send buffer has drained after a backpressure situation.

Deep Dive – How sscanf Parses “0rw” Style Arguments

The key line in the parsing loop is:

numRead = sscanf(argv[j], "%d%2[rw]", &fd, buf);

The format string %d%2[rw] has two parts. The %d reads the integer (the descriptor number). The %2[rw] is a scanset — it reads up to 2 characters that must be either ‘r’ or ‘w’ and no other characters. This means:

Input string fd gets buf gets numRead Result
"0r" 0 “r” 2 OK – add fd 0 to readfds
"1w" 1 “w” 2 OK – add fd 1 to writefds
"5rw" 5 “rw” 2 OK – add fd 5 to both sets
"3" (no letter) 3 1 Error – numRead != 2, usageError called
"abc" 0 Error – numRead != 2, usageError called

After parsing, strchr(buf, 'r') checks whether ‘r’ appears in the buf string, and similarly for ‘w’. This handles both “r”, “w”, and “rw” input cleanly without needing separate conditions.

Practical Mini-Project – Monitor stdin with Heartbeat

Here is a practical example that shows a common real-world pattern: a program that waits for user input but prints a heartbeat message every few seconds if nothing arrives.

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

int main(void)
{
    char line[256];

    printf("Type something (or wait 3 seconds for heartbeat):\n");

    while (1) {
        fd_set rfds;
        FD_ZERO(&rfds);
        FD_SET(0, &rfds);   /* watch stdin */

        /* Reinitialize every iteration – required! */
        struct timeval tv;
        tv.tv_sec  = 3;
        tv.tv_usec = 0;

        int n = select(1, &rfds, NULL, NULL, &tv);

        if (n == -1) {
            if (errno == EINTR) continue;   /* signal, retry */
            perror("select");
            return 1;
        }

        if (n == 0) {
            /* timeout: no input yet */
            printf("[heartbeat – still waiting...]\n");
            fflush(stdout);
            continue;
        }

        /* n == 1: stdin has data */
        if (fgets(line, sizeof(line), stdin) == NULL)
            break;   /* EOF */

        /* Strip newline */
        line[strcspn(line, "\n")] = '\0';

        if (strcmp(line, "quit") == 0) {
            printf("exiting\n");
            break;
        }

        printf("You typed: [%s]\n", line);
    }

    return 0;
}

Expected output when user types nothing for 6 seconds then types “hello”:

Type something (or wait 3 seconds for heartbeat):
[heartbeat – still waiting...]
[heartbeat – still waiting...]
hello
You typed: [hello]

Common Mistakes with select() – Quick Reference
Mistake What goes wrong Fix
Reusing fd_sets without rebuilding Only previously-ready fds are monitored next time; others are silently dropped FD_ZERO + FD_SET before every call
Not re-initializing timeval in a loop On Linux, timeval is modified by select(); subsequent calls may use zero timeout (poll) Set tv_sec and tv_usec before every select() call
Not handling EINTR Program exits or errors on signal arrival instead of retrying if (errno == EINTR) continue;
Wrong nfds (not max+1) Highest descriptor(s) are never checked; select() silently ignores them nfds = highest_fd + 1
Adding fd ≥ FD_SETSIZE Undefined behavior — may corrupt memory or silently fail Check fd < FD_SETSIZE before FD_SET; use poll/epoll for large fds
Not calling FD_ZERO first Uninitialized bits look like set bits; select() monitors random descriptors Always FD_ZERO before any FD_SET
Using return value as unique fd count Same fd in two sets ready for both events counts as 2 in the return Always use FD_ISSET to check each fd individually

Interview Questions – select() Example & Practical Usage
Q1. In the t_select shell demo, why do two shell prompts appear after one run?

The program detected that stdin (fd 0) was readable because the user pressed Enter, which put a newline into the terminal buffer. But the program itself did not call read() or fgets() to consume that newline. When the program exited, the shell inherited the terminal and read the waiting newline, causing it to print an extra prompt. This illustrates an important rule: detecting readiness and actually reading the data are two separate steps.

Q2. Why is stdout (fd 1) almost always writable according to select()?

stdout connected to a terminal has a large kernel buffer. Unless you are writing enormous amounts of data at very high speed, that buffer almost never fills up. So the write side is almost always ready — there is always space to accept more output. This is why select() returns immediately when fd 1 is in writefds. For network sockets the situation is different: the send buffer can fill up under flow control, making the socket temporarily non-writable.

Q3. Why must you pass nfds as the highest fd plus one, not the count of descriptors you are watching?

The kernel scans the bit array from position 0 up to position nfds-1 to check which bits are set. The value nfds defines the scan boundary by position, not by count. If you are watching fds 0, 7, and 15, you must pass nfds=16 so the kernel scans positions 0 through 15. Passing nfds=3 (the count) would make the kernel only scan positions 0, 1, and 2, missing fds 7 and 15 completely.

Q4. In a loop that calls select() repeatedly, what two things must you reinitialize before each call?

First, the fd_sets — because select() clears bits for unready descriptors, modifying the sets in place. Second, the timeval structure — because on Linux, select() updates timeval to show remaining time, and relying on the modified value as input to the next call will use the wrong timeout. Both must be explicitly rebuilt from scratch before every iteration.

Q5. How would you build a server that handles multiple client sockets using select()?

You maintain a master set of all active client fds plus the listening socket. At the start of each loop iteration, copy the master set into a working set and pass the working set to select(). When select() returns, check the listening socket first — if readable, call accept() to get a new client and add it to the master set. Then loop through all client fds, use FD_ISSET on the working set to find which clients sent data, read from them, and respond. Remove clients from the master set when they disconnect. This is the classic select()-based server pattern.

Q6. What is the difference between select() returning 0 and an fd_set with all bits cleared after a positive return?

A return of 0 means the timeout expired and no descriptor was ready — the fd_sets are cleared because nothing was ready. A positive return means some descriptors were ready, but a specific fd might still have its bit cleared in the returned set because that particular fd was not among the ready ones. You should always check FD_ISSET on individual descriptors after a positive return, not assume all watched descriptors are ready.

Chapter 63 – select() Series Navigation
Part 1
Introduction – fd_set macros, timeval, FD_SETSIZE
Part 2
Return Values – -1/0/positive, EINTR, blocking behavior
Part 3 (this page)
Complete example, shell sessions, common mistakes

Series Complete!

You now understand select() from fundamentals to practical usage. Visit EmbeddedPathashala for more Linux systems programming tutorials.

← Back to Part 1 EmbeddedPathashala Home

Leave a Reply

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