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.
| 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) |
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;
}
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;
}
Terminal 1:
./sig_receiver 15 & (blocks signals for 15 sec)Terminal 2:
./sig_sender <PID> 1000000 10 2Result: signal 10 caught only 1 time even though sent 1,000,000 times.
/* 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 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.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.