Linux - Signals

Linux signals are an asynchronous inter-process communication (IPC) mechanism used by the kernel to notify a process that a specific event has occurred (such as a hardware exception, a terminal interrupt, or a custom event).

Core Architecture & Concepts

When a signal is sent to a process, it transitions through three primary stages:

+-----------------+      +-----------------+      +-------------------+
| Signal Sent     | ---> | Signal Pending  | ---> | Signal Delivered  |
| (kill / kernel) |      | (Pending Bitset)|      | (Handler/Action)  |
+-----------------+      +-----------------+      +-------------------+
  1. Generation: A signal is generated by the kernel (e.g., page fault for SIGSEGV) or sent by another process via systems calls like kill(2) or tgkill(2).
  2. Pending: The kernel updates the task's pending signal set (sigpending). If the signal is blocked by the thread's signal mask (sigprocmask), it remains pending until unblocked.
  3. Delivery: When the thread is about to return from kernel mode back to user mode (e.g., after a system call or timer interrupt), the kernel checks for unblocked pending signals and executes the configured handler.

Signal Configuration: sigaction

The standard POSIX interface for signal handling is sigaction(2), which replaces the legacy signal() function.

#include <signal.h>

struct sigaction sa;
sa.sa_sigaction = my_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO | SA_ONSTACK;

sigaction(SIGUSR1, &sa, NULL);
  • sa_handler vs sa_sigaction:
    • sa_handler: Traditional handler function pointer taking a single argument: void (*sa_handler)(int).
    • sa_sigaction: Advanced handler pointer used when SA_SIGINFO flag is set. It takes three parameters:
      • void (*sa_sigaction)(int sig, siginfo_t *info, void *ucontext);
  • sa_mask: A set of signals that are automatically blocked during the execution of the signal handler to prevent recursive re-entrancy issues.
  • sa_flags: Flags modifying signal behavior:
    • SA_SIGINFO: Enables extra payload details in siginfo_t and ucontext_t.
    • SA_NODEFER: Prevents automatically blocking the signal currently being handled.
    • SA_RESTART: Automatically restarts certain interrupted system calls (e.g., read, write).
    • SA_ONSTACK: Tells the kernel to execute the handler on an alternate signal stack configured via sigaltstack.

Alternate Signal Stacks: sigaltstack

When a thread triggers a SIGSEGV due to stack overflow, the execution stack pointer (RSP/ESP) has hit its limit or invalid memory. If the kernel attempts to push a signal frame onto the current user stack, it triggers another memory fault, causing a kernel crash or immediate process crash before any handler can run.

sigaltstack(2) allows a process to designate a separate region of memory (usually allocated via malloc or mmap) dedicated strictly to signal handlers.

stack_t ss;
ss.ss_sp = malloc(SIGSTKSZ);  // Alternate stack memory space
ss.ss_size = SIGSTKSZ;
ss.ss_flags = 0;

if (sigaltstack(&ss, NULL) == 0) {
    // Registered stack; handlers registered with SA_ONSTACK will now use this area
}

When a signal arrives for a handler registered with SA_ONSTACK, the kernel switches the thread's stack pointer to ss.ss_sp + ss.ss_size before jumping into user space.

Signal Trampoline (sigtramp) & Delivery Lifecycle

Executing a user-space C function from the kernel requires safely jumping into user space and then returning control to the kernel once the function finishes.

Signal Control Flow

[ Kernel Space ]
       |
  1. Detect Pending Unblocked Signal
  2. Setup User Frame (Save context + inject trampoline return address)
       |
       v
[ User Space ]
  3. Execute Signal Handler (`sighandler`)
       |
  4. Return to Trampoline Code (`sigtramp`)
  5. Call `rt_sigreturn()` system call
       |
       v
[ Kernel Space ]
  6. Restore original user context (Registers, PC, Stack)
  7. Resume normal process execution

Dynamic Breakdown of the Lifecycle

  1. Kernel Trap / Transition: A process is running in kernel space (returning from a syscall or interrupt).
  2. Signal Detection: Before returning to user space, the kernel checks the task struct for pending, unblocked signals.
  3. Stack Frame Injection:
  • The kernel constructs a ucontext_t structure containing the saved CPU registers, register states, and signal mask on the user stack (or altstack).
  • It sets the return address on the user stack (or in a register like RA / LR) to point to a trampoline instruction sequence (sigtramp).
  1. Jump to Handler: The kernel updates the program counter (RIP/EIP) to point to the address of sighandler and returns to user mode.
  2. Handler Execution: The signal handler (sighandler) executes normally in user space.
  3. Trampoline Jump (sigtramp): When sighandler returns, execution pops off the return address, hitting the sigtramp code (often provided by glibc or the Linux vDSO).
  4. System Call rt_sigreturn: The sigtramp executes a specific system call (e.g., mov $15, %rax; syscall on x86-64 for sys_rt_sigreturn).
  5. Context Restoration: The kernel receives the rt_sigreturn syscall, inspects the saved ucontext_t frame on the user stack, restores the CPU registers to their pre-signal state, and resumes normal program execution.

Signal Safety Considerations

Because signal handlers interrupt execution at arbitrary instruction boundaries, they can cause deadlocks or state corruption if they call non-reentrant functions.

  • Async-Signal-Safe Functions: Only call functions guaranteed to be async-signal-safe by POSIX (e.g., write, _exit, read, signal, sigaction).
  • Non-Safe Operations: Do not call dynamic memory allocation (malloc, free), standard I/O functions (printf), or lock-acquiring operations inside a signal handler.
  • Volatile Flags: If setting a flag inside a handler to be read by the main thread, use volatile atomic types:
volatile sig_atomic_t flag = 0;
Loading...