The C Path — learn C, visually

📚 The Standard Library

signal.h & setjmp.h: interrupts and teleports

⏱ 14 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

Pressing Ctrl+C doesn't just kill your program — it sends it a message the program can catch, which is how editors and servers manage to save your work before exiting instead of corrupting files. The "Segmentation fault" you've been seeing since Part 2 is literally one of these messages arriving. Today you learn to intercept them — plus C's strangest trick: a jump that teleports straight out of deeply nested function calls.

Two of the standard library's strangest headers. signal.h lets the outside world interrupt your program mid-statement; setjmp.h lets your program teleport back through the call stack — the tower of function calls currently in progress — ignoring every return in between. Both are powerful, both are minefields — and both are worth understanding even if you use them rarely.

Signals: asynchronous interruptions

A signal is a tiny notification delivered to your process — by the OS, another process, or itself. When one arrives, normal execution is suspended wherever it happens to be, a handler function runs, and then execution resumes. The classics:

signalwhendefault action
SIGINTuser presses Ctrl+Cterminate
SIGSEGVinvalid memory access (your old friend segfault)terminate + core dump
SIGFPEfatal arithmetic error, e.g. integer division by zeroterminate + core dump
SIGTERMpolite kill request (kill PID)terminate
SIGABRTabort() — e.g. a failed assertterminate + core dump

signal(SIGINT, handler) installs your function; raise(SIGINT) sends a signal to yourself; signal(SIGINT, SIG_IGN) ignores one, SIG_DFL restores the default.

🧠 Checkpoint: What does pressing Ctrl+C in a terminal actually do to the foreground program?

  • Kills it directly, no questions asked
  • Sends it SIGINT, whose default action is termination — but a handler can intercept it
  • Sends SIGSEGV
  • Closes its stdin
Show answer

Sends it SIGINT, whose default action is termination — but a handler can intercept it — Ctrl+C makes the terminal send SIGINT. Untouched, the default action terminates the process — but a program that installs a handler can finish its current job, save state, and exit cleanly instead. That’s the graceful-shutdown pattern below.

The one correct handler pattern

Here's the hard rule: your handler may run between any two instructions — possibly in the middle of a printf or a malloc that's holding an internal lock. Calling those from the handler can deadlock or corrupt state. The C standard blesses almost nothing inside a handler: essentially, set a flag of type volatile sig_atomic_t and get out. The main loop notices the flag at its leisure:

graceful.c
#include <stdio.h>
#include <signal.h>

volatile sig_atomic_t stop_requested = 0;

void on_sigint(int sig) {
    (void)sig;              /* unused parameter        */
    stop_requested = 1;     /* the ONLY safe action:   */
}                           /* set a flag and return   */

int main(void) {
    signal(SIGINT, on_sigint);
    printf("working... press Ctrl+C to stop gracefully\n");

    unsigned long processed = 0;
    while (!stop_requested) {
        processed++;        /* pretend this is real work */
    }

    /* normal code again — printf is fine HERE */
    printf("\ncleanly stopped after %lu items\n", processed);
    return 0;
}
terminal
$ gcc graceful.c -o graceful && ./graceful
working... press Ctrl+C to stop gracefully
^C
cleanly stopped after 1846503219 items
# no half-written files, no lost buffers — WE chose when to exit

Why that exact type? volatile (Part 3) forbids the compiler from caching the flag in a register — it must re-read memory each loop, or it would never see the handler's write. sig_atomic_t guarantees reads and writes happen in one indivisible step, so the handler can't observe a half-written value.

💀

Don't printf in a signal handler. It may seem to work — until the signal lands while printf holds its internal buffer lock, and your program deadlocks or scrambles output. Same for malloc, free, exit, and most of the library. POSIX defines a list of "async-signal-safe" functions (write is on it); standard C promises even less. Flag-and-return is the only fully portable pattern.

🧠 Checkpoint: Why must the shutdown flag be volatile sig_atomic_t rather than plain int?

  • Signals can only write to that type
  • int is too small to store a signal number
  • volatile forces the loop to re-read memory each iteration, and sig_atomic_t makes each access indivisible
  • It’s just convention — int works identically
Show answer

volatile forces the loop to re-read memory each iteration, and sig_atomic_t makes each access indivisible — Without volatile, the optimizer may hoist the flag into a register — while(!stop) becomes an infinite loop that never sees the handler’s write. And a type with non-atomic access could be caught half-updated by a signal arriving mid-write. The combination closes both holes.

setjmp / longjmp: the non-local goto

setjmp(buf) bookmarks the current execution point (stack position, registers) and returns 0. Later — possibly many function calls deeper — longjmp(buf, v) teleports back to that bookmark: every intervening stack frame is abandoned, and the program resumes as if setjmp had just returned v (forced to 1 if you pass 0). One setup, two returns:

This spot has an interactive flow widget — open the interactive lesson to play with it.

This is C's "mini exception mechanism" — libraries like libpng and Lua use it to escape from deep errors without threading error codes through every call:

miniexcept.c
#include <stdio.h>
#include <setjmp.h>

static jmp_buf error_jump;

double safe_div(double a, double b) {
    if (b == 0.0)
        longjmp(error_jump, 1);      /* "throw"           */
    return a / b;
}

double average_rate(double dist, double time) {
    return safe_div(dist, time);     /* deep call chain   */
}

int main(void) {
    if (setjmp(error_jump) != 0) {   /* "catch"           */
        fprintf(stderr, "math error — recovered\n");
        return 1;
    }
    printf("rate = %.1f\n", average_rate(100.0, 2.0));
    printf("rate = %.1f\n", average_rate(100.0, 0.0));
    printf("never reached\n");
    return 0;
}
terminal
$ gcc miniexcept.c -o miniexcept && ./miniexcept
rate = 50.0
math error — recovered
# the second call never returned: longjmp leapt over
# average_rate AND safe_div straight back into main
⚠️

The volatile locals caveat: after longjmp lands, any local variable of the setjmp function that was modified after setjmp and isn't declared volatile has an indeterminate value — it may have lived in a register that the jump rewound. Rule: locals you change between setjmp and longjmp and read afterwards must be volatile.

🧠 Checkpoint: After a longjmp back into main, which local of main has a guaranteed value?

  • All of them
  • None of them
  • Those unchanged since setjmp, plus changed ones declared volatile
  • Only global variables
Show answer

Those unchanged since setjmp, plus changed ones declared volatile — The jump may rewind registers to their setjmp-time snapshot. Locals modified after setjmp might live in those registers — unless volatile forces them to memory. Unchanged locals and volatile-qualified ones are safe; the rest are indeterminate.

Why it's a last resort

longjmp skips every cleanup on the way down: free calls never happen (leaks), fclose never runs, locks stay locked. Jumping into a function that already returned is UB. And code with hidden teleports is simply hard to read. Prefer returning error codes; reserve setjmp/longjmp for genuinely exceptional escapes — a parser bailing out of deep recursion, a library protecting its user from internal failure — where it earns its keep.

This spot has an interactive editor widget — open the interactive lesson to play with it.

One mystery remains from lesson one of this part: how can printf accept two arguments, or five, or ten? Time to write variadic functions ourselves.

▶ Practice this lesson interactively (with live gcc)