Signal trampoline
Signals are generated asynchronously. The kernel delivers them before returning to user mode, for example after a syscall or reschedule. After an installed handler runs, execution needs to continue from where it left off. Regular function calls use stack frames to this end, but with handlers there is no explicit call in user code. The jump is rigged by the kernel.
Before returning to user mode, the kernel looks for pending, unblocked signals for the thread. It manufactures a new frame on the user stack, and uses it to store the current execution context. The kernel is effectively staging a separate execution context within the same thread, so it needs to save the general registers, processor status, signal mask, and signal stack settings. With everything ready, it arranges for execution to continue on the handler.
The signal handler runs in user mode; the kernel needs to get control when it finishes, to restore the execution context and continue running the thread from the point where it was interrupted. A system call at the very end of the handler could serve this purpose.
This system call is sigreturn(2), but a number of
issues appear with a hypothetical explicit call made by user code.
It would make handling of nested signals problematic. It would
push architecture specific details onto application code. Most
importantly, it would give too much latitude to user code; the
kernel must retain control over context restoration to prevent the
execution state from being forged or corrupted. The kernel avoids
all these complications by arranging for the handler to return to
a small piece of code whose sole responsibility is to call
sigreturn(2). This piece of code is the signal
trampoline.
The kernel placed a stack frame for the handler before running it. As any regular stack frame, this frame contains an address to return to; thus, the handler can return normally into the trampoline, where the cleanup syscall gets executed at the very end and without risk.
sigreturn(2) is a special syscall. It exists
solely to implement signal handlers, and expects to be called from
the trampoline. It is not intended to be called directly. It
reverses all the preparations done by the kernel to run the
handler: it restores the signal mask, processor state, alternate
stack configuration and registers, so the thread continues
executing from the point where it was interrupted.
Extra
Returning from a signal handler to the main program is not the
only available course of action. The handler may also terminate
the thread by using _exit(2) or by calling
abort(3) to exit with a core dump.
Otherwise, the handler can perform a nonlocal goto. This is a
nuanced alternative. By default, the kernel blocks the handled
signal when running the handler, and unblocks it when the handler
returns. The behavior upon exiting by way of
longjmp(3) depends on the implementation. On System V
the signal mask is not restored, so the signal remains blocked;
Linux follows this behavior. On BSDs, setjmp(3) saves
the signal mask and longjmp() restores it. Due to
these differences, POSIX defines the functions
sigsetjmp(3) and siglongjmp(3) to give
explicit control over the signal mask when returning via a
nonlocal goto.
Further reading
Other interesting aspects of signals are alternate stacks; the details of reentrant and async-signal-safe functions; the differences between regular and realtime signals; the potential for races between unblocking and pausing for signals; the behavior upon the interruption of a system call; the relationship of signals to process groups, sessions and job control; and the historical details of unreliable and reliable signals.
- Advanced Programming in the UNIX Environment, chapter 10.
- UNIX Internals, chapter 4.
- The Linux Programming Interface, chapters 20 and 21.
- signal(7) and signal-safety(7)
- sigreturn(2), _exit(2), setjmp(3)