Signals are a fundamental inter-process communication mechanism in Unix-like operating systems, providing a way to handle asynchronous events. From keyboard interrupts (Ctrl+C) to segmentation faults, signals are everywhere in systems programming. This comprehensive guide covers everything from basic signal concepts to advanced signal handling techniques in C.
What are Signals?
Signals are software interrupts delivered to a process, notifying it that an event has occurred. They can be generated by:
- The kernel (hardware exceptions, timers)
- Other processes (using
kill()system call) - The process itself (using
raise()) - Terminal interactions (Ctrl+C, Ctrl+Z)
Process A Process B | | | -------- kill(SIGUSR1) --> | | | | |--> Signal Handler | | | | |<-----+ | | | <------ Signal Handled ----|
Common Signals
#include <signal.h> // Terminal signals #define SIGHUP 1 // Hangup (terminal closed) #define SIGINT 2 // Interrupt (Ctrl+C) #define SIGQUIT 3 // Quit (Ctrl+\) #define SIGTSTP 20 // Terminal stop (Ctrl+Z) // Program errors #define SIGILL 4 // Illegal instruction #define SIGABRT 6 // Abort (abort() call) #define SIGFPE 8 // Floating point exception #define SIGSEGV 11 // Segmentation fault #define SIGPIPE 13 // Broken pipe // Process control #define SIGKILL 9 // Kill (cannot be caught/ignored) #define SIGTERM 15 // Termination (default kill) #define SIGSTOP 19 // Stop (cannot be caught/ignored) #define SIGCONT 18 // Continue if stopped // User-defined #define SIGUSR1 10 // User-defined signal 1 #define SIGUSR2 12 // User-defined signal 2 // Timers #define SIGALRM 14 // Alarm clock #define SIGVTALRM 26 // Virtual timer #define SIGPROF 27 // Profiling timer
Basic Signal Handling
1. Using signal() Function (Simplified)
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// Signal handler function
void signal_handler(int signum) {
printf("Received signal %d\n", signum);
// Reset handler (System V semantics)
signal(signum, signal_handler);
}
int main() {
// Register signal handlers
signal(SIGINT, signal_handler); // Ctrl+C
signal(SIGTERM, signal_handler); // kill command
signal(SIGUSR1, signal_handler); // User signal 1
printf("PID: %d\n", getpid());
printf("Press Ctrl+C to trigger SIGINT\n");
// Infinite loop
while (1) {
printf("Working...\n");
sleep(2);
}
return 0;
}
2. Using sigaction() (Recommended)
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
// Signal handler
void handle_signal(int signum, siginfo_t *info, void *context) {
printf("\n=== Signal Handler ===\n");
printf("Signal: %d\n", signum);
printf("From process: %d\n", info->si_pid);
printf("From user: %d\n", info->si_uid);
printf("Signal code: %d\n", info->si_code);
// Additional info based on signal type
if (signum == SIGFPE) {
printf("FPE code: %d\n", info->si_code);
} else if (signum == SIGILL) {
printf("ILL code: %d\n", info->si_code);
} else if (signum == SIGSEGV) {
printf("Fault address: %p\n", info->si_addr);
}
}
int main() {
struct sigaction sa;
// Clear structure
memset(&sa, 0, sizeof(sa));
// Set handler with extended info
sa.sa_sigaction = handle_signal;
sa.sa_flags = SA_SIGINFO; // Use extended handler
// Block other signals during handler execution
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGINT);
sigaddset(&sa.sa_mask, SIGTERM);
// Register for various signals
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGUSR1, &sa, NULL);
printf("PID: %d\n", getpid());
printf("Send signals with: kill -USR1 %d\n", getpid());
while (1) {
pause(); // Wait for signals
}
return 0;
}
Signal Sets and Masks
#include <signal.h>
#include <stdio.h>
void demonstrate_signal_sets() {
sigset_t set;
sigset_t oldset;
// Initialize empty set
sigemptyset(&set);
printf("Empty set initialized\n");
// Add signals to set
sigaddset(&set, SIGINT);
sigaddset(&set, SIGTERM);
sigaddset(&set, SIGUSR1);
// Check if signals are in set
if (sigismember(&set, SIGINT)) {
printf("SIGINT is in set\n");
}
if (sigismember(&set, SIGKILL)) {
printf("SIGKILL is in set (this won't print)\n");
}
// Fill set with all signals
sigfillset(&set);
printf("Set contains all signals\n");
// Remove signals
sigdelset(&set, SIGINT);
sigdelset(&set, SIGTERM);
// Block signals in set
sigprocmask(SIG_BLOCK, &set, &oldset);
printf("Signals blocked\n");
// Check pending signals
sigset_t pending;
sigpending(&pending);
if (sigismember(&pending, SIGINT)) {
printf("SIGINT is pending\n");
}
// Restore old mask
sigprocmask(SIG_SETMASK, &oldset, NULL);
printf("Original mask restored\n");
}
Signal Blocking and Unblocking
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
volatile sig_atomic_t signal_received = 0;
void handler(int signum) {
signal_received = 1;
}
void block_demonstration() {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGUSR1, &sa, NULL);
sigset_t new_mask, old_mask, pending;
// Block SIGUSR1
sigemptyset(&new_mask);
sigaddset(&new_mask, SIGUSR1);
printf("Blocking SIGUSR1...\n");
sigprocmask(SIG_BLOCK, &new_mask, &old_mask);
// Send signal to ourselves
printf("Sending SIGUSR1...\n");
kill(getpid(), SIGUSR1);
// Check if it's pending
sigpending(&pending);
if (sigismember(&pending, SIGUSR1)) {
printf("SIGUSR1 is pending\n");
}
printf("Signal received flag: %d\n", signal_received);
// Unblock signal
printf("Unblocking SIGUSR1...\n");
sigprocmask(SIG_SETMASK, &old_mask, NULL);
// Signal should now be delivered
printf("Signal received flag: %d\n", signal_received);
// Wait a moment for handler to execute
sleep(1);
}
int main() {
block_demonstration();
return 0;
}
Reliable Signal Handling Patterns
1. Async-Signal-Safe Functions
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
// Async-signal-safe functions:
// write(), read(), open(), close(), fork(), exec(), exit()
// wait(), sigaction(), sigprocmask(), sigpending()
// kill(), raise(), alarm(), pause(), sleep()
// NOT async-signal-safe:
// printf(), malloc(), free(), mutex locks, most stdio functions
// Safe handler
void safe_handler(int signum) {
const char msg[] = "Signal received\n";
write(STDERR_FILENO, msg, sizeof(msg) - 1);
// Can set volatile sig_atomic_t variables
static volatile sig_atomic_t counter = 0;
counter++;
}
// Unsafe handler (don't do this!)
void unsafe_handler(int signum) {
printf("Signal received\n"); // Unsafe!
malloc(100); // Unsafe!
}
2. Self-Pipe Trick
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
int signal_pipe[2];
void pipe_handler(int signum) {
int saved_errno = errno;
write(signal_pipe[1], "x", 1); // Write to pipe
errno = saved_errno;
}
void setup_signal_pipe() {
// Create pipe
if (pipe(signal_pipe) == -1) {
perror("pipe");
return;
}
// Make read end non-blocking
int flags = fcntl(signal_pipe[0], F_GETFL);
fcntl(signal_pipe[0], F_SETFL, flags | O_NONBLOCK);
// Set up signal handler
struct sigaction sa;
sa.sa_handler = pipe_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGUSR1, &sa, NULL);
printf("Signal pipe ready. Send SIGUSR1 to %d\n", getpid());
}
void process_signals_from_pipe() {
char buffer[256];
int n = read(signal_pipe[0], buffer, sizeof(buffer));
if (n > 0) {
printf("Processed %d signals from pipe\n", n);
// Handle signals here (safe to call any function)
}
}
int main() {
setup_signal_pipe();
while (1) {
// Use select/poll to wait for both pipe and other events
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(signal_pipe[0], &readfds);
struct timeval tv = {1, 0}; // 1 second timeout
int ret = select(signal_pipe[0] + 1, &readfds, NULL, NULL, &tv);
if (ret > 0 && FD_ISSET(signal_pipe[0], &readfds)) {
process_signals_from_pipe();
}
printf("Main loop working...\n");
}
return 0;
}
3. Signal Queues with sigqueue()
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
// Handler for queued signals
void queue_handler(int signum, siginfo_t *info, void *context) {
printf("Received signal %d with value: %d (from process %d)\n",
signum, info->si_value.sival_int, info->si_pid);
}
int main() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = queue_handler;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sigaction(SIGUSR1, &sa, NULL);
printf("Receiver PID: %d\n", getpid());
// Queue multiple signals with data
union sigval value;
for (int i = 0; i < 5; i++) {
value.sival_int = i * 100;
if (sigqueue(getpid(), SIGUSR1, value) == -1) {
perror("sigqueue");
}
sleep(1);
}
// Wait for signals
pause();
return 0;
}
Handling Specific Signals
1. SIGINT (Ctrl+C) Graceful Shutdown
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
volatile sig_atomic_t shutdown_flag = 0;
void shutdown_handler(int signum) {
shutdown_flag = 1;
}
int main() {
struct sigaction sa;
sa.sa_handler = shutdown_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
printf("Running. Press Ctrl+C to stop.\n");
while (!shutdown_flag) {
printf("Working...\n");
sleep(2);
}
printf("\nCleaning up...\n");
// Perform cleanup here
printf("Goodbye!\n");
return 0;
}
2. SIGALRM and Timers
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void alarm_handler(int signum) {
static int count = 0;
printf("Alarm %d triggered\n", ++count);
}
int main() {
signal(SIGALRM, alarm_handler);
printf("Setting one-shot alarm for 3 seconds\n");
alarm(3);
sleep(4);
printf("Setting interval timer\n");
struct itimerval timer;
timer.it_value.tv_sec = 1; // First alarm in 1 second
timer.it_value.tv_usec = 0;
timer.it_interval.tv_sec = 2; // Repeat every 2 seconds
timer.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL, &timer, NULL);
sleep(10);
// Cancel timer
timer.it_value.tv_sec = 0;
setitimer(ITIMER_REAL, &timer, NULL);
return 0;
}
3. SIGCHLD - Child Process Management
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
void child_handler(int signum) {
int status;
pid_t pid;
// Wait for all terminated children (non-blocking)
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status)) {
printf("Child %d exited with status %d\n",
pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("Child %d killed by signal %d\n",
pid, WTERMSIG(status));
}
}
}
int main() {
struct sigaction sa;
sa.sa_handler = child_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);
// Create child processes
for (int i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid == 0) {
// Child process
printf("Child %d starting\n", getpid());
sleep(i + 1);
exit(i * 10);
}
}
// Parent continues
printf("Parent waiting...\n");
pause(); // Wait for signals
return 0;
}
4. SIGSEGV - Segmentation Fault Handling
#include <signal.h>
#include <stdio.h>
#include <setjmp.h>
#include <stdlib.h>
jmp_buf segv_jump;
void segv_handler(int signum, siginfo_t *info, void *context) {
printf("Segmentation fault at address %p\n", info->si_addr);
printf("Attempting recovery...\n");
longjmp(segv_jump, 1);
}
void risky_operation() {
if (setjmp(segv_jump) == 0) {
// Try risky operation
int *p = NULL;
*p = 42; // This will segfault
} else {
// Recovery code
printf("Recovered from segfault\n");
}
}
int main() {
struct sigaction sa;
sa.sa_sigaction = segv_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO;
sigaction(SIGSEGV, &sa, NULL);
printf("Attempting risky operation...\n");
risky_operation();
printf("Program continues after fault\n");
return 0;
}
5. SIGPIPE - Broken Pipe Handling
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
void pipe_handler(int signum) {
static int count = 0;
printf("Broken pipe detected (%d)\n", ++count);
}
int main() {
// Option 1: Ignore SIGPIPE
signal(SIGPIPE, SIG_IGN);
// Option 2: Handle SIGPIPE
// signal(SIGPIPE, pipe_handler);
// Create socket pair
int sv[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sv);
close(sv[0]); // Close one end
// Writing to closed socket will generate SIGPIPE
ssize_t written = write(sv[1], "test", 4);
if (written == -1) {
perror("write");
// EPIPE error when SIGPIPE is ignored/handled
}
close(sv[1]);
return 0;
}
Advanced Signal Techniques
1. Signal Stack (sigaltstack)
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
// Signal handler using alternate stack
void stack_handler(int signum) {
char buffer[1024];
printf("Handler running on alternate stack\n");
printf("Handler stack address: %p\n", buffer);
}
int main() {
// Allocate alternate stack
stack_t ss;
ss.ss_sp = malloc(SIGSTKSZ);
ss.ss_size = SIGSTKSZ;
ss.ss_flags = 0;
if (sigaltstack(&ss, NULL) == -1) {
perror("sigaltstack");
exit(1);
}
// Set up handler to use alternate stack
struct sigaction sa;
sa.sa_handler = stack_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_ONSTACK; // Use alternate stack
sigaction(SIGUSR1, &sa, NULL);
printf("Main stack address: %p\n", main);
printf("Send SIGUSR1 to %d\n", getpid());
pause();
free(ss.ss_sp);
return 0;
}
2. Signal Interruption and Restart
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
void handler(int signum) {
write(STDERR_FILENO, "Signal received\n", 16);
}
int main() {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
// Without SA_RESTART
sa.sa_flags = 0;
sigaction(SIGUSR1, &sa, NULL);
printf("Without restart (sleep will be interrupted):\n");
int remaining = sleep(10);
if (remaining > 0) {
printf("Sleep interrupted, %d seconds remaining\n", remaining);
}
// With SA_RESTART
sa.sa_flags = SA_RESTART;
sigaction(SIGUSR1, &sa, NULL);
printf("\nWith restart (sleep will auto-restart):\n");
remaining = sleep(10);
printf("Sleep completed, remaining: %d\n", remaining);
return 0;
}
3. Synchronous Signal Handling
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
void demonstrate_sigwait() {
sigset_t set;
int sig;
// Block SIGUSR1
sigemptyset(&set);
sigaddset(&set, SIGUSR1);
sigprocmask(SIG_BLOCK, &set, NULL);
printf("Waiting for SIGUSR1 (PID: %d)\n", getpid());
// Synchronously wait for signal
if (sigwait(&set, &sig) == 0) {
printf("Received signal %d via sigwait\n", sig);
}
}
void demonstrate_sigtimedwait() {
sigset_t set;
int sig;
siginfo_t info;
struct timespec timeout = {5, 0}; // 5 seconds
sigemptyset(&set);
sigaddset(&set, SIGUSR1);
sigprocmask(SIG_BLOCK, &set, NULL);
printf("Waiting for SIGUSR1 with timeout (5s)...\n");
int ret = sigtimedwait(&set, &info, &timeout);
if (ret == SIGUSR1) {
printf("Got signal from process %d with value %d\n",
info.si_pid, info.si_value.sival_int);
} else if (ret == -1 && errno == EAGAIN) {
printf("Timeout waiting for signal\n");
}
}
int main() {
demonstrate_sigwait();
return 0;
}
4. Real-time Signals
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#define SIGRT_MIN (__SIGRTMIN)
#define SIGRT_MAX (__SIGRTMAX)
volatile int signal_count = 0;
void rthandler(int signum, siginfo_t *info, void *context) {
signal_count++;
printf("RT signal %d received (count: %d, value: %d)\n",
signum, signal_count, info->si_value.sival_int);
}
int main() {
struct sigaction sa;
sa.sa_sigaction = rthandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO;
// Set up handlers for real-time signals
for (int i = SIGRTMIN; i <= SIGRTMIN + 3; i++) {
sigaction(i, &sa, NULL);
}
printf("Real-time signals range: %d - %d\n", SIGRTMIN, SIGRTMAX);
printf("PID: %d\n", getpid());
// Queue multiple RT signals (they are queued, unlike standard signals)
union sigval val;
for (int i = 0; i < 10; i++) {
val.sival_int = i * 10;
sigqueue(getpid(), SIGRTMIN + 1, val);
}
// Wait for signals
sleep(2);
printf("Total signals received: %d\n", signal_count);
return 0;
}
Complete Example: Signal-Based Event Loop
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
typedef struct {
int sigint_count;
int sigterm_count;
int sigusr1_count;
int sigusr2_count;
volatile sig_atomic_t quit;
} SignalStats;
SignalStats stats = {0};
void handler(int signum) {
switch (signum) {
case SIGINT:
stats.sigint_count++;
break;
case SIGTERM:
stats.sigterm_count++;
stats.quit = 1;
break;
case SIGUSR1:
stats.sigusr1_count++;
break;
case SIGUSR2:
stats.sigusr2_count++;
break;
}
}
void print_stats() {
printf("\n=== Signal Statistics ===\n");
printf("SIGINT: %d\n", stats.sigint_count);
printf("SIGTERM: %d\n", stats.sigterm_count);
printf("SIGUSR1: %d\n", stats.sigusr1_count);
printf("SIGUSR2: %d\n", stats.sigusr2_count);
printf("========================\n");
}
int main() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
// Block signals during handler execution
sigaddset(&sa.sa_mask, SIGINT);
sigaddset(&sa.sa_mask, SIGTERM);
sigaddset(&sa.sa_mask, SIGUSR1);
sigaddset(&sa.sa_mask, SIGUSR2);
// Register handlers
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGUSR1, &sa, NULL);
sigaction(SIGUSR2, &sa, NULL);
printf("Signal Monitor Running\n");
printf("PID: %d\n", getpid());
printf("Commands:\n");
printf(" kill -INT %d (Ctrl+C)\n", getpid());
printf(" kill -TERM %d (quit)\n", getpid());
printf(" kill -USR1 %d\n", getpid());
printf(" kill -USR2 %d\n", getpid());
printf("\n");
// Main loop
int iteration = 0;
while (!stats.quit) {
printf("Working... (%d)\r", ++iteration);
fflush(stdout);
// Use pselect to handle signals atomically
sigset_t emptymask;
sigemptyset(&emptymask);
struct timespec ts = {2, 0};
// pselect will be interrupted by signals
int ret = pselect(0, NULL, NULL, NULL, &ts, &emptymask);
if (ret == -1 && errno == EINTR) {
// Signal received - handler already updated stats
continue;
}
// Print stats every 10 iterations
if (iteration % 10 == 0) {
print_stats();
}
}
printf("\nShutting down...\n");
print_stats();
return 0;
}
Signal Safety Table
| Safe Functions | Unsafe Functions |
|---|---|
| write() | printf() |
| read() | fprintf() |
| open() | sprintf() |
| close() | malloc() |
| fork() | free() |
| exec() | mutex_lock() |
| exit() (careful) | pthread functions |
| _exit() | rand() |
| kill() | time() |
| raise() | localtime() |
| sigaction() | strerror() |
| sigprocmask() | getenv() |
| sigpending() | setenv() |
| sigsuspend() | syslog() |
| pause() | Any stdio function |
| alarm() | Any heap allocation |
| pipe() | Any locking function |
Best Practices
- Keep handlers simple: Only set flags or write to pipes
- Use volatile sig_atomic_t: For shared variables
- Block signals during critical sections: Prevent race conditions
- Use sigaction() over signal(): More portable and reliable
- Check return values: Always verify signal calls succeed
- Know async-signal-safe functions: Only use safe functions in handlers
- Use SA_RESTART appropriately: Decide which syscalls should restart
- Handle EINTR: Check for interrupted system calls
- Avoid longjmp() in handlers: Can cause resource leaks
- Document signal behavior: Especially in libraries
Common Pitfalls
// WRONG - printf in handler
void bad_handler(int signum) {
printf("Got signal\n"); // UNSAFE!
}
// RIGHT - use write
void good_handler(int signum) {
const char msg[] = "Got signal\n";
write(STDERR_FILENO, msg, sizeof(msg) - 1); // SAFE
}
// WRONG - sharing non-atomic variable
int counter = 0; // Not atomic
void unsafe_handler(int signum) {
counter++; // May be interrupted
}
// RIGHT - use sig_atomic_t
volatile sig_atomic_t counter = 0; // Atomic
void safe_handler(int signum) {
counter++;
}
// WRONG - assuming signal handler runs immediately
signal(SIGINT, handler);
while (1) {
// Signal might be pending
if (some_condition) {
break; // Handler hasn't run yet
}
}
// RIGHT - check flag
volatile sig_atomic_t flag = 0;
while (!flag) {
pause(); // Wait for signal
}
Conclusion
Signal handling in C is a powerful but subtle mechanism for managing asynchronous events. From simple interrupt handling to complex real-time signal queues, understanding signals is essential for systems programming, daemon development, and robust application design.
The key to mastering signals is recognizing their asynchronous nature and following strict safety rules. By using async-signal-safe functions, proper blocking techniques, and well-designed communication patterns (like self-pipe), you can build reliable systems that gracefully handle everything from user interrupts to system events.
Remember: signals are a low-level tool. Use them when appropriate, but consider alternatives like event loops, message queues, or higher-level IPC mechanisms for complex applications. When you do need signals, the techniques in this guide will help you implement them correctly and safely.