|$ curl https://forge-ai.dev/api/markdown?path=docs/c/signals
$cat docs/c-—-signal-handling.md
updated Recently·40 min read·published

C — Signal Handling

CSignalsSignal HandlingSystemsAdvancedAdvanced🎯Free Tools
What Are Signals?

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.

signal_basics.c
C
1#include <stdio.h>
2#include <signal.h>
3
4void handle_sigint(int sig) {
5 printf("\nCaught signal %d (SIGINT)\n", sig);
6}
7
8int 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

The example above uses signal() for simplicity. In production code, always use sigaction() — it's portable, reliable, and avoids subtle race conditions. See the dedicated section below.
Common Signals

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.

SignalDefault ActionTypical Cause
SIGINTTerminateCtrl+C (interactive interrupt)
SIGTERMTerminatekill command (polite shutdown request)
SIGKILLTerminate (forced)kill -9 (cannot be caught or ignored)
SIGSEGVCore dumpInvalid memory access (segfault)
SIGFPECore dumpFloating-point exception (divide by zero)
SIGABRTCore dumpabort() called
SIGILLCore dumpIllegal instruction (corrupted code)
SIGPIPETerminateWrite to broken pipe or closed socket
SIGALRMTerminatealarm() timer expired
SIGHUPTerminateTerminal hangup (controlling terminal closed)
SIGCHLDIgnoreChild process stopped or terminated
SIGUSR1/SIGUSR2TerminateUser-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.
Signal Categories

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.

CategorySignalsDescription
TerminalSIGINT, SIGQUIT, SIGTSTP, SIGCONTInteractive terminal events
FatalSIGSEGV, SIGILL, SIGFPE, SIGBUSProgram errors that usually crash the process
ProcessSIGTERM, SIGKILL, SIGHUP, SIGCHLDLifecycle events (start, stop, die)
ResourceSIGALRM, SIGPROF, SIGVTALRMTimer and resource limits exceeded
User-definedSIGUSR1, SIGUSR2Custom signals for application IPC
signal() — The Simple Way

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.

signal_basic.c
C
1#include <stdio.h>
2#include <signal.h>
3
4volatile int quit = 0;
5
6void handler(int sig) {
7 if (sig == SIGINT) {
8 printf("\nReceived SIGINT, cleaning up...\n");
9 quit = 1;
10 }
11}
12
13int 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

Why 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:

ValueMeaning
SIG_DFLUse default action for this signal
SIG_IGNIgnore this signal entirely
func_ptrCall this function when signal arrives
sigaction() — The Proper Way

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.

sigaction_example.c
C
1#include <stdio.h>
2#include <signal.h>
3#include <unistd.h>
4
5volatile int got_sigint = 0;
6volatile int got_sigterm = 0;
7
8void handle_signal(int sig) {
9 if (sig == SIGINT) {
10 got_sigint = 1;
11 } else if (sig == SIGTERM) {
12 got_sigterm = 1;
13 }
14}
15
16int 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:

FieldTypePurpose
sa_handlervoid (*)(int)Simple signal handler function
sa_sigactionvoid (*)(int, siginfo_t *, void *)Extended handler with signal info (when SA_SIGINFO set)
sa_masksigset_tSignals to block while this handler runs
sa_flagsintBehavior flags (SA_RESTART, SA_SIGINFO, etc.)
SA Flags in Detail

The sa_flags field controls important behavior details. The most commonly used flags are SA_RESTART and SA_SIGINFO.

sa_flags_example.c
C
1#include <stdio.h>
2#include <signal.h>
3#include <unistd.h>
4#include <errno.h>
5
6void handle_sigint_restart(int sig) {
7 /* SA_RESTART: interrupted syscalls resume automatically */
8 printf("Caught SIGINT (syscall restarted)\n");
9}
10
11void handle_sigint_norestart(int sig) {
12 /* Without SA_RESTART: syscalls return -1 with EINTR */
13 printf("Caught SIGINT (syscall interrupted)\n");
14}
15
16int 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

Use 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-safe Functions

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.

signal_safe.c
C
1#include <signal.h>
2#include <unistd.h> /* write() is signal-safe */
3#include <string.h>
4
5volatile sig_atomic_t got_signal = 0;
6
7void 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 */
17void 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
26int 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

The standard pattern for signal handlers is: (1) set a 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.
Signal Masks and Blocking

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.

signal_mask.c
C
1#include <stdio.h>
2#include <signal.h>
3#include <unistd.h>
4
5void 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
18void handle_sigterm(int sig) {
19 const char msg[] = "SIGTERM handler running\n";
20 write(STDOUT_FILENO, msg, sizeof(msg) - 1);
21}
22
23int 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

When a blocked signal becomes pending and is later unblocked (by returning from the handler or calling sigprocmask()), it is delivered at that point. Multiple instances of the same signal are coalesced — you only get one delivery.
Extended Handlers with SA_SIGINFO

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.

siginfo_example.c
C
1#include <stdio.h>
2#include <signal.h>
3#include <unistd.h>
4
5void 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
14int 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}
Sending and Pausing for Signals

