Signals and signal handlers
Handle Unix signals in C — installing signal handlers, the restrictions on async-signal-safe code, and using sigaction for reliable signal handling.
- Explain what a signal is and when the OS sends one
- Install a signal handler using sigaction
- Apply the async-signal-safe rules to handler code
- Use a volatile sig_atomic_t flag to communicate from a signal handler to the main loop
A signal is an asynchronous notification delivered by the OS to a process. When you press Ctrl-C in a terminal, the OS sends SIGINT to the foreground process. When a program accesses an invalid memory address, the OS sends SIGSEGV. When a child process exits, the parent receives SIGCHLD. Understanding signals is essential for writing robust Unix tools and daemons.
Common signals
| Signal | Default action | Common cause |
|---|---|---|
SIGINT (2) | Terminate | Ctrl-C |
SIGTERM (15) | Terminate | kill pid |
SIGKILL (9) | Terminate (uncatchable) | kill -9 pid |
SIGSEGV (11) | Core dump | Null/invalid pointer |
SIGCHLD (17) | Ignore | Child process exited |
SIGHUP (1) | Terminate | Terminal closed; reload config |
SIGPIPE (13) | Terminate | Write to closed pipe |
SIGALRM (14) | Terminate | Timer expired (from alarm()) |
sigaction — the reliable way to install a handler
The older signal() function has platform-specific behaviour. sigaction() is the POSIX-standard, reliable way:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
static volatile sig_atomic_t shutdown_requested = 0;
static void handle_sigint(int signum) {
(void)signum;
shutdown_requested = 1; /* set a flag; do nothing else */
}
int main(void) {
struct sigaction sa = {0};
sa.sa_handler = handle_sigint;
sigemptyset(&sa.sa_mask); /* do not block additional signals during handler */
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
printf("Press Ctrl-C to exit\n");
while (!shutdown_requested) {
printf("Running...\n");
sleep(1);
}
printf("Shutting down cleanly\n");
return 0;
}The async-signal-safe requirement
Signal handlers can interrupt any code at any moment — including code inside malloc, printf, or your own mutex operations. These functions are not async-signal-safe — calling them from a signal handler can deadlock or corrupt data.
The only safe operations inside a signal handler:
- Set a
volatile sig_atomic_tflag. - Call a small set of explicitly async-signal-safe functions (
write,_exit,sem_post,sigaction). - Perform async-signal-safe arithmetic (integer operations on
sig_atomic_t).
The printf and fprintf functions are not safe to call from a signal handler. Use write(STDOUT_FILENO, msg, len) for output inside a handler.
static void safe_handler(int sig) {
(void)sig;
const char msg[] = "Signal received\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1); /* async-signal-safe */
shutdown_requested = 1;
}Self-pipe trick for integrating signals with event loops
When a program uses select/poll/epoll for I/O, signals interrupt the call and cause it to return EINTR. The self-pipe trick converts the signal into a readable file descriptor event:
int pipefd[2];
pipe(pipefd); /* create a pipe */
/* Signal handler: write one byte to the write end */
static void handler(int sig) {
(void)sig;
char byte = 0;
write(pipefd[1], &byte, 1);
}
/* Event loop: add pipefd[0] to your select/poll set */
/* When pipefd[0] is readable, a signal arrived */This makes signals composable with I/O in event-driven programs.
SIGPIPE and network programming
When you write to a socket after the remote end has closed, the OS sends SIGPIPE. The default action is to terminate the process — which is usually not what you want. The common fix:
/* Ignore SIGPIPE -- write() will return EPIPE error instead */
struct sigaction sa = {0};
sa.sa_handler = SIG_IGN;
sigaction(SIGPIPE, &sa, NULL);Then check for EPIPE in the write return value.
Signals and threads. In a multithreaded program, signals are delivered to one thread (either a specific thread or an arbitrary one). Blocking signals in worker threads and handling them only in a dedicated signal-handling thread (using sigwait) is the cleanest approach for multithreaded servers.
Where to go next
Next: fork and exec — creating child processes to run other programs, and the fork/exec/wait lifecycle.
Challenge: Thread-safe bounded queue
Implement a thread-safe bounded queue in C using mutexes and condition variables — supporting multiple producers and multiple consumers.
fork and exec
Create child processes with fork, run programs with exec, and wait for children with waitpid — the Unix process lifecycle in C.