SELECT_TUT(2)

πŸ¦‹ select_tut β€” bestie edition, part 1 πŸ¦‹

okay so you opened the select_tut page of the System Calls Manual and it's a tutorial, not the reference β€” the real deets live in select(2). This one is the vibe explanation. Stay with me, it's long and that's fine, we're getting through it together πŸ’–


✨ NAME ✨

select, pselect β€” synchronous I/O multiplexing

πŸ“š LIBRARY πŸ“š

Standard C library (libc, -lc)

🌸 SYNOPSIS 🌸

See select(2)

πŸ’… DESCRIPTION πŸ’…

So the select() and pselect() system calls are your little watchdogs πŸ• β€” they keep an eye on a whole bunch of file descriptors at once, waiting to see if any of them is, or becomes, "ready". Translation: they tell you whether I/O is even possible right now, or whether something called an "exceptional condition" has popped off on any of those file descriptors.

This page is the backstory + tutorial tea β˜•. For the actual arguments and semantics of select() and pselect() β€” like, the literal facts β€” go read select(2).

πŸŽ€ Combining signal and data events πŸŽ€

pselect() is the one you want when you're waiting on a signal and on file descriptor(s) becoming ready for I/O at the same time.

Here's how programs that catch signals usually do it: the signal handler does literally nothing except raise a global flag 🚩. That flag means "hey, this event needs to get handled in the main loop of the program." A signal makes the select() (or pselect()) call return with errno set to EINTR. And that behavior is essential β€” it's the whole reason signals can get processed in the main loop. Without it, select() would just block forever, no cap.

Now. Somewhere in the main loop there's a conditional checking that global flag. So bestie, here's the question: what if a signal arrives after the conditional, but before the select() call? 😭 Answer: select() would block indefinitely, even though an event is literally pending. Rude.

This race condition gets solved by the pselect() call. You use it to set the signal mask to a set of signals that are to be received only within the pselect() call. Example time β€” let's say the event we care about is a child process exiting. Before the main loop starts, we'd block SIGCHLD using sigprocmask(2). Then our pselect() call enables SIGCHLD by using an empty signal mask. Our program would look like:

static volatile sig_atomic_t got_SIGCHLD = 0;

static void
child_sig_handler(int sig)
{
    got_SIGCHLD = 1;
}

