22.9 – 22.11 Waiting for Signals Safely

 

22.9 – 22.11 Waiting for Signals Safely
sigsuspend(), sigwaitinfo(), sigtimedwait(), and signalfd() — three ways to wait for signals correctly

22.9 — Waiting with a Mask: sigsuspend()

A common pattern is: block a signal during a critical section, then unblock it and wait for it to arrive. The naive approach has a race condition. sigsuspend() solves this with an atomic operation.

The Race Condition Problem

Consider this sequence:

The Race Condition with sigprocmask + pause()
Step Code What Can Go Wrong
1 sigprocmask(SIG_BLOCK, &intMask, …)
/* critical section */
OK so far
2 sigprocmask(SIG_SETMASK, &prevMask, …) /* unblock SIGINT */ Signal arrives HERE ↓
3 /* SIGINT arrives now — handler runs! */ BUG! Signal was handled before pause()
4 pause(); /* wait for SIGINT */ Waits FOREVER — signal already delivered!

The Fix: sigsuspend() is Atomic

sigsuspend(mask) atomically: (1) replaces the process signal mask with mask, (2) suspends until a signal is caught, (3) restores the original mask. Steps 1 and 2 happen with no gap between them — the race condition is eliminated.

#include <signal.h>
int sigsuspend(const sigset_t *mask);
/* Returns -1 with errno=EINTR when a signal handler returns */
/* sigsuspend_demo.c - Correct pattern: block, critical section, sigsuspend */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>

static volatile sig_atomic_t got_sigint = 0;
static volatile sig_atomic_t got_sigquit = 0;

void handler(int sig) {
    if (sig == SIGINT)  got_sigint = 1;
    if (sig == SIGQUIT) got_sigquit = 1;
    /* Note: printf is NOT async-signal-safe, used here for demo only */
    const char *msg = (sig == SIGINT) ? "Got SIGINT\n" : "Got SIGQUIT\n";
    write(STDOUT_FILENO, msg, 10);
}

int main(void) {
    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGQUIT, &sa, NULL);

    /* Block SIGINT and SIGQUIT */
    sigset_t block_mask, prev_mask;
    sigemptyset(&block_mask);
    sigaddset(&block_mask, SIGINT);
    sigaddset(&block_mask, SIGQUIT);
    sigprocmask(SIG_BLOCK, &block_mask, &prev_mask);

    printf("PID=%d: In critical section (signals blocked)\n", getpid());
    printf("Press Ctrl+C or Ctrl+\\ -- signal is queued until sigsuspend\n");

    /* Simulate critical work */
    sleep(3);

    printf("Critical section done. Waiting for signal via sigsuspend...\n");

    /* Atomically: restore original mask (unblocking signals) + suspend.
       Any signal that arrived during critical section is now delivered.
       sigsuspend returns after the handler runs, with the old mask restored. */
    while (!got_sigquit) {
        /* prev_mask allows SIGINT and SIGQUIT through */
        sigsuspend(&prev_mask);
        if (got_sigint) {
            printf("Processed SIGINT, looping back to wait\n");
            got_sigint = 0;
        }
    }

    /* Restore original mask */
    sigprocmask(SIG_SETMASK, &prev_mask, NULL);
    printf("Got SIGQUIT — exiting loop\n");
    return 0;
}
$ ./sigsuspend_demo
PID=5678: In critical section (signals blocked)
Press Ctrl+C or Ctrl+\ -- signal is queued until sigsuspend
^C                  <-- Ctrl+C during critical section (queued)
Critical section done. Waiting for signal via sigsuspend...
Got SIGINT          <-- delivered immediately by sigsuspend
Processed SIGINT, looping back to wait
^\                  <-- Ctrl+\ 
Got SIGQUIT
Got SIGQUIT — exiting loop

22.10 — Synchronously Waiting: sigwaitinfo()

Sometimes you don’t want a signal handler at all. You just want to block until a signal arrives and then process it inline. sigwaitinfo() does exactly this.

sigwaitinfo() and sigtimedwait()

#define _POSIX_C_SOURCE 199309
#include <signal.h>

