Loading learning content...
Every system administrator knows the feeling: a runaway process consuming 100% CPU, a hung application refusing to respond, or a network daemon that needs graceful shutdown. In these moments, you reach for a simple yet powerful tool—Ctrl+C, kill, or killall. Behind these commands lies one of the most fundamental and elegant mechanisms in UNIX/POSIX operating systems: signals.
Signals are software interrupts—asynchronous notifications delivered to processes to inform them of events, errors, or explicit requests from users or other processes. They represent the operating system's way of tapping a process on the shoulder and saying, 'Something needs your attention.'
Understanding signals is essential for any systems programmer, DevOps engineer, or backend developer working with UNIX-like systems. They underpin process management, error handling, graceful shutdowns, and inter-process coordination throughout the entire software ecosystem.
By the end of this page, you will understand the signal mechanism at a deep level: its historical origins, architectural design, delivery semantics, and how it fits into the broader landscape of inter-process communication. You'll gain the conceptual foundation needed to implement robust signal handling in production systems.
A signal is a limited form of inter-process communication used in UNIX, POSIX-compliant, and other UNIX-like operating systems. Unlike pipes, message queues, or shared memory that transmit data between processes, signals communicate the occurrence of an event—they are notifications, not data conduits.
Technically, a signal is an asynchronous notification sent to a process (or a specific thread within a process) to notify it that an event has occurred. When a signal is delivered, it interrupts the normal flow of execution and invokes an action—either a default behavior defined by the OS, a custom handler provided by the programmer, or explicit ignoring of the signal.
Signals are often called software interrupts because they mirror the behavior of hardware interrupts at a higher abstraction level:
| Aspect | Hardware Interrupt | Software Signal |
|---|---|---|
| Source | Hardware device (keyboard, disk, network) | Kernel, process, or user action |
| Target | CPU execution context | Process execution context |
| Effect | Suspends current instruction, jumps to ISR | Suspends process execution, invokes handler |
| Timing | Asynchronous, unpredictable | Asynchronous, unpredictable |
| Purpose | Device communication | Event notification |
Just as hardware interrupts force the CPU to temporarily stop what it's doing and handle an urgent event, signals force a process to temporarily stop and respond to an event—whether that's a termination request, a child process exit, or an arithmetic error.
Unlike message-based IPC where you can send arbitrary data, traditional UNIX signals carry no payload—they only indicate that 'signal X occurred.' The signal number itself (an integer from 1 to 31 in classic UNIX, extended in POSIX) is the complete message. This simplicity enables extremely efficient delivery but limits expressiveness compared to other IPC mechanisms.
To truly understand signals, we must appreciate their historical development. The signal mechanism has evolved through three major phases, each addressing limitations of its predecessors.
The original signal mechanism in Version 7 UNIX was remarkably simple—and remarkably problematic. Key characteristics included:
This design was prone to subtle bugs that manifested unpredictably, especially under heavy load.
123456789101112131415161718192021222324
/* Classic unreliable signal handling - DO NOT USE */#include <signal.h>#include <stdio.h> void sigint_handler(int sig) { /* DANGER: Handler resets to SIG_DFL here! */ /* Must re-register immediately, but window exists for lost signals */ signal(SIGINT, sigint_handler); /* Re-register handler */ printf("Caught SIGINT\n");} int main() { signal(SIGINT, sigint_handler); while (1) { pause(); /* Wait for signal */ } return 0;} /* * PROBLEM: Between handler invocation and re-registration, * a second SIGINT will terminate the process (default behavior). * This race condition was a fundamental flaw in V7 UNIX signals. */Berkeley Software Distribution addressed the reliability issues with significant improvements:
sigblock() and sigsetmask().However, BSD signals were not portable to System V UNIX, creating compatibility nightmares for cross-platform software.
The POSIX standard unified signal handling with "reliable signals," combining the best of BSD while ensuring portability:
sigaction(): The definitive way to install signal handlers with full control over behavior.sigset_t) to manipulate groups of signals.sigprocmask(): Examine and change the blocked signal mask.sigpending(): Examine pending (blocked) signals.sigsuspend(): Atomically unblock signals and pause.Modern POSIX signals are the foundation of reliable signal handling and should be used exclusively in production code.
You will still encounter signal() in legacy code and tutorials. While POSIX environments typically implement signal() with reliable semantics (BSD-style), the behavior varies by platform. Always use sigaction() for new code—it's explicit, portable, and reliable. The signal() function should be considered deprecated for serious applications.
Understanding the internal architecture of signals illuminates how the kernel manages, delivers, and processes these software interrupts.
For each process, the kernel maintains several signal-related data structures:
Signal Disposition Table: An array (typically 32 or 64 entries) mapping each signal number to its handling action:
SIG_DFL (pointer to default handler)SIG_IGN (signal should be ignored)Signal Mask (Blocked Signals): A bitmask indicating which signals are currently blocked from delivery. Blocked signals are held pending until unblocked.
Pending Signal Set: A bitmask of signals that have been generated but not yet delivered (either because they're blocked or the process hasn't been scheduled).
Signal Action Flags: Per-signal flags controlling behavior (e.g., SA_RESTART for automatic syscall restart, SA_SIGINFO for extended signal information).
A signal progresses through several states from its origin to its effect on the target process:
Stage 1: Signal Generation A signal is generated when the kernel marks it in the pending set of the target process. Generation sources include:
kill(), raise(), sigqueue())Stage 2: Signal Pending Once generated, a signal enters the pending state. If the signal is blocked (in the process's signal mask), it remains pending indefinitely until unblocked. Multiple instances of the same standard signal don't queue—only one is recorded (unlike real-time signals which do queue).
Stage 3: Signal Delivery Delivery occurs when the kernel selects a pending, unblocked signal and takes action. This typically happens:
sigsuspend() or pause() is usedStage 4: Signal Handling Upon delivery, one of three actions occurs:
Signal delivery is fundamentally asynchronous. The exact moment a signal interrupts your code is unpredictable—it could be between any two machine instructions. This asynchrony is the source of both signals' power (immediate event notification) and their danger (race conditions, non-reentrant code issues).
Signals can be classified along several dimensions, each revealing different aspects of their purpose and behavior.
Synchronous signals are generated as a direct result of the process's own actions—they are predictable consequences of code execution:
SIGFPE: Arithmetic error (division by zero, overflow)SIGSEGV: Invalid memory access (null pointer, out-of-bounds)SIGBUS: Bus error (misaligned memory access)SIGILL: Illegal instruction (corrupted code, unsupported operation)SIGTRAP: Trace/breakpoint trap (debugger support)Asynchronous signals arrive from external sources, unpredictable from the process's perspective:
SIGINT: Interrupt from terminal (Ctrl+C)SIGTERM: Termination request (from kill command)SIGCHLD: Child process status changeSIGUSR1/SIGUSR2: User-defined signals from other processesSIGALRM: Timer expiration| Category | Examples | Cause | Predictability |
|---|---|---|---|
| Synchronous | SIGSEGV, SIGFPE, SIGILL, SIGBUS | Process's own execution (errors) | Deterministic—same bug reproduces |
| Asynchronous (User) | SIGINT, SIGTERM, SIGUSR1 | User input, explicit kill requests | Unpredictable timing |
| Asynchronous (Kernel) | SIGCHLD, SIGPIPE, SIGIO | Kernel-detected events | Depends on external state |
| Timer-based | SIGALRM, SIGVTALRM, SIGPROF | Interval timer expiration | Scheduled but async to code |
POSIX defines five default actions that signals can trigger:
| Default Action | Description | Example Signals |
|---|---|---|
| Terminate | Process exits immediately | SIGTERM, SIGINT, SIGKILL, SIGHUP |
| Terminate + Core Dump | Process exits and writes core file | SIGQUIT, SIGABRT, SIGSEGV, SIGFPE |
| Ignore | Signal has no effect | SIGCHLD, SIGURG (default on most systems) |
| Stop | Process execution is suspended | SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU |
| Continue | Resume stopped process | SIGCONT |
POSIX distinguishes between two categories based on queuing behavior:
Standard signals (1-31): Have traditional semantics:
Real-time signals (SIGRTMIN to SIGRTMAX): Extended semantics:
sigqueue() (an integer and a pointer)Real-time signals are essential for applications requiring guaranteed delivery counts (e.g., event notifications in multimedia systems).
When designing systems that rely on signal-based notifications, prefer real-time signals if you need: (1) guaranteed delivery of each occurrence, (2) payload data transfer, or (3) defined ordering. Standard signals are suitable for 'at least once' notifications where the count doesn't matter—like termination requests or interrupt handling.
Understanding how signals are generated—the various pathways by which they come into existence—is fundamental to anticipating and handling them correctly.
The CPU hardware detects certain error conditions during instruction execution. The kernel translates these hardware exceptions into signals:
| Hardware Exception | Resulting Signal | Common Cause |
|---|---|---|
| Division by zero | SIGFPE | Integer division by zero, floating-point errors |
| Invalid memory access | SIGSEGV | NULL pointer dereference, stack overflow, buffer overflow |
| Bus error | SIGBUS | Misaligned memory access, non-existent physical address |
| Illegal instruction | SIGILL | Corrupted executable, JIT compilation errors |
| Breakpoint | SIGTRAP | Debugger breakpoints, single-stepping |
These are synchronous signals—they are the immediate consequence of executing a faulty instruction.
123456789101112131415161718192021222324252627282930313233
#include <signal.h>#include <stdio.h>#include <stdlib.h> /* Handler for SIGSEGV - demonstrates synchronous signal */void sigsegv_handler(int sig) { fprintf(stderr, "Caught SIGSEGV: Invalid memory access\n"); fprintf(stderr, "This handler was invoked synchronously - the process\n"); fprintf(stderr, "tried to access memory it doesn't own.\n"); exit(1); /* Cannot resume from SIGSEGV safely */} int main() { struct sigaction sa; sa.sa_handler = sigsegv_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGSEGV, &sa, NULL); /* Intentionally trigger SIGSEGV */ int *ptr = NULL; printf("About to dereference NULL pointer...\n"); *ptr = 42; /* SIGSEGV generated here - synchronously */ printf("This line will never execute\n"); return 0;} /* * Key insight: SIGSEGV is generated at the exact instruction that * attempts the invalid access. The signal handler runs immediately, * and the faulting instruction never completes. */The kernel generates signals in response to events it detects:
SIGCHLD: Sent to parent when child process terminates, stops, or continuesSIGPIPE: Writing to a pipe with no readersSIGIO: Asynchronous I/O event availableSIGURG: Out-of-band data on socketSIGXCPU: CPU time limit exceededSIGXFSZ: File size limit exceededProcesses explicitly request signal delivery via system calls:
| System Call | Description | Use Case |
|---|---|---|
kill(pid, sig) | Send signal to process | Process termination, notifications |
raise(sig) | Send signal to self | Self-notification, testing |
sigqueue(pid, sig, val) | Send with payload (real-time) | Data-carrying notifications |
pthread_kill(tid, sig) | Send to specific thread | Thread-level control |
killpg(pgrp, sig) | Send to process group | Session-wide signals |
The terminal driver generates signals based on special characters:
| Keystroke | Signal | Effect |
|---|---|---|
| Ctrl+C | SIGINT | Interrupt foreground process group |
| Ctrl+\ | SIGQUIT | Quit with core dump |
| Ctrl+Z | SIGTSTP | Stop foreground process |
| (bg→fg transition) | SIGCONT | Resume stopped process |
| (bg read from terminal) | SIGTTIN | Stop background process |
| (bg write to terminal) | SIGTTOU | Stop background process (if configured) |
A crucial distinction: signal generation marks the signal as pending for the target process; signal delivery is when the action is taken. If the signal is blocked, it remains pending indefinitely. SIGKILL and SIGSTOP are special—they cannot be blocked, caught, or ignored, ensuring administrators always have termination control.
How and when signals are actually delivered to processes involves subtle semantics that profoundly affect program correctness.
Signal delivery doesn't happen at arbitrary times—it occurs at well-defined points:
sleep(), pause(), select()), pending signals are checkedThis means a tight computational loop with no system calls may delay signal delivery until the next scheduling opportunity—signals don't magically interrupt pure computation.
One of the most practically important aspects of signal handling is how signals interact with blocking system calls. When a signal is delivered while a process is blocked in a system call:
Without SA_RESTART flag:
errno = EINTRWith SA_RESTART flag:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
#include <errno.h>#include <signal.h>#include <unistd.h>#include <stdio.h> volatile sig_atomic_t alarm_fired = 0; void alarm_handler(int sig) { alarm_fired = 1;} ssize_t safe_read(int fd, void *buf, size_t count) { ssize_t result; /* * Production-quality read: handles interruption properly. * This pattern is essential for robust signal-aware code. */ while ((result = read(fd, buf, count)) == -1 && errno == EINTR) { /* * read() was interrupted by a signal. * Check if we should continue or abort based on application logic. */ if (alarm_fired) { fprintf(stderr, "Read interrupted by alarm - timeout!\n"); return -1; /* Application-level timeout */ } /* Otherwise, retry the read */ } return result;} int main() { char buffer[256]; struct sigaction sa; /* Install alarm handler WITHOUT SA_RESTART */ sa.sa_handler = alarm_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; /* No SA_RESTART - intentionally allow EINTR */ sigaction(SIGALRM, &sa, NULL); printf("Reading from stdin (5 second timeout)...\n"); alarm(5); /* Generate SIGALRM in 5 seconds */ ssize_t bytes = safe_read(STDIN_FILENO, buffer, sizeof(buffer)); alarm(0); /* Cancel pending alarm */ if (bytes > 0) { printf("Read %zd bytes: %.*s\n", bytes, (int)bytes, buffer); } else if (alarm_fired) { printf("No input within timeout\n"); } else { perror("read"); } return 0;}In multi-threaded programs, signal targeting becomes nuanced:
Process-directed signals (e.g., from kill(pid, sig)):
Thread-directed signals (e.g., from pthread_kill(tid, sig)):
Synchronous signals (SIGSEGV, SIGFPE, etc.):
The implication: in multi-threaded programs, it's common practice to block signals in all threads except a dedicated 'signal handler' thread. This thread runs a loop calling sigwait() to synchronously receive and process signals, avoiding the complexity of asynchronous handler execution.
If you don't explicitly manage the signal mask in multi-threaded programs, signals may be delivered to unpredictable threads. This can cause race conditions, especially if handlers access shared data. Always design your signal strategy at application startup: block signals in workers, and use a dedicated thread or signalfd() for synchronous handling.
Signal masking is the mechanism by which processes temporarily block delivery of specified signals. Blocked signals are not lost—they are held pending and delivered when unblocked. This is essential for implementing critical sections and preventing reentrancy issues.
POSIX provides a set of functions to manipulate sigset_t data structures:
int sigemptyset(sigset_t *set); /* Initialize empty set */
int sigfillset(sigset_t *set); /* Initialize with all signals */
int sigaddset(sigset_t *set, int signum); /* Add signal to set */
int sigdelset(sigset_t *set, int signum); /* Remove signal from set */
int sigismember(const sigset_t *set, int signum); /* Test membership */
The sigprocmask() system call examines or modifies the process signal mask:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
| how | Effect |
|---|---|
| SIG_BLOCK | Add signals in set to current mask |
| SIG_UNBLOCK | Remove signals in set from current mask |
| SIG_SETMASK | Replace current mask with set |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
#include <signal.h>#include <stdio.h>#include <unistd.h> /* * Demonstrates using signal masking to protect critical sections. * Signals blocked during critical work are delivered afterward. */ volatile int shared_counter = 0;volatile int signal_count = 0; void sigint_handler(int sig) { signal_count++; printf("\n[Handler] SIGINT received (total: %d)\n", signal_count);} void critical_section() { sigset_t block_mask, old_mask; /* Create mask with SIGINT */ sigemptyset(&block_mask); sigaddset(&block_mask, SIGINT); /* * Block SIGINT during critical section. * Save old mask for restoration. */ printf("Entering critical section (SIGINT blocked)...\n"); if (sigprocmask(SIG_BLOCK, &block_mask, &old_mask) == -1) { perror("sigprocmask"); return; } /* * Critical work - SIGINT is deferred, not lost. * Press Ctrl+C multiple times - signals pending. */ for (int i = 0; i < 5; i++) { shared_counter++; printf(" Critical work step %d (counter=%d)\n", i + 1, shared_counter); sleep(1); /* Slow enough to try Ctrl+C */ } /* * Restore original mask - pending signals now delivered. * Note: Multiple SIGINTs during block collapse to ONE delivery. */ printf("Exiting critical section (SIGINT unblocked)...\n"); if (sigprocmask(SIG_SETMASK, &old_mask, NULL) == -1) { perror("sigprocmask"); return; } printf("Counter final value: %d, signals received: %d\n", shared_counter, signal_count);} int main() { struct sigaction sa; sa.sa_handler = sigint_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGINT, &sa, NULL); printf("Press Ctrl+C during critical section...\n"); critical_section(); return 0;}When a signal handler is invoked via sigaction(), the signal being handled is automatically added to the signal mask for the duration of the handler. This prevents the same signal from interrupting its own handler (reentrancy protection).
Additionally, the sa_mask field of struct sigaction specifies additional signals to block during handler execution:
struct sigaction {
void (*sa_handler)(int); /* Handler function */
void (*sa_sigaction)(int, siginfo_t *, void *); /* Extended handler */
sigset_t sa_mask; /* Signals blocked during handler */
int sa_flags; /* Behavioral flags */
};
This allows you to say: 'While handling SIGTERM, also block SIGINT and SIGQUIT to ensure atomic cleanup.'
To see which signals are currently pending (generated but blocked):
int sigpending(sigset_t *set);
This is useful for deciding whether to remain in a blocking state or to handle accumulated notifications.
A common robust pattern: block signals at the start of a complex operation, perform the operation atomically (from the signal perspective), then unblock and handle any pending signals. Use sigsuspend() to atomically unblock and wait if you need to pause for signals after a critical section.
We've established the foundational understanding of signals required for effective Unix/POSIX systems programming. Let's consolidate the core concepts:
sigaction() exclusively; legacy signal() has unreliable, non-portable semantics.sigprocmask() to temporarily block signals during sensitive operations.What's Next:
With the conceptual foundation established, the next page catalogs the common signals you'll encounter in practice—their purposes, default behaviors, and typical use cases. Understanding the signal vocabulary is essential for debugging, process management, and writing robust system software.
You now understand signals as software interrupts—their architecture, categories, lifecycle, and blocking mechanisms. This foundation prepares you to work with specific signals and implement robust handlers in production systems.