Signal Handlers and Masks
The previous chapter introduced signals. This chapter is about controlling them
properly: installing handlers with sigaction(), restricting what runs inside
a handler, and using signal masks to create critical sections where signals
are deferred.
sigaction(): The Proper Way
sigaction() replaces signal() with well-defined, portable behavior. It does
not reset the handler after delivery, it lets you control which signals are
blocked during handler execution, and it provides additional flags.
/* sigaction_basic.c */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
static volatile sig_atomic_t got_int = 0;
static void handler(int sig)
{
(void)sig;
got_int = 1;
}
int main(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGINT, &sa, NULL) < 0) {
perror("sigaction");
return 1;
}
printf("PID %d: Press Ctrl+C\n", getpid());
while (!got_int) {
printf("Waiting...\n");
sleep(2);
}
printf("Caught SIGINT, exiting gracefully.\n");
return 0;
}
The struct sigaction fields:
| Field | Purpose |
|---|---|
sa_handler | Pointer to handler function, SIG_DFL, or SIG_IGN |
sa_mask | Additional signals to block during handler execution |
sa_flags | Behavior flags (see below) |
sa_sigaction | Extended handler (used with SA_SIGINFO) |
Common flags:
| Flag | Effect |
|---|---|
SA_RESTART | Auto-restart interrupted system calls |
SA_NOCLDSTOP | Do not deliver SIGCHLD when child stops (only on exit) |
SA_SIGINFO | Use sa_sigaction instead of sa_handler |
SA_RESETHAND | Reset to SIG_DFL after one delivery (like old signal()) |
SA_RESTART: Restarting Interrupted System Calls
Without SA_RESTART, a caught signal causes blocking calls like read() to
return -1 with errno == EINTR. With it, the kernel restarts the call
automatically.
/* sa_restart.c */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
static void handler(int sig)
{
(void)sig;
const char msg[] = "[signal caught]\n";
write(STDERR_FILENO, msg, sizeof(msg) - 1);
}
int main(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
/* Try with and without SA_RESTART */
sa.sa_flags = SA_RESTART; /* Comment this out to see EINTR */
sigaction(SIGINT, &sa, NULL);
printf("PID %d: Type something (Ctrl+C to test):\n", getpid());
char buf[256];
ssize_t n = read(STDIN_FILENO, buf, sizeof(buf) - 1);
if (n < 0) {
if (errno == EINTR)
printf("read() interrupted by signal (EINTR)\n");
else
perror("read");
} else {
buf[n] = '\0';
printf("Read: %s", buf);
}
return 0;
}
Try It: Compile and run with
SA_RESTART. Press Ctrl+C, then type something. The read completes normally. Now removeSA_RESTART, recompile, and press Ctrl+C. The read returnsEINTR.
Async-Signal-Safe Functions: The Short List
Inside a signal handler, you can only call async-signal-safe functions. These are functions guaranteed to work correctly even when interrupting arbitrary code.
The POSIX-mandated safe list (selected):
_exit write read open
close signal sigaction sigprocmask
sigaddset sigdelset sigemptyset sigfillset
kill raise alarm pause
fork execve waitpid getpid
Not safe (most common traps):
printf fprintf malloc free
syslog strerror localtime gmtime
pthread_* exit atexit
Caution: Calling
printf()from a signal handler is undefined behavior. It can deadlock if the signal interruptsprintf()in the main program (both try to acquire the stdio lock). Usewrite()with a fixed-size buffer for any handler output.
/* safe_handler_output.c */
#include <signal.h>
#include <unistd.h>
#include <string.h>
static void handler(int sig)
{
/* Only async-signal-safe calls here */
const char msg[] = "Caught SIGINT\n";
write(STDOUT_FILENO, msg, strlen(msg));
(void)sig;
}
int main(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
pause(); /* Wait for signal */
return 0;
}
Why printf in a Handler Is Undefined Behavior
Here is the scenario:
Main program Signal handler
| |
printf("Working...\n") |
| |
+-- acquires stdio lock |
| |
+-- SIGINT arrives here! |
| |
| printf("Caught!\n")
| |
| +-- tries to acquire stdio lock
| |
| +-- DEADLOCK (same thread holds lock)
The program hangs forever. This is not theoretical -- it happens in production.
sig_atomic_t: The Shared Flag
The only safe way to communicate between a handler and the main program is
through variables of type volatile sig_atomic_t.
/* sig_atomic_demo.c */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
static volatile sig_atomic_t signal_count = 0;
static void handler(int sig)
{
(void)sig;
signal_count++;
}
int main(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
printf("PID %d: Press Ctrl+C multiple times. Ctrl+\\ to quit.\n", getpid());
while (1) {
pause();
printf("Signal count: %d\n", (int)signal_count);
}
return 0;
}
Caution:
sig_atomic_tis guaranteed to be atomically readable/writable, but it is NOT a general-purpose atomic type. It is typically just anint. Only use it for simple flags and counters in signal handlers. For anything more complex, use signal masks or the self-pipe trick (next chapter).
Signal Masks: sigprocmask
A signal mask is a set of signals that are blocked (deferred) for the calling thread. Blocked signals stay pending until unblocked.
/* sigmask_demo.c */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
static void handler(int sig)
{
const char msg[] = "SIGINT delivered!\n";
write(STDOUT_FILENO, msg, strlen(msg));
(void)sig;
}
int main(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
/* Block SIGINT */
sigprocmask(SIG_BLOCK, &block_set, &old_set);
printf("SIGINT blocked. Press Ctrl+C now (within 5 seconds).\n");
sleep(5);
/* Check if SIGINT is pending */
sigset_t pending;
sigpending(&pending);
if (sigismember(&pending, SIGINT))
printf("SIGINT is pending (was sent while blocked).\n");
/* Unblock SIGINT -- pending signal will be delivered now */
printf("Unblocking SIGINT...\n");
sigprocmask(SIG_SETMASK, &old_set, NULL);
printf("After unblock.\n");
return 0;
}
$ ./sigmask_demo
SIGINT blocked. Press Ctrl+C now (within 5 seconds).
^C <-- pressed Ctrl+C during sleep
SIGINT is pending (was sent while blocked).
Unblocking SIGINT...
SIGINT delivered! <-- handler runs when unblocked
After unblock.
Signal mask operations:
| Operation | Meaning |
|---|---|
SIG_BLOCK | Add signals to the mask |
SIG_UNBLOCK | Remove signals from the mask |
SIG_SETMASK | Replace the entire mask |
Signal Mask (per-thread):
+---+---+---+---+---+---+---+---+---+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |...|
+---+---+---+---+---+---+---+---+---+
0 1 0 0 0 0 0 0 ...
^
|
SIGINT blocked (bit set = blocked)
Pending Signals:
+---+---+---+---+---+---+---+---+---+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |...|
+---+---+---+---+---+---+---+---+---+
0 1 0 0 0 0 0 0 ...
^
|
SIGINT pending (received while blocked)
The Critical Section Pattern
Block signals around code that must not be interrupted:
/* critical_section.c */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
static volatile sig_atomic_t got_signal = 0;
static void handler(int sig)
{
(void)sig;
got_signal = 1;
}
int main(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
sigset_t block, old;
sigemptyset(&block);
sigaddset(&block, SIGINT);
/* --- Critical section: SIGINT deferred --- */
sigprocmask(SIG_BLOCK, &block, &old);
printf("Updating shared data structure...\n");
sleep(3); /* Simulate long update */
printf("Update complete.\n");
sigprocmask(SIG_SETMASK, &old, NULL);
/* --- End critical section: pending SIGINT delivered here --- */
if (got_signal)
printf("Signal was deferred and delivered after critical section.\n");
return 0;
}
This pattern is essential for data structures that must be consistent. If a signal handler touches the same data, blocking the signal prevents corruption.
Blocking Signals During Handler Execution
The sa_mask field of struct sigaction specifies additional signals to block
while the handler is running. The caught signal is always blocked by default
(unless SA_NODEFER is set).
/* sa_mask_demo.c */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
static void int_handler(int sig)
{
(void)sig;
const char msg[] = "SIGINT handler start\n";
write(1, msg, strlen(msg));
sleep(3); /* SIGTERM blocked during this time via sa_mask */
const char msg2[] = "SIGINT handler end\n";
write(1, msg2, strlen(msg2));
}
static void term_handler(int sig)
{
(void)sig;
const char msg[] = "SIGTERM handler\n";
write(1, msg, strlen(msg));
}
int main(void)
{
struct sigaction sa_int, sa_term;
memset(&sa_term, 0, sizeof(sa_term));
sa_term.sa_handler = term_handler;
sigemptyset(&sa_term.sa_mask);
sa_term.sa_flags = 0;
sigaction(SIGTERM, &sa_term, NULL);
memset(&sa_int, 0, sizeof(sa_int));
sa_int.sa_handler = int_handler;
sigemptyset(&sa_int.sa_mask);
sigaddset(&sa_int.sa_mask, SIGTERM); /* Block SIGTERM during SIGINT handler */
sa_int.sa_flags = 0;
sigaction(SIGINT, &sa_int, NULL);
printf("PID %d: Press Ctrl+C, then quickly send SIGTERM\n", getpid());
printf(" kill -TERM %d\n", getpid());
for (;;) pause();
return 0;
}
Try It: Run the program. Press Ctrl+C. While "SIGINT handler start" is displayed, send
kill -TERM <pid>from another terminal. Notice that SIGTERM is delivered only after the SIGINT handler finishes.
SA_SIGINFO: Extended Signal Information
With SA_SIGINFO, the handler receives a siginfo_t struct with details about
who sent the signal and why.
/* siginfo_demo.c */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
static void handler(int sig, siginfo_t *info, void *ucontext)
{
(void)ucontext;
char buf[128];
int len = snprintf(buf, sizeof(buf),
"Signal %d from PID %d (uid %d)\n",
sig, info->si_pid, info->si_uid);
write(STDOUT_FILENO, buf, len);
}
int main(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO;
sigaction(SIGUSR1, &sa, NULL);
printf("PID %d: send SIGUSR1 to me\n", getpid());
printf(" kill -USR1 %d\n", getpid());
pause();
return 0;
}
Caution:
snprintfis technically not on the async-signal-safe list. For production code, format the output manually usingwrite()and integer-to- string conversion. The example above is simplified for clarity.
Rust: Safe Signal Handling with signal-hook
The signal-hook crate provides multiple safe patterns:
// signal_hook_iterator.rs // Cargo.toml: // [dependencies] // signal-hook = "0.3" use signal_hook::consts::{SIGINT, SIGTERM, SIGUSR1}; use signal_hook::iterator::Signals; use std::thread; use std::time::Duration; fn main() { let mut signals = Signals::new(&[SIGINT, SIGTERM, SIGUSR1]) .expect("register signals"); // Spawn a thread to handle signals let handle = thread::spawn(move || { for sig in signals.forever() { match sig { SIGINT => { println!("Received SIGINT (Ctrl+C)"); println!("Shutting down..."); return; } SIGTERM => { println!("Received SIGTERM"); println!("Shutting down..."); return; } SIGUSR1 => { println!("Received SIGUSR1 -- reloading config"); } _ => unreachable!(), } } }); println!("PID {}: Running... (Ctrl+C to stop)", std::process::id()); println!(" kill -USR1 {} to reload", std::process::id()); // Main work loop loop { if handle.is_finished() { break; } println!("Working..."); thread::sleep(Duration::from_secs(2)); } handle.join().expect("signal thread panicked"); println!("Clean shutdown complete."); }
Rust Note:
signal-hook'sSignals::forever()uses a self-pipe internally. The signal handler writes a byte to a pipe; the iterator reads from it. This converts asynchronous signals into synchronous iteration, completely avoiding async-signal-safety issues. All the complex, unsafe C patterns become a simple for loop.
Rust Note: The nix crate also exposes
sigprocmaskvianix::sys::signal::sigprocmask(). The API mirrors C: create aSigSet, add signals to it, and callsigprocmask(SigmaskHow::SIG_BLOCK, Some(&set)). The old mask is returned for later restoration.
Driver Prep: Kernel signal handling is different -- kernel code does not receive signals. Instead, the kernel checks
signal_pending(current)when returning from a blocking operation. If a signal is pending, the kernel returns-ERESTARTSYSto allow the system call to be restarted. Driver authors must handle this return code in any sleeping function.
Knowledge Check
-
What happens if you call
printf()inside a signal handler that interrupts anotherprintf()call in the main program? -
What does
SA_RESTARTdo, and when would you want to omit it? -
How do you temporarily block a signal, do some work, then unblock it so any pending instances are delivered?
Common Pitfalls
-
Using signal() instead of sigaction():
signal()has undefined reset behavior across platforms. Always usesigaction(). -
Calling malloc/free in handlers: Both use global state (the heap free list) and can deadlock or corrupt memory.
-
Not blocking signals during data structure updates: If a handler accesses shared data, block the signal during modifications in the main code.
-
Forgetting SA_RESTART: Without it, every
read(),write(),accept(), andselect()must check forEINTRand retry manually. -
Using complex types in handlers: Only
volatile sig_atomic_tis safe for communication between handlers and the main program. -
Not saving errno: Signal handlers can be called between a system call failing and the program reading
errno. Save and restore it.