Ch20.16 – Signals Are Not Queued

 

Ch20.16 – Signals Are Not Queued
Linux System Programming Β· EmbeddedPathashala
πŸ“‘ Topic 16 of 19
🎯 Intermediate
πŸ’» 3 Code Examples
❓ Interview Q&A
πŸ”‘ Key Terms
pending signal set signal coalescing sig_sender sig_receiver sig_atomic_t volatile realtime signals NSIG
The Key Rule: One Bit, Not a Counter

The kernel represents pending signals as a bitmask, not a queue or counter. Each signal has exactly one bit. If signal N is generated 1000 times while it is blocked, that bit is set to 1 β€” still just 1. When the signal is unblocked, it is delivered exactly once.

This is the fundamental difference between standard signals (1–31) and POSIX realtime signals (SIGRTMIN–SIGRTMAX). Realtime signals are queued β€” you get one delivery per send. Standard signals are not.

πŸ“Š Standard vs. Realtime Signals
Property Standard Signals (1–31) Realtime Signals
Pending representation Bitmask (1 bit per signal) Queue (one entry per send)
Multiple sends while blocked Delivered only ONCE Delivered once per send
Order of delivery Unspecified Lowest number first (FIFO)
Can carry data? No (signal number only) Yes (via sigqueue() + siginfo_t)
Never rely on standard signals for counting events. If you send SIGUSR1 ten times to a sleeping process, it may only wake up once.
πŸ’» Code Example 1 – sig_sender.c

This program sends a large number of signals to a target process. You run it alongside sig_receiver to observe signal coalescing.

/* sig_sender.c β€” send many signals to a process
   Usage: ./sig_sender PID num-sigs sig-num [sig-num-2]
   Compile: gcc -o sig_sender sig_sender.c */

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>

int main(int argc, char *argv[])
{
    int numSigs, sig, j;
    pid_t pid;

    if (argc < 4) {
        fprintf(stderr, "Usage: %s pid num-sigs sig-num [sig-num-2]\n", argv[0]);
        return 1;
    }

    pid     = (pid_t) atol(argv[1]);
    numSigs = atoi(argv[2]);
    sig     = atoi(argv[3]);

    printf("%s: sending signal %d to PID %ld, %d times\n",
           argv[0], sig, (long)pid, numSigs);

    for (j = 0; j < numSigs; j++) {
        if (kill(pid, sig) == -1) {
            perror("kill");
            return 1;
        }
    }

    /* Optional: send a termination signal after the burst */
    if (argc > 4) {
        int sig2 = atoi(argv[4]);
        if (kill(pid, sig2) == -1)
            perror("kill sig2");
    }

    printf("%s: exiting\n", argv[0]);
    return 0;
}
πŸ’» Code Example 2 – sig_receiver.c

This program catches all signals and counts how many deliveries it actually receives. Run with a sleep argument to block signals before receiving the burst.

/* sig_receiver.c β€” catch and count signals
   Usage: ./sig_receiver [sleep-secs]
   Compile: gcc -o sig_receiver sig_receiver.c */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>

static int sigCnt[NSIG];                    /* count per signal */
static volatile sig_atomic_t gotSigint = 0; /* exit flag */

/* Handler for all signals */
static void handler(int sig)
{
    if (sig == SIGINT)
        gotSigint = 1;
    else
        sigCnt[sig]++;
}