int
main(int argc, char *argv[])
{
    sigset_t sigmask, empty_mask;
    struct sigaction sa;
    fd_set readfds, writefds, exceptfds;
    int r;

    sigemptyset(&sigmask);
    sigaddset(&sigmask, SIGCHLD);
    if (sigprocmask(SIG_BLOCK, &sigmask, NULL) == -1) {
        perror("sigprocmask");
        exit(EXIT_FAILURE);
    }

    sa.sa_flags = 0;
    sa.sa_handler = child_sig_handler;
    sigemptyset(&sa.sa_mask);
    if (sigaction(SIGCHLD, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    sigemptyset(&empty_mask);

    for (;;) {          /* main loop */
        /* Initialize readfds, writefds, and exceptfds
           before the pselect() call.  (Code omitted.)  */

        r = pselect(nfds, &readfds, &writefds, &exceptfds,
                    NULL, &empty_mask);
        if (r == -1 && errno != EINTR) {
            /* Handle error */
        }

        if (got_SIGCHLD) {
            got_SIGCHLD = 0;

            /* Handle signalled event here; e.g., wait() for all
               terminated children.  (Code omitted.)  */
        }

        /* main body of program */
    }
}

🌈 Practical 🌈

So like… what is even the point of select()? Can't I just read and write to my file descriptors whenever I feel like it?? πŸ™ˆ

Okay so here's the thing: the point of select() is that it watches multiple descriptors at the same time, and it properly puts the process to sleep when there's no activity. ✨

UNIX programmers constantly end up in a situation where they have to handle I/O from more than one file descriptor, and the data flow is all intermittent and unpredictable. If you just made a big sequence of read(2) and write(2) calls, you'd find that one of your calls blocks waiting for data from/to a file descriptor β€” while another file descriptor sits there unused even though it's totally ready for I/O. 😀 select() copes with this situation efficiently. Iconic behavior honestly.

πŸ“œ Select law πŸ“œ

A lot of people try to use select() and hit behavior that is difficult to understand, and that produces nonportable or borderline results. For instance, that program above is carefully written so it doesn't block at any point β€” even though it never sets its file descriptors to nonblocking mode. It's genuinely easy to introduce subtle errors that torch the whole advantage of using select(). So here's the list of essentials to watch for when you use select(). Read it. All of it. 🫢

  1. You should always try to use select() without a timeout. Your program should have nothing to do if there's no data available. Code that depends on timeouts is not usually portable and is difficult to debug.
  2. The value nfds must be properly calculated for efficiency, as explained above.
  3. No file descriptor must be added to any set if you do not intend to check its result after the select() call, and respond appropriately. See the next rule.
  4. After select() returns, all file descriptors in all sets should be checked to see if they are ready.
  5. The functions read(2), recv(2), write(2), and send(2) do not necessarily read/write the full amount of data that you requested. If they do read/write the full amount, it's because you have a low traffic load and a fast stream. That is not always going to be the case. You should cope with the case where your functions manage to send or receive only a single byte.
  6. Never read/write only in single bytes at a time unless you are really sure you have a small amount of data to process. It is extremely inefficient not to read/write as much data as you can buffer each time. The buffers in the example below are 1024 bytes, although they could easily be made larger.
  7. Calls to read(2), recv(2), write(2), send(2), and select() can fail with the error EINTR, and calls to read(2), recv(2), write(2), and send(2) can fail with errno set to EAGAIN (EWOULDBLOCK). These results must be properly managed (not done properly above, jsyk). If your program isn't going to receive any signals, then it's unlikely you'll get EINTR. If your program doesn't set nonblocking I/O, you won't get EAGAIN.
  8. Never call read(2), recv(2), write(2), or send(2) with a buffer length of zero.
  9. If the functions read(2), recv(2), write(2), and send(2) fail with errors other than those listed in 7., or one of the input functions returns 0, indicating end of file, then you should not pass that file descriptor to select() again. In the example below, I close the file descriptor immediately, and then set it to -1 to prevent it being included in a set.
  10. The timeout value must be initialized with each new call to select(), since some operating systems modify the structure. pselect() however does not modify its timeout structure.
  11. Since select() modifies its file descriptor sets, if the call is being used in a loop, then the sets must be reinitialized before each call.
bestie if you remember nothing else: rules 4, 7, 10, 11 are the ones that eat people alive 😭 like genuinely, re-read them.

🫢 RETURN VALUE 🫢

See select(2).

πŸ’œ NOTES πŸ’œ

Generally speaking, all operating systems that support sockets also support select(). And select() can be used to solve many problems in a portable and efficient way β€” problems that naive programmers try to solve in a much more complicated manner using threads, forking, IPCs, signals, memory sharing, and so on. πŸŽ€

The poll(2) system call has the same functionality as select(), and is somewhat more efficient when monitoring sparse file descriptor sets. It is nowadays widely available, but historically it was less portable than select().

The Linux-specific epoll(7) API provides an interface that is more efficient than select(2) and poll(2) when monitoring large numbers of file descriptors. πŸ‘‘


okay that's the lore, the rules, and the receipts πŸ’… β€” part 2 is right behind me, don't go anywhere bestie πŸ¦‹

✨ EXAMPLES ✨

okay bestie, THIS is the moment πŸ’– here's an example that actually shows off what select() is really about. Like, its true utility. It's a TCP forwarding program β€” it forwards stuff from one TCP port to another. Iconic behavior, honestly πŸŽ€

#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

static int forward_port;

#undef max
#define max(x, y) ((x) > (y) ? (x) : (y))

static int
listen_socket(int listen_port)
{
    int                 lfd;
    int                 yes;
    struct sockaddr_in  addr;

    lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd == -1) {
        perror("socket");
        return -1;
    }

    yes = 1;
    if (setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR,
                   &yes, sizeof(yes)) == -1)
    {
        perror("setsockopt");
        close(lfd);
        return -1;
    }

    memset(&addr, 0, sizeof(addr));
    addr.sin_port = htons(listen_port);
    addr.sin_family = AF_INET;
    if (bind(lfd, (struct sockaddr *) &addr, sizeof(addr)) == -1) {
        perror("bind");
        close(lfd);
        return -1;
    }

    printf("accepting connections on port %d\n", listen_port);
    listen(lfd, 10);
    return lfd;
}

