Signals and Asynchronous Event Management

Signals are software interrupts that provide a mechanism for handling asynchronous events within an operating system. Operating as a primitive form of interprocess communication (IPC), signals originate from hardware faults, user actions (such as keyboard interrupts), kernel events, or explicit system calls generated by other processes.

The lifecycle of a signal consists of three distinct phases:

  • Generation (Raised/Sent): An event triggers the creation of the signal.
  • Storage (Pending): The kernel stores the signal until it can safely interrupt the target process.
  • Delivery (Handled): The kernel interrupts the target process to process the signal according to the designated action.

When a signal is delivered, the kernel enforces one of three actions:

  • Ignore: The signal is discarded silently.
  • Catch: Execution of the process’s current instruction stream is suspended, and a predefined handler function is executed. Execution resumes at the interrupted point once the handler returns.
  • Default Action: The kernel performs the default system behavior, which is typically to terminate the process, generate a core dump, stop the process, or ignore the signal entirely.

Two specific signals, SIGKILL and SIGSTOP, cannot be caught or ignored, guaranteeing the kernel and system administrators the absolute ability to terminate or suspend runaway processes.

Understanding the asynchronous lifecycle and possible actions forms the foundation for utilizing the specific signal identifiers defined by the system.

Standard Signal Identifiers

Signals are represented by positive integers starting at 1, typically mapped to symbolic constants prefixed with SIG. The integer value 0 acts as a special “null signal” utilized by certain system calls for permission testing.

Notable signals and their default behaviors include:

  • Process Termination & Core Dumps:
    • SIGKILL: Uncatchable, unconditional process termination.
    • SIGTERM: Catchable, graceful process termination sent by utilities like kill.
    • SIGINT: Sent to foreground processes when the user inputs the interrupt character (Ctrl-C).
    • SIGQUIT: Sent to foreground processes upon the quit character (Ctrl-); defaults to termination with a core dump.
    • SIGABRT: Sent by the abort() function; terminates and dumps core.
  • Hardware & Execution Faults:
    • SIGSEGV (Segmentation Violation): Triggered by invalid memory access, such as reading unmapped or protected memory.
    • SIGBUS: Triggered by hardware faults or memory mapping errors (e.g., accessing an invalid mapped region).
    • SIGILL: Triggered by the execution of illegal machine instructions.
    • SIGFPE: Triggered by arithmetic exceptions, including division by zero and integer overflows.
  • Process & Job Control:
    • SIGCHLD: Sent to a parent process when its child terminates or stops. Ignored by default.
    • SIGSTOP: Uncatchable, unconditional suspension of a process.
    • SIGCONT: Resumes a stopped process.
    • SIGTSTP: Sent to foreground processes upon the suspend character (Ctrl-Z).
  • Miscellaneous:
    • SIGHUP: Sent to a session leader when a terminal disconnects. Often co-opted by daemons to trigger configuration reloads.
    • SIGALRM: Sent when a real-time timer (like alarm()) expires.
    • SIGUSR1 / SIGUSR2: Reserved explicitly for user-defined application behavior.

With standard signal identifiers established, processes require interfaces to specify how these asynchronous events should be handled upon delivery.

Basic Signal Management

The POSIX C standard provides signal() as the most basic interface for overriding the default action of a given signal:

  • Prototype: sighandler_t signal(int signo, sighandler_t handler);
  • Handler Options: The handler parameter accepts a pointer to a custom function (void my_handler(int signo)), the SIG_DFL macro to restore default behavior, or the SIG_IGN macro to ignore the signal.
  • Waiting: A process can voluntarily sleep until a signal arrives using pause(), which suspends execution until any non-ignored signal is handled.

Signal disposition behaves predictably across process boundaries:

  • fork(): A newly spawned child inherits all signal actions (ignored, default, or handled) from its parent. Pending signals are not inherited.
  • exec(): Because an exec replaces the process address space, custom signal handlers are lost. Signals that were previously caught are reset to SIG_DFL, while ignored signals remain SIG_IGN.

Signal identifiers can be converted into human-readable strings using the globally defined array sys_siglist or the strsignal() function.

Once a process configures handlers for incoming signals, it can also act as a generator to dispatch signals to other processes.

Signal Generation

