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. π«Ά
- 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. - The value
nfdsmust be properly calculated for efficiency, as explained above. - 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. - After
select()returns, all file descriptors in all sets should be checked to see if they are ready. - The functions
read(2),recv(2),write(2), andsend(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. - 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.
- Calls to
read(2),recv(2),write(2),send(2), andselect()can fail with the errorEINTR, and calls toread(2),recv(2),write(2), andsend(2)can fail witherrnoset toEAGAIN(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 getEINTR. If your program doesn't set nonblocking I/O, you won't getEAGAIN. - Never call
read(2),recv(2),write(2), orsend(2)with a buffer length of zero. - If the functions
read(2),recv(2),write(2), andsend(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 toselect()again. In the example below, I close the file descriptor immediately, and then set it to-1to prevent it being included in a set. - 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. - 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
- Copyright 2001, Paul Sheer
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms