What is pselect()?
When you write programs that use select() and also need to handle signals, you can run into a dangerous timing problem called a race condition. The pselect() system call was designed to solve this problem by letting you atomically unblock signals and wait for file descriptors — all in one single kernel call.
Think of it this way: with select(), there is a tiny gap of time between when you unblock a signal and when select() starts waiting. A signal could arrive in that tiny gap and be lost. pselect() closes that gap completely.
Imagine your program wants to do two things at the same time:
- Wait for a file descriptor to become ready using select()
- Respond to a signal like SIGUSR1
The normal approach is: block the signal, do some work, then unblock it just before calling select(). But there is a danger — what if the signal arrives in the tiny moment after you unblock it but before select() starts? The signal gets delivered, your signal handler runs, and then select() starts waiting — but it missed the signal. This is a classic race condition.
The pselect() system call takes all the same arguments as select() but adds one more — sigmask. This is the signal mask that the kernel will apply atomically while it waits.
#define _XOPEN_SOURCE 600
#include <sys/select.h>
int pselect(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timespec *timeout, /* nanosecond precision */
const sigset_t *sigmask); /* signal mask to apply atomically */
/* Returns:
* Number of ready file descriptors — on success
* 0 — on timeout
* -1 — on error (errno set)
*/
When you call pselect(), the kernel does all of the following steps as one atomic operation — meaning no signal can sneak in between them:
The equivalent C code (but not atomic — just shown for illustration):
/* This is what pselect() does CONCEPTUALLY — but NOT how you write it.
* The actual pselect() does this atomically inside the kernel. */
sigset_t origmask;
sigprocmask(SIG_SETMASK, &sigmask, &origmask); /* Step 1 */
ready = select(nfds, &readfds, &writefds, /* Step 2 */
&exceptfds, timeout);
sigprocmask(SIG_SETMASK, &origmask, NULL); /* Step 3 */
This example shows how to use pselect() correctly. We block SIGUSR1 before registering the handler, then pass an empty signal mask to pselect() so that SIGUSR1 is unblocked only during the wait — atomically.
#define _XOPEN_SOURCE 600
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/select.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
/* Flag set by signal handler */
static volatile sig_atomic_t got_signal = 0;
/* Signal handler for SIGUSR1 */
static void
handler(int sig, siginfo_t *si, void *uc)
{
/* Only async-signal-safe operations here! */
got_signal = 1;
}
int main(void)
{
fd_set readfds;
sigset_t emptyset, blockset;
struct sigaction sa;
struct timespec timeout;
int ready;
int nfds;
/* Step 1: Block SIGUSR1 BEFORE setting up handler.
* This prevents the signal arriving before pselect() is called. */
sigemptyset(&blockset);
sigaddset(&blockset, SIGUSR1);
if (sigprocmask(SIG_BLOCK, &blockset, NULL) == -1) {
perror("sigprocmask");
exit(EXIT_FAILURE);
}
/* Step 2: Install signal handler */
sa.sa_sigaction = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_SIGINFO;
if (sigaction(SIGUSR1, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}
/* Step 3: Prepare fd_set — watch stdin (fd 0) */
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
nfds = STDIN_FILENO + 1;
/* Step 4: Prepare timeout — wait up to 10 seconds */
timeout.tv_sec = 10;
timeout.tv_nsec = 0;
/* Step 5: Prepare empty signal mask.
* When passed to pselect(), SIGUSR1 will be UNBLOCKED during the wait.
* This is safe because the unblocking happens atomically. */
sigemptyset(&emptyset);
printf("Waiting for stdin or SIGUSR1 (PID=%d)...\n", (int)getpid());
/* Step 6: Call pselect() — atomically unblock SIGUSR1 + wait */
ready = pselect(nfds, &readfds, NULL, NULL, &timeout, &emptyset);
if (ready == -1) {
if (errno == EINTR)
printf("pselect() interrupted by signal\n");
else
perror("pselect");
} else if (ready == 0) {
printf("Timeout — no activity for 10 seconds\n");
} else {
if (FD_ISSET(STDIN_FILENO, &readfds))
printf("stdin is ready to read\n");
}
if (got_signal)
printf("SIGUSR1 was received!\n");
return 0;
}
gcc -o pselect_demo pselect_demo.c. Run it, then from another terminal send: kill -SIGUSR1 <PID>. You will see “pselect() interrupted by signal” and “SIGUSR1 was received!”.If you pass NULL as the sigmask argument to pselect(), it behaves almost exactly like select() — no signal mask changes are made. The only remaining differences are the nanosecond timeout precision and the non-modification of timeout on return.
/* pselect() with NULL sigmask — same behavior as select() except:
* 1. Timeout uses struct timespec (nanosecond precision)
* 2. Timeout is NOT modified on return */
struct timespec timeout;
timeout.tv_sec = 5;
timeout.tv_nsec = 500000000; /* 5.5 seconds */
ready = pselect(nfds, &readfds, NULL, NULL, &timeout, NULL);
/* sigmask = NULL means: don't change the signal mask */
Linux extended the same “atomic signal mask” concept to poll() and epoll_wait() as well. These are Linux-specific (non-standard) additions:
Before kernel 2.6.16, glibc provided a library-level implementation of pselect(). This library version did NOT provide real atomicity — it was just a thin wrapper that called sigprocmask() + select() in sequence, with the race condition still present. The real atomic guarantee can only be provided by a proper kernel implementation, which arrived in Linux 2.6.16.
Library wrapper only. Not atomic. Race condition still present.
True syscall. Atomic mask change + wait. Race condition eliminated.
