linux-sysprog · beginner · ~10 min

Translate legacy signal() into sigaction()

Refactor a legacy `signal()` install into the modern `sigaction()` install — race-free and explicit.

Challenge

Upgrade a legacy signal handler

Old C code (pre-2010) used the simple signal(SIGTERM, my_handler). It works — until it doesn't. On some platforms signal() resets the handler back to the default after one delivery, and on most systems it never sets up the per-signal blocking mask explicitly. POSIX added sigaction() to fix both problems.

Real-world frame

This is the kind of one-line refactor you'll do in every existing C codebase older than ~10 years. The original code works in tests; the upgrade makes it portable, race-free, and explicit about its choices.

Task

Implement:

int install_sigterm(void (*handler)(int));

It should install handler as the SIGTERM handler using sigaction(), with:

  • sa_mask empty (don't block other signals during handling).
  • sa_flags = SA_RESTART (interrupted syscalls auto-restart instead of returning EINTR).

Return 0 on success, -1 on failure.

Function signature

int install_sigterm(void (*handler)(int));

Rules

  • Zero-init the struct sigaction (memset, or = {0} and then sigemptyset(&sa.sa_mask)).
  • Use sigemptyset(&sa.sa_mask) — the field is not reliably zero across libc versions.
  • Don't call signal() anywhere in your solution.

Examples

Action Result
install_sigterm(my_handler) returns 0; SIGTERM now goes to my_handler
raise(SIGTERM) after install my_handler runs
Re-install same handler still works (sigaction is idempotent)

Edge cases

  • Calling install twice with the same handler should still succeed.
  • A NULL handler should probably be rejected (the harness doesn't test this; up to your defensive style).

Hints

  1. Conceptual: sigaction() is the explicit, race-free replacement for signal(). Each field of the struct is an explicit choice; nothing is implicit.
  2. Implementation: struct sigaction sa; memset(&sa, 0, sizeof sa); sa.sa_handler = handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; return sigaction(SIGTERM, &sa, NULL) == 0 ? 0 : -1;
  3. Common bug: skipping sigemptyset. The mask field has arbitrary bits and the kernel may refuse the call, or block signals you didn't intend to block.

Common mistakes

  • Using signal() "because it's shorter" — it isn't portable.
  • Forgetting SA_RESTART and then being surprised that blocking syscalls return -1 with errno == EINTR.
  • Returning sigaction()'s raw return value (which is 0 or -1) instead of normalising to 0 / -1.

Learning connection

After this you can confidently refactor any pre-2010 C codebase to use sigaction. Pair it with safe-signal-handlers to round out the basics.

Why this matters

A one-line refactor that pays back forever: portable, explicit, no surprise reset to default.

Input format

One function pointer (the handler).

Output format

0 on success, -1 on failure.

Constraints

Use sigaction(). Zero the struct. sa_mask = empty. SA_RESTART set.

Starter code

#include <signal.h>
int install_sigterm(void (*handler)(int)) {
    /* TODO: use sigaction(), not signal() */
    return -1;
}

Common mistakes

Using signal() (the legacy API you're meant to replace). Skipping sigemptyset. Forgetting SA_RESTART.

Edge cases to handle

Re-installing the same handler. NULL handler (defensive style: reject).

Background lessons

Up next

Solve this exercise in the browser editor — compile and run against the test harness, no setup required.