Signals

Learn about signal handling, signal masks, sigaction, and async notifications between processes in Unix/Linux systems.

published: reading time: 28 min read author: GeekWorkBench

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:

  1. Runs a signal handler — a function you define to handle the specific signal
  2. Takes the default action — which varies by signal (terminate, ignore, stop, dump core)
  3. 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 sends SIGTSTP
  • 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_DFL for default, SIG_IGN to ignore)
  • sa_mask: Set of signals to block during this handler’s execution
  • sa_flags: Modifiers like SA_RESTART (restart syscalls), SA_SIGINFO (use sa_sigaction with 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

Featuresignal()sigaction()sigwaitinfo()kill()
PortabilityOlder, less consistent across platformsPOSIX, consistentPOSIX (real-time signals)Very portable
Handler parametersOnly signal numberSignal number + siginfo_t + ucontextSignal already receivedsender info not in handler
Atomic signal mask during handlerNoYes (sa_mask)YesN/A
Syscall restart on EINTRDepends on systemYes (SA_RESTART flag)N/AN/A
Queueing (multiple signals)No (signals coalesce)No (standard signals)Yes (real-time signals queue)No
Use caseSimple ignore/defaultFull-featured handlersSynchronous signal waitSending 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=signal to see signal-related system calls
  • Process state on signal: ps aux shows processes in interruptible sleep (state S) waiting for signals
  • Core dumps: Check ulimit -c and /proc/sys/kernel/core_pattern to ensure core dumps are captured for crashes
  • Signal delivery timing: Use perf sched or ftrace to measure signal delivery latency
  • SIGCHLD zombies: ps aux | grep -i zombie to 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

  1. Calling non-async-signal-safe functions in handlersprintf(), malloc(), free(), pthread_mutex_lock() are all unsafe in signal handlers. Keep handlers minimal: set a flag, write to a pipe, call _exit().

  2. Not handling EINTR on blocking calls — always check for and retry interrupted system calls, or use SA_RESTART to let the kernel handle it.

  3. 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.

  4. Ignoring SIGCHLD — not handling child exits causes zombie processes that accumulate and consume process table entries.

  5. Not setting SA_RESTART on time-consuming operations — if a signal arrives during a long read() from a slow device, and you did not set SA_RESTART, the read() returns -1 with EINTR. Handle this or use select()/poll().

  6. Signal handler modifying errno — signal handlers clobber errno. Save and restore errno in handlers if needed.

  7. Mixing signal() and sigaction() — the behavior of signal() varies across platforms (some restart syscalls, some do not). Always use sigaction() for portability.

  8. Sending SIGKILL to processes that need cleanupSIGKILL cannot be caught, blocked, or ignored. It terminates the process immediately without running cleanup handlers. Use SIGTERM for 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 over signal())
  • Signal handlers must only call async-signal-safe functions — keep handlers minimal
  • volatile sig_atomic_t for simple flags; use self-pipe pattern for complex synchronization
  • SIGPIPE defaults to terminating the process — set SIG_IGN or use MSG_NOSIGNAL
  • SA_RESTART flag automatically restarts certain interrupted system calls
  • SIGCHLD should be handled or ignored to prevent zombie processes
  • kill(pid, sig) sends a signal to a specific process; raise(sig) sends to current process
  • Real-time signals (SIGRTMIN to SIGRTMAX) support queuing and carry extra payload
  • Always handle EINTR on blocking calls or use SA_RESTART

Interview Questions

1. What is the difference between signal() and sigaction()?

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_RESTART to control syscall behavior on EINTR
  • Install handlers that receive extra information about the signal's origin (using SA_SIGINFO flag)

The consistent, portable behavior of sigaction() makes it the correct choice for production code. Always use sigaction().

2. What functions are safe to call from a signal handler?

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 not malloc/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.

3. How do you prevent zombie processes with SIGCHLD?

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:

  1. 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.
  2. Use SA_NOCLDWAIT flag: With sigaction(), set sa_flags |= SA_NOCLDWAIT. This has similar effect but more portable.
  3. 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);
4. What happens when a signal arrives during a blocking system call?

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_RESTART flag in sigaction(). 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.
5. What is the self-pipe trick and why is it useful?

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:

  1. Create a pipe before entering the event loop
  2. Add the read end of the pipe to your select()/poll()/epoll() event loop
  3. In the signal handler, write a single byte to the pipe (which is async-signal-safe)
  4. 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.

6. What are real-time signals and how do they differ from standard signals?

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.

7. How does a process send a signal to another process and what permissions are required?

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.

8. What is the signal mask and how do you atomically block and unblock signals?

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.

9. What is sigwaitinfo() and how does it differ from using a signal handler?

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().

10. How does SA_SIGINFO change the signal handler signature and what does it enable?

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.

11. What happens when SIGKILL is sent to a process?

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.

12. What is the relationship between process groups, job control, and signals?

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.

13. What is a core dump and how does it relate to signal delivery?

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.

14. What is the difference between SIGTERM and SIGINT for process termination?

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.

15. How does the kernel deliver a signal to a multi-threaded process?

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.

16. What is the signalfd interface and what advantages does it provide?

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.

17. What is the SA_RESTART flag and which system calls does it automatically restart?

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.

18. What is the difference between waiting on a single child and waiting on any child using waitpid?

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.

19. What is the sigaltstack mechanism and why might you need it?

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.

20. How does eventfd differ from signals for cross-thread notification in Linux?

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

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

#operating-systems #aslr-stack-protection #computer-science

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.

#operating-systems #assembly-language-basics #computer-science

Boolean Logic & Gates

Understanding AND, OR, NOT gates and how they combine into arithmetic logic units — the building blocks of every processor.

#operating-systems #boolean-logic-gates #computer-science