signalfd

signalfd turns asynchronous signal delivery into synchronous file-descriptor reads. Instead of registering a handler that the kernel can call at any instruction boundary (with all the async-signal-safety hazards that entails), you create a descriptor with signalfd(-1, &mask, flags) that becomes readable whenever any signal in mask is pending for the caller; a read() then returns one or more struct signalfd_siginfo records, dequeuing those signals from the pending queue (signalfd(2)). The critical idiom that makes it work is to block the signals in the mask first with sigprocmask(SIG_BLOCK, ...): a blocked signal does not run its handler or take its default action but instead stays pending, and signalfd consumes it from that pending queue. The payoff is that you process signals in normal context — inside your event loop, with the full C library available — instead of inside a handler constrained to a tiny set of async-signal-safe functions. Because the descriptor is pollable, signals join sockets, timers, and eventfds in a single epoll loop. This note pins the mechanism to Linux 6.12 LTS (fs/signalfd.c, released 2024-11-17); the file’s original 2003 author is Linus Torvalds, with the siginfo-returning read() added by Davide Libenzi in 2007.

signalfd() was added in Linux 2.6.22 (glibc wrapper since glibc 2.8); the signalfd4() variant that accepts a flags argument (SFD_CLOEXEC, SFD_NONBLOCK) arrived in Linux 2.6.27 (signalfd(2) HISTORY). Up to Linux 2.6.26 the flags argument did not exist and had to be zero; the glibc wrapper today routes to signalfd4() so the userspace function exposes flags regardless.


Mental Model

The right way to think about signalfd is “signals delivered by read() instead of by interrupt.” A classic signal handler is a callback the kernel invokes by hijacking your thread mid-execution — it runs on a borrowed stack, between two arbitrary instructions, and may not call anything that is not async-signal-safe (no malloc, no printf, almost nothing). signalfd inverts this: the signal does not interrupt you; it sits quietly in the pending queue and merely flips your descriptor to readable. You decide when to look — at the top of your event loop, in plain code, with the whole standard library available. The signal becomes data you pull at your convenience, not control flow forced upon you.

The one non-obvious prerequisite is blocking: a delivered signal normally either runs a handler or takes its default action (often: kill the process). To redirect it to the descriptor instead, you must block it (add it to the thread’s signal mask) so the kernel parks it as pending rather than acting on it. signalfd then dequeues from that pending pool. signalfd does not change a signal’s disposition — it does not install a handler — so a signal that is not blocked still delivers the old-fashioned way even while a signalfd watches for it.

flowchart LR
  SRC["signal source<br/>kill / sigqueue / SIGCHLD"] -->|"becomes pending"| PEND["pending queue<br/>current-&gt;pending +<br/>shared_pending"]
  BLK["sigprocmask(SIG_BLOCK, mask)<br/>keeps it pending, not acted on"] -.->|"required"| PEND
  PEND -->|"signalfd_notify wakes wqh"| WQ["sighand-&gt;signalfd_wqh"]
  WQ --> EPOLL["epoll / poll / select<br/>readable if a masked signal is pending"]
  EPOLL --> READ["read(fd, &amp;ssi, 128*n)<br/>dequeues signals,<br/>returns signalfd_siginfo records"]

How a signal reaches a signalfd reader. What it shows: a signal that arrives becomes pending on either the per-thread (current->pending) or process-wide (shared_pending) queue. Provided it is blocked (via sigprocmask) so the kernel does not act on it, it sits there; signalfd_notify() wakes the signalfd_wqh wait queue, the descriptor reports readable, and a read() of N×128 bytes dequeues up to N signals as signalfd_siginfo records. The insight to take: signalfd does not catch signals — it reads them out of the same pending queue a synchronous sigwaitinfo() would, but exposes the wait as a pollable fd so it composes with an event loop. Blocking the signals is what keeps them in the queue for signalfd to find.


Mechanical Walk-through

The object. A signalfd is almost embarrassingly small — struct signalfd_ctx in fs/signalfd.c is a single field:

struct signalfd_ctx {
	sigset_t sigmask;
};

The entire state is the set of signals this descriptor is interested in. There is no per-fd pending queue: signalfd is a view onto the task’s existing pending signal queues, filtered by sigmask. This is the key to its semantics — two signalfds, or a signalfd and a sigwaitinfo() call, all draw from the same underlying pending pool.

