select() – Return Values & Blocking Behavior

 

select() – Return Values & Blocking Behavior
Chapter 63 – Alternative I/O Models | The Linux Programming Interface
Topic
select() Return Values
Level
Intermediate
Part
2 of 3

Understanding What select() Tells You

After select() returns, you need to know two things: why it returned, and which descriptors are ready. The integer return value answers the first question; scanning the fd_sets answers the second. Getting this right is the core of writing a correct event loop.

In addition, you need to understand what happens when a signal arrives while select() is blocking — because this is a common source of bugs in real programs.

Key Terms in This Page

Return value EBADF EINTR Signal interruption Timeout expiry Ready descriptor FD_ISSET SA_RESTART

The Three Possible Return Values

select() returns an integer that falls into one of three categories. Every robust program must handle all three.

-1
Error occurred
select() failed. Check errno immediately. Two important error codes:
EBADF – one of the file descriptors in the sets is invalid (already closed, or was never opened). This is a programming bug.
EINTR – a signal handler ran while select() was blocking. This is not a fatal error. Retry select() in a loop.

0
Timeout expired
The time limit passed and not a single descriptor became ready. All three returned fd_sets will be completely empty — every bit is cleared. The program can do other work, update a display, or simply retry.

>0
Descriptors are ready
The number tells you how many descriptors are ready across all three sets combined. You must still loop through all descriptors you added and call FD_ISSET() on each one to find out exactly which ones are ready.
#include <sys/select.h>
#include <stdio.h>
#include <errno.h>

void event_loop(int fd1, int fd2)
{
    while (1) {
        fd_set rfds;
        FD_ZERO(&rfds);
        FD_SET(fd1, &rfds);
        FD_SET(fd2, &rfds);

        struct timeval tv = { .tv_sec = 5, .tv_usec = 0 };
        int nfds = (fd1 > fd2 ? fd1 : fd2) + 1;

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

        if (n == -1) {
            if (errno == EINTR) {
                /* signal arrived – just retry */
                printf("select interrupted by signal, retrying\n");
                continue;
            }
            perror("select"); /* real error */
            break;
        }

        if (n == 0) {
            printf("no activity for 5 seconds\n");
            continue;
        }

        /* n > 0: at least one fd is ready */
        if (FD_ISSET(fd1, &rfds))
            printf("fd1 (%d) is readable\n", fd1);
        if (FD_ISSET(fd2, &rfds))
            printf("fd2 (%d) is readable\n", fd2);
    }
}

How the Return Count Works When a Descriptor Is in Multiple Sets

If you add the same file descriptor to both readfds and writefds, and it turns out to be ready for both reading and writing, select() counts it twice in the return value. The return value is the total number of (fd, event) pairs that are ready, not the number of unique file descriptors.

fd In readfds? In writefds? Read ready? Write ready? Counts as
3 Yes Yes YES YES 2
7 Yes No YES 1
9 Yes No not ready 0
select() return value 3

The return value of 3 does not mean 3 unique file descriptors are ready. fd 3 contributed 2 (once for read, once for write) and fd 7 contributed 1. You still need FD_ISSET() to find out the details.

Signal Interruption – EINTR and Why select() Never Auto-Restarts

When select() is blocked waiting, and a signal arrives, the kernel runs the signal handler, then makes select() return with -1 and errno == EINTR. This is by design — the program gets a chance to react to the signal.

Key point: Unlike some other system calls, select() is never automatically restarted even if you set the SA_RESTART flag when installing the signal handler. POSIX leaves this behavior up to the implementation, and Linux does not restart select() on EINTR even with SA_RESTART. You must handle EINTR manually.

Timeline: signal arriving during select()
Program calls
select()
Kernel blocks,
watching fds
SIGNAL
arrives!
Signal handler
runs
select() returns
-1, errno=EINTR
Your code:
retry loop

On Linux, there is an additional detail: if select() is interrupted by a signal, the timeval timeout structure is updated to show the remaining time — just as it is on a successful return. You can use this remaining time when retrying.

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

volatile sig_atomic_t got_signal = 0;

void sig_handler(int sig) { got_signal = 1; }

int main(void)
{
    struct sigaction sa = { .sa_handler = sig_handler };
    sigaction(SIGINT, &sa, NULL);
    /* Note: SA_RESTART is NOT set – select() won't auto-restart */

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

        struct timeval tv = { .tv_sec = 10, .tv_usec = 0 };
        int n = select(fd + 1, &rfds, NULL, NULL, &tv);

        if (n == -1) {
            if (errno == EINTR) {
                if (got_signal) {
                    printf("caught signal, cleaning up\n");
                    got_signal = 0;
                    break; /* or continue, depending on logic */
                }
                continue; /* some other interrupt, retry */
            }
            perror("select");
            break;
        }

        if (n == 0) { printf("timeout\n"); continue; }
        if (FD_ISSET(fd, &rfds)) printf("stdin is readable\n");
    }
    return 0;
}

What “Ready” Actually Means for Each Set

The definition of “ready” depends on which set the descriptor is in. Understanding this prevents confusion when select() returns unexpectedly early or late.

