Signals
Learn about signal handling, signal masks, sigaction, and async notifications between processes in Unix/Linux systems.
Signals
Every process in a Unix-like system lives under the constant awareness of signals — asynchronous notifications from the kernel and other processes that something important has happened. When you press Ctrl+C in a terminal, a signal is sent. When a process crashes and the kernel detects an illegal memory access, a signal is delivered. When a child process exits, its parent receives a signal. Signals are the oldest and most fundamental form of inter-process communication, predating pipes, message queues, and sockets.
Despite their simplicity, signals are often misunderstood and mishandled. The async nature of signals makes them notoriously tricky to reason about — the signal can arrive at any point in your program’s execution, interrupting system calls, modifying global state, and causing subtle bugs if not handled carefully. Getting signal handling right is a hallmark of systems programming competence.
Introduction
A signal is a notification sent to a process (or thread) to indicate that an event has occurred. Signals are asynchronous — they can arrive at any time, interrupting the normal flow of execution. When a signal arrives, the process either:
- Runs a signal handler — a function you define to handle the specific signal
- Takes the default action — which varies by signal (terminate, ignore, stop, dump core)
- Ignores it — if you explicitly request to ignore the signal
There are over 30 standard signals defined in POSIX, each with a name (like SIGTERM, SIGINT, SIGSEGV) and a numeric value. Signals are delivered for various reasons:
- User-initiated: Ctrl+C sends
SIGINT, Ctrl+Z sendsSIGTSTP - Process communication: One process sending a signal to another via
kill() - Kernel-generated: Hardware exception (
SIGSEGV,SIGFPE), timer (SIGALRM), child exit (SIGCHLD) - Resource limits:
SIGXCPU(CPU time exceeded),SIGXFSZ(file size exceeded)
Signals are delivered to a specific thread. If the process has multiple threads, the signal is delivered to one arbitrarily selected thread that is not blocking the signal.
When to Use / When Not to Use
Use signals when:
- You need to send a simple notification to a process (terminate, pause, resume)
- You need to handle asynchronous events like timer expirations or child process exits
- You need to implement a clean shutdown mechanism (catch SIGTERM and cleanup gracefully)
- You are building a daemon that responds to external control commands
- You need to interrupt a blocking system call
Do not use signals when:
- You need to transfer data (signals carry no payload — use pipes or sockets)
- You need synchronous, ordered communication (use message queues or pipes)
- You need many different message types (use sockets with custom protocols)
- You are building complex IPC that requires confirmation/reply patterns
Architecture or Flow Diagram
sequenceDiagram
participant P1 as Process A (Sender)
participant Kernel
participant P2 as Process B (Receiver)
Note over P1: kill(PID_B, SIGTERM)
P1->>Kernel: raise(SIGTERM, target=PID_B)
Kernel->>Kernel: Validate target process exists<br/>Check sender permissions
alt Process B has handler
Kernel->>P2: Deliver signal (may interrupt syscall)
P2->>P2: Run signal handler sigaction(SIGTERM)
P2-->>P2: Resume execution after handler
else Process B ignores
Kernel->>P2: Signal delivered but ignored
Note over P2: No action taken
else Default action
Kernel->>P2: Terminate process
Note over P2: Process exits, status = 143 (SIGTERM)
end
Core Concepts
Signal Dispositions and sigaction
Every signal has a “disposition” — what happens when the signal is delivered. The default disposition varies by signal. You can change the disposition using signal() (simple, legacy) or sigaction() (more control, portable):
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
void handle_sigterm(int sig) {
// Signal-safe operations only here
// No printf, malloc, or non-async-signal-safe functions!
const char msg[] = "Received SIGTERM, shutting down...\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
// Perform cleanup...
_exit(0);
}
int main() {
struct sigaction sa;
sa.sa_handler = handle_sigterm; // Handler function
sigemptyset(&sa.sa_mask); // No signals blocked during handler
sa.sa_flags = 0; // No special flags
// SA_RESTART: restart syscalls interrupted by this signal
// SA_NODEFER: don't block this signal during its own handler
if (sigaction(SIGTERM, &sa, NULL) == -1) {
perror("sigaction");
exit(1);
}
// Now SIGTERM will call handle_sigterm instead of terminating
while (1) {
pause(); // Wait for signals
}
}
Key sigaction fields:
sa_handler: Function pointer (SIG_DFLfor default,SIG_IGNto ignore)sa_mask: Set of signals to block during this handler’s executionsa_flags: Modifiers likeSA_RESTART(restart syscalls),SA_SIGINFO(usesa_sigactionwith extra info)
Signal Masks
Each thread (and by extension, process) has a signal mask — a set of signals that are currently blocked. Blocked signals are not delivered, but they may be pending. You can manipulate the signal mask with pthread_sigmask() (for threads) or sigprocmask() (for single-threaded processes):
#include <signal.h>
void block_sigchld() {
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigset_t oldmask;
pthread_sigmask(SIG_BLOCK, &mask, &oldmask);
// In this block, SIGCHLD won't interrupt us
// ... do work ...
// Restore previous mask
pthread_sigmask(SIG_SETMASK, &oldmask, NULL);
}
Sending Signals
You send signals with the kill() system call:
#include <signal.h>
#include <unistd.h>
pid_t target_pid = 1234;
// Send SIGTERM to a process
if (kill(target_pid, SIGTERM) == -1) {
perror("kill");
}
// Send SIGUSR1 with no default action (user-defined)
kill(target_pid, SIGUSR1);
The raise() function sends a signal to the current process:
raise(SIGALRM); // Equivalent to kill(getpid(), SIGALRM)
Signal Safety and Async Signal Safety
Not all functions can be safely called from a signal handler. A function is async-signal-safe if it can be safely called from a signal handler (it does not use non-reentrant locks, does not allocate memory, etc.). Functions like write(), _exit(), sync(), wait() are safe. Functions like printf(), malloc(), free(), pthread_mutex_lock() are NOT safe.
The list of async-signal-safe functions is given in man 7 signal-safety. When writing signal handlers, keep them minimal — set a flag, write to a pipe, call _exit(). Do complex cleanup in the main program after checking the flag.
Waiting for Signals: pause(), sigsuspend(), sigwaitinfo()
// Simple pause (wait for any signal)
pause();
// Block in sigsuspend atomically replacing signal mask
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigprocmask(SIG_BLOCK, &mask, NULL); // Block SIGINT
// Critical section - SIGINT blocked
sigprocmask(SIG_SETMASK, &mask, NULL); // Unblock, delivering SIGINT
// Synchronous wait for specific signals (real-time signals)
sigset_t waitmask;
sigemptyset(&waitmask);
sigaddset(&waitmask, SIGRTMIN);
sigwaitinfo(-1, &waitmask, NULL); // Wait for SIGRTMIN
Production Failure Scenarios
Interrupted System Calls (EINTR)
When a signal handler runs during a blocking system call (like read(), write(), connect(), sleep()), the kernel may interrupt the call and return -1 with errno = EINTR. If you do not handle this, your program may interpret the interrupted call as an error.
Mitigation: Always check for EINTR and retry interrupted calls. Use SA_RESTART flag with sigaction() to automatically restart some syscalls. Review all blocking calls for EINTR handling:
ssize_t n;
while ((n = read(fd, buf, sizeof(buf))) == -1 && errno == EINTR) {
// Retry
}
if (n == -1) {
perror("read");
}
Signal Handler Race Conditions
If your signal handler modifies a global variable that is also modified by the main program without synchronization, you get a race condition. For example, a handler setting a shutdown_requested flag while the main loop checks it:
volatile sig_atomic_t shutdown_requested = 0; // Volatile needed!
void handler(int sig) {
shutdown_requested = 1;
}
int main() {
// ...
while (!shutdown_requested) {
// Do work - but this is still racy on some architectures!
}
}
Mitigation: Use volatile sig_atomic_t for simple flags (atomic enough for single-flag access). For more complex synchronization, use a self-pipe — the signal handler writes to a pipe, and the main loop monitors the pipe with select()/epoll().
Missing SIGCHLD Handling — Zombie Processes
If a child process exits and the parent does not wait() for it, the child becomes a zombie. If the parent never calls wait() (or uses waitpid() with WNOHANG), zombie processes accumulate. In modern init systems (systemd), this is less of a problem, but in long-running daemons it is a real issue.
Mitigation: Call signal(SIGCHLD, SIG_IGN) to ignore SIGCHLD and let the system automatically reap children (on Linux). Or use sigaction() with SA_NOCLDWAIT flag. Or implement a SIGCHLD handler that calls waitpid() in a loop with WNOHANG.
SIGPIPE Misconfiguration
When writing to a pipe or socket whose reading end has closed, the writer receives SIGPIPE. The default action terminates the process. If you have not handled SIGPIPE, any write() to a broken pipe terminates your program.
Mitigation: Always set signal(SIGPIPE, SIG_IGN) early in program initialization, or use MSG_NOSIGNAL flag on send():
signal(SIGPIPE, SIG_IGN);
Signal Masking Deadlocks
If a signal is blocked and arrives while your program is in a critical section (holding a lock), and that signal’s handler tries to acquire the same lock, you deadlock. This is especially tricky with recursive mutexes.
Mitigation: Keep signal handlers very simple and lock-free. Design your locking to ensure that signal handlers never need locks that the interrupted code might hold.
Trade-off Table
| Feature | signal() | sigaction() | sigwaitinfo() | kill() |
|---|---|---|---|---|
| Portability | Older, less consistent across platforms | POSIX, consistent | POSIX (real-time signals) | Very portable |
| Handler parameters | Only signal number | Signal number + siginfo_t + ucontext | Signal already received | sender info not in handler |
| Atomic signal mask during handler | No | Yes (sa_mask) | Yes | N/A |
| Syscall restart on EINTR | Depends on system | Yes (SA_RESTART flag) | N/A | N/A |
| Queueing (multiple signals) | No (signals coalesce) | No (standard signals) | Yes (real-time signals queue) | No |
| Use case | Simple ignore/default | Full-featured handlers | Synchronous signal wait | Sending signals |
Implementation Snippet(s)
C: Clean Shutdown with SIGTERM
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdbool.h>
volatile sig_atomic_t g_shutdown = false;
void sigterm_handler(int sig) {
g_shutdown = true;
}
int main() {
struct sigaction sa;
sa.sa_handler = sigterm_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // Restart interrupted syscalls
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
printf("Service running (PID %d). Send SIGTERM to stop.\n", getpid());
while (!g_shutdown) {
// Do work — simulate with sleep
sleep(1);
}
printf("Shutting down gracefully...\n");
// Cleanup: close files, release resources, flush buffers
printf("Done.\n");
return 0;
}
Python: Signal Handling
import signal
import sys
def sigterm_handler(signum, frame):
print("Received SIGTERM, shutting down gracefully...")
# Note: in Python, most functions are safe to call from signal handlers
# because the GIL provides some protection
sys.exit(0)
def sigint_handler(signum, frame):
print("Received Ctrl+C, shutting down...")
sys.exit(0)
signal.signal(signal.SIGTERM, sigterm_handler)
signal.signal(signal.SIGINT, sigint_handler)
print(f"PID: {sys.executable}")
while True:
signal.pause()
Bash: Signal Trapping in Scripts
#!/bin/bash
# Trap signals for graceful shutdown
cleanup() {
echo "Received signal, cleaning up..."
rm -f /tmp/myapp.lock
exit 0
}
trap cleanup SIGTERM SIGINT SIGQUIT
# Or ignore signals during critical section
trap '' SIGINT
echo "In critical section - Ctrl+C ignored"
sleep 5
trap SIGINT # Restore default handling
# Background job with signal control
sleep 100 &
sleep_pid=$!
# Wait for job or signal
wait $sleep_pid 2>/dev/null || echo "Interrupted"
Observability Checklist
- Pending signals: Check pending signals for a process with
cat /proc/<pid>/status | grep -i SigPnd - Blocked signals: Check blocked signals with
cat /proc/<pid>/status | grep -i SigBlk - Signal handler registration: Use
strace -e trace=signalto see signal-related system calls - Process state on signal:
ps auxshows processes in interruptible sleep (stateS) waiting for signals - Core dumps: Check
ulimit -cand/proc/sys/kernel/core_patternto ensure core dumps are captured for crashes - Signal delivery timing: Use
perf schedorftraceto measure signal delivery latency - SIGCHLD zombies:
ps aux | grep -i zombieto find zombie processes indicating improper wait handling
Common Pitfalls / Anti-Patterns
Signal permissions: Sending a signal requires appropriate permissions — either the sender must have the same effective user ID as the receiver (kill(pid, sig) succeeds), or the sender must have CAP_KILL capability (root). This prevents arbitrary processes from sending signals to each other.
Signal DoS: A process could flood another process with signals (e.g., repeatedly sending SIGURG which has no default action but still triggers handler execution), consuming CPU time in the handler. Use rate limiting if you are implementing signal-sending functionality.
Signal-based attacks: Some privilege escalation exploits use signals to manipulate process state. Ensure that signal handlers do not perform unsafe operations and that security-sensitive state is not accessible via signal handler interfaces.
Audit: Signal delivery may not generate standard audit events. For compliance requirements requiring complete process behavior tracking, implement application-level logging around signal handling.
Common Pitfalls / Anti-patterns
-
Calling non-async-signal-safe functions in handlers —
printf(),malloc(),free(),pthread_mutex_lock()are all unsafe in signal handlers. Keep handlers minimal: set a flag, write to a pipe, call_exit(). -
Not handling EINTR on blocking calls — always check for and retry interrupted system calls, or use
SA_RESTARTto let the kernel handle it. -
Using non-volatile global flags — the compiler may optimize away reads of a global variable that the signal handler sets, causing infinite loops. Use
volatile sig_atomic_t. -
Ignoring SIGCHLD — not handling child exits causes zombie processes that accumulate and consume process table entries.
-
Not setting SA_RESTART on time-consuming operations — if a signal arrives during a long
read()from a slow device, and you did not setSA_RESTART, theread()returns-1withEINTR. Handle this or useselect()/poll(). -
Signal handler modifying errno — signal handlers clobber
errno. Save and restoreerrnoin handlers if needed. -
Mixing signal() and sigaction() — the behavior of
signal()varies across platforms (some restart syscalls, some do not). Always usesigaction()for portability. -
Sending SIGKILL to processes that need cleanup —
SIGKILLcannot be caught, blocked, or ignored. It terminates the process immediately without running cleanup handlers. UseSIGTERMfor graceful shutdown.
Quick Recap Checklist
- Signals are async notifications delivered to processes; they can interrupt any execution point
- Use
sigaction()for portable, full-featured signal handling (preferred oversignal()) - Signal handlers must only call async-signal-safe functions — keep handlers minimal
volatile sig_atomic_tfor simple flags; use self-pipe pattern for complex synchronizationSIGPIPEdefaults to terminating the process — setSIG_IGNor useMSG_NOSIGNALSA_RESTARTflag automatically restarts certain interrupted system callsSIGCHLDshould be handled or ignored to prevent zombie processeskill(pid, sig)sends a signal to a specific process;raise(sig)sends to current process- Real-time signals (
SIGRTMINtoSIGRTMAX) support queuing and carry extra payload - Always handle
EINTRon blocking calls or useSA_RESTART
Interview Questions
signal() is the older, simpler API for setting signal dispositions. Its behavior varies across Unix platforms — on some systems it automatically restarts interrupted syscalls, on others it does not. Some systems reset the disposition to default after the first signal is delivered. This inconsistency makes it less reliable for portable code.
sigaction() is the POSIX-standard, feature-rich interface. It allows you to:
- Specify a handler function with three parameters (signal number, siginfo_t pointer, ucontext pointer)
- Set a signal mask that blocks additional signals during the handler's execution
- Set flags like
SA_RESTARTto control syscall behavior on EINTR - Install handlers that receive extra information about the signal's origin (using
SA_SIGINFOflag)
The consistent, portable behavior of sigaction() makes it the correct choice for production code. Always use sigaction().
Only async-signal-safe functions can be safely called from a signal handler. These are functions that do not use internal locks, do not allocate memory dynamically, and are reentrant. The canonical list includes:
- I/O:
write(),_exit(),sync() - Process control:
_exit(),wait(),kill()(sender info only) - Synchronization:
sigprocmask(),sem_post() - String/byte operations:
memcpy(),memset()(but notmalloc/free)
Unsafe: printf(), malloc(), free(), pthread_mutex_lock(), most libc functions. The practical rule: in a signal handler, only call write() to a pipe or file descriptor, set a volatile sig_atomic_t flag, or call _exit(). For complex cleanup, use the self-pipe trick — the handler writes a byte to a pipe, and the main loop monitors that pipe and performs cleanup.
When a child process terminates, it remains in the process table as a zombie until the parent explicitly retrieves its exit status with wait() or waitpid(). If the parent never calls these, zombie processes accumulate.
Three solutions:
- Ignore SIGCHLD: Call
signal(SIGCHLD, SIG_IGN). On Linux, this tells the kernel to automatically reap children without creating zombies. Simple but prevents the parent from waiting for specific children. - Use SA_NOCLDWAIT flag: With
sigaction(), setsa_flags |= SA_NOCLDWAIT. This has similar effect but more portable. - Handle SIGCHLD explicitly: Install a handler that loops calling
waitpid(-1, NULL, WNOHANG)to reap any terminated children. This allows you to track child exits and maintain a process count.
struct sigaction sa;
sa.sa_handler = handle_sigchld;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDWAIT;
sigaction(SIGCHLD, &sa, NULL);
When a signal handler is invoked during a blocking system call (e.g., read(), write(), connect(), sleep()), the kernel interrupts the call and returns -1 with errno = EINTR (Interrupted System Call). The call did not complete — it was prematurely terminated.
Your options for handling this:
- Retry the call: Loop until the call succeeds or returns an error other than EINTR. Most network code written for Unix uses this pattern.
- Use SA_RESTART: Set the
SA_RESTARTflag insigaction(). The kernel automatically restarts some (but not all) syscalls.read(),write(),ioctl(),select()are restarted;connect(),accept(),recv()are not. - Check for partial success: Some calls like
read()may return a positive length before being interrupted. Handle this case too.
The self-pipe trick is a pattern for handling signals safely in a main event loop. The problem: you want to do complex cleanup in response to a signal, but you cannot call complex functions from a signal handler (most are not async-signal-safe). The solution:
- Create a pipe before entering the event loop
- Add the read end of the pipe to your
select()/poll()/epoll()event loop - In the signal handler, write a single byte to the pipe (which is async-signal-safe)
- When the event loop detects readability on the pipe, read the byte and perform the complex cleanup in the main program context
int sigpipefd[2]; pipe(sigpipefd); signal(SIGTERM, handler); // handler does write(sigpipefd[1], "!", 1)
while (epoll_wait(efd, …) > 0) { for each event: if (event.fd == sigpipefd[0]) { read(sigpipefd[0], &byte, 1); // Do cleanup now - we are in safe context } }
This keeps signal handlers minimal and allows full cleanup logic in the main program, where all functions are available.
Real-time signals (SIGRTMIN to SIGRTMAX, at least 8 on Linux) were introduced to address the limitation that standard signals coalesce: if three SIGUSR1 signals arrive before the handler runs, only one is delivered. Real-time signals queue: each signal is delivered individually and the handler can retrieve the full set of pending signals using sigwaitinfo().
Real-time signals carry a siginfo_t payload with the sender's PID, UID, and an optional pointer-sized value (si_value.sival_ptr or si_value.sival_int) that can be used to pass data. They are delivered in numerical order (lowest signal number first). Standard signals (SIGTERM, SIGINT, SIGUSR1, etc.) cannot reliably carry data and do not queue, making real-time signals appropriate for applications that need multiple pending signal notifications.
The kill(pid, sig) syscall sends a signal to a specific process. The sender must have the same effective UID as the receiver (or be root), or the sender must have the CAP_KILL capability. This prevents arbitrary processes from sending signals to unrelated processes. The zero signal (SIGKILL number 0) is special: it validates that the target process exists and the sender has permission, without actually sending a signal.
For thread-level signals, pthread_kill(tid, sig) sends a signal to a specific thread within the same process, which is always permitted since threads share credentials. raise(sig) sends a signal to the current process (equivalent to kill(getpid(), sig)). tgkill(tgid, tid, sig) sends a signal to a specific thread, useful when thread IDs may be reused.
The signal mask is a per-thread set of signals that are currently blocked from delivery. Blocked signals are not delivered but remain pending. sigprocmask() manipulates the signal mask of the calling thread (in single-threaded processes). For multi-threaded programs, use pthread_sigmask() which operates on the calling thread's mask.
The critical pattern for atomic mask manipulation with synchronization: block the signal, enter a critical section, atomically unblock the signal while waiting (so the signal can be delivered immediately upon arrival), then re-block and handle the pending signal. The sigsuspend() syscall atomically replaces the signal mask with a specified mask and suspends the process, making the block/unblock/suspend sequence atomic and avoiding a race window between separate calls.
sigwaitinfo() synchronously waits for a specific pending signal, blocking until it arrives. It returns the signal info structure including sender PID, UID, and optional payload (for real-time signals). Unlike a signal handler, sigwaitinfo() does not involve asynchronous execution interruption—it runs in the context of the calling thread as a blocking call.
sigwaitinfo() is useful for dedicated signal-handler threads that avoid the complexity of async signal safety. The signal must be blocked (via sigprocmask) before calling sigwaitinfo(), otherwise the default handler may run instead. sigtimedwait() is a variant with a timeout. signalfd() provides an even cleaner interface by converting signals into file descriptor events readable by select()/poll()/epoll().
With SA_SIGINFO flag, the signal handler receives three arguments instead of one: void handler(int sig, siginfo_t *info, void *ucontext). The siginfo_t structure contains: the signal number, error number, signal code, sender PID and UID, address that caused the fault (for SIGSEGV, SIGBUS), and for real-time signals, the sival_ptr payload data.
This enables rich signal handling: a SIGCHLD handler can retrieve the child's PID and exit status without relying on wait() in a loop. A SIGSEGV handler can inspect the fault address to detect stack overflow (address near stack pointer) versus bad pointer access. SIGILL, SIGFPE, and SIGBUS handlers benefit similarly from code and address information. The ucontext contains saved register state for architectures that support it.
SIGKILL (signal 9) cannot be caught, blocked, or ignored. When delivered, the kernel immediately terminates the process without running any signal handler or cleanup code. The process does not have an opportunity to flush buffers, close files, or perform any graceful shutdown. For this reason, SIGKILL should be a last resort after SIGTERM has been given time to take effect.
In some states (like zombie), SIGKILL is not actually delivered to the process because the process is already exiting (zombie) and its entry will be reaped by the parent. SIGKILL also does not terminate processes in uninterruptible sleep (state D in ps) waiting for I/O or kernel resources—the process is stuck in the kernel and cannot be killed until the I/O completes or times out.
Processes belong to a process group (created via setpgid() or implicit inheritance from parent's process group). A terminal's foreground process group receives signals from the terminal (SIGINT from Ctrl+C, SIGTSTP from Ctrl+Z) and job control signals. Background process groups ignore terminal-generated signals by default but can receive them if they modify signal handling.
kill(-pgid, sig) sends a signal to all processes in the process group. SIGSTOP (not maskable) suspends a process group; SIGCONT resumes it. Shell job control uses these: Ctrl+Z sends SIGTSTP to the foreground process group, suspending it and returning control to the shell. jobs lists background jobs; fg/bg use SIGCONT to resume suspended jobs.
Certain signals (SIGQUIT, SIGILL, SIGTRAP, SIGABRT, SIGFPE, SIGSEGV, SIGBUS, SIGSYS) cause the kernel to write a core dump—a file named core or core.PID containing the process's memory image (registers, stack, memory mappings) when delivered to a process. This is the default action for these signals. SIGKILL and SIGSTOP never produce core dumps.
Core dump generation can be disabled (ulimit -c 0 or setrlimit(RLIMIT_CORE, 0)), limited by size, or configured via /proc/sys/kernel/core_pattern (which can pipe core dumps through a helper program like systemd-coredump). For production servers where core dumps are disabled, enabling them temporarily for crashes provides crucial debugging information. gdb can inspect a core dump to determine the crash location and stack trace.
SIGINT (signal 2, generated by Ctrl+C in a terminal) is the interrupt signal—a polite request to terminate, intended for interactive processes. Processes can catch or ignore SIGINT, allowing them to perform cleanup before exiting. SIGTERM (signal 15) is the terminate signal—a more formal termination request, also catchable and ignorable. Both allow the process to clean up.
For daemon processes and services, SIGTERM is the standard termination signal because it carries the semantic of "shut down gracefully." Container orchestrators like Docker send SIGTERM first and wait for a grace period before sending SIGKILL. Scripts and programs should treat SIGTERM as the signal to initiate graceful shutdown (close connections, flush buffers, exit cleanly) and SIGINT as a user interrupt in interactive contexts.
Signals are delivered to a single thread within a multi-threaded process. The kernel selects an arbitrary thread that is not currently blocking the signal (checking the thread's signal mask). If all threads are blocking the signal, the signal remains pending for the entire process until at least one thread unblocks it. The first thread to unblock that signal receives it.
Process-directed signals (sent to a PID) may be delivered to any thread. Thread-directed signals (sent to a specific thread ID via pthread_kill or tgkill) are delivered to that exact thread. When a process receives a signal with default action SIGIGN, all threads ignore it. When a thread handles a signal with a signal handler, the handler runs in that thread's context—all other threads continue execution normally.
signalfd() creates a file descriptor that wraps signal delivery into a file descriptor event model. You create it with signalfd(-1, &mask, SFD_CLOEXEC), which returns a file descriptor representing the set of blocked signals in mask. When any of those signals arrive, read(fd, &info, sizeof(info)) returns a signalfd_siginfo structure with signal details.
Advantages over traditional signal handlers: signals become first-class I/O events compatible with select()/poll()/epoll(), eliminating the need for the self-pipe trick to integrate signals into an event loop. The handler is invoked synchronously from read() rather than asynchronously at arbitrary points, so you can call any library function from the "handler" (which runs in your event loop context). signalfd is Linux-specific but available on all major Linux distributions.
SA_RESTART causes the kernel to automatically restart certain system calls that were interrupted by a signal. The kernel restarts: read(), write(), open(), ioctl(), waitpid(), creat(), and a few others. The restarted call does not return -1 with errno=EINTR—it acts as if it was never interrupted.
Not restarted: connect(), accept(), recvfrom(), sendto(), recvmsg(), sendmsg(), semop(), and tcdrain(). These must handle EINTR explicitly. The SA_RESTART flag applies to the specific signal handler being installed, not globally. This inconsistency is why many robust network programs explicitly handle EINTR rather than relying on SA_RESTART.
waitpid(-1, &status, 0) waits for any child process to terminate (equivalent to wait(&status)). waitpid(pid, &status, 0) waits for a specific child (by PID). For a specific child, if the child has already exited, the status is returned immediately. For any child (pid=-1), the call blocks until at least one child exits.
waitpid(-1, &status, WNOHANG) is non-blocking: it returns immediately with 0 if no child has exited, or the PID of a terminated child with its status. This is the basis of SIGCHLD handlers that reap children without blocking: install the handler, and periodically call waitpid(-1, NULL, WNOHANG) in the main loop to reap any exited children. Without WNOHANG in the loop, waitpid() would block when no children have exited.
By default, signal handlers execute on the same stack as the interrupted code. For programs that use small stacks (common in threaded programs where each thread has a small stack, like 8KB), a signal handler that needs deep recursion or large local variables could overflow the stack. sigaltstack() creates an alternative signal stack—a separate region of memory used for signal handler execution.
Use sigaltstack() to allocate a dedicated stack region (typically SIGSTKSZ or larger) and register it with SA_ONSTACK flag when installing the signal handler. On systems with limited stacks (embedded, some threading implementations), this prevents signal handler stack overflow. Linux's SA_SIGINFO handlers can also use ucontext_t to save and restore the full machine state when handling signals delivered during system calls.
eventfd() creates a file descriptor representing a 64-bit counter. write(fd, &value, 8) atomically adds value to the counter; read(fd, &value, 8) reads and resets the counter. The file descriptor is pollable via select()/poll()/epoll(), making it ideal for integrating event notification into an event loop without signal handling complexity.
Unlike signals, eventfd does not interrupt execution asynchronously—the read() blocks until data is available, which is the normal event-loop pattern. Unlike pipes (used in the self-pipe trick), eventfd requires only one file descriptor instead of two, supports epoll, and has lower overhead. It is particularly useful for thread notification, wake-up pipes in event loops, and synchronization between a signal handler thread and a main event loop.
Further Reading
- signal(7) — Linux man page — Overview of signals on Linux
- sigaction(2) — Linux man page — Signal action API reference
- signal-safety(7) — Linux man page — Async-signal-safe functions list
- kill(2) — Linux man page — Sending signals between processes
Conclusion
Signals are the oldest form of process communication in Unix systems — async notifications that can interrupt execution at any point. While the concept is simple, signal handling exposes the tension between the kernel’s event delivery model and safe user-space execution. The rules around async-signal-safety, EINTR handling, and signal mask management exist because signals cross protection boundaries in ways that normal function calls do not.
As systems evolved, signals gave way to more sophisticated event delivery mechanisms: event loops with select()/poll()/epoll(), kernel event queues in modern kernels, and asynchronous I/O APIs. Yet signals persist because they remain the only kernel-to-process notification mechanism for certain events (child exit, resource limits, crashes) that have no alternative.
For continued learning, explore how eventfd() provides a signal-like notification mechanism compatible with epoll(), and study how modern Linux uses real-time signals (SIGRTMIN to SIGRTMAX) with siginfo delivery for richer signal payloads beyond simple notifications.
Category
Related Posts
ASLR & Stack Protection
Address Space Layout Randomization, stack canaries, and exploit mitigation techniques
Assembly Language Basics: Writing Code the CPU Understands
Learn to read and write simple programs in x86 and ARM assembly, understanding registers, instructions, and the art of thinking in low-level operations.
Boolean Logic & Gates
Understanding AND, OR, NOT gates and how they combine into arithmetic logic units — the building blocks of every processor.