In Linux and UNIX systems, Signals are asynchronous notifications sent by the Linux kernel to a process to inform it that a specific system or hardware event has occurred. Signals interrupt the normal linear execution flow of a process, forcing it to immediately address the event. Understanding signals is critical for developing robust system daemons, network servers, and fault-tolerant C applications.
Figure 1: The 3 stages of a Linux signal: Generation, Kernel Pending Queue, and Process Disposition.
Linux defines over 30 standard POSIX signals in the <signal.h> header file. The table below lists the most vital signals every systems programmer must know:
Signal Name |
Number |
Trigger / Origin |
Default Action |
|---|---|---|---|
|
1 |
Hangup detected on controlling terminal or death of controlling process. Daemons use it to reload config files. |
Terminate |
|
2 |
Terminal interrupt generated when user presses Ctrl + C. |
Terminate |
|
3 |
Terminal quit generated when user presses Ctrl + \. |
Core Dump |
|
8 |
Erroneous arithmetic operation (e.g., integer division by zero). |
Core Dump |
|
9 |
Unconditional process kill. CANNOT BE CAUGHT OR IGNORED! |
Forced Kill |
|
11 |
Invalid memory reference / Segmentation fault (dereferencing NULL pointer). |
Core Dump |
|
13 |
Broken pipe: write to pipe or socket with no readers reading from it. |
Terminate |
|
14 |
Timer signal generated by |
Terminate |
|
15 |
Standard graceful termination request sent by |
Terminate |
|
17 |
Sent to parent process when a child process terminates or stops. |
Ignore |
|
19 |
Stop process execution. CANNOT BE CAUGHT OR IGNORED! |
Pause Process |
When a signal arrives at a process, it can respond in one of three ways:
SIG_DFL): The process executes the kernel's default behavior (usually terminating the process or creating a memory core dump).SIG_IGN): The process discards the signal completely. The program continues execution unaffected. (Note: SIGKILL and SIGSTOP cannot be ignored!).signal()The traditional C library function for registering a signal handler has the prototype:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
SIGINT (Ctrl+C) and SIGALRM#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
// Custom signal handler function
void handle_sigint(int sig) {
printf("\n[CAUGHT SIGNAL %d] Ctrl+C intercepted! Cleaning up resources...\n", sig);
}
void handle_alarm(int sig) {
printf("[ALARM TIMER] 3 seconds elapsed! Waking up background worker...\n");
}
int main() {
// 1. Register handler for SIGINT (Ctrl+C)
signal(SIGINT, handle_sigint);
// 2. Register handler for SIGALRM
signal(SIGALRM, handle_alarm);
// Schedule an alarm signal in 3 seconds
alarm(3);
printf("Process PID: %d running. Press Ctrl+C or wait 3s for alarm.\n", getpid());
// Loop indefinitely to demonstrate signal interruptions
for (int i = 1; i <= 6; i++) {
printf("Working step %d/6...\n", i);
sleep(1);
}
printf("Program finished gracefully.\n");
return 0;
}
sigaction()In professional production software and modern Linux systems, sigaction() is strongly preferred over signal(). The historical signal() function resets the signal handler back to SIG_DFL on older systems after one execution, causing potential race conditions. sigaction() avoids this by providing persistent handlers and explicit signal masking.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
volatile sig_atomic_t keep_running = 1;
void graceful_shutdown(int sig) {
// sig_atomic_t ensures atomic write without lock corruption
keep_running = 0;
}
int main() {
struct sigaction sa;
// Clear structure
sa.sa_handler = graceful_shutdown;
sigemptyset(&sa.sa_mask); // Block no extra signals during handler
sa.sa_flags = 0; // Standard options
// Register sigaction for SIGINT (Ctrl+C) and SIGTERM (kill command)
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("Error registering SIGINT");
return 1;
}
if (sigaction(SIGTERM, &sa, NULL) == -1) {
perror("Error registering SIGTERM");
return 1;
}
printf("Server listening (PID: %d). Send SIGTERM or press Ctrl+C to stop.\n", getpid());
while (keep_running) {
printf("Server processing requests...\n");
sleep(2);
}
printf("\nGraceful shutdown sequence complete. Exiting safely.\n");
return 0;
}
When writing signal handlers, you must be careful: not all C library functions are safe to call inside a signal handler! Standard I/O functions like printf(), malloc(), and free() use internal lock mutexes. If a signal arrives while printf is already executing, calling printf again inside the handler will cause a permanent application deadlock!
write(), _exit(), read(), close(), kill(), signal().printf(), sprintf(), malloc(), free(), exit().Problem: Write a C program where a child process waits for a custom user signal (SIGUSR1) from its parent process before proceeding with execution.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
volatile sig_atomic_t signal_received = 0;
void user_signal_handler(int sig) {
signal_received = 1;
}
int main() {
pid_t pid;
// Register custom signal handler
signal(SIGUSR1, user_signal_handler);
pid = fork();
if (pid < 0) {
perror("Fork failed");
return 1;
}
if (pid == 0) {
// --- CHILD PROCESS ---
printf("[CHILD PID: %d] Waiting for authorization signal from parent...\n", getpid());
while (!signal_received) {
pause(); // Suspend process until any signal is received
}
printf("[CHILD] Received SIGUSR1 authorization! Commencing critical task.\n");
exit(0);
} else {
// --- PARENT PROCESS ---
printf("[PARENT PID: %d] Preparing data (waiting 2 seconds)...\n", getpid());
sleep(2);
printf("[PARENT] Sending SIGUSR1 to Child PID %d...\n", pid);
kill(pid, SIGUSR1); // Send signal to child
wait(NULL); // Wait for child to finish
printf("[PARENT] Child finished. Parent exiting.\n");
}
return 0;
}
SIGINT (2) is generated interactively from terminal by Ctrl+C. SIGTERM (15) is the default software termination request sent by kill <pid>, allowing the program to flush files and release ports. SIGKILL (9) directly kills the process via the kernel without allowing the program to catch, intercept, or clean up.
SIGKILL (signal 9) and SIGSTOP (signal 19) cannot be caught, blocked, or ignored. This guarantees that system administrators can always stop or terminate rogue processes.
sig_atomic_t is an integer data type guaranteed to be accessed atomically (as a single instruction) by the CPU architecture, ensuring that reads and writes cannot be interrupted midway by incoming signals.
printf() is non-reentrant because it locks an internal standard output mutex. If interrupted by a signal while executing printf(), attempting to run printf() inside the handler results in deadlock. Use the low-level write() system call instead.
Implement multi-threaded concurrency and mutex locking in Class 29: POSIX Threads (pthreads).
Continue to Class 29: POSIX Threads (pthreads) → Previous: ← IPC | Full Syllabus