Linux Signal Handling Guide: Correct Usage of signal() and sigaction() in C/C++

Linux signals are a process-level asynchronous notification mechanism used to handle interrupts, termination, timeouts, and child process reaping. This article focuses on the signal lifecycle, core APIs, blocking control, and reentrancy safety to help developers build practical system programming knowledge. Keywords: Linux signals, sigaction, reentrant functions

Technical Specification Snapshot

Parameter Description
Primary Language C / C++
Runtime Platform Linux / POSIX
Core Protocol POSIX Signals
Source Format Reconstructed from a CSDN technical blog post
Star Count Not provided in the original content
Core Dependencies signal.h, unistd.h, sys/wait.h, errno.h

Linux Signals Are the Foundation of Asynchronous Process Control

A signal is an asynchronous notification sent by the kernel to a process. Unlike a function call, it does not occur in a predictable sequence. Instead, it may interrupt the program at almost any point and require the process to handle an interruption, error, exit request, or state change.

In user space, common signal sources include Ctrl+C, the kill command, timer expiration, child process termination, and invalid memory access. Understanding signals is essentially understanding how Linux injects external events into a process execution flow.

The Learning Path Works Best in Four Layers

  1. First, memorize common signals and their default behaviors.
  2. Then, understand the signal lifecycle from generation to delivery.
  3. Next, master signal, kill, alarm, and sigaction.
  4. Finally, deal with blocking, EINTR, and reentrancy safety.

AI Visual Insight: This diagram shows the layered learning path for Linux signals. It typically progresses from conceptual understanding and common signals to handling models, core APIs, and advanced topics, making it suitable for building a complete knowledge loop from semantics to engineering practice.

Common Signals and Their Default Behaviors Define a Program’s Basic Survival Rules

Only a small number of signals appear frequently in practice, but each one maps to a high-frequency scenario. SIGINT comes from terminal interruption, SIGTERM is used for graceful shutdown, SIGKILL forces termination, SIGCHLD handles child process reaping, and SIGALRM supports timeout control.

Signal Number Default Action Catchable Typical Scenario
SIGINT 2 Terminate Yes User presses Ctrl+C
SIGTERM 15 Terminate Yes Graceful service shutdown
SIGKILL 9 Terminate No Force-kill a process
SIGSTOP 19 Stop No Suspend a process
SIGSEGV 11 Terminate and dump core Yes Invalid memory access
SIGCHLD 17 Ignore Yes Child process exit notification
SIGPIPE 13 Terminate Yes Writing to a closed pipe
SIGALRM 14 Terminate Yes Timer expiration

Among these, SIGKILL and SIGSTOP cannot be caught, blocked, or ignored. They represent the operating system’s final control authority.

The Signal Lifecycle Explains Why a Handler Does Not Run Immediately

A signal usually goes through four stages: generation, registration as pending, delivery, and handling. It does not execute immediately after being sent. Instead, the kernel waits until the process reaches an appropriate point, then checks the pending set and blocking mask to decide whether to deliver it.

AI Visual Insight: This diagram describes the signal state flow from generation to handling. It highlights the pending set, the blocking mask, and the transition points between user mode and kernel mode. These are the keys to understanding delayed handling and immediate triggering after unblocking.

Signal Handling Has Only Three Modes, but Their Engineering Consequences Differ Completely

After a process receives a signal, it has only three possible handling paths: ignore it, perform the default action, or invoke a custom handler. In real systems, the most important task is to distinguish which signals are safe to ignore, which ones must be handled explicitly, and which ones should be left to the system default.

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void my_handler(int signum) {
    // Warning: printf is not safe inside a signal handler; this is for demonstration only
    printf("收到信号 %d\n", signum);
}

int main() {
    signal(SIGINT, my_handler);   // Custom handler for Ctrl+C
    signal(SIGTERM, SIG_IGN);     // Ignore termination requests

    while (1) {
        printf("运行中...\n");  // Main loop keeps printing
        sleep(1);
    }
    return 0;
}

This example demonstrates the basic approach of customizing SIGINT handling and ignoring SIGTERM, but you should not use it directly as a production pattern.

sigaction Is the Stable Interface Recommended by POSIX

signal() carries significant historical baggage, and its behavior is not fully consistent across implementations. Modern Linux programs should prefer sigaction() because it provides more stable semantics, clearer mask control, and extended signal context.

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void handler(int sig, siginfo_t *info, void *context) {
    (void)context; // Explicitly ignore the context parameter
    printf("收到信号 %d, 来自进程 %d\n", sig, info->si_pid);
}

int main() {
    struct sigaction sa;
    sigemptyset(&sa.sa_mask);          // Clear the additional blocked set during handling
    sa.sa_flags = SA_SIGINFO;          // Enable extended signal information
    sa.sa_sigaction = handler;         // Register a three-argument handler

    sigaction(SIGUSR1, &sa, NULL);     // Install the handler
    printf("等待信号...\n");

    while (1) pause();                 // Suspend until a signal arrives
    return 0;
}

This example shows the standard sigaction registration flow and demonstrates how to obtain sender context such as the PID.

Sending and Waiting for Signals Forms the Smallest Control Loop

kill(pid, sig) sends a signal to a target process, raise(sig) sends a signal to the current process itself, alarm(seconds) starts a timeout event, and pause() blocks until any signal arrives.

# List all signals supported by the current system
kill -l

# Send SIGTERM to the specified process
kill 1234

# Force-terminate the process
kill -9 1234

These commands form the most common terminal-side entry point for signal debugging.

Signal Blocking Protects Critical Sections Instead of Dropping Events

