Realtime Signals
Realtime signals are the numbered signals from
SIGRTMINtoSIGRTMAXthat POSIX.1b (the “realtime extensions”) added to fix three deficiencies of the classic Unix signals. Unlike a standard signal — which collapses to a single pending bit, carries no data, and has no defined delivery order among different signals — a realtime signal queues (multiple instances of the same signal are all delivered, bounded by theRLIMIT_SIGPENDINGresource limit), carries a value (anintor pointer attached at send time viasigqueue(3), received insiginfo_t.si_value), and has a defined delivery order (the lowest-numbered signal first, and multiple instances of one signal in first-in-first-out order) (signal(7)). On Linux there are 33 of them — numbers 32 through 64 — though glibc reserves the first two or three for its threading implementation, which is why portable code writesSIGRTMIN+n, never a hard number. The default action for an unhandled realtime signal is to terminate the process.
This note is about the queued, data-carrying signals specifically. The masking machinery they share with standard signals — sigprocmask, the pending set, synchronous acceptance via sigwaitinfo — is the subject of Signal Sets and Masking. The non-queuing classic signals and their default behaviors are Standard Signals and Their Default Actions. The overall delivery model and async handlers live in Linux Signals Overview and sigaction and Signal Handlers. Start from any of those for context; this note stands alone on what makes RT signals different.
Mental Model
The defining contrast is queue vs. flag. A standard signal is a single bit in a per-thread/per-process bitmap: raise SIGUSR1 ten times while it is blocked and exactly one delivery happens when it unblocks — the nine extra occurrences are coalesced away, because there is nowhere to store “it happened ten times.” A realtime signal is instead a linked list of queued events: raise SIGRTMIN+0 ten times and (limits permitting) ten separate deliveries occur, each potentially carrying a different payload. That single difference — having a queue instead of a flag — is what makes RT signals usable as a genuine, lossless, data-bearing notification channel rather than a lossy “something happened” nudge.
flowchart TB subgraph STD["Standard signal (e.g. SIGUSR1) — a FLAG"] S1["send #1"] --> BIT["pending bit = 1"] S2["send #2"] --> BIT S3["send #3"] --> BIT BIT --> DEL1["ONE delivery<br/>(occurrences 2,3 lost)"] end subgraph RT["Realtime signal (e.g. SIGRTMIN+0) — a QUEUE"] R1["sigqueue val=10"] --> Q["pending queue<br/>(list_add_tail, FIFO)"] R2["sigqueue val=20"] --> Q R3["sigqueue val=30"] --> Q Q --> DEL2["THREE deliveries<br/>10, then 20, then 30"] end
Standard signal as a flag versus realtime signal as a FIFO queue. What it shows: repeated standard signals coalesce to one pending bit and one delivery, whereas repeated realtime signals each get their own queue entry (appended with list_add_tail in the kernel) and are delivered in send order with their individual payloads. The insight to take: if you need to count events or attach data to each one, you need a realtime signal — a standard signal physically cannot represent “this happened three times with values 10, 20, 30.”
The SIGRTMIN..SIGRTMAX Range
In the kernel UAPI the bounds are fixed: SIGRTMIN is 32 and SIGRTMAX is _NSIG, which is 64 (include/uapi/asm-generic/signal.h v6.12):
#define _NSIG 64
#define SIGRTMIN 32
#define SIGRTMAX _NSIG /* 64 */That gives signals 32–64, i.e. 33 realtime signals at the kernel level (signal(7)). Standard signals occupy 1–31. The kernel’s queue/no-queue decision keys off exactly this boundary: anything sig < SIGRTMIN is treated as a non-queuing “legacy” signal (more below).
Why you must not hard-code numbers. glibc’s POSIX-threads implementation steals the lowest realtime signals for its own use. The man page is explicit: “the glibc POSIX threads implementation internally uses two (for NPTL) or three (for LinuxThreads) real-time signals… and adjusts the value of SIGRTMIN suitably (to 34 or 35)” (signal(7)). On a modern NPTL system, then, the SIGRTMIN macro your program sees is typically 34, not 32 — signals 32 and 33 are glibc’s (thread cancellation and setuid/timer plumbing). Consequently: “programs should never refer to real-time signals using hard-coded numbers, but instead should always refer to real-time signals using the notation SIGRTMIN+n, and include suitable (run-time) checks that SIGRTMIN+n does not exceed SIGRTMAX.” Writing kill(pid, 34) is fragile; writing kill(pid, SIGRTMIN+0) is portable and dodges glibc’s reserved band automatically. You get SIGRTMAX - SIGRTMIN + 1 usable realtime signals — commonly around 30 once glibc has taken its cut.
Uncertain
Verify: that on a current glibc (e.g. 2.39+) on Linux 6.12,
SIGRTMINevaluates to exactly 34 (two NPTL-reserved signals) rather than 35. Reason: the man page gives “34 or 35” depending on NPTL vs the long-obsolete LinuxThreads, and the exact reserved count is a glibc runtime decision not pinned to a kernel source blob consulted here. To resolve: on the target system run a program that printsSIGRTMINandSIGRTMAX, or read glibc’snptl/pthreadP.h/__libc_current_sigrtmin. uncertain
How Queueing Actually Works in the Kernel
The mechanism lives in __send_signal_locked() in kernel/signal.c. The first gate is legacy_queue(), which is what makes standard signals not queue (kernel/signal.c v6.12):
static inline bool legacy_queue(struct sigpending *signals, int sig)
{
return (sig < SIGRTMIN) && sigismember(&signals->signal, sig);
}For a signal below SIGRTMIN that is already a member of the pending bitmap, the send returns early — no second queue entry, no second delivery. That is the kernel-level reason standard signals coalesce. For sig >= SIGRTMIN, legacy_queue is false, so every send proceeds to allocate a fresh queue entry.
Allocation goes through __sigqueue_alloc(), which is also where the RLIMIT_SIGPENDING accounting happens. It bumps a per-user counter and checks it against the limit:
sigpending = inc_rlimit_get_ucounts(ucounts, UCOUNT_RLIMIT_SIGPENDING, override_rlimit);
if (!sigpending)
return NULL;
if (override_rlimit ||
likely(sigpending <= task_rlimit(t, RLIMIT_SIGPENDING))) {
q = kmem_cache_alloc(sigqueue_cachep, gfp_flags);
} else {
print_dropped_signal(sig); /* over limit, no override */
}So the number of simultaneously queued signals for a user is capped by RLIMIT_SIGPENDING (introduced in Linux 2.6.8 to replace an older /proc knob — signal(7)). When the cap is hit and no override applies, allocation yields NULL. The crucial subtlety is who overrides the limit — computed at the call site as:
if (sig < SIGRTMIN)
override_rlimit = (is_si_special(info) || info->si_code >= 0);
else
override_rlimit = 0; /* realtime signals NEVER override */Standard signals that are kernel-generated or have a non-negative si_code can override the limit (you must always be able to deliver, say, a SIGTERM). Realtime signals never override. That has a hard consequence: if you flood a target with sigqueue past its RLIMIT_SIGPENDING, the q == NULL path is taken and the sender gets -EAGAIN:
else if (!is_si_special(info) && sig >= SIGRTMIN && info->si_code != SI_USER) {
/* Queue overflow, abort. We may abort if the signal was rt
and sent by user using something other than kill(). */
result = TRACE_SIGNAL_OVERFLOW_FAIL;
ret = -EAGAIN;
goto ret;
}This is exactly the EAGAIN that sigqueue(3) documents as “the limit of signals which may be queued has been reached” (sigqueue(3)). For a non-RT signal over the limit, the kernel instead takes a “silent loss of information” path — it still delivers the signal but drops the siginfo payload.
Finally, when a queue entry is allocated, the payload is copied in and the entry is appended to the tail of the pending list — which is what produces FIFO order among instances of the same signal:
copy_siginfo(&q->info, info); /* full siginfo: si_value, si_code, ... */
...
list_add_tail(&q->list, &pending->list);list_add_tail (not list_add) means newer entries go to the end, so they are dequeued oldest-first. Cross-different-number ordering — lowest number first — is enforced at dequeue time by dequeue_signal scanning from the low end. Together these give the guarantee from signal(7): “Multiple real-time signals of the same type are delivered in the order they were sent. If different real-time signals are sent to a process, they are delivered starting with the lowest-numbered signal.”
The Sender Side: sigqueue
Sending a realtime signal with data uses sigqueue(3):
union sigval {
int sival_int;
void *sival_ptr;
};
int sigqueue(pid_t pid, int sig, const union sigval value);It “sends the signal specified in sig to the process whose PID is given in pid,” attaching value (sigqueue(3)). The receiver, if it installed a handler with the SA_SIGINFO flag (see sigaction and Signal Handlers), reads the payload from siginfo_t.si_value and finds si_code == SI_QUEUE marking it as sigqueue-originated. Return is 0 on success; the errors that matter are EAGAIN (queue limit reached — see the kernel path above), EPERM (no permission to signal the target), ESRCH (no such PID), and EINVAL (bad signal number).
Under the hood sigqueue is the glibc wrapper over the rt_sigqueueinfo(2) system call (pthread_sigqueue over rt_tgsigqueueinfo for a specific thread). These are “the low-level interfaces used to send a signal plus data to a process or thread… not intended for direct application use” (rt_sigqueueinfo(2)). There is a security wrinkle here: when sending to another process, the caller cannot forge an arbitrary si_code — it “can’t be a value greater than or equal to zero” and specifically cannot be SI_USER, SI_KERNEL, or (since Linux 2.6.39) SI_TKILL. This stops a process from impersonating the kernel or a different sender in the siginfo the target receives.
You can also send a realtime signal without a payload using plain kill(2) — but then it behaves like any signal sent by kill (si_code == SI_USER, no si_value), and on Linux a kill-sent realtime signal still queues. The point of sigqueue is the attached value.
The Receiver Side: handler or synchronous wait
There are two ways to consume the queued signals and their payloads.
Asynchronously, with an SA_SIGINFO handler (sigaction and Signal Handlers):
void handler(int sig, siginfo_t *si, void *ucontext)
{
/* si->si_value.sival_int / sival_ptr carries the payload */
/* si->si_code == SI_QUEUE if sent via sigqueue */
int payload = si->si_value.sival_int;
/* ... but: this runs in async-signal context — be careful ... */
}
struct sigaction sa = { .sa_flags = SA_SIGINFO, .sa_sigaction = handler };
sigemptyset(&sa.sa_mask);
sigaction(SIGRTMIN+0, &sa, NULL);Synchronously, by blocking the signal and accepting it — usually the better design, because the consuming code runs in normal thread context with no async-signal-safety constraints (see Async-Signal-Safety and Reentrant Handlers):
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGRTMIN+0);
sigprocmask(SIG_BLOCK, &set, NULL); /* block so it queues, never runs default */
for (;;) {
siginfo_t si;
int sig = sigwaitinfo(&set, &si); /* dequeue oldest, lowest-numbered */
if (sig == -1) continue;
int payload = si.si_value.sival_int; /* the value from sigqueue */
/* process one queued event ... */
}sigwaitinfo/sigtimedwait dequeue one queued instance per call and fill the full siginfo_t, so a loop drains the queue in the guaranteed order (sigwaitinfo(2)). The masking precondition — block first — is the same one detailed in Signal Sets and Masking.
Worked Example: a Worker Pool Signalling Completion with a Payload
Consider a supervisor that hands jobs to worker processes and wants each worker to report “job N done” with the job id, losslessly. Realtime signals fit because they queue (no completion is lost even if several land at once) and carry the id.
Worker side — report completion of job job_id:
/* In each worker, when a job finishes: */
union sigval v;
v.sival_int = job_id; /* attach the completed job's id */
if (sigqueue(supervisor_pid, SIGRTMIN+0, v) == -1) {
if (errno == EAGAIN) {
/* supervisor's queue is full (RLIMIT_SIGPENDING). Back off / retry,
because for an RT signal the kernel did NOT override the limit and
the notification was dropped — we must resend. */
}
}Supervisor side — drain completions synchronously:
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGRTMIN+0);
sigprocmask(SIG_BLOCK, &set, NULL); /* block in main before forking workers */
/* ... fork workers; they inherit the block, good ... */
for (;;) {
siginfo_t si;
if (sigwaitinfo(&set, &si) != SIGRTMIN+0) continue;
int done_job = si.si_value.sival_int; /* the job id the worker put in sival_int */
pid_t who = si.si_pid; /* which worker (kernel fills si_pid) */
mark_complete(done_job, who);
}The completions arrive in send order (FIFO for one signal number), each with its job id, and none is silently coalesced the way ten SIGUSR1s would be. The one operational caveat the EAGAIN branch makes explicit: because RT signals do not override RLIMIT_SIGPENDING, a slow supervisor that lets the queue fill will drop notifications unless the worker retries — so a robust protocol treats EAGAIN as “retry later,” and often a high-throughput design abandons signals for this entirely (next section).
Failure Modes and Common Misunderstandings
“Realtime means low-latency / scheduling priority.” It does not. “Realtime” is the POSIX feature name for the queued/data-carrying behavior; these signals have no special scheduling latency guarantee. Naming them after the POSIX.1b “realtime extensions” is the source of endless confusion.
Hard-coding 34 or 32. If you target a literal number you may collide with glibc’s NPTL-reserved signals (32, 33), breaking thread cancellation or worse, or you may pick a number that is SIGRTMIN+n > SIGRTMAX on a system where glibc reserved more. Always SIGRTMIN+n with a <= SIGRTMAX check.
Assuming infinite queueing. The queue is bounded by RLIMIT_SIGPENDING per user. Past it, sigqueue returns EAGAIN and the signal is dropped (RT signals never override the limit). Treating sigqueue as a reliable unbounded channel under load is a bug; check errno.
Doing real work or non-async-signal-safe calls in an SA_SIGINFO handler. A handler runs asynchronously and may interrupt almost anything; only async-signal-safe functions are legal there. The synchronous sigwaitinfo loop sidesteps this entirely and is usually the right design.
Forgetting to block before sigwaitinfo. If the signal is not blocked, its default action (terminate, for an unhandled RT signal) or an installed handler fires instead of being delivered to your waiter — the same precondition as all synchronous acceptance (Signal Sets and Masking).
Expecting kill-sent RT signals to carry data. kill(2) sends with si_code == SI_USER and no si_value. Only sigqueue attaches a payload.
Alternatives and When to Choose Them
- A pipe, eventfd, or Unix socket. For high-throughput or large payloads, a byte-stream/datagram channel beats signals: no per-user pending limit, no async-handler hazards, fits an
epollloop naturally. Signals shine for small, infrequent, out-of-band notifications. - signalfd. If you like the signal model but want the events as a readable file descriptor in an existing
epollloop,signalfddeliverssignalfd_siginfo(including the realtimessi_int/ssi_ptrpayload) without an async handler. - POSIX Message Queues. When you want queued, prioritized, data-carrying messages as a first-class IPC object rather than piggybacked on signals, POSIX mqueues are the cleaner tool — and they were designed alongside RT signals in POSIX.1b. Notably, mqueues can use realtime signals for async arrival notification (
mq_notify), tying the two together. - Standard signals (Standard Signals and Their Default Actions). When you only need “something happened” once and don’t care how many times, a standard signal’s coalescing is a feature (e.g.
SIGCHLD), not a limitation.
Production Notes
Realtime signals appear most in two places. First, timers: POSIX per-process timers (timer_create with SIGEV_SIGNAL) deliver expirations as a chosen realtime signal carrying the timer’s id in si_value, precisely so an application can multiplex many timers onto one handler and tell them apart — a use case standard signals cannot serve because they neither queue nor carry data. Second, mq_notify for POSIX message queues uses an RT signal to announce that a previously-empty queue became readable. Many event-loop libraries and language runtimes nonetheless avoid application-level RT signals in favor of signalfd or fd-based channels because async handlers compose poorly with everything else; the realtime-signal queue is a sharp tool best reserved for the timer/mqueue notification niches it was designed for. When debugging “my queued signals vanish under load,” check the target’s RLIMIT_SIGPENDING (ulimit -i / prlimit) and the sender’s EAGAINs — that pair is the signature of overrunning the per-user queue.
See Also
- Linux Signals Overview — the parent concept and full delivery model
- Signal Sets and Masking — the mask, pending set, and synchronous acceptance (
sigwaitinfo) RT signals rely on - Standard Signals and Their Default Actions — the non-queuing classic signals this note contrasts with
- sigaction and Signal Handlers — installing an
SA_SIGINFOhandler to readsi_value - Async-Signal-Safety and Reentrant Handlers — why doing work in an RT-signal handler is dangerous
- signalfd — receiving signals (including RT payloads) as a readable file descriptor
- POSIX Message Queues — the queued, data-carrying IPC object that pairs with RT signals via
mq_notify - Linux IPC MOC — the parent map of content