C provides several functions for sending signals, pausing execution, and waiting for signals. These are essential building blocks for inter-process communication and synchronization.

signal_ipc.c
C
1#include <stdio.h>
2#include <signal.h>
3#include <unistd.h>
4
5volatile int gotUSR1 = 0;
6volatile int gotUSR2 = 0;
7
8void handle_usr1(int sig) { gotUSR1 = 1; }
9void handle_usr2(int sig) { gotUSR2 = 1; }
10
11int 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().

sigsuspend_example.c
C
1#include <stdio.h>
2#include <signal.h>
3#include <unistd.h>
4
5volatile sig_atomic_t received_sigterm = 0;
6
7void handle_sigterm(int sig) {
8 received_sigterm = 1;
9}
10
11int 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.
Alarm Signals and Timeouts

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.

alarm_timeout.c
C
1#include <stdio.h>
2#include <signal.h>
3#include <unistd.h>
4
5volatile int timed_out = 0;
6
7void alarm_handler(int sig) {
8 timed_out = 1;
9}
10
11int 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).

alarm_cancel.c
C
1#include <stdio.h>
2#include <signal.h>
3#include <unistd.h>
4
5volatile sig_atomic_t alarm_count = 0;
6
7void alarm_handler(int sig) {
8 alarm_count++;
9 /* Re-arm the alarm for repeated timeouts */
10 alarm(2);
11}
12
13int 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

For modern applications, consider using timer_create() and timer_settime() instead ofalarm(). They offer nanosecond precision and support multiple independent timers.
SIGPIPE and Broken Pipes

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.

sigpipe_handling.c
C
1#include <stdio.h>
2#include <signal.h>
3#include <unistd.h>
4#include <errno.h>
5
6int 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

For servers that handle multiple clients, always ignore SIGPIPE. A client disconnecting shouldn't crash your server. Handle EPIPE and ECONNRESET explicitly in your write/send code paths.
SIGCHLD and Child Process Reaping

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.

sigchld_reap.c
C
1#include <stdio.h>
2#include <signal.h>
3#include <sys/wait.h>
4#include <unistd.h>
5#include <stdlib.h>
6
7void 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
16int 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.

sigchld_status.c
C
1#include <stdio.h>
2#include <signal.h>
3#include <sys/wait.h>
4#include <unistd.h>
5#include <stdlib.h>
6
7void 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
27int 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}
Practical: Graceful Shutdown

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.

graceful_shutdown.c
C
1#include <stdio.h>
2#include <signal.h>
3#include <unistd.h>
4#include <stdlib.h>
5
6volatile sig_atomic_t shutdown_requested = 0;
7volatile sig_atomic_t reload_requested = 0;
8
9void handle_shutdown(int sig) {
10 shutdown_requested = sig; /* store which signal */
11}
12
13void handle_reload(int sig) {
14 reload_requested = 1;
15}
16
17static void cleanup(void) {
18 printf("Cleaning up resources...\n");
19 printf("Closing connections...\n");
20 printf("Flushing buffers...\n");
21}
22
23int 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 Threads

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.

signals_threads.c
C
1#include <stdio.h>
2#include <signal.h>
3#include <pthread.h>
4#include <unistd.h>
5
6volatile sig_atomic_t got_sigint = 0;
7
8void handle_sigint(int sig) {
9 got_sigint = 1;
10}
11
12void *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
29int 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

Always call 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.
volatile sig_atomic_t

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.

volatile_sigatomic.c
C
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 */
6volatile sig_atomic_t counter = 0;
7volatile sig_atomic_t last_signal = 0;
8
9void handler(int sig) {
10 counter++; /* atomic increment */
11 last_signal = sig; /* atomic store */
12}
13
14int 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.
Practical Patterns

Here are battle-tested patterns you'll use in production C code. Each addresses a real-world problem that signal handling solves.

signal_patterns.c
C
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 */
7volatile sig_atomic_t timed_out = 0;
8void on_alarm(int sig) { timed_out = 1; }
9
10void 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 */
25volatile sig_atomic_t pending_signal = 0;
26void on_signal(int sig) { pending_signal = sig; }
27
28void 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 */
48void 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
56int 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}
Signal Handling Best Practices
PracticeWhy
Use sigaction() not signal()Portable, reliable, no reset behavior
Set SA_RESTARTAvoids EINTR on syscalls — less error handling code
Use volatile sig_atomic_tOnly safe type for handler ↔ main communication
Call only async-signal-safe functionswrite(), not printf(); never malloc in a handler
Block signals with sa_maskPrevents nested handler calls and race conditions
Ignore SIGPIPE in serversPrevents crashes when clients disconnect
Use WNOHANG in SIGCHLD handlersAvoids blocking inside signal handler
Set up handlers before creating threadspthread_sigmask must be set before fork/pthread_create

best practice

The golden rule: keep signal handlers minimal. Set a flag, call write() if you must, and return. Do all real work in the main loop where it's safe.
$Blueprint — Engineering Documentation·Section ID: C-SIGNALS·Revision: 1.0