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
- First, memorize common signals and their default behaviors.
- Then, understand the signal lifecycle from generation to delivery.
- Next, master
signal,kill,alarm, andsigaction. - 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/