Signals and Signal Handling
Handle UNIX/Linux signals in C! Learn about SIGINT, SIGTERM, SIGSEGV, signal handlers, sigaction, and writing signal-safe code.
Track Your Progress
Sign in to save your learning progress
What You Will Learn
- ✓Understand what signals are
- ✓Handle common signals (SIGINT, SIGTERM)
- ✓Write signal handler functions
- ✓Use sigaction for robust handling
- ✓Know signal-safe programming rules
A Brief History of Signals
1970s - Unix Origins
Signals were introduced in early Unix as a simple inter-process communication mechanism. They were like "software interrupts" — a way for the kernel to tap a process on the shoulder.
The Problem: Unreliable Signals
Early signal() implementations had race conditions — the handler would reset after each call, and signals could be lost. This made robust programming difficult.
POSIX sigaction() - The Solution
POSIX introduced sigaction()to fix these issues. It provides reliable, portable signal handling with precise control over signal behavior.
Today
Signals remain essential for Unix/Linux programming. They're used for graceful shutdown, handling errors, timers, and communication between processes.
?Why Handle Signals?
Signals are how the operating system communicates with your program. When you press Ctrl+C, the OS sends a signal! Handling them lets you clean up resources, save state, or gracefully shutdown.
How Signals Work
Source
Ctrl+C, kill, alarm
Kernel
OS delivers signal
Process
Your program
Handler
Your code runs
What Happens When a Signal Arrives?
Your program is running normally...
INTERRUPT! Signal arrives — execution stops immediately
Signal handler runs (your custom code)
Program resumes from where it was interrupted (or exits)
SIGINT
Ctrl+C pressed
SIGTERM
kill command
SIGSEGV
Segfault
SIGALRM
Timer expired
Real-World Use Cases
01Common Signals
| Signal | Number | Description | Default Action |
|---|---|---|---|
| SIGINT | 2 | Interrupt (Ctrl+C) | Terminate |
| SIGTERM | 15 | Termination request | Terminate |
| SIGKILL | 9 | Force kill | Cannot catch! |
| SIGSEGV | 11 | Segmentation fault | Core dump |
| SIGALRM | 14 | Alarm clock | Terminate |
| SIGCHLD | 17 | Child terminated | Ignore |
| SIGUSR1/2 | 10/12 | User-defined | Terminate |
02Basic Signal Handling
Use signal() to register a handler function that runs when a signal is received. This is the simple (but limited) approach.
1#include <stdio.h>2#include <signal.h>3#include <stdlib.h>4#include <unistd.h>56// Signal handler function7void handle_sigint(int sig) {8 printf("\nCaught SIGINT (Ctrl+C)! Signal number: %d\n", sig);9 printf("Cleaning up and exiting...\n");10 exit(0);11}1213int main() {14 // Register handler for SIGINT15 signal(SIGINT, handle_sigint);16 17 printf("Running... Press Ctrl+C to interrupt\n");18 19 // Infinite loop20 while (1) {21 printf("Working...\n");22 sleep(2);23 }24 25 return 0;26}Output:
Working...
^C
Caught SIGINT (Ctrl+C)! Signal number: 2
Cleaning up and exiting...
03sigaction(): The Robust Way
sigaction() is more reliable than signal(). It gives you control over signal flags and doesn't reset the handler after each call.
🆚 signal() vs sigaction()
signal() — Old Way
✓sigaction() — Modern Way
The sigaction Structure
struct sigaction {
void (*sa_handler)(int); // Handler function
sigset_t sa_mask; // Signals to block during handler
int sa_flags; // Behavior flags
};
sa_handler
Your signal handler function
sa_mask
Block these while handling
sa_flags
SA_RESTART, SA_NODEFER...
1#include <stdio.h>2#include <signal.h>3#include <stdlib.h>4#include <unistd.h>5#include <string.h>67volatile sig_atomic_t running = 1;89void handle_signal(int sig) {10 if (sig == SIGINT) {11 printf("\nReceived SIGINT\n");12 } else if (sig == SIGTERM) {13 printf("\nReceived SIGTERM\n");14 }15 running = 0; // Signal main loop to stop16}1718int main() {19 struct sigaction sa;20 21 // Clear the struct22 memset(&sa, 0, sizeof(sa));23 24 // Set handler25 sa.sa_handler = handle_signal;26 27 // Block other signals during handler28 sigemptyset(&sa.sa_mask);29 30 // Flags: SA_RESTART to restart interrupted syscalls31 sa.sa_flags = SA_RESTART;32 33 // Register for multiple signals34 sigaction(SIGINT, &sa, NULL);35 sigaction(SIGTERM, &sa, NULL);36 37 printf("PID: %d - Send signals with: kill -INT %d\n", 38 getpid(), getpid());39 40 while (running) {41 printf("Working...\n");42 sleep(1);43 }44 45 printf("Graceful shutdown complete\n");46 return 0;47}Why sigaction()?
- Handler stays registered (no resetting)
- Can block other signals during handler
- More portable behavior across systems
- Can use
SA_RESTARTflag
04Signal-Safe Programming
Signal handlers run asynchronously — they can interrupt your code at any point! Only certain functions are safe to call in a handler.
The Danger: Signals Interrupt ANYWHERE
// malloc() is allocating memory...
ptr = malloc(100);
// heap data structures being updated
// SIGNAL ARRIVES HERE!
// heap is in inconsistent state!
void handler(int sig) {
malloc(50); // CORRUPTION!
}
If your handler calls malloc() while the main code is already inside malloc(), the heap gets corrupted!
✓The Safe Pattern: Flag + Main Loop
Handler
Just set a flag!
Main Loop
Check flag & handle
NOT Safe in Signal Handlers
printf()— Usewrite()insteadmalloc()/free()— Can corrupt heapexit()— Use_exit()instead- Most standard library functions
✓Async-Signal-Safe Functions
These are safe to call:
write, _exit, signal, kill, getpid, raise, abort1#include <signal.h>2#include <unistd.h>34volatile sig_atomic_t got_signal = 0;56// Safe signal handler - just set a flag7void safe_handler(int sig) {8 got_signal = 1;9 // Use write() instead of printf()10 const char msg[] = "Signal received!\n";11 write(STDOUT_FILENO, msg, sizeof(msg) - 1);12}1314int main() {15 signal(SIGINT, safe_handler);16 17 while (!got_signal) {18 // Do work19 sleep(1);20 }21 22 // Handle signal outside the handler23 // Here printf() is safe!24 printf("Cleaning up after signal...\n");25 26 return 0;27}sig_atomic_t
Use volatile sig_atomic_t for variables shared between signal handlers and main code. It guarantees atomic read/write.
05Ignoring and Blocking Signals
1#include <stdio.h>2#include <signal.h>3#include <unistd.h>45int main() {6 // Ignore SIGINT - Ctrl+C won't work!7 signal(SIGINT, SIG_IGN);8 printf("SIGINT is now ignored. Ctrl+C won't stop me!\n");9 10 // Restore default behavior11 // signal(SIGINT, SIG_DFL);12 13 // Block signals temporarily with sigprocmask14 sigset_t block_set, old_set;15 sigemptyset(&block_set);16 sigaddset(&block_set, SIGINT);17 sigaddset(&block_set, SIGTERM);18 19 // Block the signals20 sigprocmask(SIG_BLOCK, &block_set, &old_set);21 printf("Critical section - signals blocked\n");22 23 sleep(5); // Signals delayed, not lost24 25 // Unblock - pending signals delivered now26 sigprocmask(SIG_SETMASK, &old_set, NULL);27 printf("Signals unblocked\n");28 29 return 0;30}SIG_IGN
Completely ignore the signal. It's discarded.
sigprocmask
Block signals temporarily. They queue up and arrive when unblocked.
06Sending Signals
1#include <stdio.h>2#include <signal.h>3#include <unistd.h>4#include <sys/wait.h>56int main() {7 pid_t pid = fork();8 9 if (pid == 0) {10 // Child process11 printf("Child (PID %d) waiting for signal...\n", getpid());12 pause(); // Wait for any signal13 printf("Child received signal!\n");14 return 0;15 }16 17 // Parent process18 sleep(2);19 20 // Send signal to child21 printf("Parent sending SIGUSR1 to child %d\n", pid);22 kill(pid, SIGUSR1);23 24 // Wait for child25 wait(NULL);26 27 // Send signal to self28 printf("Sending SIGTERM to self\n");29 raise(SIGTERM); // Same as kill(getpid(), SIGTERM)30 31 return 0;32}07Common Signal Patterns
Graceful Shutdown
Handle SIGINT/SIGTERM to clean up before exit.
// In handler:
running = 0;
// In main:
while (running) { work(); }
cleanup_and_exit();
Config Reload (SIGHUP)
Daemons use SIGHUP to reload configuration.
// In handler:
reload_config = 1;
// In main:
if (reload_config) {
load_config_file();
}
Timeout (SIGALRM)
Cancel slow operations with alarm().
alarm(30); // 30 second timeout
slow_network_call();
alarm(0); // Cancel if completed
Child Reaping (SIGCHLD)
Clean up zombie child processes.
// In handler:
while (waitpid(-1, NULL,
WNOHANG) > 0);
!Code Pitfalls: Common Mistakes & What to Watch For
Common Mistakes with Signal Handling
Copying code often generate signal handling code that looks correct but is dangerous:
- ✗Using printf() in handlers: Beginners often call non-async-signal-safe functions, causing deadlocks or corruption
- ✗Using signal() instead of sigaction(): Code often uses the simpler but unreliable signal() function
- ✗Missing volatile keyword: Beginners often forget volatile sig_atomic_t for handler-to-main communication flags
- ✗Complex logic in handlers: Beginners often put too much work in handlers instead of just setting a flag
Always Understand Before Using
Signal handlers run asynchronously and can interrupt any code at any point. Keep handlers minimal: set a flag, let main handle the work. Check the man page for async-signal-safe functions — anything else is dangerous.
09Frequently Asked Questions
Q:What's the difference between signal() and sigaction()?
A: sigaction() is portable and reliable — handler stays installed after use. signal() behavior varies by system and may reset after each signal. Always use sigaction() for production code.
Q:Why can't I call printf() in a signal handler?
A: Signal handlers can interrupt code anywhere, including inside printf() which uses locks. Calling printf() in the handler causes deadlock or corruption. Only "async-signal-safe" functions are allowed — see the man page for the full list.
Q:What is volatile sig_atomic_t?
A: volatile tells the compiler not to optimize away reads — the variable can change unexpectedly.sig_atomic_t is a type guaranteed to be read/written atomically. Together, they're the only safe way to communicate between handler and main code.
Q:Can I catch SIGKILL or SIGSTOP?
A: No. These signals cannot be caught, blocked, or ignored — by design. SIGKILL (kill -9) always terminates, SIGSTOP always pauses. This ensures the system can always control runaway processes.
Q:How do I send a signal from my program?
A: Use kill(pid, SIGTERM) to send a signal to another process. Use raise(SIGTERM) to send to yourself. Use pthread_kill() to send to a specific thread.
09Summary
Complete Signal Handling Workflow
sigaction()
Register handler
Signal arrives
Execution stops
flag = 1
Set flag safely
Main checks
Handle in main
Key Functions
sigaction()— Reliable handler setupkill(pid, sig)— Send signalraise(sig)— Signal to selfsigprocmask()— Block signals
✓Best Practices
- Use
volatile sig_atomic_tfor flags - Only async-signal-safe in handlers
- Set flag in handler, handle in main
- Use sigaction(), not signal()
Quick Reference: Most Used Signals
SIGINT (2)
Ctrl+C
SIGTERM (15)
kill command
SIGKILL (9)
Force kill
SIGHUP (1)
Reload config
Remember: SIGKILL and SIGSTOP cannot be caught — always handle SIGTERM for graceful shutdown!
Test Your Knowledge
Related Tutorials
UNIX/Linux System Calls
Interact with the operating system directly! Learn open, read, write, fork, exec, and other system calls that give C its power on UNIX/Linux.
Multithreading in C
Learn parallel programming in C! Master pthreads (POSIX threads), C11 threads, mutex locks, condition variables, and thread synchronization.
Environment Variables in C
Read and modify environment variables in C! Learn getenv(), setenv(), unsetenv(), environ array, and how to configure program behavior via environment.
Have Feedback?
Found something missing or have ideas to improve this tutorial? Let us know on GitHub!