static int
connect_socket(int connect_port, char *address)
{
    int                 cfd;
    struct sockaddr_in  addr;

    cfd = socket(AF_INET, SOCK_STREAM, 0);
    if (cfd == -1) {
        perror("socket");
        return -1;
    }

    memset(&addr, 0, sizeof(addr));
    addr.sin_port = htons(connect_port);
    addr.sin_family = AF_INET;

    if (!inet_aton(address, (struct in_addr *) &addr.sin_addr.s_addr)) {
        fprintf(stderr, "inet_aton(): bad IP address format\n");
        close(cfd);
        return -1;
    }

    if (connect(cfd, (struct sockaddr *) &addr, sizeof(addr)) == -1) {
        perror("connect()");
        shutdown(cfd, SHUT_RDWR);
        close(cfd);
        return -1;
    }
    return cfd;
}

#define SHUT_FD1 do {                                \
                     if (fd1 >= 0) {                 \
                         shutdown(fd1, SHUT_RDWR);   \
                         close(fd1);                 \
                         fd1 = -1;                   \
                     }                               \
                     } while (0)

#define SHUT_FD2 do {                                \
                     if (fd2 >= 0) {                 \
                         shutdown(fd2, SHUT_RDWR);   \
                         close(fd2);                 \
                         fd2 = -1;                   \
                     }                               \
                     } while (0)

#define BUF_SIZE 1024

int
main(int argc, char *argv[])
{
    int      h;
    int      ready, nfds;
    int      fd1 = -1, fd2 = -1;
    int      buf1_avail = 0, buf1_written = 0;
    int      buf2_avail = 0, buf2_written = 0;
    char     buf1[BUF_SIZE], buf2[BUF_SIZE];
    fd_set   readfds, writefds, exceptfds;
    ssize_t  nbytes;

    if (argc != 4) {
        fprintf(stderr, "Usage\n\tfwd <listen-port> "
                "<forward-to-port> <forward-to-ip-address>\n");
        exit(EXIT_FAILURE);
    }

    signal(SIGPIPE, SIG_IGN);

    forward_port = atoi(argv[2]);

    h = listen_socket(atoi(argv[1]));
    if (h == -1)
        exit(EXIT_FAILURE);

    for (;;) {
        nfds = 0;

        FD_ZERO(&readfds);
        FD_ZERO(&writefds);
        FD_ZERO(&exceptfds);
        FD_SET(h, &readfds);
        nfds = max(nfds, h);

        if (fd1 > 0 && buf1_avail < BUF_SIZE)
            FD_SET(fd1, &readfds);
            /* Note: nfds is updated below, when fd1 is added to
               exceptfds.  */
        if (fd2 > 0 && buf2_avail < BUF_SIZE)
            FD_SET(fd2, &readfds);

        if (fd1 > 0 && buf2_avail - buf2_written > 0)
            FD_SET(fd1, &writefds);
        if (fd2 > 0 && buf1_avail - buf1_written > 0)
            FD_SET(fd2, &writefds);

        if (fd1 > 0) {
            FD_SET(fd1, &exceptfds);
            nfds = max(nfds, fd1);
        }
        if (fd2 > 0) {
            FD_SET(fd2, &exceptfds);
            nfds = max(nfds, fd2);
        }

        ready = select(nfds + 1, &readfds, &writefds, &exceptfds, NULL);

        if (ready == -1 && errno == EINTR)
            continue;

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

        if (FD_ISSET(h, &readfds)) {
            socklen_t addrlen;
            struct sockaddr_in client_addr;
            int fd;

            addrlen = sizeof(client_addr);
            memset(&client_addr, 0, addrlen);
            fd = accept(h, (struct sockaddr *) &client_addr, &addrlen);
            if (fd == -1) {
                perror("accept()");
            } else {
                SHUT_FD1;
                SHUT_FD2;
                buf1_avail = buf1_written = 0;
                buf2_avail = buf2_written = 0;
                fd1 = fd;
                fd2 = connect_socket(forward_port, argv[3]);
                if (fd2 == -1)
                    SHUT_FD1;
                else
                    printf("connect from %s\n",
                           inet_ntoa(client_addr.sin_addr));

                /* Skip any events on the old, closed file
                   descriptors.  */

                continue;
            }
        }

        /* NB: read OOB data before normal reads.  */

        if (fd1 > 0 && FD_ISSET(fd1, &exceptfds)) {
            char c;

            nbytes = recv(fd1, &c, 1, MSG_OOB);
            if (nbytes < 1)
                SHUT_FD1;
            else
                send(fd2, &c, 1, MSG_OOB);
        }
        if (fd2 > 0 && FD_ISSET(fd2, &exceptfds)) {
            char c;

            nbytes = recv(fd2, &c, 1, MSG_OOB);
            if (nbytes < 1)
                SHUT_FD2;
            else
                send(fd1, &c, 1, MSG_OOB);
        }
        if (fd1 > 0 && FD_ISSET(fd1, &readfds)) {
            nbytes = read(fd1, buf1 + buf1_avail,
                          BUF_SIZE - buf1_avail);
            if (nbytes < 1)
                SHUT_FD1;
            else
                buf1_avail += nbytes;
        }
        if (fd2 > 0 && FD_ISSET(fd2, &readfds)) {
            nbytes = read(fd2, buf2 + buf2_avail,
                          BUF_SIZE - buf2_avail);
            if (nbytes < 1)
                SHUT_FD2;
            else
                buf2_avail += nbytes;
        }
        if (fd1 > 0 && FD_ISSET(fd1, &writefds) && buf2_avail > 0) {
            nbytes = write(fd1, buf2 + buf2_written,
                           buf2_avail - buf2_written);
            if (nbytes < 1)
                SHUT_FD1;
            else
                buf2_written += nbytes;
        }
        if (fd2 > 0 && FD_ISSET(fd2, &writefds) && buf1_avail > 0) {
            nbytes = write(fd2, buf1 + buf1_written,
                           buf1_avail - buf1_written);
            if (nbytes < 1)
                SHUT_FD2;
            else
                buf1_written += nbytes;
        }

        /* Check if write data has caught read data.  */

        if (buf1_written == buf1_avail)
            buf1_written = buf1_avail = 0;
        if (buf2_written == buf2_avail)
            buf2_written = buf2_avail = 0;

        /* One side has closed the connection, keep
           writing to the other side until empty.  */

        if (fd1 < 0 && buf1_avail - buf1_written == 0)
            SHUT_FD2;
        if (fd2 < 0 && buf2_avail - buf2_written == 0)
            SHUT_FD1;
    }
    exit(EXIT_SUCCESS);
}

