Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sigwait and Sigsuspend — Synchronous and Race-Free Waiting (IF-008 – IF-010)

Waiting for a signal sounds trivial — until you meet the race between “checking for work” and “blocking”. pause(2) alone loses signals that arrive between the check and the sleep. The POSIX answer is the atomic mask swap of sigsuspend(2); the threads answer is synchronous consumption with sigwait(3)/sigwaitinfo(2). This chapter covers both and contrasts consumption via sigwait with delivery via a handler (IF-008, IF-009, IF-010).

sigwait(3) (IF-008)

#include <signal.h>        /* glibc: _POSIX_C_SOURCE >= 199506L since 2.26 */
int sigwait(const sigset_t *restrict set, int *restrict sig);

Category: libc function on Linux — per sigwait(3) it is implemented using sigtimedwait(2) (i.e. the rt_sigtimedwait(2) syscall); POSIX specifies it as a function, not a syscall. MT-Safe.

Semantics (per sigwait(3)): suspend the calling thread until one of the signals in set becomes pending for it, then accept the signal — remove it from the pending set — and store the number in *sig. For the signal to have a chance to be pending rather than delivered to a handler, it must first be blocked (typically process-wide; see signal-masks). Return convention: 0 on success, otherwise an error number (EINVAL for an invalid set member). glibc’s sigwait silently ignores the two NPTL-internal RT signals in set (per sigwait(3) VERSIONS).

Waiting for SIGKILL/SIGSTOP is silently ignored (same rule as blocking them).

sigwaitinfo(2) / sigtimedwait(2) (IF-009)

#include <signal.h>        /* _POSIX_C_SOURCE >= 199309L */
int sigwaitinfo(const sigset_t *restrict set, siginfo_t *_Nullable restrict info);
int sigtimedwait(const sigset_t *restrict set, siginfo_t *_Nullable restrict info,
                 const struct timespec *restrict timeout);

Category: sigwaitinfo(3) is a library function implemented on top of sigtimedwait(2), which itself wraps the rt_sigtimedwait(2) syscall (kernel ≥ 2.2, with sigsetsize argument); per sigtimedwait.2. POSIX.1-2024 documents both; the raw syscall is Linux-only.

Differences from sigwait(3): they return the signal number as the function result (> 0), report errors the usual way (-1 + errno), and can fill a siginfo_t — for a sigqueue(3)-delivered real-time signal, si_value carries the payload; the queue resource is released on consumption (per sigwaitinfo(2) and real-time-signals).

Selection and timeout semantics (per sigtimedwait.2 / POSIX):

  • If several signals in set are pending, selection follows the usual ordering rules: lowest-numbered real-time signal first; standard-before-real-time on Linux; order among standard signals unspecified.
  • sigtimedwait with a zero-valued timeout is a poll; with timeout == NULL, POSIX leaves the meaning unspecified and Linux treats it as sigwaitinfo (infinite wait).
  • Errors: EAGAIN (timeout expired), EINTR (interrupted by a handler for a signal not in set), EINVAL (bad timeout).

Notes that shape real usage (per sigtimedwait.2):

  • Block the signals in set beforehand (in all threads of the process), or the signal may be consumed by default disposition or a handler in another thread.
  • If multiple threads wait for the same signal, exactly one of them receives each process-directed instance; which one is indeterminate.
  • You cannot sigwait for synchronously generated signals: SIGSEGV/SIGFPE-class faults go to a handler or kill the process — sigwaitinfo/sigtimedwait are documented as unable to receive them.

On 32-bit architectures with 64-bit time_t (glibc ≥ 2.34 era), the timeout is marshalled through the rt_sigtimedwait_time64 syscall variant — an ABI detail glibc hides (Linux ≥ 5.1 time64 family). For IF-009 completeness: applications see only sigtimedwait(3); the *64 split is a libc/kernel implementation detail.

sigsuspend(2) (IF-010)

#include <signal.h>        /* _POSIX_C_SOURCE */
int sigsuspend(const sigset_t *mask);

Category: libc wrapper around rt_sigsuspend(2) (kernel ≥ 2.2; per sigsuspend.2). POSIX.1.

Semantics: atomically replace the calling thread’s mask with mask, then sleep until a signal whose action is to run a handler (or terminate the process) arrives. If the signal terminates, sigsuspend never returns; if it is caught, sigsuspend returns -1 with errno == EINTR after the handler returns, and the previous mask is restored by the kernel (per sigsuspend.2). SIGKILL/SIGSTOP in mask are ignored (they cannot be blocked). It always “fails” — EINTR is the success path.

The race it exists to close

/* WRONG — classic lost-wakeup race */
sigprocmask(SIG_BLOCK, &block, &old);
while (!work_pending())
    pause();                 /* if the signal arrives between the check and
                                pause(), it is delivered now (mask is still
                                blocking? no: pause() doesn't unblock) — the
                                wakeup is lost and we sleep forever */
sigprocmask(SIG_SETMASK, &old, NULL);
/* RIGHT — atomic swap closes the window */
sigprocmask(SIG_BLOCK, &block, &old);
while (!work_pending())
    sigsuspend(&old);        /* unblocks AND sleeps in one atomic step:
                                a signal delivered after the check but before
                                the sleep is caught by sigsuspend itself */
sigprocmask(SIG_SETMASK, &old, NULL);

Per sigsuspend.2 NOTES, this block-then-suspend pattern is the canonical use: protect the critical section with a blocked mask, then pass the saved mask to sigsuspend.

Consuming via sigwait vs delivery via handler

Aspectsigwait/sigwaitinfohandler via sigaction
Execution contextan ordinary thread, full program state, any (thread-safe) codeasync-signal-safe code only (see async-signal-safety)
Error returnsordinary return values/siginfonone — return to interrupted code
Ordering/priorityrespects the real-time ordering rules at selection timesame kernel ordering; ordering of handler runs follows delivery
Interaction with blocked masksignal must be blocked or it may never reach sigwaitworks whether or not blocked (unblocked signals interrupt anytime)
EINTR exposurenone for signals in set (they’re consumed); EINTR if interrupted by a handler for a signal outside setthe interrupted syscall below the handler sees EINTR unless SA_RESTART
Typical usededicated signal thread in servers/daemonsminimal flags (volatile sig_atomic_t) in simple programs

EINTR semantics summary (per sigtimedwait.2 and signal(7)): sigwait-family calls are never restarted after interruption by a handler; sigsuspend is likewise never restarted and always returns -1/EINTR — that is its defined control flow.

Example — race-free wait with a timeout

/* timedwait.c — consume SIGUSR1 synchronously, 5s timeout.
 * Expected: kill -USR1 <pid> => "got SIGUSR1 from pid N (code SI_USER)";
 *           no signal within 5s => "timed out: EAGAIN".
 */
#define _GNU_SOURCE
#include <signal.h>
#include <stdio.h>
#include <time.h>
#include <errno.h>

int main(void) {
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGUSR1);
    sigprocmask(SIG_BLOCK, &set, NULL);        /* block so nothing else takes it */

    struct timespec ts = { .tv_sec = 5, .tv_nsec = 0 };
    siginfo_t si;
    int sig = sigtimedwait(&set, &si, &ts);
    if (sig == -1) {
        perror(errno == EAGAIN ? "timeout" : "sigtimedwait");
        return 1;
    }
    printf("got SIGUSR1 from pid %d (si_code %d)\n", si.si_pid, si.si_code);
    return 0;
}

References