# Unix Signals: The API Everyone Uses and Nobody Uses Safely

> Signals interrupt your program between any two instructions and run your handler in the wreckage. Why almost everything is illegal inside a handler, what async-signal-safe really means, and the self-pipe/signalfd escape hatches.

URL: https://harshith.in/blog/unix-signals-sharp-edges/
Author: Harshith Sunku (Staff Engineer) — https://harshith.in/about/
Tags: linux, systems, concurrency

Signals are the oldest IPC mechanism in Unix and the most casually misused.
Every long-running daemon handles SIGTERM; most tutorials show a handler that
calls `printf` and `exit` — and both calls are undefined behavior. The bugs
this breeds are the worst kind: deadlocks and corruption that strike once a
month under load, in code that reads perfectly naturally. The reason is one
brutal fact about *when* a handler runs.

## Between Any Two Instructions

A signal handler doesn't run "when convenient." The kernel delivers it on the
way back to userspace — which means your main thread could be *anywhere*:

```text
  main thread:  malloc() → takes heap lock → updating free lists...
                                   ▲
                              SIGTERM arrives
                                   │
  handler runs NOW, on this stack, with the heap lock held
  handler calls printf → printf calls malloc → malloc wants the lock
  → deadlock, single-threaded, no second thread required
```

That's the whole problem in one picture. The handler is a forced function call
injected into a suspended computation whose invariants are mid-flight. Any
function that touches shared state — locks, the heap, stdio buffers, errno —
is a landmine. POSIX's answer is the **async-signal-safe list**
(`man 7 signal-safety`): a short whitelist — `write`, `_exit`, `kill`,
`sigaction`, and friends — of functions guaranteed reentrant-or-atomic.
`printf`, `malloc`, `syslog`, and virtually all of your own code are not on
it. (Even `errno` must be saved and restored in a handler that makes
syscalls — the interrupted code was allowed to be mid-check.)

## The Pattern That Works

The professional answer is to do *nothing* in the handler:

```c
static volatile sig_atomic_t got_sigterm;
static void on_term(int sig) { got_sigterm = 1; }   /* the whole handler */
```

…and let the main loop notice the flag. `volatile sig_atomic_t` is the one
type the C standard blesses for this. But there's a hole: if the main loop is
parked in `poll()` for the next hour, a flag nobody looks at is a shutdown
that never happens. Signal delivery *does* interrupt blocking syscalls
(EINTR — more on it below), but there's a race: the signal can land just
*before* the poll call, flag set, poll blocks anyway.

Two standard escapes:

- **The self-pipe trick** (portable): handler does
  `write(pipe_wr, "x", 1)` — async-signal-safe — and the pipe's read end sits
  in your poll set. Signal becomes I/O; the race vanishes because the byte
  waits in the pipe.
- **signalfd / EVFILT_SIGNAL** (Linux/BSD): skip handlers entirely — block the
  signals, get an fd that reads `struct signalfd_siginfo`. Signals become
  ordinary events in the loop, which is exactly where they belong in any
  event-driven program. (`sigwaitinfo` in a dedicated thread is the
  thread-pool flavor of the same move.)

## EINTR and SA_RESTART, the Perennial Papercut

When a signal interrupts a blocking syscall, the call can fail with `EINTR` —
and code that doesn't expect `read` to "fail for no reason" breaks in ways
that only manifest once handlers get installed (a classic: adding a harmless
SIGCHLD handler to a working program and watching unrelated I/O paths start
erroring). Installing with `SA_RESTART` makes *most* syscalls resume
automatically — but not all: `poll`, `select`, `epoll_wait`, and
`sleep`-family calls return EINTR regardless. Robust code wraps the syscall
in a retry loop and moves on with life; robust *reviewers* grep for bare
`read(` in daemons.

Two more field notes. **SIGCHLD**: children must be reaped (`waitpid` with
`WNOHANG` in a loop — signals don't queue, five deaths may arrive as one
signal) or they accumulate as zombies; the loop matters more than the
handler. **SIGKILL/SIGSTOP** can't be caught — by design, so `kill -9` is a
contract, not a request; and a process in uninterruptible D-state (stuck NFS,
dead disk) will ignore even that until the I/O aborts, which is why kill -9
"not working" is a storage symptom, not a signals one.

## Takeaways

- Handlers run between arbitrary instructions with your program's invariants
  suspended — only async-signal-safe functions are legal; printf and malloc
  are not.
- The only sane handler bodies: set a `volatile sig_atomic_t` flag, or write
  one byte to a self-pipe. signalfd removes the handler entirely.
- Handle EINTR everywhere; SA_RESTART covers most calls but never poll/select
  /epoll_wait.
- Reap children in a WNOHANG loop — signals coalesce, one SIGCHLD may mean
  many exits.
- Uncatchable ≠ unignorable: D-state processes shrug off kill -9; that's a
  storage bug wearing a signals costume.