okay so let's talk about what that little beauty actually does 🌸

The program above properly forwards most kinds of TCP connections β€” including the OOB signal data that telnet servers transmit πŸ‘‘ It handles the tricky problem of having data flow in both directions simultaneously. Like, at the same time. Both ways. It's giving multitasking main character energy fr fr πŸ’…

You might be sitting there thinking "bestie wouldn't it be way more efficient to just use a fork(2) call and give each stream its own thread?" πŸ™ˆ Nah. That actually becomes more tricky than you might suspect. Another idea you might have: set nonblocking I/O using fcntl(2). Nope. That also has its own little problems, because you end up using inefficient timeouts. So. Yeah. πŸ’€

Now for the honest part, because I love you and I won't lie to you: this program does not handle more than one simultaneous connection at a time 😭 It totally could be extended to do that β€” you'd use a linked list of buffers, one for each connection β€” but right now? At the moment? A new connection causes the current connection to be dropped. She's monogamous, okay? one connection at a time periodt 🫢


🌈 SEE ALSO 🌈


and that's the whole page bestie, we did it πŸŽ€πŸ’– give yourself a little treat, you read a whole manpage. this one came from the Linux man-pages 6.20, dated 2026-02-08 ✨ go forth and select() responsibly, mwah πŸ’…

πŸ“¦ where this page comes from
  • arch rolling: man-pages 6.20-1 Β· BSD-2-Clause AND BSD-3-Clause AND BSD-4.3TAHOE AND BSD-4-Clause-UC AND GPL-1.0-or-later AND GPL-2.0-only AND GPL-2.0-or-later AND GPL-3.0-or-later AND LGPL-3.0-linking-exception AND LGPL-3.0-or-later AND LicenseRef-Public-Domain AND LicenseRef-UltraPermissive AND Linux-man-pages-1-para AND Linux-man-pages-copyleft AND Linux-man-pages-copyleft-2-para AND Linux-man-pages-copyleft-var AND MIT AND Spencer-94
  • nixpkgs unstable: man-pages 6.20
  • πŸ“œ the page's license: Linux-man-pages-copyleft
  • πŸ’– girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms