QUESTION
Why do signal generation and handler execution happen at different times?
Once generated, a signal enters process-directed or thread-directed pending state. A handler or default action is applied only when a target thread does not block the signal and reaches a point where it returns from the kernel to userspace.
A handler runs by creating a sigframe on the original userspace stack and changing the instruction pointer to the handler. Returning is not completed by an ordinary function return alone; an rt_sigreturn trampoline restores registers and the mask.
STRUCTURE
Structure diagram
Process signal state
- shared disposition
- process pending
- SIGUSR1 siginfo
Thread A
- mask: SIGUSR1 blocked
- thread pending
- not deliverable
Thread B
- mask: unblocked
- selected by get_signal
- use user stack
rt_sigframe
- saved registers
- old mask
- ucontext · return trampoline
After entering the pending queue, a process-directed signal can be delivered to any one thread that does not block it.
CALL PATH
Call path
Separate generation, pending, and delivery. A mask delays delivery rather than deleting a signal, and repeated standard signals may coalesce into one.
This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.
SOURCE COORDINATES
Linux 6.18.37 LTS source locations
Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.
| File | Function / structure | What to inspect |
|---|---|---|
| kernel/signal.c | __send_signal_locked(), get_signal() | Select from the pending queue and disposition |
| arch/x86/kernel/signal.c | arch_do_signal_or_restart(), setup_rt_frame() | Construct a userspace signal frame |
| arch/x86/entry/entry_64.S | syscall/interrupt return | Connect signal processing before return to userspace |
COMPLETE PROGRAM
Complete runnable example
The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.
cc -std=c17 -Wall -Wextra -O2 signal_wait.c -o signal_wait01#define _POSIX_C_SOURCE 200809L
02#include <signal.h>
03#include <stdio.h>
04#include <unistd.h>
05
06int main(void)
07{
08 sigset_t set;
09 sigemptyset(&set);
10 sigaddset(&set, SIGUSR1);
11 if (sigprocmask(SIG_BLOCK, &set, NULL) < 0)
12 return 1;
13
14 printf("pid=%ld; waiting for SIGUSR1\n", (long)getpid());
15 fflush(stdout);
16
17 siginfo_t info;
18 int signo = sigwaitinfo(&set, &info);
19 if (signo < 0)
20 return 1;
21 printf("received signal=%d sender=%ld value=%d\n", signo,
22 (long)info.si_pid, info.si_value.sival_int);
23 return 0;
24}
CODE NOTES
Code notes
sigemptyset(&set)Creates an empty set through the API rather than assuming an internal all-0 representation for sigset_t.
SIG_BLOCKBlocks the target signal first so it stays in the pending queue instead of taking the default action or entering a handler.
fflush(stdout)Makes the PID immediately visible to the signal sender even when stdout is redirected to a pipe and is not line-buffered.
sigwaitinfo(&setConsumes the signal and siginfo through a synchronous function return rather than an asynchronous handler. The waited-for signal must be blocked in the calling thread.
info.si_value.sival_intCan read a realtime/queued value sent by sigqueue. Do not expect a meaningful payload for a standard signal sent with kill.
DETAILS
Detailed behavior
Thread selection for a process-directed signal
A signal sent to a process with kill can be delivered to any thread that does not block it. Use the pthread_kill/tgkill family to target a particular thread.
A multithreaded program can reduce handler races by blocking signals in the initial thread and consuming them in a dedicated sigwait/signalfd thread.
Only a limited set of functions may be called from a handler
If a handler interrupts malloc, printf, or pthread_mutex_lock while it is updating internal state, reentry can cause deadlock or corruption. Call only functions on the async-signal-safe list.
A handler usually does no more than set a sig_atomic_t flag or write to a self-pipe, deferring the real cleanup to the main loop.
Queueing rules for standard and realtime signals
Repeated instances of the same standard signal can coalesce into one pending bit while blocked. Realtime signals form an ordered queue with payloads, but are subject to resource limits.
Do not represent a workload that must not lose event counts as a count of standard signals.
OBJECTS
Objects and lifetimes
| Object | Creation and release | Values to inspect |
|---|---|---|
sighand_struct | A thread group shares signal dispositions; caught actions are reset by exec | handler, flags, mask |
sigpending | Exists on the process or task from signal generation through delivery/consumption | signal bitmap, queued siginfo |
rt_sigframe | Created on the user stack during delivery and consumed by sigreturn | ucontext, old mask, registers |
FAILURE PATH
Failure conditions and common misconceptions
| Observed symptom | Likely causes | How to verify |
|---|---|---|
| Signal occurrences are lost | standard signal coalescing | Consider a realtime signal or eventfd |
| handler deadlock | An async-signal-unsafe function is reentered | Inspect the handler call graph and core stack |
| The desired thread does not receive the signal | Process-directed delivery and mask configuration | /proc/PID/task/*/status SigBlk |
LAB
Verify it yourself
- Send kill -USR1 to the running PID and verify that sigwaitinfo returns the sender PID.
- Send SIGUSR1 several times while it is blocked and observe SigPnd in /proc/PID/status plus one consumption.
- Write a sender that transmits integer payloads with sigqueue and verify the order of several values on a realtime signal.
./signal_waitstrace -e trace=rt_sigprocmask,rt_sigtimedwait,kill ./signal_waitPRIMARY REFERENCES