Creation and update. Both syscalls land in do_signalfd4(ufd, mask, flags). It validates that SFD_CLOEXEC == O_CLOEXEC and SFD_NONBLOCK == O_NONBLOCK (build assertions) and rejects unknown flags. Then comes a crucial normalization step:

sigdelsetmask(mask, sigmask(SIGKILL) | sigmask(SIGSTOP));
signotset(mask);

The first line silently strips SIGKILL and SIGSTOP from the requested set — those two signals can never be caught, blocked, or redirected, so asking signalfd to watch them is quietly ignored (no error). The second line complements the mask (signotset) because the kernel’s internal dequeue_signal() takes a “which signals to block from this dequeue” set, the inverse of “which signals I want.” After that, behavior splits on ufd:

  • ufd == -1 (create): allocate a signalfd_ctx, store the (normalized) mask, grab an unused fd, and wrap the context in an anonymous inode file named "[signalfd]" via anon_inode_getfile(), wiring up signalfd_fops (.poll, .read_iter, .release, fdinfo). The fd gets O_RDWR plus the requested O_NONBLOCK/O_CLOEXEC, and FMODE_NOWAIT.
  • ufd >= 0 (update): look up the existing fd, verify it really is a signalfd (f_op == &signalfd_fops, else -EINVAL), then replace its sigmask under the sighand->siglock and wake signalfd_wqh. This lets you change which signals a live descriptor watches without recreating it.

How a pending signal wakes the descriptor. The general signal machinery (in kernel/signal.c) calls signalfd_notify(tsk, sig) whenever a signal is added to a task’s pending set. That hook, defined in include/linux/signalfd.h, is deliberately tiny:

static inline void signalfd_notify(struct task_struct *tsk, int sig)
{
	if (unlikely(waitqueue_active(&tsk->sighand->signalfd_wqh)))
		wake_up(&tsk->sighand->signalfd_wqh);
}

It wakes the per-sighand wait queue signalfd_wqhevery signalfd on the process shares this one wait queue, hanging off the signal-handler table. Each woken poller then re-checks whether the new signal matches its mask. Note signalfd_notify does not filter by mask; it wakes everyone and lets signalfd_poll sort out relevance.

Polling — the readiness rule. signalfd_poll() registers on current->sighand->signalfd_wqh and, under siglock, asks whether any signal in the mask is pending on either the per-thread or the process-wide queue:

if (next_signal(&current->pending, &ctx->sigmask) ||
    next_signal(&current->signal->shared_pending, &ctx->sigmask))
	events |= EPOLLIN;

current->pending holds thread-directed signals (e.g. from pthread_kill or a synchronous fault directed at this thread); current->signal->shared_pending holds process-directed signals (e.g. kill(pid, ...) or SIGCHLD), deliverable to any thread in the group. The descriptor is readable if a matching signal sits in either. This dual check is exactly why the man page says a signalfd read “will read the signals that are directed to the thread itself and the signals that are directed to the process.”

Reading — dequeue into siginfo. signalfd_read_iter() divides the user buffer by sizeof(struct signalfd_siginfo) (128 bytes, asserted at build time) to learn how many records will fit; a buffer smaller than one record is -EINVAL. It then loops, calling signalfd_dequeue() per record. signalfd_dequeue takes siglock and calls the kernel’s dequeue_signal(&ctx->sigmask, info, &type) — the same function a synchronous sigwaitinfo() uses — which removes a matching signal from the pending queue and returns its kernel_siginfo_t. If nothing is pending and the fd is non-blocking, it returns -EAGAIN; otherwise it sleeps on signalfd_wqh (interruptibly) until a signal arrives or the read is itself interrupted (-ERESTARTSYS). For each dequeued signal, signalfd_copyinfo() translates the kernel siginfo into the stable, fixed-128-byte signalfd_siginfo ABI and copies it out. The first record blocks (if blocking); subsequent records in the same read() are non-blocking, so a single read() drains as many pending signals as fit, then returns.