The kill() system call dispatches a signal to a specified process or process group:

  • Targeting Rules (pid):
    • > 0: Sends the signal to the specific process ID.
    • 0: Sends the signal to all processes in the invoking process’s process group.
    • -1: Sends the signal to all processes the sender has permission to signal (excluding init and itself).
    • < -1: Sends the signal to the process group matching the absolute value of the PID.
  • Permissions: Signal delivery requires the sender to either possess the CAP_KILL capability (typically root) or have a real/effective User ID matching the real/saved User ID of the target process. An exception allows SIGCONT to be sent to any process in the same session.
  • Validation: Sending the null signal (signo = 0) performs validity and permission checks without actually dispatching a signal.

Processes can send signals to themselves using the raise(signo) function, which is functionally equivalent to kill(getpid(), signo).

Because signals are dispatched and delivered asynchronously, executing handler code interrupts the main program flow, necessitating strict safety constraints.

Reentrancy and Signal Safety

When a signal is caught, the kernel suspends the main program stream and executes the handler, meaning the interruption can occur in the middle of data modification or library execution. To avoid corrupting program state, signal handlers must only invoke reentrant functions.

A function is guaranteed to be reentrant (signal-safe) if it does not manipulate static data, uses only stack-allocated or caller-provided data, and avoids invoking nonreentrant functions. Nonreentrant functions include standard allocation (malloc(), free()) and buffered I/O routines. POSIX dictates a strict list of signal-safe functions, including low-level system calls like read(), write(), open(), and _exit().

To prevent reentrancy violations and protect critical regions, processes must be able to temporarily block asynchronous signal delivery.

Signal Sets and Blocking

A process can temporarily block the delivery of signals by modifying its signal mask, protecting critical regions from interruption. Blocked signals remain in a pending state until they are unblocked, at which point the kernel delivers them.

Signal sets, represented by the sigset_t type, are constructed using management functions:

  • sigemptyset() / sigfillset(): Initialize sets to empty or full.
  • sigaddset() / sigdelset(): Add or remove specific signals.

The sigprocmask() system call modifies the active signal mask:

  • SIG_SETMASK: Replaces the current mask entirely.
  • SIG_BLOCK: Adds the specified set to the current mask (binary OR).
  • SIG_UNBLOCK: Removes the specified set from the current mask.

A process can query currently pending (blocked but raised) signals using sigpending(). To safely exit a critical region and immediately wait for a signal, sigsuspend() temporarily replaces the signal mask and puts the process to sleep in a single, atomic operation, returning once a signal is handled.

While basic blocking and handling satisfy simple use cases, robust applications require the fine-grained control and context provided by advanced signal management interfaces.

Advanced Signal Management

The sigaction() system call replaces the rudimentary signal() function, offering precise control over handler execution and advanced capabilities. It utilizes the struct sigaction object:

  • sa_handler / sa_sigaction: Pointers to the handler function. The latter is used if advanced payload processing is required.
  • sa_mask: A signal set to be blocked implicitly during the execution of the handler, preventing overlapping interruptions.
  • sa_flags: Bitmask modifying behavior:
    • SA_SIGINFO: Enables the advanced sa_sigaction handler, allowing receipt of extended context (siginfo_t).
    • SA_RESETHAND: Restores the signal to its default action after the handler returns (one-shot mode).
    • SA_NODEFER: Prevents the handled signal from being automatically blocked during handler execution.
    • SA_RESTART: Automatically restarts system calls interrupted by the signal.
    • SA_NOCLDWAIT: Instructs the kernel to automatically reap children, preventing zombies and nullifying the need to call wait().

The advanced sigaction interface enables handlers to process detailed context, allowing signals to carry specific operational data and payloads.

Signal Payloads and siginfo_t

When a handler is registered with SA_SIGINFO, the kernel passes a siginfo_t structure as the second argument, providing deep context on why the signal was raised and who sent it.

Key siginfo_t fields include:

  • si_signo: The signal number.
  • si_pid / si_uid: The PID and UID of the sending process (essential for authorization).
  • si_code: Details the exact cause of the signal.
    • Kernel/User origin: E.g., SI_USER (sent by kill()), SI_QUEUE (sent by sigqueue()), SI_KERNEL.
    • Fault context: E.g., SEGV_MAPERR (invalid memory region), FPE_INTDIV (integer division by zero).
  • si_addr: For hardware faults (SIGSEGV, SIGILL, SIGBUS), this holds the memory address that triggered the fault.

Furthermore, processes can send signals containing custom payloads utilizing the sigqueue() system call. Instead of the standard kill(), sigqueue() accepts a union sigval:

  • The payload can be an integer (sival_int) or a void pointer (sival_ptr).
  • The receiving process retrieves this data via the si_value field inside the siginfo_t struct, enabling signals to function as a richer IPC mechanism.