Signals

Table of contents
- The Foundation: What Are Signals?
- The Historical Context That Shapes Everything
- The Complete Signal Architecture
- How Signal Delivery Actually Works
- The Three Ways Processes Handle Signals
- Understanding Signal Numbers and Names
- The Most Important Signals You'll Encounter
- Real-World Signal Usage Patterns
- The Programming Perspective
- Debugging and Observing Signals
- Hands-on
The Foundation: What Are Signals?
At the most basic level, a signal is a software interrupt. Think of an interrupt as someone tapping you on the shoulder while you're reading - it forces you to stop what you're doing and pay attention to something else. When a process receives a signal, the operating system interrupts whatever that process was doing and instructs it to "Handle this message right now."
The Historical Context That Shapes Everything
To understand why signals work the way they do, we need to step back to the early days of Unix in the 1970s. Back then, computers were shared by many users simultaneously, and the operating system needed simple, efficient ways to manage processes. The designers created signals as a lightweight communication mechanism that could work even when processes were in trouble or consuming too many resources.
This historical context explains some quirks you'll encounter. For example, why can't you catch or ignore SIGKILL? Because the original Unix designers knew that sometimes a process becomes so problematic that it needs to be terminated immediately, regardless of what it wants to do.
The Complete Signal Architecture
Linux defines exactly 64 signals, but they're not all created equal. Let me help you understand the structure:
Standard Signals (1-31): These are the original Unix signals that have been around for decades. Each one has a specific, well-defined purpose. They're reliable, portable across different Unix-like systems, and form the core of process communication.
Real-Time Signals (32-64): These were added later to address limitations in the original signal system. Unlike standard signals, real-time signals can be queued (meaning you won't lose them if multiple instances arrive quickly), and they can carry additional data beyond just "this signal happened."
How Signal Delivery Actually Works
When you understand the mechanics of signal delivery, the whole system becomes much clearer. Let's trace through what happens when one process sends a signal to another.
First, the sending process (or the kernel itself) identifies the target process and calls a system function to deliver the signal. The kernel then marks that signal as "pending" for the target process. This is crucial to understand: the signal doesn't interrupt the target process immediately. Instead, it's like placing a note in the process's mailbox.
The actual interruption happens the next time the target process transitions from kernel mode back to user mode. This might sound technical, but think of it this way: every time your process makes a system call (like reading a file or allocating memory), it briefly enters kernel mode to let the operating system handle the request. When that system call finishes and control returns to your process, the kernel checks for pending signals and delivers them.
This delay mechanism serves an important purpose. It ensures that signals are delivered at safe points in the process's execution, when the process state is consistent and can be safely interrupted.
The Three Ways Processes Handle Signals
Every signal can be handled in exactly three ways, and understanding these options is key to mastering signal behavior:
Default Action: If a process doesn't specify how to handle a signal, the kernel performs a predefined default action. For SIGTERM
, the default is to terminate the process gracefully. For SIGSTOP
, it's to suspend the process. For SIGCHLD
(sent when a child process terminates), the default is to ignore it.
Ignore the Signal: A process can tell the kernel, "If this signal arrives, just throw it away." This works for most signals, but not for SIGKILL
or SIGSTOP
. The system designers made these two signals uncatchable because they represent the kernel's ultimate authority over processes.
Custom Handler: This is where signals become really powerful. A process can register a function to run whenever a specific signal arrives. When the signal is delivered, the kernel temporarily suspends the process's normal execution, runs the handler function, and then resumes normal execution where it left off.
Understanding Signal Numbers and Names
Each signal has both a number and a symbolic name, and both matter for different reasons. The numbers (like 9 for SIGKILL
) are what the kernel actually uses internally. The symbolic names (like SIGKILL) are human-readable identifiers that make code more understandable.
Here's something important to grasp: while most signal numbers are standardized across Unix-like systems, there can be slight variations. This is why good programs always use the symbolic names rather than hard-coding numbers. When you write kill -TERM 1234
instead of kill -15 1234
, your code becomes more portable and readable.
The Most Important Signals You'll Encounter
Let me walk you through the signals you'll interact with most frequently, explaining not just what they do but when and why you'd use them:
SIGINT (Signal 2): This is what gets sent when you press Ctrl+C. It's designed to be a polite interruption, giving the process a chance to clean up before exiting. Many interactive programs catch this signal to save work or ask if you really want to quit.
SIGTERM (Signal 15): This is the "please terminate" signal. It's what most system shutdown procedures use to ask processes to exit gracefully by pressing Ctrl + Z. Well-behaved programs catch this signal, finish critical operations, save data, and then exit cleanly.
SIGKILL (Signal 9): This is the nuclear option. When you send SIGKILL, you're telling the kernel, "remove this process immediately, regardless of what it's doing." The process gets no opportunity to clean up, save data, or respond in any way. Use this only when SIGTERM
fails.
SIGSTOP and SIGCONT (Signals 19 and 18): These control process execution. SIGSTOP
immediately pauses a process (like hitting pause on a video), while SIGCONT
resumes it. Neither can be caught or ignored, giving the system reliable process control.
SIGCHLD (Signal 17): This gets sent to a parent process when one of its child processes terminates or stops. This mechanism allows parent processes to keep track of their children without constantly checking their status.
Those were the ones you would see often in the shell, but there are many more like SIGSEGV
(segmentation violation) a signal sent to a process when it attempts to access memory that it's not allowed to access. This typically happens when your program tries to dereference a null pointer, access memory outside allocated bounds, or write to read-only memory.
The default action for SIGSEGV
is to terminate the process and generate a core dump (if core dumps are enabled on your system). This abrupt termination helps prevent memory corruption from spreading, but obviously stops your program immediately.
You can absolutely alter this behavior using signal handlers.
Real-World Signal Usage Patterns
When you run a command in your shell and then press Ctrl+C, you're sending SIGINT to the foreground process group. If that process has children, they typically receive the signal too. This is why pressing Ctrl+C often stops an entire pipeline of commands.
Shell job control uses SIGSTOP and SIGCONT extensively. When you press Ctrl+Z to suspend a job, the shell sends SIGSTOP. When you type fg
to resume it, the shell sends SIGCONT. This allows you to juggle multiple tasks in a single terminal session.
The Programming Perspective
If you're interested in how programs actually work with signals, the system provides several functions that let processes interact with the signal system. The signal()
function allows a process to register a handler for a specific signal. The kill()
function (despite its name) can send any signal to any process you have permission to signal.
Here's what makes signal programming tricky: signal handlers run asynchronously, meaning they can interrupt your program at almost any point. This creates potential race conditions and requires careful programming to avoid problems. Modern programs often use more sophisticated signal handling mechanisms like sigaction()
which provides better control over signal behavior.
you can list all the signals available on your system using
kill -l
also the man pages include an overview about signals that has tables that contain the default action for every signal and more.
man 7 signal
Debugging and Observing Signals
You can observe signal behavior in real-time using various tools. The strace
command shows you every system call a process makes, including signal-related calls. Running strace -e signal kill -TERM 1234
would show you the exact system calls involved in sending a SIGTERM signal.
The /proc
filesystem also provides signal information. Looking at /proc/[pid]/status
shows you which signals are pending, blocked, or being caught by a specific process.
this is the output related to the signals for the bash process running on my machine
SigQ: 1/31206
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000010000
SigIgn: 0000000000384004
SigCgt: 000000004b813efb
of course, this does not only show you the signals information, I only showed the info related to the signals here
maybe I’ll explore the other parts in another article
SigQ: 1/31206
Meaning:
The process has 1 queued signal (likely a pending real-time signal).
It can have up to 31,206 queued signals in total.
Format:
queued_signals / max_queued_signals
.
SigPnd: 0000000000000000
Meaning:
- No pending signals for this process (signals sent but not yet delivered because the process is blocked or handling others).
ShdPnd: 0000000000000000
Meaning:
- No shared (thread group) pending signals—applies to multithreaded programs. Since
bash
is single-threaded; this is usually 0.
- No shared (thread group) pending signals—applies to multithreaded programs. Since
SigBlk: 0000000000010000
Meaning:
Bitmask of blocked signals.
0x0000000000010000
means signal 17 (SIGCHLD) is currently blocked by the process.SIGCHLD
is sent to a parent when a child process terminates.
SigIgn: 0000000000384004
Meaning:
Bitmask of ignored signals.
This value shows which signals the process has chosen to ignore.
Common for
bash
to ignoreSIGPIPE
,SIGXFSZ
,SIGTTIN
, etc.
SigCgt: 000000004b813efb
Meaning:
Bitmask of signals with user-defined handlers.
These signals will invoke custom behavior instead of the default.
Hands-on
Try writing a box that gets resized in the terminal when resizing the terminal window
This is a challenge I saw on CSPrimer, it’s an amazing website. You should check it out for sure.
My solution
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/ioctl.h>
void handle_winch(int sig) {
printf("\033[H\033[J");
}
void draw_unicode_box(int width, int height) {
// Unicode box-drawing characters
const char* horizontal = "─"; // Horizontal line
const char* vertical = "│"; // Vertical line
const char* top_left = "┌"; // Top-left corner
const char* top_right = "┐"; // Top-right corner
const char* bottom_left = "└"; // Bottom-left corner
const char* bottom_right = "┘"; // Bottom-right corner
for (int j = 0; j < height / 2; j++) {
printf("\n");
}
for (int j = 0; j < width / 2; j++) {
printf(" ");
}
// Draw the top border
printf("%s", top_left);
for (int i = 0; i < width - 2; i++) {
printf("%s", horizontal);
}
printf("%s\n", top_right);
// Draw the middle rows
for (int i = 0; i < height - 2; i++) {
for (int j = 0; j < width / 2; j++) {
printf(" ");
}
printf("%s", vertical); // Left border
// Fill the middle with spaces
for (int j = 0; j < width - 2; j++) {
printf(" ");
}
printf("%s\n", vertical); // Right border
}
for (int j = 0; j < width / 2; j++) {
printf(" ");
}
// Draw the bottom border
printf("%s", bottom_left);
for (int i = 0; i < width - 2; i++) {
printf("%s", horizontal);
}
printf("%s\n", bottom_right);
}
int main() {
signal(SIGWINCH, handle_winch);
struct winsize w;
do {
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == -1) {
perror("ioctl");
return 1;
}
draw_unicode_box(w.ws_col / 2, w.ws_row / 2);
pause(); // Wait for signals
} while (1);
return 0;
}
Subscribe to my newsletter
Read articles from AGX directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
