linux-sysprog · beginner · ~10 min

Catch SIGINT and set a flag

Install a sigaction-based handler that sets an async-signal-safe flag — the cornerstone of graceful shutdown.

Challenge

Catch Ctrl+C without killing your program

When a user presses Ctrl+C in your terminal, the kernel sends SIGINT to the foreground program. The default response is "die immediately." For a real program — one with open files, in-flight writes, or a server with connected clients — that's a recipe for corrupted state. You want to catch the signal, set a flag, finish what you're doing, then exit cleanly.

Task

Implement two pieces:

volatile sig_atomic_t g_caught_sigint = 0;
void install_sigint_handler(void);

install_sigint_handler must install a sigaction-based handler for SIGINT. When SIGINT is delivered (now or later), the handler sets g_caught_sigint = 1. The handler must do only that — no printf, no malloc.

Function signature

void install_sigint_handler(void);

Output

  • Calling install_sigint_handler once arms the handler.
  • After the first SIGINT delivery, g_caught_sigint is 1.
  • After a second SIGINT delivery, the handler still fires (it's not a one-shot).

Rules

  • Use sigaction(), not signal()signal()'s portability footguns are exactly why POSIX added the replacement.
  • Inside the handler, only touch async-signal-safe state. A volatile sig_atomic_t flag is the canonical primitive.
  • Don't change g_caught_sigint's declaration — the harness reads it.

Examples

Sequence After
install_sigint_handler() g_caught_sigint == 0
raise(SIGINT) g_caught_sigint == 1
reset to 0; raise(SIGINT) again g_caught_sigint == 1 (handler stays installed)

Edge cases

  • The handler must survive multiple deliveries. sigaction() (unlike legacy signal() on some BSDs) does this for free; don't reinstall inside the handler.
  • The handler may be interrupted in the middle of any code. Only set the flag.

Hints

  1. Conceptual: a signal handler interrupts your code at an arbitrary instruction. Anything more than setting one atomic flag is a bug waiting to happen.
  2. Implementation: build a struct sigaction sa = {0};, set sa.sa_handler, sigemptyset(&sa.sa_mask), then call sigaction(SIGINT, &sa, NULL).
  3. Common bug: forgetting to call sigemptyset on sa.sa_mask. The struct's sa_mask field is not reliably zero-initialised across libc versions.

Common mistakes

  • Using signal() — works in tests, breaks on the next platform.
  • Calling printf inside the handler — not async-signal-safe. Use write() if you must print, but for this exercise just set the flag.
  • Re-installing inside the handler. sigaction() doesn't need it.

Learning connection

This is the foundational pattern for every long-running C program: graceful shutdown. The same flag-then-react pattern shows up in nginx, systemd, every TCP server you'll write.

Why this matters

If you only learn one signal pattern, learn this one. Every long-running C program — daemon, server, batch job — needs it.

Input format

(no runtime input — the harness calls the function and raises SIGINT)

Output format

(no return; observable effect is the g_caught_sigint flag flipping to 1)

Constraints

Use sigaction, not signal. The handler must be tiny — set the flag, nothing else.

Starter code

#include <signal.h>
#include <stdio.h>
volatile sig_atomic_t g_caught_sigint = 0;
void install_sigint_handler(void) {
    /* TODO: install a handler that sets g_caught_sigint = 1 */
}

Common mistakes

Using legacy signal(). Forgetting sigemptyset(&sa.sa_mask). Doing real work (printf, malloc) inside the handler.

Edge cases to handle

Multiple SIGINTs in rapid succession — the handler stays installed; the flag stays set until your code clears it.

Background lessons

Up next

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