/* Block until a signal in 'set' arrives. Returns signal number.
   If 'info' is not NULL, fills it with siginfo_t data. */
int sigwaitinfo(const sigset_t *set, siginfo_t *info);

/* Same as sigwaitinfo but with a timeout */
int sigtimedwait(const sigset_t *set, siginfo_t *info,
                 const struct timespec *timeout);
/* Returns signal number, or -1 with errno=EAGAIN if timeout */
Comparison: sigsuspend vs sigwaitinfo
Feature sigsuspend() sigwaitinfo()
Needs a signal handler? YES — signal handler runs NO — processes signal inline
Gets siginfo_t data? Only if SA_SIGINFO handler Always (if info != NULL)
Performance Slower (context switch to handler) Faster (no handler overhead)
Mask after signal? Restored to pre-sigsuspend mask Signal removed from pending set; mask unchanged
Timeout support? NO YES (sigtimedwait)
/* sigwaitinfo_demo.c - Wait for signals without a handler */
#define _POSIX_C_SOURCE 199309
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <time.h>

int main(void) {
    /* Block the signals we want to receive synchronously */
    sigset_t wait_set;
    sigfillset(&wait_set);  /* block everything */
    sigprocmask(SIG_SETMASK, &wait_set, NULL);

    printf("PID=%d: waiting for signals via sigwaitinfo()\n", getpid());
    printf("Send signals with: kill -USR1 %d or kill -RTMIN %d\n\n",
           getpid(), getpid());

    while (1) {
        siginfo_t si;
        int sig = sigwaitinfo(&wait_set, &si);

        if (sig == -1) {
            perror("sigwaitinfo");
            continue;
        }

        printf("Got signal: %d (%s)\n", sig, strsignal(sig));
        printf("  si_code = %d (%s)\n", si.si_code,
               (si.si_code == SI_USER)  ? "SI_USER (from kill)" :
               (si.si_code == SI_QUEUE) ? "SI_QUEUE (from sigqueue)" :
               "other");
        printf("  si_pid  = %d\n", si.si_pid);
        printf("  si_uid  = %d\n", si.si_uid);
        if (si.si_code == SI_QUEUE)
            printf("  si_value.sival_int = %d\n", si.si_value.sival_int);
        printf("\n");

        if (sig == SIGTERM || sig == SIGINT) {
            printf("Got termination signal, exiting.\n");
            break;
        }
    }

    return 0;
}
/* sigtimedwait_demo.c - sigwaitinfo with timeout (poll mode) */
#define _POSIX_C_SOURCE 199309
#include <stdio.h>
#include <signal.h>
#include <time.h>
#include <errno.h>
#include <unistd.h>

int main(void) {
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGUSR1);
    sigprocmask(SIG_BLOCK, &mask, NULL);

    struct timespec timeout = { .tv_sec = 2, .tv_nsec = 0 };  /* 2 seconds */

    printf("PID=%d: waiting up to 2 seconds for SIGUSR1\n", getpid());

    while (1) {
        siginfo_t si;
        int sig = sigtimedwait(&mask, &si, &timeout);

        if (sig == -1) {
            if (errno == EAGAIN) {
                printf("Timeout! No signal in 2 seconds. Checking other work...\n");
                /* Do other work here while waiting */
                continue;
            }
            perror("sigtimedwait");
            break;
        }

        printf("Got SIGUSR1 from PID=%d\n", si.si_pid);
        break;
    }

    return 0;
}
/* timeout.tv_sec=0, timeout.tv_nsec=0 → immediate poll: check if signal pending */
struct timespec zero_timeout = { 0, 0 };
int sig = sigtimedwait(&mask, &si, &zero_timeout);
if (sig == -1 && errno == EAGAIN)
    printf("No signal currently pending\n");
else if (sig > 0)
    printf("Signal %d was pending!\n", sig);

22.11 — Signals as File Descriptors: signalfd()

Linux-specific (since kernel 2.6.22): signalfd() creates a file descriptor from which you can read() signals. This is very useful for event-loop architectures where you want to monitor signals alongside sockets, pipes, and timers using select()/poll()/epoll().

signalfd() API and signalfd_siginfo Structure

