t_select.c Walkthrough
Intermediate
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.
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.
argv[1]
timeout or –
argv[2..]
parse “Nrw”
fd into
readfds/writefds
to compute
nfds
select()
fds are ready
& remaining time
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
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
$
$
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
$
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.
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
$
- (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.
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.
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]
| 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 |
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.
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.
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.
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.
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.
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.
You now understand select() from fundamentals to practical usage. Visit EmbeddedPathashala for more Linux systems programming tutorials.
