Why Some Linux Signals Are Unreliable and How Real‑Time Signals Fix the Problem
This article explains the difference between unreliable (1‑31) and reliable real‑time (32‑64) Linux signals, demonstrates the behavior with two C programs, and reveals the kernel data structures and limits that govern signal queuing and delivery.
Background
Traditional UNIX signal() cannot block arbitrary signals during handling, and non‑real‑time signals are not queued; if a pending instance already exists the kernel discards new ones. Modern glibc fixes some historic issues but the fundamental reliability problem remains.
Reliable vs Unreliable Signals
Signals whose numbers are in the range [1,31] are unreliable: the kernel keeps at most one pending instance and drops subsequent deliveries. Signals in the range [32,64] (real‑time signals) are reliable: each instance is queued, up to a system‑wide limit.
Demo Programs
Two small C programs illustrate the behavior.
signal_receiver.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
static int sig_cnt[NSIG];
static volatile sig_atomic_t get_SIGINT = 0;
void handler(int signo) {
if (signo == SIGINT) {
get_SIGINT = 1;
} else {
sig_cnt[signo]++;
}
}
int main(int argc, char *argv[]) {
int i = 0;
sigset_t blockall_mask, pending_mask, empty_mask;
printf("%s:PID is %ld
", argv[0], getpid());
for (i = 1; i < NSIG; i++) {
if (i == SIGKILL || i == SIGSTOP) continue;
if (signal(i, &handler) == SIG_ERR) {
fprintf(stderr, "signal for signo(%d) failed(%s)
", i, strerror(errno));
}
}
if (argc > 1) {
int sleep_time = atoi(argv[1]);
sigfillset(&blockall_mask);
if (sigprocmask(SIG_SETMASK, &blockall_mask, NULL) == -1) {
fprintf(stderr, "setprocmask to block all signal failed(%s)
", strerror(errno));
return -2;
}
printf("I will sleep %d second
", sleep_time);
sleep(sleep_time);
if (sigpending(&pending_mask) == -1) {
fprintf(stderr, "sigpending failed(%s)
", strerror(errno));
return -2;
}
for (i = 1; i < NSIG; i++) {
if (sigismember(&pending_mask, i)) {
printf("signo(%d) :%s
", i, strsignal(i));
}
}
sigemptyset(&empty_mask);
if (sigprocmask(SIG_SETMASK, &empty_mask, NULL) == -1) {
fprintf(stderr, "setprocmask to release all signal failed(%s)
", strerror(errno));
return -3;
}
}
while (!get_SIGINT) continue; // wait for SIGINT
for (i = 1; i < NSIG; i++) {
if (sig_cnt[i] != 0) {
printf("%s:signal %d caught %d time%s
", argv[0], i, sig_cnt[i], (sig_cnt[i] > 1) ? "s" : "");
}
}
return 0;
}signal_sender.c
#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
void usage() {
fprintf(stderr, "USAGE:
");
fprintf(stderr, "--------------------------------
");
fprintf(stderr, "signal_sender pid signo times
");
}
int main(int argc, char *argv[]) {
pid_t pid = -1;
int signo = -1, times = -1, i;
if (argc < 4) { usage(); return -1; }
pid = atol(argv[1]);
signo = atoi(argv[2]);
times = atoi(argv[3]);
if (pid <= 0 || times < 0 || signo < 1 || signo >= 64 || signo == 32 || signo == 33) {
usage(); return -1;
}
printf("pid = %ld,signo = %d,times = %d
", (long)pid, signo, times);
for (i = 0; i < times; i++) {
if (kill(pid, signo) == -1) {
fprintf(stderr, "send signo(%d) to pid(%ld) failed,reason(%s)
", signo, (long)pid, strerror(errno));
return -2;
}
}
fprintf(stdout, "done
");
return 0;
}Running the Demo
Start signal_receiver (optionally with a sleep interval to block all signals). While it is sleeping, run signal_sender to send 10 000 instances of SIGUSR1 (signal 10, unreliable) and 10 000 instances of SIGRTMIN+2 (signal 36, reliable). The receiver reports only a few thousand catches for the unreliable signal but all 10 000 catches for the reliable one. When the pending‑signal limit (e.g., 15408, visible via ulimit -i) is reached, further reliable signals are dropped.
Kernel Implementation
The kernel tracks pending signals in two structures: struct task_struct contains a struct sigpending pending field. struct signal_struct contains a struct sigpending shared_pending field.
Both sigpending structures hold a list_head and a sigset_t signal bitmap.
When a non‑real‑time signal ( signo < 32) arrives, the kernel checks whether the same signal is already pending; if so, the new instance is discarded. For real‑time signals the kernel allocates a struct sigqueue and links it into the pending list, guaranteeing delivery until the global pending‑signal limit is reached.
Pending‑Signal Limit
The per‑process pending‑signal limit can be queried with ulimit -i (default 15408 on many Linux systems). Sending more reliable signals than this limit results in the excess being dropped.
Conclusion
Non‑real‑time signals can be lost because the kernel does not queue them, while real‑time signals are reliably queued up to a configurable limit. Understanding these differences and the underlying kernel data structures helps developers choose the appropriate inter‑process communication mechanism on Linux.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
ITPUB
Official ITPUB account sharing technical insights, community news, and exciting events.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
