22.6 — Timing and Order of Signal Delivery
You know that a process can receive signals. But precisely when does the kernel deliver a pending signal? And if multiple signals are pending at once, which one comes first?
When Is a Pending Signal Delivered?
| Signal Type | When Delivered |
|---|---|
| Synchronous (raise, hardware exception) | Immediately — before the raising call returns |
| Asynchronous (kill from another process, Ctrl+C) | At the next switch from kernel mode → user mode: (a) when process is rescheduled after a time slice (b) at completion of a system call There can be a small delay even if signal is not blocked. |
Order of Delivery When Multiple Signals Are Pending
If several signals are unblocked at the same time (e.g. you call sigprocmask(SIG_SETMASK, ...) to unblock all of them), the Linux kernel delivers them in ascending signal number order. So SIGINT (2) is delivered before SIGQUIT (3).
Nested Signal Handlers
When multiple signals are pending and one handler is already executing, the kernel can interrupt the running handler to invoke a second one. This creates a nested call stack.
| Main program | SIGINT handler | SIGQUIT handler |
| 1. SIGINT + SIGQUIT both unblocked at same time | ||
| 2. Kernel invokes SIGINT handler first (lower number) | ||
| 3. SIGINT handler makes a system call… | ||
| 4. Kernel invokes SIGQUIT handler (interrupts SIGINT handler) | ||
| 5. SIGQUIT handler returns | ||
| 6. SIGINT handler resumes, completes, returns | ||
| 7. Main program resumes |
/* signal_order_demo.c - Observe order of pending signal delivery */
#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
void handler(int sig) {
/* async-signal-safe write() instead of printf() */
char buf[64];
int len = snprintf(buf, sizeof(buf), "Handler: signal %d (%s)\n",
sig, strsignal(sig));
write(STDOUT_FILENO, buf, len);
}
int main(void) {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
/* Install handlers for several signals */
for (int s = SIGHUP; s <= SIGTERM; s++) {
if (s != SIGKILL && s != SIGSTOP)
sigaction(s, &sa, NULL);
}
/* Block ALL signals */
sigset_t all, prev;
sigfillset(&all);
sigprocmask(SIG_SETMASK, &all, &prev);
printf("PID=%d: All signals blocked. Send multiple signals now:\n",
getpid());
printf("Example: kill -2 %d; kill -3 %d; kill -1 %d\n",
getpid(), getpid(), getpid());
sleep(5); /* Allow time to send signals from another terminal */
printf("Unblocking all signals — watch delivery order:\n");
sigprocmask(SIG_SETMASK, &prev, NULL); /* unblock all at once */
sleep(1);
printf("Done.\n");
return 0;
}
## Terminal 1:
$ ./signal_order_demo
PID=9999: All signals blocked. Send multiple signals now:
## Terminal 2 (while program sleeps):
$ kill -3 9999 # SIGQUIT
$ kill -2 9999 # SIGINT
$ kill -1 9999 # SIGHUP
## Back in Terminal 1 after 5 seconds:
Unblocking all signals — watch delivery order:
Handler: signal 1 (Hangup) <-- SIGHUP delivered first (lowest number)
Handler: signal 2 (Interrupt)
Handler: signal 3 (Quit) <-- SIGQUIT last (highest number of the three)
Done.
22.7 — Implementation and Portability of signal()
signal() is the oldest way to install a signal handler. The problem is that it has had different, incompatible semantics across UNIX versions. This makes it dangerous to use in portable code. Understanding why helps you understand what sigaction() fixes.
The Problem: Unreliable Signal Semantics
Early UNIX (System V heritage) signal() had two serious bugs:
| Problem | What Happens | Risk |
|---|---|---|
| Signal disposition reset | On entry to the handler, the disposition resets to SIG_DFL. The handler must call signal() again to re-arm itself. | Window of vulnerability between handler entry and re-arm. Second signal during this window uses default action (usually termination). |
| Signal not blocked during handler | The same signal can be delivered recursively while the handler is still running. | Fast stream of signals → recursive handler calls → stack overflow |
| Implementation | Semantics |
|---|---|
| Early System V | Unreliable (reset on entry, signal not blocked) |
| 4.2BSD | Reliable (no reset, signal blocked during handler) |
| glibc 2+ (Linux default) | Reliable (calls sigaction() internally with SA_RESTART) |
| Linux kernel signal() syscall | Unreliable System V semantics |
| glibc with _SVID_SOURCE or _XOPEN_SOURCE | Uses sysv_signal() — unreliable semantics |
/* signal_unreliable_demo.c - Show the "reset on entry" problem */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int count = 0;
void handler(int sig) {
count++;
printf("Handler invoked (count=%d)\n", count);
/* With unreliable semantics, disposition is now SIG_DFL.
There is a small window here where another SIGINT would
terminate the process.
A "reliable" handler must re-arm itself: */
signal(SIGINT, handler); /* Re-arm (still has a race window) */
}
int main(void) {
signal(SIGINT, handler);
printf("PID=%d. Keep pressing Ctrl+C quickly.\n", getpid());
while (count < 5)
pause();
printf("Got 5 SIGINTs, exiting.\n");
return 0;
}
The Fix: Always Use sigaction()
sigaction() gives you full control over signal semantics through flags. It is the correct, portable, POSIX-standard way to install signal handlers.
/* signal_impl.c - Implement signal() correctly using sigaction() */
#include <signal.h>
#include <errno.h>
typedef void (*sighandler_t)(int);
/* This is how glibc implements signal() internally */
sighandler_t my_signal(int sig, sighandler_t handler) {
struct sigaction newDisp, prevDisp;
newDisp.sa_handler = handler;
sigemptyset(&newDisp.sa_mask);
/* SA_RESTART: automatically restart interrupted system calls
This is the "modern reliable" behavior.
For OLD/unreliable behavior, you would use:
SA_RESETHAND | SA_NODEFER (resets handler + allows recursive) */
newDisp.sa_flags = SA_RESTART;
if (sigaction(sig, &newDisp, &prevDisp) == -1)
return SIG_ERR;
return prevDisp.sa_handler;
}
/* sigaction_correct.c - The RIGHT way to install a signal handler */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
volatile sig_atomic_t got_signal = 0;
void handler(int sig) {
/* sa_mask in sigaction ensures SIGINT is blocked DURING this handler.
No recursive invocation. No disposition reset.
This is reliable, POSIX-correct behavior. */
got_signal = 1;
write(STDOUT_FILENO, "Signal caught\n", 14);
}
int main(void) {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGINT); /* block SIGINT during handler */
sa.sa_flags = SA_RESTART; /* restart interrupted syscalls */
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
printf("Waiting for Ctrl+C (PID=%d)\n", getpid());
while (!got_signal)
pause();
printf("Exiting cleanly after signal\n");
return 0;
}
| Flag | Effect | When to Use |
|---|---|---|
| SA_RESTART | Interrupted system calls restart automatically (no EINTR) | Almost always — simplifies code |
| SA_SIGINFO | Handler receives siginfo_t with extended signal info | When you need sender PID, realtime signal data |
| SA_RESETHAND | Reset disposition to SIG_DFL after first delivery | One-shot handler (old unreliable behavior) |
| SA_NODEFER | Don’t block signal during handler (allow recursive) | Rarely — old unreliable behavior |
| SA_NOCLDSTOP | SIGCHLD not sent when child is stopped (only on termination) | When you only care about child exit |
| SA_NOCLDWAIT | Children become zombies only briefly; auto-reaped | When you don’t want to call wait() |
Interview Questions — Timing, Order, and signal() Portability
The kernel delivers pending signals at the next switch from kernel mode to user mode for the process. This happens either when the process is rescheduled after a time slice expiry, or when a system call completes. There can be a brief delay between when a signal is generated and when it is delivered, even if the signal is not blocked.
Linux delivers them in ascending signal number order, so SIGINT (2) is delivered before SIGQUIT (3). However, SUSv3 says delivery order of multiple pending standard signals is implementation-defined, so portable code must not rely on this. (Realtime signals, by contrast, have a guaranteed order defined by the POSIX standard.)
(1) Disposition reset: On entry to the signal handler, the disposition was reset to SIG_DFL. If the same signal arrived again during the window between handler entry and calling signal() again to re-arm, it would be handled by the default action (often termination). (2) Signal not blocked: The same signal could be delivered again while the handler was still running, causing recursive invocation and potential stack overflow.
Because signal() has inconsistent semantics across UNIX variants — System V gives unreliable behavior (reset + not blocked) while BSD gives reliable behavior. Modern glibc implements signal() with reliable semantics by default, but this can change depending on which feature test macros are defined (_SVID_SOURCE, _XOPEN_SOURCE). sigaction() gives you explicit, portable control over all signal semantics through flags (SA_RESTART, SA_RESETHAND, etc.) with no ambiguity.
SA_RESTART tells the kernel to automatically restart “slow” system calls (like read(), write(), wait(), select()) if they are interrupted by the signal handler. Without it, those calls return -1 with errno=EINTR, and your code must manually restart them. Using SA_RESTART simplifies code by eliminating the need for EINTR handling loops in most cases. Some calls (like sleep(), nanosleep()) are not automatically restarted even with SA_RESTART.
Next Topic
Realtime signals — queued, ordered, and data-carrying signals for advanced applications.
