C — Signal Handling
Signals are software interrupts delivered to a process by the operating system (or another process). They provide a way to notify a process of asynchronous events — a user pressing Ctrl+C, a child process terminating, a timer expiring, or an invalid memory access. Signal handling is one of the most powerful and dangerous features in C programming.
| 1 | #include <stdio.h> |
| 2 | #include <signal.h> |
| 3 | |
| 4 | void handle_sigint(int sig) { |
| 5 | printf("\nCaught signal %d (SIGINT)\n", sig); |
| 6 | } |
| 7 | |
| 8 | int main(void) { |
| 9 | /* Register handler for Ctrl+C */ |
| 10 | signal(SIGINT, handle_sigint); |
| 11 | |
| 12 | printf("Press Ctrl+C to test signal handling...\n"); |
| 13 | while (1) { |
| 14 | /* Keep running until signal arrives */ |
| 15 | } |
| 16 | return 0; |
| 17 | } |
warning
signal() for simplicity. In production code, always use sigaction() — it's portable, reliable, and avoids subtle race conditions. See the dedicated section below.The POSIX standard defines several signals that every conforming system must support. Understanding what each signal means and when it arrives is essential for writing robust C programs.
| Signal | Default Action | Typical Cause |
|---|---|---|
SIGINT | Terminate | Ctrl+C (interactive interrupt) |
SIGTERM | Terminate | kill command (polite shutdown request) |
SIGKILL | Terminate (forced) | kill -9 (cannot be caught or ignored) |
SIGSEGV | Core dump | Invalid memory access (segfault) |
SIGFPE | Core dump | Floating-point exception (divide by zero) |
SIGABRT | Core dump | abort() called |
SIGILL | Core dump | Illegal instruction (corrupted code) |
SIGPIPE | Terminate | Write to broken pipe or closed socket |
SIGALRM | Terminate | alarm() timer expired |
SIGHUP | Terminate | Terminal hangup (controlling terminal closed) |
SIGCHLD | Ignore | Child process stopped or terminated |
SIGUSR1/SIGUSR2 | Terminate | User-defined signals (custom IPC) |
note
SIGKILL and SIGSTOP cannot be caught, blocked, or ignored. They always perform their default action. This is by design — it ensures the OS always has ultimate control over processes.Signals fall into distinct categories based on their semantics and how the kernel generates them. Understanding these categories helps you know which signals can be caught and how to respond to them.
| Category | Signals | Description |
|---|---|---|
| Terminal | SIGINT, SIGQUIT, SIGTSTP, SIGCONT | Interactive terminal events |
| Fatal | SIGSEGV, SIGILL, SIGFPE, SIGBUS | Program errors that usually crash the process |
| Process | SIGTERM, SIGKILL, SIGHUP, SIGCHLD | Lifecycle events (start, stop, die) |
| Resource | SIGALRM, SIGPROF, SIGVTALRM | Timer and resource limits exceeded |
| User-defined | SIGUSR1, SIGUSR2 | Custom signals for application IPC |
The signal() function is the simplest way to register a signal handler. It takes a signal number and a pointer to a handler function. While easy to use, it has significant limitations that make it unreliable for production code.
| 1 | #include <stdio.h> |
| 2 | #include <signal.h> |
| 3 | |
| 4 | volatile int quit = 0; |
| 5 | |
| 6 | void handler(int sig) { |
| 7 | if (sig == SIGINT) { |
| 8 | printf("\nReceived SIGINT, cleaning up...\n"); |
| 9 | quit = 1; |
| 10 | } |
| 11 | } |
| 12 | |
| 13 | int main(void) { |
| 14 | /* Register the handler */ |
| 15 | signal(SIGINT, handler); |
| 16 | |
| 17 | printf("Running... press Ctrl+C\n"); |
| 18 | while (!quit) { |
| 19 | /* simulate work */ |
| 20 | } |
| 21 | |
| 22 | printf("Clean shutdown complete.\n"); |
| 23 | return 0; |
| 24 | } |
warning
signal() is unreliable: On some systems (notably System V), after a signal is delivered, the handler is reset to SIG_DFL. This means a second signal of the same type will kill your process. POSIX fixed this, but behavior still varies across platforms. Always prefer sigaction().The three possible return values from signal() as a handler parameter:
| Value | Meaning |
|---|---|
SIG_DFL | Use default action for this signal |
SIG_IGN | Ignore this signal entirely |
func_ptr | Call this function when signal arrives |
sigaction() is the POSIX replacement for signal(). It gives you full control over signal handling: you can specify which signals to block during handling, choose between handler styles, and set flags that control restart behavior.
| 1 | #include <stdio.h> |
| 2 | #include <signal.h> |
| 3 | #include <unistd.h> |
| 4 | |
| 5 | volatile int got_sigint = 0; |
| 6 | volatile int got_sigterm = 0; |
| 7 | |
| 8 | void handle_signal(int sig) { |
| 9 | if (sig == SIGINT) { |
| 10 | got_sigint = 1; |
| 11 | } else if (sig == SIGTERM) { |
| 12 | got_sigterm = 1; |
| 13 | } |
| 14 | } |
| 15 | |
| 16 | int main(void) { |
| 17 | struct sigaction sa; |
| 18 | |
| 19 | /* Configure the handler */ |
| 20 | sa.sa_handler = handle_signal; |
| 21 | sigemptyset(&sa.sa_mask); /* don't block any extra signals */ |
| 22 | sa.sa_flags = 0; /* no special flags */ |
| 23 | sa.sa_sigaction = NULL; /* not using siginfo style */ |
| 24 | |
| 25 | /* Register for both SIGINT and SIGTERM */ |
| 26 | sigaction(SIGINT, &sa, NULL); |
| 27 | sigaction(SIGTERM, &sa, NULL); |
| 28 | |
| 29 | printf("Running with sigaction... press Ctrl+C or kill\n"); |
| 30 | while (!got_sigint && !got_sigterm) { |
| 31 | pause(); /* wait for a signal */ |
| 32 | } |
| 33 | |
| 34 | printf("Received signal, shutting down cleanly.\n"); |
| 35 | return 0; |
| 36 | } |
The struct sigaction has several important fields:
| Field | Type | Purpose |
|---|---|---|
sa_handler | void (*)(int) | Simple signal handler function |
sa_sigaction | void (*)(int, siginfo_t *, void *) | Extended handler with signal info (when SA_SIGINFO set) |
sa_mask | sigset_t | Signals to block while this handler runs |
sa_flags | int | Behavior flags (SA_RESTART, SA_SIGINFO, etc.) |
The sa_flags field controls important behavior details. The most commonly used flags are SA_RESTART and SA_SIGINFO.
| 1 | #include <stdio.h> |
| 2 | #include <signal.h> |
| 3 | #include <unistd.h> |
| 4 | #include <errno.h> |
| 5 | |
| 6 | void handle_sigint_restart(int sig) { |
| 7 | /* SA_RESTART: interrupted syscalls resume automatically */ |
| 8 | printf("Caught SIGINT (syscall restarted)\n"); |
| 9 | } |
| 10 | |
| 11 | void handle_sigint_norestart(int sig) { |
| 12 | /* Without SA_RESTART: syscalls return -1 with EINTR */ |
| 13 | printf("Caught SIGINT (syscall interrupted)\n"); |
| 14 | } |
| 15 | |
| 16 | int main(int argc, char *argv[]) { |
| 17 | struct sigaction sa; |
| 18 | |
| 19 | /* With SA_RESTART: read() resumes after handler returns */ |
| 20 | sa.sa_handler = handle_sigint_restart; |
| 21 | sigemptyset(&sa.sa_mask); |
| 22 | sa.sa_flags = SA_RESTART; |
| 23 | sigaction(SIGINT, &sa, NULL); |
| 24 | |
| 25 | char buf[64]; |
| 26 | printf("Type something (SA_RESTART enabled): "); |
| 27 | ssize_t n = read(STDIN_FILENO, buf, sizeof(buf) - 1); |
| 28 | if (n > 0) { |
| 29 | buf[n] = '\0'; |
| 30 | printf("Read: %s", buf); |
| 31 | } |
| 32 | |
| 33 | /* Without SA_RESTART: read() returns -1, errno = EINTR */ |
| 34 | sa.sa_handler = handle_sigint_norestart; |
| 35 | sigemptyset(&sa.sa_mask); |
| 36 | sa.sa_flags = 0; |
| 37 | sigaction(SIGINT, &sa, NULL); |
| 38 | |
| 39 | printf("Type something (no SA_RESTART): "); |
| 40 | n = read(STDIN_FILENO, buf, sizeof(buf) - 1); |
| 41 | if (n < 0 && errno == EINTR) { |
| 42 | printf("\nRead interrupted by signal!\n"); |
| 43 | } else if (n > 0) { |
| 44 | buf[n] = '\0'; |
| 45 | printf("Read: %s", buf); |
| 46 | } |
| 47 | |
| 48 | return 0; |
| 49 | } |
best practice
SA_RESTART unless you specifically need to handle EINTR. Without it, every blocking syscall (read, write, accept, sleep) will be interrupted by signals, and you must handle the error manually.Signal handlers run asynchronously — they can interrupt your program at any point. This means you can only call async-signal-safe functions inside a handler. Calling non-safe functions like printf(), malloc(), or free() can cause deadlocks, corrupted memory, or undefined behavior.
| 1 | #include <signal.h> |
| 2 | #include <unistd.h> /* write() is signal-safe */ |
| 3 | #include <string.h> |
| 4 | |
| 5 | volatile sig_atomic_t got_signal = 0; |
| 6 | |
| 7 | void safe_handler(int sig) { |
| 8 | /* Only call async-signal-safe functions! */ |
| 9 | const char msg[] = "Signal received\n"; |
| 10 | write(STDOUT_FILENO, msg, sizeof(msg) - 1); |
| 11 | |
| 12 | /* Set flag — sig_atomic_t is the only safe shared type */ |
| 13 | got_signal = 1; |
| 14 | } |
| 15 | |
| 16 | /* UNSAFE handler — do NOT do this */ |
| 17 | void unsafe_handler(int sig) { |
| 18 | /* printf() is NOT signal-safe — can deadlock */ |
| 19 | printf("This is unsafe!\n"); |
| 20 | |
| 21 | /* malloc/free are NOT signal-safe */ |
| 22 | char *p = malloc(100); |
| 23 | free(p); |
| 24 | } |
| 25 | |
| 26 | int main(void) { |
| 27 | struct sigaction sa; |
| 28 | sa.sa_handler = safe_handler; |
| 29 | sigemptyset(&sa.sa_mask); |
| 30 | sa.sa_flags = 0; |
| 31 | sigaction(SIGINT, &sa, NULL); |
| 32 | |
| 33 | while (!got_signal) { |
| 34 | pause(); |
| 35 | } |
| 36 | |
| 37 | /* Now it's safe to use printf */ |
| 38 | printf("Clean shutdown\n"); |
| 39 | return 0; |
| 40 | } |
POSIX guarantees these functions are async-signal-safe: write, read, close, open, alarm, pause, kill, getpid, sigaction, sigprocmask, sigsuspend, _exit, and a few others. Everything else — including all stdio functions, memory allocation, and most system calls — is unsafe.
pro tip
volatile sig_atomic_t flag, (2) return from the handler, (3) check the flag in your main loop. This avoids calling any complex functions inside the handler itself.A signal mask is a set of signals that are blocked (deferred) while another signal is being handled. This prevents nested signal handling — for example, blocking SIGTERM while handling SIGINT.
| 1 | #include <stdio.h> |
| 2 | #include <signal.h> |
| 3 | #include <unistd.h> |
| 4 | |
| 5 | void handle_sigint(int sig) { |
| 6 | /* While this handler runs, SIGTERM is blocked |
| 7 | (because we added it to sa_mask) */ |
| 8 | const char msg[] = "SIGINT handler running\n"; |
| 9 | write(STDOUT_FILENO, msg, sizeof(msg) - 1); |
| 10 | |
| 11 | /* Do some work that takes time */ |
| 12 | sleep(2); |
| 13 | |
| 14 | const char done[] = "SIGINT handler done\n"; |
| 15 | write(STDOUT_FILENO, done, sizeof(done) - 1); |
| 16 | } |
| 17 | |
| 18 | void handle_sigterm(int sig) { |
| 19 | const char msg[] = "SIGTERM handler running\n"; |
| 20 | write(STDOUT_FILENO, msg, sizeof(msg) - 1); |
| 21 | } |
| 22 | |
| 23 | int main(void) { |
| 24 | struct sigaction sa_int, sa_term; |
| 25 | |
| 26 | /* SIGINT handler — block SIGTERM while handling SIGINT */ |
| 27 | sa_int.sa_handler = handle_sigint; |
| 28 | sigemptyset(&sa_int.sa_mask); |
| 29 | sigaddset(&sa_int.sa_mask, SIGTERM); /* block SIGTERM */ |
| 30 | sa_int.sa_flags = 0; |
| 31 | sigaction(SIGINT, &sa_int, NULL); |
| 32 | |
| 33 | /* SIGTERM handler */ |
| 34 | sa_term.sa_handler = handle_sigterm; |
| 35 | sigemptyset(&sa_term.sa_mask); |
| 36 | sa_term.sa_flags = 0; |
| 37 | sigaction(SIGTERM, &sa_term, NULL); |
| 38 | |
| 39 | printf("Running... try Ctrl+C then kill\n"); |
| 40 | while (1) { |
| 41 | pause(); |
| 42 | } |
| 43 | return 0; |
| 44 | } |
note
sigprocmask()), it is delivered at that point. Multiple instances of the same signal are coalesced — you only get one delivery.When you set SA_SIGINFO in sa_flags, the handler receives a siginfo_t struct with rich information about the signal — the sender's PID, user ID, signal code, and more.
| 1 | #include <stdio.h> |
| 2 | #include <signal.h> |
| 3 | #include <unistd.h> |
| 4 | |
| 5 | void handle_signal(int sig, siginfo_t *info, void *ctx) { |
| 6 | printf("Signal %d received\n", sig); |
| 7 | printf(" Sender PID: %d\n", info->si_pid); |
| 8 | printf(" Sender UID: %d\n", info->si_uid); |
| 9 | printf(" Signal code: %d\n", info->si_code); |
| 10 | |
| 11 | (void)ctx; /* ucontext_t — register state at signal time */ |
| 12 | } |
| 13 | |
| 14 | int main(void) { |
| 15 | struct sigaction sa; |
| 16 | sa.sa_sigaction = handle_signal; |
| 17 | sigemptyset(&sa.sa_mask); |
| 18 | sa.sa_flags = SA_SIGINFO; /* enable extended handler */ |
| 19 | |
| 20 | sigaction(SIGUSR1, &sa, NULL); |
| 21 | |
| 22 | printf("PID %d waiting for SIGUSR1\n", getpid()); |
| 23 | |
| 24 | /* Send signal to ourselves */ |
| 25 | kill(getpid(), SIGUSR1); |
| 26 | |
| 27 | return 0; |
| 28 | } |
C provides several functions for sending signals, pausing execution, and waiting for signals. These are essential building blocks for inter-process communication and synchronization.
| 1 | #include <stdio.h> |
| 2 | #include <signal.h> |
| 3 | #include <unistd.h> |
| 4 | |
| 5 | volatile int gotUSR1 = 0; |
| 6 | volatile int gotUSR2 = 0; |
| 7 | |
| 8 | void handle_usr1(int sig) { gotUSR1 = 1; } |
| 9 | void handle_usr2(int sig) { gotUSR2 = 1; } |
| 10 | |
| 11 | int main(void) { |
| 12 | struct sigaction sa; |
| 13 | sa.sa_handler = handle_usr1; |
| 14 | sigemptyset(&sa.sa_mask); |
| 15 | sa.sa_flags = 0; |
| 16 | sigaction(SIGUSR1, &sa, NULL); |
| 17 | |
| 18 | sa.sa_handler = handle_usr2; |
| 19 | sigaction(SIGUSR2, &sa, NULL); |
| 20 | |
| 21 | printf("PID: %d\n", getpid()); |
| 22 | |
| 23 | /* raise() sends a signal to the current process */ |
| 24 | printf("Sending SIGUSR1 to self...\n"); |
| 25 | raise(SIGUSR1); |
| 26 | printf("Got SIGUSR1: %d\n", gotUSR1); |
| 27 | |
| 28 | /* kill() sends a signal to any process */ |
| 29 | printf("Sending SIGUSR2 to self...\n"); |
| 30 | kill(getpid(), SIGUSR2); |
| 31 | printf("Got SIGUSR2: %d\n", gotUSR2); |
| 32 | |
| 33 | /* pause() blocks until any signal is delivered */ |
| 34 | printf("Calling pause()...\n"); |
| 35 | pause(); |
| 36 | printf("pause() returned!\n"); |
| 37 | |
| 38 | return 0; |
| 39 | } |
For more precise waiting, use sigsuspend() which atomically sets a signal mask and pauses. This avoids the race condition where a signal arrives between checking a flag and calling pause().
| 1 | #include <stdio.h> |
| 2 | #include <signal.h> |
| 3 | #include <unistd.h> |
| 4 | |
| 5 | volatile sig_atomic_t received_sigterm = 0; |
| 6 | |
| 7 | void handle_sigterm(int sig) { |
| 8 | received_sigterm = 1; |
| 9 | } |
| 10 | |
| 11 | int main(void) { |
| 12 | struct sigaction sa; |
| 13 | sa.sa_handler = handle_sigterm; |
| 14 | sigemptyset(&sa.sa_mask); |
| 15 | sa.sa_flags = 0; |
| 16 | sigaction(SIGTERM, &sa, NULL); |
| 17 | |
| 18 | /* Block SIGTERM first */ |
| 19 | sigset_t block_set, empty_set; |
| 20 | sigemptyset(&block_set); |
| 21 | sigaddset(&block_set, SIGTERM); |
| 22 | sigprocmask(SIG_BLOCK, &block_set, NULL); |
| 23 | |
| 24 | printf("Waiting for SIGTERM (PID %d)...\n", getpid()); |
| 25 | |
| 26 | /* Atomically unblock SIGTERM and wait */ |
| 27 | sigemptyset(&empty_set); |
| 28 | while (!received_sigterm) { |
| 29 | sigsuspend(&empty_set); |
| 30 | } |
| 31 | |
| 32 | printf("Received SIGTERM, shutting down.\n"); |
| 33 | return 0; |
| 34 | } |
pro tip
sigsuspend() always returns -1 with errno = EINTR. Don't treat this as an error — it means a signal was delivered and your handler ran. Check your flag variable instead.The alarm() function schedules a SIGALRM signal to be delivered after a specified number of seconds. This is useful for implementing timeouts on operations that might hang.
| 1 | #include <stdio.h> |
| 2 | #include <signal.h> |
| 3 | #include <unistd.h> |
| 4 | |
| 5 | volatile int timed_out = 0; |
| 6 | |
| 7 | void alarm_handler(int sig) { |
| 8 | timed_out = 1; |
| 9 | } |
| 10 | |
| 11 | int main(void) { |
| 12 | struct sigaction sa; |
| 13 | sa.sa_handler = alarm_handler; |
| 14 | sigemptyset(&sa.sa_mask); |
| 15 | sa.sa_flags = 0; |
| 16 | sigaction(SIGALRM, &sa, NULL); |
| 17 | |
| 18 | /* Set a 5-second timeout */ |
| 19 | alarm(5); |
| 20 | |
| 21 | printf("Working... you have 5 seconds\n"); |
| 22 | int count = 0; |
| 23 | while (!timed_out) { |
| 24 | /* Simulate slow work */ |
| 25 | sleep(1); |
| 26 | printf("tick %d\n", ++count); |
| 27 | } |
| 28 | |
| 29 | printf("Timed out after %d seconds!\n", count); |
| 30 | return 0; |
| 31 | } |
For repeated alarms, set a new alarm in the handler. To cancel an pending alarm, call alarm(0).
| 1 | #include <stdio.h> |
| 2 | #include <signal.h> |
| 3 | #include <unistd.h> |
| 4 | |
| 5 | volatile sig_atomic_t alarm_count = 0; |
| 6 | |
| 7 | void alarm_handler(int sig) { |
| 8 | alarm_count++; |
| 9 | /* Re-arm the alarm for repeated timeouts */ |
| 10 | alarm(2); |
| 11 | } |
| 12 | |
| 13 | int main(void) { |
| 14 | struct sigaction sa; |
| 15 | sa.sa_handler = alarm_handler; |
| 16 | sigemptyset(&sa.sa_mask); |
| 17 | sa.sa_flags = 0; |
| 18 | sigaction(SIGALRM, &sa, NULL); |
| 19 | |
| 20 | alarm(2); /* first alarm in 2 seconds */ |
| 21 | |
| 22 | while (alarm_count < 3) { |
| 23 | pause(); |
| 24 | } |
| 25 | |
| 26 | /* Cancel any pending alarm */ |
| 27 | alarm(0); |
| 28 | printf("Received %d alarms, done.\n", alarm_count); |
| 29 | return 0; |
| 30 | } |
note
timer_create() and timer_settime() instead ofalarm(). They offer nanosecond precision and support multiple independent timers.When you write to a pipe or socket whose read end has been closed, the kernel delivers SIGPIPE and the write call fails with errno = EPIPE. By default, SIGPIPE terminates the process — which surprises many developers. Most server programs ignore SIGPIPE and handle EPIPE from the write call.
| 1 | #include <stdio.h> |
| 2 | #include <signal.h> |
| 3 | #include <unistd.h> |
| 4 | #include <errno.h> |
| 5 | |
| 6 | int main(void) { |
| 7 | /* Ignore SIGPIPE — handle EPIPE from write() instead */ |
| 8 | struct sigaction sa; |
| 9 | sa.sa_handler = SIG_IGN; |
| 10 | sigemptyset(&sa.sa_mask); |
| 11 | sa.sa_flags = 0; |
| 12 | sigaction(SIGPIPE, &sa, NULL); |
| 13 | |
| 14 | int pipefd[2]; |
| 15 | pipe(pipefd); |
| 16 | |
| 17 | /* Close read end — writes will now fail */ |
| 18 | close(pipefd[0]); |
| 19 | |
| 20 | char buf[] = "hello"; |
| 21 | ssize_t n = write(pipefd[1], buf, sizeof(buf)); |
| 22 | if (n < 0) { |
| 23 | if (errno == EPIPE) { |
| 24 | printf("Write failed: broken pipe (EPIPE)\n"); |
| 25 | } else { |
| 26 | perror("write"); |
| 27 | } |
| 28 | } else { |
| 29 | printf("Wrote %zd bytes\n", n); |
| 30 | } |
| 31 | |
| 32 | close(pipefd[1]); |
| 33 | return 0; |
| 34 | } |
best practice
EPIPE and ECONNRESET explicitly in your write/send code paths.When a child process terminates, the parent receives SIGCHLD. If the parent doesn't call wait(), the child becomes a zombie process. The proper pattern is to set up a SIGCHLD handler that reaps children with waitpid() using WNOHANG.
| 1 | #include <stdio.h> |
| 2 | #include <signal.h> |
| 3 | #include <sys/wait.h> |
| 4 | #include <unistd.h> |
| 5 | #include <stdlib.h> |
| 6 | |
| 7 | void handle_sigchld(int sig) { |
| 8 | /* Reap all terminated children (WNOHANG to avoid blocking) */ |
| 9 | int saved_errno = errno; |
| 10 | while (waitpid(-1, NULL, WNOHANG) > 0) { |
| 11 | /* keep reaping */ |
| 12 | } |
| 13 | errno = saved_errno; /* restore errno */ |
| 14 | } |
| 15 | |
| 16 | int main(void) { |
| 17 | struct sigaction sa; |
| 18 | sa.sa_handler = handle_sigchld; |
| 19 | sigemptyset(&sa.sa_mask); |
| 20 | sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; |
| 21 | sigaction(SIGCHLD, &sa, NULL); |
| 22 | |
| 23 | /* Fork some children */ |
| 24 | for (int i = 0; i < 3; i++) { |
| 25 | pid_t pid = fork(); |
| 26 | if (pid == 0) { |
| 27 | /* Child: do some work and exit */ |
| 28 | printf("Child %d (PID %d) started\n", i, getpid()); |
| 29 | sleep(i + 1); |
| 30 | printf("Child %d exiting\n", i); |
| 31 | exit(0); |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | /* Parent waits */ |
| 36 | printf("Parent (PID %d) waiting...\n", getpid()); |
| 37 | sleep(5); |
| 38 | printf("Parent done\n"); |
| 39 | return 0; |
| 40 | } |
note
SA_NOCLDSTOP prevents the handler from being called when children stop (but not terminate). This is usually what you want — you only care about children that have finished.A more robust approach uses a loop to handle the case where multiple children terminate simultaneously. The while (waitpid(-1, NULL, WNOHANG) > 0) loop reaps all available children in one handler invocation.
| 1 | #include <stdio.h> |
| 2 | #include <signal.h> |
| 3 | #include <sys/wait.h> |
| 4 | #include <unistd.h> |
| 5 | #include <stdlib.h> |
| 6 | |
| 7 | void handle_sigchld(int sig) { |
| 8 | int saved_errno = errno; |
| 9 | int status; |
| 10 | pid_t pid; |
| 11 | |
| 12 | while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { |
| 13 | if (WIFEXITED(status)) { |
| 14 | printf("Child %d exited with status %d\n", |
| 15 | pid, WEXITSTATUS(status)); |
| 16 | } else if (WIFSIGNALED(status)) { |
| 17 | printf("Child %d killed by signal %d\n", |
| 18 | pid, WTERMSIG(status)); |
| 19 | } else if (WIFSTOPPED(status)) { |
| 20 | printf("Child %d stopped by signal %d\n", |
| 21 | pid, WSTOPSIG(status)); |
| 22 | } |
| 23 | } |
| 24 | errno = saved_errno; |
| 25 | } |
| 26 | |
| 27 | int main(void) { |
| 28 | struct sigaction sa; |
| 29 | sa.sa_handler = handle_sigchld; |
| 30 | sigemptyset(&sa.sa_mask); |
| 31 | sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; |
| 32 | sigaction(SIGCHLD, &sa, NULL); |
| 33 | |
| 34 | for (int i = 0; i < 3; i++) { |
| 35 | if (fork() == 0) { |
| 36 | sleep(i + 1); |
| 37 | exit(i * 10); |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | sleep(5); |
| 42 | printf("All children reaped.\n"); |
| 43 | return 0; |
| 44 | } |
A complete server-style graceful shutdown handler. This pattern handles SIGINT (Ctrl+C), SIGTERM (kill), and SIGHUP (reload config), with proper signal masking and cleanup.
| 1 | #include <stdio.h> |
| 2 | #include <signal.h> |
| 3 | #include <unistd.h> |
| 4 | #include <stdlib.h> |
| 5 | |
| 6 | volatile sig_atomic_t shutdown_requested = 0; |
| 7 | volatile sig_atomic_t reload_requested = 0; |
| 8 | |
| 9 | void handle_shutdown(int sig) { |
| 10 | shutdown_requested = sig; /* store which signal */ |
| 11 | } |
| 12 | |
| 13 | void handle_reload(int sig) { |
| 14 | reload_requested = 1; |
| 15 | } |
| 16 | |
| 17 | static void cleanup(void) { |
| 18 | printf("Cleaning up resources...\n"); |
| 19 | printf("Closing connections...\n"); |
| 20 | printf("Flushing buffers...\n"); |
| 21 | } |
| 22 | |
| 23 | int main(void) { |
| 24 | struct sigaction sa; |
| 25 | |
| 26 | /* SIGINT and SIGTERM trigger graceful shutdown */ |
| 27 | sa.sa_handler = handle_shutdown; |
| 28 | sigemptyset(&sa.sa_mask); |
| 29 | sigaddset(&sa.sa_mask, SIGHUP); /* block SIGHUP while handling */ |
| 30 | sa.sa_flags = SA_RESTART; |
| 31 | sigaction(SIGINT, &sa, NULL); |
| 32 | sigaction(SIGTERM, &sa, NULL); |
| 33 | |
| 34 | /* SIGHUP triggers config reload */ |
| 35 | sa.sa_handler = handle_reload; |
| 36 | sigemptyset(&sa.sa_mask); |
| 37 | sigaddset(&sa.sa_mask, SIGINT); /* block SIGINT while handling */ |
| 38 | sigaddset(&sa.sa_mask, SIGTERM); |
| 39 | sa.sa_flags = 0; |
| 40 | sigaction(SIGHUP, &sa, NULL); |
| 41 | |
| 42 | printf("Server running (PID %d). Press Ctrl+C to stop.\n", |
| 43 | getpid()); |
| 44 | |
| 45 | while (!shutdown_requested) { |
| 46 | if (reload_requested) { |
| 47 | printf("Reloading configuration...\n"); |
| 48 | reload_requested = 0; |
| 49 | } |
| 50 | |
| 51 | /* Main server work loop */ |
| 52 | pause(); |
| 53 | } |
| 54 | |
| 55 | /* Clean shutdown */ |
| 56 | printf("Shutdown signal %d received.\n", shutdown_requested); |
| 57 | cleanup(); |
| 58 | printf("Server stopped.\n"); |
| 59 | return 0; |
| 60 | } |
Signals and pthreads interact in complex ways. Signals are delivered to the process, not a specific thread — but only one thread runs the handler. Use pthread_sigmask() to control which thread receives which signals.
| 1 | #include <stdio.h> |
| 2 | #include <signal.h> |
| 3 | #include <pthread.h> |
| 4 | #include <unistd.h> |
| 5 | |
| 6 | volatile sig_atomic_t got_sigint = 0; |
| 7 | |
| 8 | void handle_sigint(int sig) { |
| 9 | got_sigint = 1; |
| 10 | } |
| 11 | |
| 12 | void *worker_thread(void *arg) { |
| 13 | int id = *(int *)arg; |
| 14 | |
| 15 | /* Block SIGINT in worker threads */ |
| 16 | sigset_t set; |
| 17 | sigemptyset(&set); |
| 18 | sigaddset(&set, SIGINT); |
| 19 | pthread_sigmask(SIG_BLOCK, &set, NULL); |
| 20 | |
| 21 | printf("Worker %d running (no SIGINT)\n", id); |
| 22 | while (!got_sigint) { |
| 23 | sleep(1); |
| 24 | printf("Worker %d working...\n", id); |
| 25 | } |
| 26 | return NULL; |
| 27 | } |
| 28 | |
| 29 | int main(void) { |
| 30 | /* Set up handler in main thread */ |
| 31 | struct sigaction sa; |
| 32 | sa.sa_handler = handle_sigint; |
| 33 | sigemptyset(&sa.sa_mask); |
| 34 | sa.sa_flags = 0; |
| 35 | sigaction(SIGINT, &sa, NULL); |
| 36 | |
| 37 | /* Block SIGINT in main thread temporarily */ |
| 38 | sigset_t set, old_set; |
| 39 | sigemptyset(&set); |
| 40 | sigaddset(&set, SIGINT); |
| 41 | pthread_sigmask(SIG_BLOCK, &set, &old_set); |
| 42 | |
| 43 | pthread_t threads[3]; |
| 44 | int ids[3] = {1, 2, 3}; |
| 45 | for (int i = 0; i < 3; i++) { |
| 46 | pthread_create(&threads[i], NULL, worker_thread, &ids[i]); |
| 47 | } |
| 48 | |
| 49 | /* Restore signal mask in main thread */ |
| 50 | pthread_sigmask(SIG_SETMASK, &old_set, NULL); |
| 51 | |
| 52 | printf("Main thread waiting for SIGINT...\n"); |
| 53 | while (!got_sigint) { |
| 54 | pause(); |
| 55 | } |
| 56 | |
| 57 | printf("SIGINT received, joining workers...\n"); |
| 58 | for (int i = 0; i < 3; i++) { |
| 59 | pthread_join(threads[i], NULL); |
| 60 | } |
| 61 | |
| 62 | printf("All threads joined.\n"); |
| 63 | return 0; |
| 64 | } |
warning
pthread_sigmask() before creating threads. If a signal arrives before the mask is set in the new thread, it may be delivered to the wrong thread.The sig_atomic_t type is guaranteed to be atomic for reads and writes, even in the presence of signals. Combined with volatile, it's the only safe way to share data between a signal handler and your main code.
| 1 | #include <stdio.h> |
| 2 | #include <signal.h> |
| 3 | #include <unistd.h> |
| 4 | |
| 5 | /* MUST be volatile sig_atomic_t — not int, not volatile int */ |
| 6 | volatile sig_atomic_t counter = 0; |
| 7 | volatile sig_atomic_t last_signal = 0; |
| 8 | |
| 9 | void handler(int sig) { |
| 10 | counter++; /* atomic increment */ |
| 11 | last_signal = sig; /* atomic store */ |
| 12 | } |
| 13 | |
| 14 | int main(void) { |
| 15 | struct sigaction sa; |
| 16 | sa.sa_handler = handler; |
| 17 | sigemptyset(&sa.sa_mask); |
| 18 | sa.sa_flags = 0; |
| 19 | sigaction(SIGUSR1, &sa, NULL); |
| 20 | sigaction(SIGUSR2, &sa, NULL); |
| 21 | |
| 22 | /* Send signals to ourselves */ |
| 23 | for (int i = 0; i < 100; i++) { |
| 24 | kill(getpid(), SIGUSR1); |
| 25 | } |
| 26 | kill(getpid(), SIGUSR2); |
| 27 | |
| 28 | printf("counter = %d\n", counter); |
| 29 | printf("last_signal = %d (SIGUSR2 = %d)\n", |
| 30 | last_signal, SIGUSR2); |
| 31 | return 0; |
| 32 | } |
note
sig_atomic_t is typically an int, but the standard only guarantees it's at least as wide as an int and can be read/written atomically. Don't assume its exact size — use sizeof(sig_atomic_t) when needed.Here are battle-tested patterns you'll use in production C code. Each addresses a real-world problem that signal handling solves.
| 1 | #include <stdio.h> |
| 2 | #include <signal.h> |
| 3 | #include <unistd.h> |
| 4 | #include <string.h> |
| 5 | |
| 6 | /* Pattern 1: One-shot alarm with cleanup */ |
| 7 | volatile sig_atomic_t timed_out = 0; |
| 8 | void on_alarm(int sig) { timed_out = 1; } |
| 9 | |
| 10 | void do_with_timeout(int seconds) { |
| 11 | struct sigaction sa = {0}; |
| 12 | sa.sa_handler = on_alarm; |
| 13 | sigemptyset(&sa.sa_mask); |
| 14 | sigaction(SIGALRM, &sa, NULL); |
| 15 | |
| 16 | alarm(seconds); |
| 17 | /* ... do work ... */ |
| 18 | alarm(0); /* cancel if completed in time */ |
| 19 | if (timed_out) { |
| 20 | printf("Operation timed out!\n"); |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | /* Pattern 2: Deferred signal processing */ |
| 25 | volatile sig_atomic_t pending_signal = 0; |
| 26 | void on_signal(int sig) { pending_signal = sig; } |
| 27 | |
| 28 | void process_signals(void) { |
| 29 | while (pending_signal != 0) { |
| 30 | int sig = pending_signal; |
| 31 | pending_signal = 0; /* clear before processing */ |
| 32 | |
| 33 | switch (sig) { |
| 34 | case SIGHUP: |
| 35 | printf("Reloading config...\n"); |
| 36 | break; |
| 37 | case SIGUSR1: |
| 38 | printf("USR1: dump stats\n"); |
| 39 | break; |
| 40 | case SIGUSR2: |
| 41 | printf("USR2: toggle debug\n"); |
| 42 | break; |
| 43 | } |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | /* Pattern 3: Sigignore for pipes */ |
| 48 | void setup_pipe_signal(void) { |
| 49 | struct sigaction sa; |
| 50 | sa.sa_handler = SIG_IGN; |
| 51 | sigemptyset(&sa.sa_mask); |
| 52 | sa.sa_flags = 0; |
| 53 | sigaction(SIGPIPE, &sa, NULL); |
| 54 | } |
| 55 | |
| 56 | int main(void) { |
| 57 | /* Register handlers */ |
| 58 | struct sigaction sa; |
| 59 | sa.sa_handler = on_signal; |
| 60 | sigemptyset(&sa.sa_mask); |
| 61 | sa.sa_flags = SA_RESTART; |
| 62 | sigaction(SIGHUP, &sa, NULL); |
| 63 | sigaction(SIGUSR1, &sa, NULL); |
| 64 | sigaction(SIGUSR2, &sa, NULL); |
| 65 | |
| 66 | setup_pipe_signal(); |
| 67 | |
| 68 | printf("Server running (PID %d)\n", getpid()); |
| 69 | while (1) { |
| 70 | pause(); |
| 71 | process_signals(); |
| 72 | } |
| 73 | return 0; |
| 74 | } |
| Practice | Why |
|---|---|
Use sigaction() not signal() | Portable, reliable, no reset behavior |
Set SA_RESTART | Avoids EINTR on syscalls — less error handling code |
Use volatile sig_atomic_t | Only safe type for handler ↔ main communication |
| Call only async-signal-safe functions | write(), not printf(); never malloc in a handler |
Block signals with sa_mask | Prevents nested handler calls and race conditions |
| Ignore SIGPIPE in servers | Prevents crashes when clients disconnect |
| Use WNOHANG in SIGCHLD handlers | Avoids blocking inside signal handler |
| Set up handlers before creating threads | pthread_sigmask must be set before fork/pthread_create |
best practice
write() if you must, and return. Do all real work in the main loop where it's safe.