How Linux Signals Work: Pending Masks, Delivery on Return to Userspace, and Why They Are Not Interrupts

A process that spends all its time in a tight userspace loop never notices SIGTERM until the kernel has a chance to put that process back on a CPU and walk the return-to-userspace path. Signals are not hardware interrupts. They are a pending-bit protocol that the kernel checks at well-defined exits from kernel mode.

Generation records a bit or a queued siginfo. Delivery happens later, on the thread allowed to run the handler, after the kernel rebuilds a user stack frame and points the instruction pointer at the handler. Between those moments the signal is pending, and a per-thread blocked mask can keep it that way.

What a signal actually is

On Linux a signal is a small integer, 1 through 64. Numbers 1-31 are traditional Unix signals. Numbers 32-64 are real-time signals. Standard signals do not queue: a second SIGINT while one is already pending is discarded. Real-time signals queue, carry siginfo_t, and are delivered in priority order.

SIGKILL and SIGSTOP cannot be caught, blocked, or ignored. The kernel drops them from mask updates. Default dispositions are terminate, core dump, stop, continue, or ignore.

Process-directed versus thread-directed

A thread group has a shared pending set for process-directed signals such as kill(2) and terminal SIGINT, and each task has a private pending set for pthread_kill and synchronous faults like SIGSEGV. Each thread has its own blocked mask. Delivery chooses a thread that is not blocking the signal. If every thread blocks it, the signal stays pending until some thread unblocks it.

Fork copies handlers and the blocked mask and starts the child with an empty pending set. Execve keeps pending signals but resets caught handlers to default.

Generation is not delivery

kill, tkill, tgkill, the terminal, fault paths, and POSIX timers all end in kernel/signal.c. That path sets a pending bit, calls recalc_sigpending_tsk, and if pending minus blocked is nonempty sets TIF_SIGPENDING. A thread in TASK_INTERRUPTIBLE is woken. A thread in TASK_UNINTERRUPTIBLE is not. That is why a task in D state can ignore SIGTERM until the wait ends or becomes killable.

No handler runs at generation time. A CPU-bound thread that never enters the kernel waits until preemption and the return-to-userspace path see TIF_SIGPENDING.

The return-to-userspace check

Before restoring user registers the architecture inspects thread flags. TIF_SIGPENDING leads to get_signal(). Default actions run there: SIGKILL tears the task down, stop signals enter job-control stop, SIGCONT cancels a stop, ignored signals are dropped.

For a user handler, handle_signal builds a frame on the user stack or the alternate signal stack, saves registers for rt_sigreturn, and overwrites the user instruction pointer so the next userspace instruction is the handler. The handler returns into a restorer trampoline that issues rt_sigreturn, which restores the mask and registers.

EINTR and SA_RESTART

A blocking syscall in TASK_INTERRUPTIBLE wakes when a signal becomes deliverable. After the handler, a handler without SA_RESTART makes many syscalls return -1 with EINTR. SA_RESTART restarts many but not all of them; poll and some timeout calls still return EINTR. That is why accept fails when a SIGCHLD handler runs without SA_RESTART.

The mask during the handler

By default the delivered signal is added to the thread blocked mask for the handler lifetime, so the same standard signal does not nest. SA_NODEFER disables that. sa_mask adds more signals. A different unblocked signal can still interrupt the handler. Only async-signal-safe functions are legal in a handler: write to an open fd, _exit, sigaction, lock-free atomics. malloc and printf are not. signalfd exists so programs can dequeue the same pending bits from an event loop instead of surviving a stolen stack frame.

Synchronous versus asynchronous

SIGSEGV, SIGBUS, SIGFPE, SIGILL, and SIGTRAP from a CPU exception go to the faulting thread. Terminal SIGINT is process-directed and may run on any thread that does not block it. Deterministic delivery means blocking the signal in every thread except one and using sigwaitinfo or a dedicated handler thread.

Misconceptions

Signals are not interrupts. Hardware interrupts preempt immediately. Signals wait for a kernel-to-user transition.

SIGKILL has no handler. get_signal takes the task down after the same pending-and-wake delay, plus any uninterruptible wait already in progress.

Blocking defers a signal. SIG_IGN discards it at delivery. Those are opposite policies.

Standard signals do not queue. Ten blocked SIGALRMs become one pending SIGALRM.

The handler returns to a restorer, not to the interrupted line. longjmp out of a handler skips mask restore unless you use sigsetjmp.

Takeaways

A signal is a pending bit plus optional queued siginfo. Generation is cheap. Delivery rewrites a thread user stack and instruction pointer on the way back to userspace. SIGKILL and SIGSTOP bypass handlers. SA_RESTART and EINTR are the contract with blocking syscalls. Handlers are a hostile environment; signalfd is the composable alternative.

Previous Post