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.