int main(int argc, char *argv[])
{
    int n, numSecs;
    sigset_t blockMask, emptyMask, pendingMask;

    printf("%s: PID is %ld\n", argv[0], (long)getpid());

    /* Install handler for every catchable signal */
    for (n = 1; n < NSIG; n++)
        (void) signal(n, handler); /* ignore errors for uncatchable sigs */

    if (argc > 1) {
        /* Block all signals, sleep, then show pending */
        numSecs = atoi(argv[1]);
        sigfillset(&blockMask);
        sigprocmask(SIG_SETMASK, &blockMask, NULL);

        printf("%s: sleeping for %d seconds (signals blocked)\n",
               argv[0], numSecs);
        sleep(numSecs);

        /* Show pending before unblocking */
        sigpending(&pendingMask);
        printf("%s: pending signals after sleep:\n", argv[0]);
        for (n = 1; n < NSIG; n++) {
            if (sigismember(&pendingMask, n))
                printf("  %d (%s)\n", n, strsignal(n));
        }

        /* Unblock β€” pending signals delivered now */
        sigemptyset(&emptyMask);
        sigprocmask(SIG_SETMASK, &emptyMask, NULL);
    }

    /* Spin until SIGINT received */
    while (!gotSigint)
        continue;

    /* Print delivery counts */
    for (n = 1; n < NSIG; n++) {
        if (sigCnt[n] != 0)
            printf("%s: signal %d caught %d time(s)\n",
                   argv[0], n, sigCnt[n]);
    }

    return 0;
}
Demo run:
Terminal 1: ./sig_receiver 15 & (blocks signals for 15 sec)
Terminal 2: ./sig_sender <PID> 1000000 10 2
Result: signal 10 caught only 1 time even though sent 1,000,000 times.
πŸ’» Code Example 3 – Fast-send without blocking (may catch a few)
/* When signals are sent without blocking the receiver,
   some are received because the scheduler alternates processes.
   Run: ./sig_receiver & then ./sig_sender <PID> 1000000 10 2
   You might see "signal 10 caught 52 times" β€” not 1,000,000. */

/* The key insight: each time the RECEIVER is scheduled,
   it delivers at most 1 pending instance of signal 10.
   All other pending copies are collapsed (coalesced) by the kernel. */

/* Minimal illustration of volatile sig_atomic_t pattern */
#include <stdio.h>
#include <signal.h>

static volatile sig_atomic_t event_count = 0;

static void count_handler(int sig)
{
    (void)sig;
    event_count++;   /* safe: sig_atomic_t is atomically read/written */
}

int main(void)
{
    struct sigaction sa = { .sa_handler = count_handler };
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGUSR1, &sa, NULL);

    /* Even if SIGUSR1 is sent 100 times rapidly from another process,
       event_count will only increment by the number of times the signal
       was actually delivered β€” which could be far fewer than 100. */
    printf("SIGUSR1 handler installed. Send signals from another terminal.\n");
    printf("PID = %ld\n", (long)getpid());

    /* Wait */
    while (event_count < 5)
        pause();

    printf("Received (at least) 5 deliveries. event_count = %d\n", event_count);
    return 0;
}
volatile sig_atomic_t β€” the volatile prevents compiler caching of the variable in a register, and sig_atomic_t ensures the read/write is atomic on the target platform. Always use this type for flags shared between a signal handler and the main program.
❓ Interview Questions
Q1. If a process blocks SIGUSR1 and 1000 instances are sent, how many times is it delivered when unblocked?
Exactly once. The pending signal set for standard signals is a bitmask β€” there is only one bit for SIGUSR1. No matter how many times it is sent, the bit is either set or not. When unblocked, the signal is delivered once and the bit is cleared.
Q2. What is the difference in queuing behaviour between standard signals and realtime signals?
Standard signals (1–31) are not queued β€” the pending set is a bitmask, so duplicates are lost. Realtime signals (SIGRTMIN to SIGRTMAX) are queued: each send via sigqueue() creates a separate entry in the queue, and each is delivered in order.
Q3. Why should you use volatile sig_atomic_t for a flag set inside a signal handler?
volatile prevents the compiler from optimizing away reads of the variable (keeping it in a register across loop iterations). sig_atomic_t is an integer type that the platform guarantees can be read and written atomically, avoiding partial-update races between the handler and main code.
Q4. Can you use standard signals to reliably count events (e.g., count how many child processes have terminated via SIGCHLD)?
No. If two children exit before the SIGCHLD handler runs, only one delivery occurs. The correct approach in a SIGCHLD handler is to loop calling waitpid() with WNOHANG until it returns 0, harvesting all children in one handler invocation.

Leave a Reply

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