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:

FieldPurpose
sa_handlerPointer to handler function, SIG_DFL, or SIG_IGN
sa_maskAdditional signals to block during handler execution
sa_flagsBehavior flags (see below)
sa_sigactionExtended handler (used with SA_SIGINFO)

Common flags:

FlagEffect
SA_RESTARTAuto-restart interrupted system calls
SA_NOCLDSTOPDo not deliver SIGCHLD when child stops (only on exit)
SA_SIGINFOUse sa_sigaction instead of sa_handler
SA_RESETHANDReset 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 remove SA_RESTART, recompile, and press Ctrl+C. The read returns EINTR.

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 interrupts printf() in the main program (both try to acquire the stdio lock). Use write() 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_t is guaranteed to be atomically readable/writable, but it is NOT a general-purpose atomic type. It is typically just an int. 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:

OperationMeaning
SIG_BLOCKAdd signals to the mask
SIG_UNBLOCKRemove signals from the mask
SIG_SETMASKReplace 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: snprintf is technically not on the async-signal-safe list. For production code, format the output manually using write() 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's Signals::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 sigprocmask via nix::sys::signal::sigprocmask(). The API mirrors C: create a SigSet, add signals to it, and call sigprocmask(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 -ERESTARTSYS to allow the system call to be restarted. Driver authors must handle this return code in any sleeping function.

Knowledge Check

  1. What happens if you call printf() inside a signal handler that interrupts another printf() call in the main program?

  2. What does SA_RESTART do, and when would you want to omit it?

  3. 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 use sigaction().

  • 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(), and select() must check for EINTR and retry manually.

  • Using complex types in handlers: Only volatile sig_atomic_t is 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.