The signalfd() Syscall: Turning Asynchronous Signals Into a Readable File Descriptor

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:

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.

Key Takeaway: signalfd() converts signals from an async, restricted-context callback into a synchronous readable event, letting your event loop handle them with the full standard library available — but you still have to drain coalesced signals like SIGCHLD in a loop.

All newsletters