select() Return Values
Intermediate
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.
select() returns an integer that falls into one of three categories. Every robust program must handle all three.
#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);
}
}
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.
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.
select()
watching fds
arrives!
runs
-1, errno=EINTR
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;
}
The definition of “ready” depends on which set the descriptor is in. Understanding this prevents confusion when select() returns unexpectedly early or late.
/* 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");
When select() blocks (timeout is not zero), exactly three events can cause it to return:
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.
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;
}
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.
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.
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.
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.
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.
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.
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.
Next: see a complete working program (t_select.c) with real shell session output.