#include <sys/signalfd.h>
int signalfd(int fd, const sigset_t *mask, int flags);
/* fd=-1: create new fd; otherwise modify existing signalfd
   flags: 0, SFD_CLOEXEC, SFD_NONBLOCK
   Returns file descriptor on success, -1 on error */

/* Read from signalfd — each read() returns one signalfd_siginfo */
struct signalfd_siginfo {
    uint32_t ssi_signo;   /* Signal number */
    int32_t  ssi_errno;   /* Errno (unused) */
    int32_t  ssi_code;    /* Signal code (SI_USER, SI_QUEUE, etc.) */
    uint32_t ssi_pid;     /* Sender PID */
    uint32_t ssi_uid;     /* Sender real UID */
    int32_t  ssi_fd;      /* File descriptor (SIGPOLL/SIGIO) */
    uint32_t ssi_tid;     /* Kernel timer ID */
    uint32_t ssi_band;    /* Band event */
    uint32_t ssi_overrun; /* Overrun count (POSIX timers) */
    int32_t  ssi_status;  /* Exit status (SIGCHLD) */
    int32_t  ssi_int;     /* Integer sent by sigqueue() */
    uint64_t ssi_ptr;     /* Pointer sent by sigqueue() */
    uint64_t ssi_utime;   /* User CPU time (SIGCHLD) */
    uint64_t ssi_stime;   /* System CPU time (SIGCHLD) */
    uint64_t ssi_addr;    /* Faulting address (hardware signals) */
};
/* signalfd_basic.c - Read signals from a file descriptor */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/signalfd.h>
#include <string.h>

int main(void) {
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTERM);
    sigaddset(&mask, SIGUSR1);
    sigaddset(&mask, SIGRTMIN);

    /* MUST block the signals before creating signalfd,
       otherwise signals are delivered normally and missed by signalfd */
    if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) {
        perror("sigprocmask");
        exit(1);
    }

    /* Create the signalfd file descriptor */
    int sfd = signalfd(-1, &mask, SFD_CLOEXEC);
    if (sfd == -1) {
        perror("signalfd");
        exit(1);
    }

    printf("PID=%d: Waiting for signals (SIGINT, SIGTERM, SIGUSR1, SIGRTMIN)\n",
           getpid());
    printf("Use: kill -INT %d  or  kill -USR1 %d\n\n", getpid(), getpid());

    while (1) {
        struct signalfd_siginfo fdsi;
        ssize_t s = read(sfd, &fdsi, sizeof(fdsi));

        if (s != sizeof(fdsi)) {
            perror("read");
            exit(1);
        }

        printf("Signal %d received:\n", fdsi.ssi_signo);
        printf("  From PID: %u\n", fdsi.ssi_pid);
        printf("  Code: %d (%s)\n", fdsi.ssi_code,
               fdsi.ssi_code == SI_USER  ? "SI_USER" :
               fdsi.ssi_code == SI_QUEUE ? "SI_QUEUE" : "other");
        if (fdsi.ssi_code == SI_QUEUE)
            printf("  Data: %d\n", fdsi.ssi_int);
        printf("\n");

        if (fdsi.ssi_signo == SIGTERM || fdsi.ssi_signo == SIGINT) {
            printf("Shutdown signal received, exiting.\n");
            break;
        }
    }

    close(sfd);
    return 0;
}
/* signalfd_epoll.c - signalfd in an epoll event loop (production pattern) */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/signalfd.h>
#include <sys/epoll.h>

