2026-08-29
Signals are the worst kind of asynchronous. A SIGTERM can arrive between any two instructions, hijack whichever thread happens to have it unblocked, and drop you into a handler where you can only call a small list of async-signal-safe functions. No malloc, no printf, no locking a mutex your main code holds. Try it and you get deadlocks that reproduce once a week in production.
signalfd() flips the model. Instead of the kernel calling your handler, you get a file descriptor that becomes readable when a signal is pending. You read() a struct signalfd_siginfo out of it — synchronously, from a normal thread context, with the full C library available.
The usage pattern is three steps:
sigprocmask(SIG_BLOCK, ...). This is not optional — if the signal isn't blocked, the kernel delivers it the old way and your fd never sees it.int fd = signalfd(-1, &mask, SFD_CLOEXEC | SFD_NONBLOCK);epoll_wait, select, whatever your event loop already uses. On readiness, read() returns one or more 128-byte signalfd_siginfo structs telling you which signal, which sender PID, which uid, and (for SIGCHLD) the exit status.Real-world example: systemd's main loop. It handles SIGCHLD (child died), SIGTERM (shutdown), SIGHUP (reload config), and SIGRTMIN+n (unit state changes) all through one signalfd tied into its epoll loop. No handler races, no self-pipe trick, no worrying about which thread caught the signal. The reload logic can grab a mutex, allocate memory, and log through journald — all illegal from an actual signal handler.
The self-pipe trick it replaces: Before signalfd (added in kernel 2.6.22, 2007), the standard hack was to install a handler that just does write(pipe_fd, &sig, 1), then poll the pipe. Signalfd removes the handler, the pipe, and the risk of EINTR in write() itself.
Rule of thumb: one signalfd_siginfo is 128 bytes. Size your read buffer as N * 128 where N is your expected signal burst — for SIGCHLD under a fork-heavy workload, 16 is plenty; for a normal daemon, 1 is fine. A single read() drains as many pending signals as fit.
Gotcha: signalfd doesn't queue signals the kernel already coalesced. If 100 SIGCHLDs fire before you read, you get one event — you must loop waitpid(-1, ..., WNOHANG) to reap all zombies, exactly like a traditional handler.
SIGCHLD in a loop.
