pselect() System Call

 

pselect() System Call
Linux Alternative I/O Models — Chapter 63 | TLPI Series
63.5
TLPI Section
Linux 2.6.16+
Kernel Version
POSIX.1g
Standard

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.

Key Concepts in This Tutorial

pselect() select() sigmask sigprocmask() atomic operation race condition timespec ppoll() epoll_pwait() POSIX.1g SUSv3

The Problem: Race Condition with select() and Signals

Imagine your program wants to do two things at the same time:

  1. Wait for a file descriptor to become ready using select()
  2. 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.

Race Condition Timeline with select()
TIME →
Step 1
sigprocmask(SIG_UNBLOCK, &blockset, …) — unblock SIGUSR1
⚠ DANGER ZONE
Signal SIGUSR1 arrives HERE — handler runs, but select() not called yet!
Step 2
select() starts waiting — but signal was already missed!
Result
select() may block forever — signal event was lost

pselect() Function Signature

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)
 */

Argument Summary
Argument
Type
Purpose
nfds
int
Highest fd number + 1
readfds
fd_set *
Set of fds to watch for reading
writefds
fd_set *
Set of fds to watch for writing
exceptfds
fd_set *
Set of fds to watch for exceptions
timeout
struct timespec *
Max wait time (nanosecond precision). NULL = wait forever
sigmask
const sigset_t *
Temporary signal mask applied atomically during the wait

How pselect() Works Internally

When you call pselect(), the kernel does all of the following steps as one atomic operation — meaning no signal can sneak in between them:

pselect() — Atomic Equivalent
1
sigprocmask(SIG_SETMASK, &sigmask, &origmask) — swap to new signal mask
2
select(nfds, &readfds, &writefds, &exceptfds, timeout) — wait for I/O
3
sigprocmask(SIG_SETMASK, &origmask, NULL) — restore original signal mask
Key point: Because all 3 steps happen atomically inside the kernel, there is NO gap for a signal to sneak in between steps 1 and 2.

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 */

pselect() vs select() — Key Differences
Feature
select()
pselect()
Signal mask
Not handled — separate sigprocmask() call needed
Atomic signal mask swap via sigmask argument
Timeout type
struct timeval (microsecond precision)
struct timespec (nanosecond precision)
Timeout modified on return?
Yes — Linux modifies it to show remaining time
No — timeout is NOT modified on return (per SUSv3)
Portability
Available everywhere
Linux 2.6.16+, SUSv3 (not on all UNIX)
Race condition safe?
No
Yes — atomic mask change + wait

Complete Working Example: pselect() with Signal Handling

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;
}
How to test: Compile with 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!”.

What if sigmask is NULL?

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 */

ppoll() and epoll_pwait() — The Same Idea for poll() and epoll()

Linux extended the same “atomic signal mask” concept to poll() and epoll_wait() as well. These are Linux-specific (non-standard) additions:

ppoll() — Linux 2.6.16+

Same relationship to poll() as pselect() has to select(). Adds a sigmask argument so that signals are unblocked atomically only during the wait.

#define _GNU_SOURCE
#include <poll.h>

int ppoll(struct pollfd *fds, nfds_t nfds,
          const struct timespec *timeout_ts,
          const sigset_t *sigmask);

epoll_pwait() — Linux 2.6.19+

Same relationship to epoll_wait(). Adds sigmask to avoid the race condition when using epoll with signal handling.

#include <sys/epoll.h>

int epoll_pwait(int epfd,
                struct epoll_event *events,
                int maxevents,
                int timeout,
                const sigset_t *sigmask);

I/O Multiplexing + Signal-Safe Variants
Base Function
Signal-Safe Variant
Available Since
Standard?
select()
pselect()
Linux 2.6.16
Yes (SUSv3)
poll()
ppoll()
Linux 2.6.16
No (Linux-specific)
epoll_wait()
epoll_pwait()
Linux 2.6.19
No (Linux-specific)

Historical Note: glibc vs Kernel pselect()

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.

glibc version (old)
Library wrapper only. Not atomic. Race condition still present.
Kernel 2.6.16+ version
True syscall. Atomic mask change + wait. Race condition eliminated.

Interview Questions and Answers
Q1. What problem does pselect() solve that select() cannot?
pselect() solves the race condition that occurs when a program needs to simultaneously wait for I/O events and handle signals. With select(), there is a window of time between unblocking a signal and calling select() during which the signal could arrive and be missed. pselect() eliminates this window by atomically unblocking the signal and beginning the wait as a single kernel operation.
Q2. What is the sigmask argument in pselect() and what does it do?
The sigmask argument is a pointer to a sigset_t that specifies which signals should be unblocked while pselect() is waiting. The kernel swaps the process’s current signal mask with sigmask atomically when entering the wait, and restores the original mask when pselect() returns. If NULL is passed, no mask change is made.
Q3. How does the timeout in pselect() differ from select()?
pselect() uses struct timespec (nanosecond precision) while select() uses struct timeval (microsecond precision). Additionally, SUSv3 specifies that pselect() must not modify the timeout argument on return, whereas Linux’s select() does modify it to show remaining time.
Q4. Why was the old glibc pselect() implementation not sufficient?
The old glibc pselect() was just a library wrapper that called sigprocmask() followed by select() in sequence. This did NOT provide true atomicity — the race condition still existed between the two calls. A real atomic pselect() can only be implemented as a kernel system call, which Linux added in version 2.6.16.
Q5. What are ppoll() and epoll_pwait() and when were they added?
ppoll() extends poll() with the same atomic sigmask mechanism as pselect(). It was added in Linux 2.6.16 and is Linux-specific (not in POSIX/SUSv3). epoll_pwait() does the same for epoll_wait(), added in Linux 2.6.19. Both allow safe use of epoll/poll with signal handling, eliminating the same race condition that pselect() fixes for select().
Q6. What happens if you pass NULL as sigmask to pselect()?
When sigmask is NULL, pselect() does not manipulate the process signal mask at all. It behaves like select() except it uses nanosecond-precision timeout (struct timespec) and does not modify the timeout on return.
Q7. What is the correct pattern for using pselect() with a signal?
The correct pattern is: (1) block the signal using sigprocmask() before installing the handler, (2) install the signal handler with sigaction(), (3) create an empty sigmask (sigemptyset), (4) call pselect() passing the empty mask. This way, the signal is only unblocked atomically inside pselect(), eliminating the race condition.

Next: Self-Pipe Trick
pselect() is not widely available. Learn the portable alternative — the self-pipe trick — that works with select(), poll(), and epoll_wait() on any UNIX system.

EmbeddedPathashala.com Next: Self-Pipe Trick →

Leave a Reply

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