Linux System Programming · intermediate · ~12 min

signal() vs sigaction() — and why you should always use sigaction()

Pick the right API for installing a signal handler.

Overview

sigaction() is the modern replacement for the older signal() function. Both install a signal handler (a function that runs when your program receives a signal like Ctrl+C).

The difference is control:

  • sigaction() takes a struct with explicit flags. You opt in to behaviors such as SA_RESTART and SA_SIGINFO.
  • signal() has portability traps ("footguns") that differ between systems.

Rule: never use signal() in new code. Use sigaction().

Syntax notes

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

struct sigaction {
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    /* ... */
};
  • signum — the signal to handle (for example, SIGINT).
  • act — the new handler settings to install.
  • oldact — if not NULL, receives the previous settings (useful for restoring them later).

Lesson

The legacy way: signal()

The old way to catch a signal is signal(signo, handler).

It works, but it has a serious trap. On some older Unix systems, the handler is reset to the default action after it fires once.

To keep catching the signal, you would have to re-install the handler from inside the handler itself. That creates a race window: if a second signal arrives before you re-install, it escapes your handler and may kill the program.

The safe way: sigaction()

POSIX (the portable Unix standard) defines a safer replacement: sigaction().

It takes a struct sigaction filled with explicit flags, so you control the behavior precisely.

Flags you'll meet

  • SA_RESTART — automatically restart blocking system calls (read, accept, and similar) instead of returning the EINTR ("interrupted") error.
  • SA_NOCLDSTOP — for SIGCHLD, deliver the signal only when a child exits, not when it stops or continues.
  • SA_SIGINFO — use the 3-argument sa_sigaction handler form, which receives a siginfo_t with extra detail (the sending process ID, the faulting address, and more).

The rule

Never use signal() in new code. Always use sigaction().

Code examples

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

static volatile sig_atomic_t got_int = 0;
static void on_int(int sig) { (void)sig; got_int = 1; }

int main(void) {
    struct sigaction sa = {0};
    sa.sa_handler = on_int;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;     /* don't break our read()s */
    if (sigaction(SIGINT, &sa, NULL) != 0) { perror("sigaction"); return 1; }

    while (!got_int) { pause(); }
    puts("caught SIGINT");
    return 0;
}

Common mistakes

  • Forgetting sigemptyset(&sa.sa_mask). The mask field is not reliably zero-initialized across libc versions. Always clear it explicitly before use.
  • Doing real work inside the handler. The handler should only set a flag. The main loop checks that flag and does the actual work.

Practice tasks

  1. Find some legacy code online that uses a signal() call. Replace it with an equivalent sigaction() call.
  2. Add the SA_RESTART flag.
  3. Observe that a blocking read no longer returns the EINTR error when a signal arrives.

Summary

  • sigaction() is the POSIX-standard, portable, race-free way to install a signal handler.
  • signal() can reset the handler to default after one use, opening a race window — avoid it in new code.
  • Flags like SA_RESTART, SA_NOCLDSTOP, and SA_SIGINFO give you precise control over handler behavior.
  • Always clear the mask with sigemptyset(&sa.sa_mask) before passing the struct.

Practice with these exercises