The signalfd_siginfo record. Defined in the uapi header, it is a fixed 128-byte structure (padded deliberately so the ABI never shifts) whose fields mirror siginfo_t: ssi_signo (the signal number), ssi_code (e.g. SI_USER, SI_QUEUE, CLD_EXITED), ssi_pid/ssi_uid (sender), ssi_status (for SIGCHLD, the child’s exit status), ssi_int/ssi_ptr (the sigqueue() payload), ssi_addr (the faulting address for SIGSEGV-class signals), and more. The kernel fills only the fields relevant to that signal’s layout (SIL_KILL, SIL_CHLD, SIL_RT, SIL_FAULT, etc.), zeroing the rest — so for a SIGCHLD you read ssi_pid, ssi_status, ssi_utime, ssi_stime; for a sigqueue-delivered realtime signal you read ssi_int/ssi_ptr.


Configuration and Code

The canonical pattern: block the signals, create the descriptor, drive it from an epoll loop. Comments are line-by-line on the load-bearing calls.

#define _GNU_SOURCE
#include <sys/signalfd.h>
#include <sys/epoll.h>
#include <signal.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
 
int main(void) {
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);     /* Ctrl-C */
    sigaddset(&mask, SIGTERM);    /* graceful shutdown */
    sigaddset(&mask, SIGCHLD);    /* child exited */
 
    /* STEP 1 — THE CRITICAL ONE. Block these signals so the kernel does NOT
     * run their handlers or default actions; it leaves them PENDING for
     * signalfd to dequeue. Without this, SIGINT/SIGTERM would kill the process
     * and SIGCHLD would be ignored, never reaching the fd. Use pthread_sigmask
     * in a multithreaded program (and do it before creating threads, so the
     * mask is inherited by all of them). */
    if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) { perror("sigprocmask"); exit(1); }
 
    /* STEP 2 — create the descriptor watching exactly that set.
     * SFD_CLOEXEC: don't leak across exec(); SFD_NONBLOCK: read() returns
     * EAGAIN rather than blocking — required for an epoll loop. */
    int sfd = signalfd(-1, &mask, SFD_CLOEXEC | SFD_NONBLOCK);
    if (sfd == -1) { perror("signalfd"); exit(1); }
 
    int ep = epoll_create1(EPOLL_CLOEXEC);
    struct epoll_event ev = { .events = EPOLLIN, .data.fd = sfd };
    epoll_ctl(ep, EPOLL_CTL_ADD, sfd, &ev);
 
    for (;;) {
        struct epoll_event out;
        epoll_wait(ep, &out, 1, -1);          /* also wait on sockets here */
 
        struct signalfd_siginfo si;
        /* Drain ALL pending signals: read() returns as many 128-byte records
         * as fit and are pending; loop until EAGAIN. */
        while (read(sfd, &si, sizeof(si)) == sizeof(si)) {
            switch (si.ssi_signo) {
            case SIGINT:
            case SIGTERM:
                printf("shutdown requested (sig %u from pid %u)\n",
                       si.ssi_signo, si.ssi_pid);
                goto done;                    /* clean exit, in NORMAL context */
            case SIGCHLD:
                /* reap; ssi_pid/ssi_status came straight from the siginfo */
                printf("child %u exited, status %d\n", si.ssi_pid, si.ssi_status);
                break;
            }
        }
    }
done:
    close(sfd);
    close(ep);
    return 0;
}

Everything in that switch runs in ordinary code — printf, malloc, reaping logic, anything. That is the whole point: contrast it with a SIGCHLD handler, which could call only async-signal-safe functions and would have to communicate with the main loop through a volatile sig_atomic_t flag or a self-pipe.

To change the watched set on a live descriptor (e.g. start ignoring SIGCHLD), pass the existing fd instead of -1:

sigdelset(&mask, SIGCHLD);
signalfd(sfd, &mask, 0);   /* same fd: replaces the mask in place */

Failure Modes and Common Misunderstandings

“My signal still killed the process / still ran my old handler.” You forgot to block it. signalfd does not change disposition; an unblocked signal is delivered normally (handler or default action) and never reaches the descriptor. Blocking via sigprocmask (single-threaded) or pthread_sigmask (multithreaded) is mandatory, and is the single most common signalfd bug.

“In my threaded program some signals reach signalfd and some don’t, unpredictably.” A process-directed signal (kill(pid)) is delivered to some arbitrary thread in the group that does not have it blocked. If only the thread holding the signalfd blocks the signal but a sibling does not, the kernel may deliver it to the sibling — which runs the default action (often: kills the whole process) — instead of leaving it pending for signalfd. The fix is to block the signal in every thread, easiest achieved by blocking it in the main thread before spawning any others, so all inherit the mask. Then only signalfd (or sigwaitinfo) ever consumes it.

