Frontend Labs
⋯

Part X — Signals, non-local jumps, and memory-mapped files

Three topics that didn't fit anywhere else in this guide because they're not "beginner," "advanced," or "hardware" specifically — they're general-purpose systems C that shows up in interviews, code reviews, and real production incidents regardless of what you're building. Unlike Part IX's kernel-space code, every example here is portable POSIX C, compiled and run on the same machine this whole guide was written on.

55. Signals: handling asynchronous events safely

55.1 What a signal actually is

A signal is the kernel interrupting your process's normal control flow to say "something happened," asynchronously — a user pressed Ctrl+C (SIGINT), a child process exited (SIGCHLD), your process wrote to a closed pipe (SIGPIPE), or it dereferenced bad memory (SIGSEGV). By default, most signals terminate the process (some also dump core); you can install a handler to run your own code instead.

55.2 sigaction, not signal — and why

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

static volatile sig_atomic_t shutdown_requested = 0;

void handle_sigint(int sig) {
    (void)sig;
    shutdown_requested = 1;   // only touch a sig_atomic_t here — nothing else is safe (55.3)
}

int main(void) {
    struct sigaction sa = {0};
    sa.sa_handler = handle_sigint;
    sigemptyset(&sa.sa_mask);   // don't block any additional signals while this handler runs
    sa.sa_flags = 0;

    if (sigaction(SIGINT, &sa, NULL) != 0) { perror("sigaction"); return 1; }

    while (!shutdown_requested) {
        printf("working...\n");
        sleep(1);
    }
    printf("shutting down cleanly\n");
    return 0;
}

The older signal() function still exists but its behavior around whether a handler stays installed after firing, and whether interrupted syscalls auto-restart, historically differed between BSD and System V — sigaction pins down every one of those choices explicitly (sa_flags), which is why every serious codebase uses it instead. This mirrors §2.2's advice to pin -std= explicitly rather than rely on "whatever the default happens to be": signal()'s portability gaps are exactly that kind of implicit, compiler/platform-dependent default.

55.3 Async-signal-safety: the rule that makes most of Part II illegal

A handler can run at any point — including in the middle of malloc updating its internal free list, or printf mid-write to a partially-flushed buffer. If the handler itself then calls malloc or printf, it can deadlock against the very state the interrupted call was in the middle of modifying. POSIX defines a short list of async-signal-safe functions safe to call from a handler (write, _exit, sig* functions, and a few others) — malloc, free, printf, and most of the C standard library are not on that list.

// BAD — printf is not async-signal-safe; can deadlock or corrupt output
void handle_sigint_bad(int sig) { (void)sig; printf("caught!\n"); }

// GOOD — write() to a raw fd is async-signal-safe; sig_atomic_t needs no library call at all
void handle_sigint_good(int sig) {
    (void)sig;
    const char msg[] = "caught!\n";
    write(STDOUT_FILENO, msg, sizeof msg - 1);
}

The pattern from 55.2 — set a volatile sig_atomic_t flag in the handler, check it in your normal control flow — sidesteps the whole problem: the handler does the absolute minimum, and all real work happens back in ordinary, non-signal context. This is precisely Part IX §52.2's "top half does the minimum, defer the real work" rule, applied to signals instead of hardware interrupts — the same constraint, the same fix, a different trigger.

55.4 Reaping children with SIGCHLD

#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

void reap_children(int sig) {
    (void)sig;
    int saved_errno = errno;          // waitpid can clobber errno; the handler must restore it
    pid_t pid;
    while ((pid = waitpid(-1, NULL, WNOHANG)) > 0) {
        // a real handler would just record `pid` somewhere async-signal-safe;
        // it must NOT log/printf here (55.3)
    }
    (void)pid;
    errno = saved_errno;
}

int main(void) {
    struct sigaction sa = {0};
    sa.sa_handler = reap_children;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;   // let interrupted syscalls (e.g. a blocking read) auto-retry
    sigaction(SIGCHLD, &sa, NULL);

    for (int i = 0; i < 3; i++) {
        if (fork() == 0) { _exit(0); }   // each child exits immediately
    }
    sleep(1);   // give SIGCHLD handlers time to reap all three before main exits
    printf("parent done\n");
    return 0;
}

Without a SIGCHLD handler (or an explicit waitpid call), an exited child becomes a zombie — gone from execution but still occupying a process-table slot until its parent collects its exit status. Section 26's mini-shell called waitpid synchronously after every fork, which works for a shell that blocks on each foreground command; a server that spawns background workers needs exactly this asynchronous SIGCHLD-driven reaping instead, since it can't afford to block waiting for any one child.

55.5 War story, and a real technique to know (not to casually run)

Installing a handler for SIGSEGV/SIGBUS paired with sigsetjmp/ siglongjmp (Section 56.3) — turning a hardware fault into a recoverable error instead of a crash — is a genuine production technique: it's how some embedded-scripting-language sandboxes and guard-page-based memory checkers turn an out-of-bounds access into a catchable error rather than taking the whole process down. It's also exactly the kind of code that behaves differently across environments in ways that matter: on this guide's own development machine, deliberately triggering a SIGSEGV inside a sandboxed shell caused the process to hang rather than reach the handler — almost certainly the OS's own crash-reporting/debugging hooks intercepting the fault first, ahead of the process's own handler. The lesson isn't "don't do this" — it's test signal-based fault recovery on a plain terminal on real hardware, never inside a CI runner or sandboxed environment, because the exact thing you're trying to observe (a raw hardware trap reaching your handler) is precisely what crash-reporting tooling is also trying to intercept first.


Generated from real-life-c-guide.md · 8 Oct 2026 16:36