readfds – Ready for Reading
At least one byte of data is available to read without blocking, OR the end-of-file condition has been reached. A closed pipe’s read end is “readable” (read() returns 0). A listening socket with a pending connection is also “readable” (accept() won’t block).
writefds – Ready for Writing
There is space in the kernel’s send buffer to write data without blocking. For a newly created socket, this is almost always true. It becomes false when the buffer is full (flow control or slow receiver).
exceptfds – Exceptional Condition
On TCP sockets, this fires when out-of-band (urgent) data arrives. In practice, this set is rarely used by most applications.
/* Example: check if a TCP socket is writable (send buffer has space) */
fd_set wfds;
FD_ZERO(&wfds);
FD_SET(sockfd, &wfds);

struct timeval tv = { .tv_sec = 2, .tv_usec = 0 };
int n = select(sockfd + 1, NULL, &wfds, NULL, &tv);

if (n > 0 && FD_ISSET(sockfd, &wfds))
    printf("socket send buffer has space – write() won't block\n");

All Conditions That Wake Up a Blocking select()

When select() blocks (timeout is not zero), exactly three events can cause it to return:

1
A descriptor becomes ready — data arrives, space opens up, or an exceptional event fires on at least one of the monitored file descriptors. This is the normal case.
2
A signal handler is invoked — select() returns -1 with errno set to EINTR. Not an error in the fatal sense; it is an interruption. Your program must retry in a loop.
3
The timeout expires — the time limit in the timeval structure passes with no descriptor becoming ready. select() returns 0 and all fd_sets are cleared.

There is no fourth reason. If select() returns and you cannot identify which of these three happened, you have a bug — check errno and the return value carefully.

Writing a Robust select() Wrapper

Because select() modifies the fd_sets on return, and because EINTR requires a retry, most real code wraps select() in a helper. Here is a clean pattern:

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

/*
 * wait_for_read: wait up to 'secs' seconds for fd to become readable.
 * Returns 1 if readable, 0 if timeout, -1 on real error.
 */
int wait_for_read(int fd, int secs)
{
    while (1) {
        fd_set rfds;
        FD_ZERO(&rfds);
        FD_SET(fd, &rfds);

        struct timeval tv;
        tv.tv_sec  = secs;
        tv.tv_usec = 0;

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

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

        if (n == 0)
            return 0;          /* timed out */

        /* n > 0 and FD_ISSET is guaranteed true here */
        return 1;              /* readable */
    }
}

int main(void)
{
    printf("Waiting for stdin (fd 0)...\n");
    int result = wait_for_read(0, 5);

    if (result == 1)
        printf("stdin is ready – go ahead and read\n");
    else if (result == 0)
        printf("timed out after 5 seconds\n");
    else
        printf("error occurred\n");

    return 0;
}

Interview Questions – select() Return Values & Blocking
Q1. What does a return value of -1 from select() mean? How should you handle it?

It means an error occurred. You must check errno. If errno is EINTR, a signal handler ran while select() was blocked — this is not fatal; simply retry the call. If errno is EBADF, one of the file descriptors in your sets was invalid (closed or never opened), which is a programming bug to be fixed. For any other errno, treat it as a real error.

Q2. Does setting SA_RESTART prevent EINTR from select() on Linux?

No. select() is never automatically restarted even when SA_RESTART is set. POSIX leaves this behavior undefined, and Linux follows the convention of not restarting select() on signal interruption. You must always handle EINTR manually by retrying the call in a loop.

Q3. If select() returns 3, does it mean exactly 3 file descriptors are ready?

Not necessarily. The return value counts the total number of (fd, set) pairs that are ready, not the number of unique file descriptors. If the same fd appears in both readfds and writefds and is ready for both, it is counted twice. You still need to use FD_ISSET() on each descriptor in each set to find out exactly what is ready.

Q4. Why must you rebuild the fd_sets before every call to select() in a loop?

select() modifies the fd_set arguments before returning. It clears bits for descriptors that were not ready. If you reuse the returned sets as input to the next select() call, you will only watch the descriptors that were ready last time, silently dropping all others from monitoring. Always start with FD_ZERO() and rebuild the sets from scratch.

Q5. Under what three conditions does a blocking select() return?

First, when at least one monitored descriptor becomes ready (for the event type it was added to). Second, when a signal handler is invoked while select() is blocking (returns -1, EINTR). Third, when the specified timeout duration expires with no descriptor becoming ready (returns 0). There are no other conditions that cause select() to return.

Q6. What does “readable” mean for a listening server socket?

A listening socket becomes readable in select() when at least one incoming connection is pending in the accept queue. Calling accept() on it at that point will not block. This is how server programs detect new client connections without polling in a tight loop.

Q7. On Linux, what happens to the timeval timeout after a signal interrupts select()?

On Linux, select() updates the timeval to reflect the remaining time even when it returns due to EINTR — the same as it does on a successful return. This is a Linux-specific behavior. On most other UNIX systems, the timeval is only modified on a successful return. Portable code should not rely on this and should always reinitialize timeval before retrying.

Continue Learning

Next: see a complete working program (t_select.c) with real shell session output.

Part 3: Complete Example Program → ← Part 1: Introduction

Leave a Reply

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