📚 The Standard Library
signal.h & setjmp.h: interrupts and teleports
▶ Open the interactive lesson — free, no signupPressing 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:
| signal | when | default action |
|---|---|---|
SIGINT | user presses Ctrl+C | terminate |
SIGSEGV | invalid memory access (your old friend segfault) | terminate + core dump |
SIGFPE | fatal arithmetic error, e.g. integer division by zero | terminate + core dump |
SIGTERM | polite kill request (kill PID) | terminate |
SIGABRT | abort() — e.g. a failed assert | terminate + 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:
#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;
}$ 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:
#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;
}$ 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.