signalfd Consumes Blocked Signals Through File Descriptor Reads

signalfd() turns selected signal notifications into records that can be consumed with read(). A matching pending signal makes the descriptor readable, which allows signal handling to share the same poll(), select(), or epoll path as sockets, timers, and other descriptors.

The descriptor does not redirect signals merely because its mask names them. Normal signal-mask rules still apply. In the usual design, the selected signals are blocked before they can be delivered through their ordinary dispositions, then a signalfd reads the pending instances.

The descriptor mask and the thread signal mask are separate

The mask passed to signalfd() selects which signals the descriptor can accept. It does not itself block delivery of those signals.

A process commonly establishes both pieces explicitly:

sigset_t mask;

sigemptyset(&mask);
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGINT);

if (pthread_sigmask(SIG_BLOCK, &mask, NULL) != 0) {
    /* handle error */
}

int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);

pthread_sigmask() changes the calling thread’s blocked set. signalfd() creates a descriptor with its own acceptance mask. Treating those masks as one setting hides an important boundary: a signal that remains unblocked in an eligible thread can be delivered through the ordinary signal mechanism instead of remaining pending for a signalfd read.

This distinction is especially relevant when threads are created. A new POSIX thread inherits a copy of its creator’s signal mask. Blocking the selected set before worker creation gives those workers the inherited blocked state unless they later change it.

A successful read consumes pending signal instances

When at least one matching signal is pending, read() returns one or more struct signalfd_siginfo records, limited by the supplied buffer. The buffer must be large enough for at least one record.

struct signalfd_siginfo info[8];

ssize_t n = read(sfd, info, sizeof(info));
if (n > 0) {
    size_t count = (size_t)n / sizeof(info[0]);
    for (size_t i = 0; i < count; ++i) {
        /* inspect info[i].ssi_signo and related fields */
    }
}

The read is not a passive copy of signal state. Returned signals are consumed and are no longer pending for the process. They cannot subsequently be accepted again by sigwaitinfo() or caught by a handler as the same pending instances.

With a nonblocking descriptor, a read with no matching pending signal fails with EAGAIN. With blocking operation, the read waits until an accepted signal becomes available.

Multiple signalfd objects can compete for the same signal

A process may create several signalfd objects with different masks. Their masks may also overlap.

If a signal is included in more than one descriptor mask, one pending occurrence can be read once from any eligible descriptor. The same occurrence is not duplicated into independent per-descriptor queues. This follows from the fact that signalfd consumes the process or thread’s pending signal state rather than maintaining a private copy of every occurrence for each descriptor.

That property matters when several event loops monitor overlapping signal sets. Separate descriptors do not create broadcast semantics. If independent components each require notification of the same event, that fan-out has to be implemented above the signal acceptance point.

Thread-directed and process-directed signals retain their normal scope

Signal routing remains tied to standard Linux signal semantics. A signal can target the process as a whole or a particular thread.

When a thread reads a signalfd, it can receive signals directed to that thread and process-directed signals available to the thread group. It cannot use that read to collect a signal directed specifically to another thread.

This makes mask placement part of the architecture in multithreaded programs. A common arrangement blocks the managed signals before creating workers and assigns one event-processing path to consume process-directed notifications. Thread-specific signaling still needs to respect the target thread and its pending state.

SIGKILL and SIGSTOP stay outside the interface

Including SIGKILL or SIGSTOP in the signalfd mask has no effect; Linux silently ignores those entries. The same signals cannot be blocked through the normal signal-mask interfaces.

Synchronous fault signals also form a separate boundary. Faults such as an invalid memory access that generates SIGSEGV are not suitable for the usual blocked-signal signalfd pattern. Linux documents synchronously generated fault signals as requiring traditional signal handling rather than signalfd acceptance.

The file descriptor fits event loops without changing signal semantics

The main structural benefit of signalfd is not a new signal-delivery model. It is a file-descriptor representation of signal acceptance that can participate in existing readiness multiplexing.

That representation can remove asynchronous handler execution from the selected signal path, but only when the surrounding signal masks are arranged consistently. The descriptor mask chooses accepted signal numbers, thread masks control ordinary delivery eligibility, and read() consumes pending instances. Keeping those roles separate preserves the exact boundary that signalfd provides.