Blocking a signal delays its delivery rather than discarding it. A process can use sigset_t and sigprocmask() to temporarily block specific signals in a critical section, then restore the original mask after the critical operation completes.

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    sigset_t block_set, old_set;
    sigemptyset(&block_set);
    sigaddset(&block_set, SIGINT);              // Add SIGINT to the blocking set

    sigprocmask(SIG_BLOCK, &block_set, &old_set); // Start blocking SIGINT
    printf("5 秒内按 Ctrl+C 不会立即生效\n");
    sleep(5);
    sigprocmask(SIG_SETMASK, &old_set, NULL);   // Restore the old mask
    printf("已恢复 SIGINT 响应\n");
    return 0;
}

This example shows that blocking only moves SIGINT into the pending state, and the signal will still be handled after unblocking.

Reentrancy Safety Inside Signal Handlers Is the Most Dangerous Pitfall

Many beginners call printf, malloc, or exit inside a signal handler. That introduces severe nondeterminism. The reason is that a signal may interrupt a library function that has not yet completed, and re-entering the same resource can cause deadlocks, corrupt buffers, or trigger undefined behavior.

The safety rule is simple: keep the signal handler minimal. In most cases, it should only set a global flag. Real cleanup, logging, and business shutdown logic should return to the main loop.

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

volatile sig_atomic_t quit_flag = 0; // Use atomic semantics compatible with signal context

void handler(int sig) {
    (void)sig;                        // Ignore the unused parameter
    quit_flag = 1;                    // Only set the exit flag
}

int main() {
    signal(SIGINT, handler);          // Register the exit handler

    while (!quit_flag) {
        printf("Working...\n");      // Keep normal business logic in the main loop
        sleep(1);
    }

    printf("检测到退出标志,开始优雅退出。\n");
    return 0;
}

This example demonstrates the most strongly recommended pattern: the handler only updates a flag, and the main thread performs the actual shutdown.

The Async-Signal-Safe Whitelist Must Take Priority Over Coding Habits

In signal context, prefer async-signal-safe functions such as write(), read(), and _exit(). By contrast, printf, malloc, new, strtok, and exit should all be treated as high-risk blacklist items.

Typical Programming Patterns Solve Three Common System Problems Directly

The first pattern is graceful shutdown. A service process catches SIGTERM or SIGINT, sets a stop flag, and lets the main loop clean up sockets, file descriptors, and buffers.

The second pattern is timeout control. alarm() combined with a handler can enforce a maximum wait time for blocking operations and prevent requests from hanging forever.

#include <signal.h>
#include <unistd.h>

int main() {
    alarm(5);     // Set a 5-second timeout alarm
    pause();      // Wait for a signal to arrive
    return 0;
}

This example shows a minimal timeout waiting model.

The third pattern is child process reaping. A parent process listens for SIGCHLD and calls waitpid(-1, NULL, WNOHANG) in a loop inside the handler to avoid zombie process accumulation.

#include <signal.h>
#include <sys/wait.h>

void sigchld_handler(int sig) {
    (void)sig;                                  // Ignore the parameter
    while (waitpid(-1, NULL, WNOHANG) > 0) {    // Reap all exited child processes in a loop
    }
}

This code supports asynchronous child reaping and is a common standard pattern in multi-process servers.

When Debugging Signal Issues, Check the Mask, Pending Set, and EINTR First

Many cases where a program seems to ignore signals are actually caused by blocked signals. Many cases where a system call unexpectedly returns -1 are actually caused by interruption from a signal and an EINTR return. During troubleshooting, inspect SigBlk, SigIgn, SigCgt, and SigPnd in `/proc/

/status` first. “`bash # View the signal state of the target process cat /proc/ /status | grep Sig # Trace all signals received by the program strace -e signal=all ./your_program “` These two commands can quickly identify the blocking mask, handler behavior, and delivery trace. ## The Common Error Table Covers Most Signal Programming Failures | Symptom | Root Cause | Fix | |—|—|—| | Handler never runs | Wrong function signature or failed registration | Check the `signal` / `sigaction` call | | Process gets killed unexpectedly | Writing to a closed socket triggers `SIGPIPE` | Explicitly ignore `SIGPIPE` | | `printf` hangs or prints garbage | A non-reentrant function is called in the handler | Use `write` or the flag-only pattern instead | | `read/accept` returns -1 | A system call was interrupted by a signal | Retry after checking `errno == EINTR` | | Child process becomes a zombie | The parent process did not reap it in time | Catch `SIGCHLD` and call `waitpid` | ## FAQ ### Why did `kill` send a signal, but the process did not exit immediately? The target process may have blocked the signal, or it may have installed a custom handler for that signal. The kernel performs the corresponding action only when the signal is unblocked and the delivery conditions are met. ### Why is `printf` not recommended inside a signal handler? Because `printf` is not async-signal-safe. If a signal interrupts another `printf` that is already in progress, re-entering it can cause lock contention, buffer corruption, or deadlock. ### In production, should I prefer `signal()` or `sigaction()`? Prefer `sigaction()`. It is the POSIX-recommended interface, provides more stable semantics, and supports `sa_mask`, `sa_flags`, and `SA_SIGINFO`, which makes it better suited for maintainable system-level programs. Core Summary: This article systematically reconstructs the core knowledge of Linux signals, covering common signals, the signal lifecycle, the differences between `signal` and `sigaction`, signal blocking, reentrancy safety, and production-ready engineering patterns. It helps developers avoid frequent pitfalls such as `printf` deadlocks, `EINTR`, and zombie processes.