“I can’t catch SIGSEGV/SIGFPE/SIGBUS with signalfd.” Correct, and by design. Synchronously generated faults are tied to the faulting instruction; the man page states “the signalfd mechanism can’t be used to receive signals that are synchronously generated, such as the SIGSEGV signal … Such signals can be caught only via a signal handler.” Blocking a fault signal and then triggering it leads to undefined behavior, not a tidy signalfd read.

SIGKILL/SIGSTOP in my mask did nothing.” They are silently stripped (sigdelsetmask in do_signalfd4). These two are uncatchable and unblockable; signalfd ignores them rather than erroring.

“After fork() the child’s signalfd behaves oddly.” The fd is shared/inherited like any other across fork, but the pending signal state and the block mask have their own inheritance rules, and a child reading a signalfd inherited from the parent reads the child’s pending signals (it is a view onto the reading task’s queues). Most designs recreate the signalfd after fork to avoid confusion.

“A read returned fewer signals than I expected.” read() returns only signals currently pending and matching the mask that fit in the buffer; if a signal arrives a microsecond later it is a separate readiness event. Also, standard (non-realtime) signals do not queue — a second SIGINT arriving while one is already pending is coalesced, so you read it once, not twice. Use realtime signals (SIGRTMIN..SIGRTMAX) if you need every instance counted; they queue and each produces a distinct signalfd_siginfo.


Alternatives and When to Choose Them

The closest sibling is sigwaitinfo()/sigtimedwait() — these synchronously dequeue a blocked signal too, and from the same pending queue. The difference is purely about composition: sigwaitinfo blocks a thread waiting only for signals, whereas signalfd exposes the wait as a pollable fd that joins an epoll set with sockets and timers. If a thread does nothing but wait for signals, sigwaitinfo is simpler; if signals are one of many event sources in a loop, signalfd wins. Both require blocking the signals first.

Against a classic signal handler (sigaction), signalfd trades asynchrony for safety. A handler delivers immediately and preemptively — good for “I must react this instant” — but is confined to async-signal-safe functions and is a notorious source of reentrancy bugs (see Async-Signal-Safety and Reentrant Handlers). signalfd delivers at your convenience in normal context, which is what almost every event-loop server actually wants.

The self-pipe trick (a handler that writes a byte to a pipe the loop polls) predates signalfd and achieves the same “signal as pollable event,” but requires you to still write an async-signal-safe handler and burns two fds; signalfd does it natively in the kernel with one fd and no handler. An eventfd is the cousin for application-defined wakeups rather than OS signals — you typically run a signalfd (for SIGTERM/SIGCHLD) and an eventfd (for “a worker thread queued work”) side by side in the same loop.


Production Notes

Graceful shutdown in servers. The dominant production use is turning SIGTERM/SIGINT into a clean loop exit: block them, watch them on a signalfd in the main epoll loop, and on readiness break the loop and run normal cleanup code (flush buffers, close connections, write a final log line) — none of which would be legal inside a signal handler. This is how many event-driven daemons and the event loops behind libraries like libev/libevent (which offer signalfd-backed signal watchers on Linux) handle termination.

SIGCHLD reaping without races. Reaping children from a SIGCHLD handler is a classic source of bugs (the handler can be coalesced, can interrupt waitpid, etc.). A signalfd lets you reap in the main loop: on SIGCHLD readiness, loop waitpid(-1, ..., WNOHANG) until it returns 0. The ssi_pid/ssi_status fields in the record give you the child identity directly, though because standard SIGCHLD coalesces you must still loop waitpid to reap all exited children, not just the one the record names.

systemd and process supervisors. Supervisors that manage many child processes rely on synchronous, race-free signal handling; signalfd (or the equivalent sigwaitinfo loop) is the idiomatic way to fold child-exit and control signals into the supervisor’s main event loop rather than scattering logic across async handlers.

/proc/PID/fdinfo debugging. With CONFIG_PROC_FS, a signalfd’s fdinfo prints sigmask: — the set of signals it watches — so you can inspect from outside which signals a process has redirected to a descriptor, useful when a signal mysteriously “disappears” (it was blocked and is being consumed by a signalfd).


See Also