What is a Signal Handler?
A signal handler is a function that your program registers to be called automatically when the kernel delivers a specific signal to your process. For example, when you press Ctrl+C, the kernel sends SIGINT to the foreground process — if you registered a handler, it runs instead of the default action (which is to terminate the process).
Writing signal handlers correctly is tricky because a signal can arrive at any point in your program’s execution, interrupting whatever it was doing. This creates hidden concurrency problems even in single-threaded programs.
Because a signal can arrive at any time and interrupt any piece of code, signal handlers must be kept as simple as possible. There are two widely accepted design patterns:
This is the most recommended approach. The handler only sets a flag. All real work is done in the main loop.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdatomic.h>
/* Use volatile sig_atomic_t for the flag — explained in section 21.1.3 */
static volatile sig_atomic_t got_sigint = 0;
static void sigint_handler(int sig)
{
/*
* DO NOT call printf() here — it is NOT async-signal-safe.
* Just set the flag and return immediately.
*/
got_sigint = 1;
}
int main(void)
{
struct sigaction sa;
sa.sa_handler = sigint_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
printf("Running... Press Ctrl+C to stop.\n");
while (1) {
/* Main loop polls the flag */
if (got_sigint) {
printf("\n[Main] Caught SIGINT via flag. Cleaning up...\n");
/* Safe to do all real work here */
break;
}
sleep(1);
printf(".");
fflush(stdout);
}
printf("Done.\n");
return 0;
}
gcc -Wall -o flag_demo flag_demo.c && ./flag_demoPress
Ctrl+C — the handler fires, sets the flag, and the main loop picks it up cleanly.When a signal handler is running, the same signal is blocked by default (unless you use SA_NODEFER). If the signal arrives again while the handler is running, it is marked pending. But if it arrives a third time, it is silently dropped — signals do not queue up.
| Event | What Happens | Delivered? |
|---|---|---|
| Signal 1 arrives (handler not running) | Handler invoked immediately | Yes |
| Signal 2 arrives (handler is running) | Marked as pending — delivered after handler returns | Yes (once) |
| Signal 3+ arrives (handler still running) | Still only 1 pending — extras are dropped | No |
This program deliberately sleeps inside the handler so you can send multiple signals and observe that only one gets queued.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
static volatile sig_atomic_t count = 0;
static void sigusr1_handler(int sig)
{
count++;
printf("[Handler] Invocation #%d — sleeping 3 seconds inside handler\n",
(int)count);
/*
* We sleep inside the handler. During this 3 seconds,
* send SIGUSR1 multiple times with: kill -USR1 <pid>
* You will see that only ONE extra delivery is queued.
*/
sleep(3);
printf("[Handler] Waking up from invocation #%d\n", (int)count);
}
int main(void)
{
struct sigaction sa;
sa.sa_handler = sigusr1_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0; /* No SA_NODEFER — signal is blocked during handler */
sigaction(SIGUSR1, &sa, NULL);
printf("PID = %d. Send SIGUSR1 rapidly: kill -USR1 %d\n",
(int)getpid(), (int)getpid());
printf("Watch: no matter how many you send, at most 2 handler calls happen.\n");
/* Wait for signals */
while (1) pause();
return 0;
}
for i in $(seq 1 10); do kill -USR1 <pid>; doneYou’ll see the handler runs at most twice regardless of how many signals you sent.
When your main loop uses select() or poll() and can’t periodically check a flag, the handler writes one byte to a pipe. The read-end of the pipe is added to the monitored fds — this is the self-pipe trick.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/select.h>
#include <fcntl.h>
#include <string.h>
static int pipe_fds[2]; /* pipe_fds[0] = read end, pipe_fds[1] = write end */
static void sigint_handler(int sig)
{
/*
* write() is async-signal-safe — safe to call here.
* We write exactly 1 byte to wake up select().
*/
char byte = 1;
write(pipe_fds[1], &byte, 1);
}
int main(void)
{
/* Create the self-pipe */
if (pipe(pipe_fds) == -1) { perror("pipe"); return 1; }
/* Make write end non-blocking to avoid deadlock in handler */
int flags = fcntl(pipe_fds[1], F_GETFL);
fcntl(pipe_fds[1], F_SETFL, flags | O_NONBLOCK);
/* Install handler */
struct sigaction sa;
sa.sa_handler = sigint_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGINT, &sa, NULL);
printf("PID=%d. Waiting in select()... Press Ctrl+C\n", (int)getpid());
while (1) {
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(pipe_fds[0], &rfds); /* monitor the pipe read end */
int ret = select(pipe_fds[0] + 1, &rfds, NULL, NULL, NULL);
if (ret == -1) { perror("select"); break; }
if (FD_ISSET(pipe_fds[0], &rfds)) {
char buf[16];
read(pipe_fds[0], buf, sizeof(buf)); /* drain the pipe */
printf("\n[Main] Woken by SIGINT via self-pipe. Exiting cleanly.\n");
break;
}
}
close(pipe_fds[0]);
close(pipe_fds[1]);
return 0;
}
write() is async-signal-safe. The single byte wakes select() without any unsafe function calls in the handler. This is the standard pattern for event-driven servers.volatile sig_atomic_t flag; main loop checks it. Use when the main loop can poll. Pattern 2 (cleanup + exit): handler does minimal cleanup then terminates or does nonlocal goto. Use when you need to abort on a fatal signal like SIGSEGV.SA_NODEFER disables this — the signal is not blocked during the handler. Useful when you want a handler to be re-entrant (e.g., a handler that uses setjmp/longjmp and must restart immediately).select()/poll(), it cannot check a flag. The self-pipe trick solves this: the signal handler writes one byte to a pipe’s write end (safe — write() is async-signal-safe), and the pipe’s read end is included in the monitored fd set. The write wakes select(), allowing the main loop to respond to the signal.printf() uses internal buffered I/O data structures (from the stdio library) that are not async-signal-safe. If the signal interrupts the main program while it is inside printf(), and the handler also calls printf(), those internal structures can become corrupted. Use write() (async-signal-safe) for output from handlers.waitpid() with WNOHANG until it returns 0, to reap all terminated children in one handler invocation.