int main(void) {
    /* Block signals we'll handle via signalfd */
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTERM);
    sigprocmask(SIG_BLOCK, &mask, NULL);

    /* Create signalfd with SFD_NONBLOCK for use in event loop */
    int sfd = signalfd(-1, &mask, SFD_CLOEXEC | SFD_NONBLOCK);

    /* Create epoll instance */
    int epfd = epoll_create1(EPOLL_CLOEXEC);

    /* Add signalfd to epoll */
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = sfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);

    /* Also add stdin to epoll */
    ev.events = EPOLLIN;
    ev.data.fd = STDIN_FILENO;
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);

    printf("PID=%d: Event loop running. Press Enter or Ctrl+C\n", getpid());

    struct epoll_event events[10];
    while (1) {
        int nfds = epoll_wait(epfd, events, 10, -1);

        for (int i = 0; i < nfds; i++) {
            if (events[i].data.fd == sfd) {
                /* Signal ready to read */
                struct signalfd_siginfo si;
                read(sfd, &si, sizeof(si));
                printf("Signal %d from PID %u\n", si.ssi_signo, si.ssi_pid);
                if (si.ssi_signo == SIGINT || si.ssi_signo == SIGTERM) {
                    printf("Shutting down event loop\n");
                    close(sfd); close(epfd);
                    return 0;
                }
            } else if (events[i].data.fd == STDIN_FILENO) {
                char buf[128];
                ssize_t n = read(STDIN_FILENO, buf, sizeof(buf)-1);
                if (n > 0) {
                    buf[n] = '\0';
                    printf("stdin: %s", buf);
                }
            }
        }
    }
}
The self-pipe trick alternative: Before signalfd, the classic way to handle signals in an event loop was the “self-pipe trick” — write a byte to a pipe in the signal handler, and select/poll the read end of the pipe. signalfd() makes this much cleaner with no pipe needed.

Interview Questions — sigsuspend, sigwaitinfo, signalfd

Q1. What is the race condition that sigsuspend() solves? Explain with an example.
Answer:

The race condition occurs when trying to: (1) unblock a signal with sigprocmask(), then (2) wait for it with pause(). If the signal arrives between step 1 and step 2, the handler runs before pause() is called. pause() then waits forever because the signal was already delivered. sigsuspend(mask) atomically performs both steps — there is no window between unblocking the signal and suspending. The kernel guarantees these happen together as a single indivisible operation.

Q2. What does sigsuspend() return and what happens to the signal mask after it returns?
Answer:

sigsuspend() always returns -1 with errno set to EINTR (when a signal handler returns). It never returns 0. After returning, the process signal mask is automatically restored to what it was before the sigsuspend() call. This restoration is also atomic — the mask is restored before the function returns to the caller.

Q3. When would you choose sigwaitinfo() over sigsuspend()?
Answer:

Use sigwaitinfo() when: (1) You don’t need a signal handler and want to process signals inline in your main code. (2) You need the siginfo_t data (sender PID, attached data) without the overhead of a handler. (3) Performance matters — sigwaitinfo() is faster than sigsuspend()+handler because there is no context switch to handler code. (4) You want timeout support (use sigtimedwait() variant). sigsuspend() is better when you already have signal handlers and just need to wait atomically.

Q4. What is signalfd() and what is its main advantage?
Answer:

signalfd() (Linux 2.6.22+) creates a file descriptor from which you can read() pending signals as signalfd_siginfo structures. The main advantage is integration with event loops: you can monitor signals alongside sockets, pipes, and timers using select(), poll(), or epoll(). This eliminates the need for the “self-pipe trick” and makes it easy to handle signals in single-threaded event-driven servers. SFD_NONBLOCK allows non-blocking reads; SFD_CLOEXEC prevents inheritance across exec().

Q5. What is critical to do BEFORE creating a signalfd, and why?
Answer:

You must block the signals you intend to receive via signalfd using sigprocmask(SIG_BLOCK, …) BEFORE calling signalfd(). If you don’t, signals may be delivered via the default/installed handler before signalfd has a chance to read them. Once blocked, signals are queued by the kernel until your code reads them from the signalfd. The rule is: block first, then create the signalfd for those same signals.

Q6. How does sigtimedwait() differ from sigwaitinfo(), and what does a zero timeout mean?
Answer:

sigtimedwait() adds a timeout parameter (struct timespec). If no signal arrives within the timeout, it returns -1 with errno=EAGAIN. sigwaitinfo() blocks indefinitely. A zero timeout (tv_sec=0, tv_nsec=0) makes sigtimedwait() a non-blocking poll — it checks if any of the specified signals are currently pending and returns immediately either with the signal number or EAGAIN. This is useful for draining a signal queue without blocking.

Next Topic

Can signals be used for IPC? Learn the limitations.

22.12 — IPC with Signals → ← Back to Index

Leave a Reply

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