A Linux signalfd becomes readable when a signal selected by its mask is pending for the reading context. A successful read() does more than observe that state: it consumes the returned signal occurrences, removing them from pending signal state.

That behavior gives signals a descriptor-facing consumption path. It does not convert the signal subsystem into a byte stream, and it does not replace the signal mask that controls ordinary delivery.

The descriptor mask selects eligible pending signals

signalfd() associates a signal set with a file descriptor. When the call receives -1 as its first argument, Linux creates a new signalfd object. Passing an existing signalfd descriptor replaces the mask associated with that object.

The descriptor mask answers which signals may be accepted through that descriptor. The calling thread’s signal mask answers a separate question: which signals are blocked from normal asynchronous delivery.

Normal signalfd use blocks the selected signals before relying on descriptor reads. Without that blocking, an eligible signal can instead follow its disposition and be delivered outside the signalfd consumption path.

sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGHUP);

pthread_sigmask(SIG_BLOCK, &mask, NULL);

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

SIGKILL and SIGSTOP cannot be accepted through signalfd. Linux silently ignores them if they appear in the descriptor mask.

Read consumes signal state

A signalfd read returns one or more struct signalfd_siginfo records. The buffer must hold at least one complete structure, and a larger buffer can receive several pending signals in one call.

struct signalfd_siginfo info[8];

ssize_t n = read(fd, info, sizeof(info));
if (n > 0) {
    size_t count = (size_t)n / sizeof(info[0]);
    /* info[0..count-1] describes consumed signals */
}

Each returned record describes a signal occurrence using fields analogous to siginfo_t, including the signal number and, when applicable, sender or payload information.

The read is destructive with respect to pending state. A signal returned through signalfd is no longer pending and cannot subsequently be accepted by sigwaitinfo() or delivered to a handler as the same pending occurrence.

With SFD_NONBLOCK, a read for which no selected signal is pending fails with EAGAIN. Without nonblocking mode, the read waits until an eligible signal becomes available.

Readiness follows pending state

poll(), select(), and epoll report a signalfd as readable when at least one signal in its descriptor mask is pending and available to the reading context.

This integrates signal acceptance with loops already waiting on sockets, pipes, timerfds, or eventfds. The readiness notification still represents state, not a detached copy of the signal record. Another consumer can consume eligible pending state before a particular reader executes.

A loop therefore treats the subsequent read() result as authoritative. Readiness only states that a nonblocking read could proceed at the point represented by the readiness machinery.

Standard and real-time signals retain signal semantics

signalfd changes the acceptance interface, not the queueing rules of Linux signals.

Standard signals do not queue multiple instances while the same signal is already pending. Several generations of one blocked standard signal can collapse into a single pending instance. A later signalfd read can therefore return one record even though the signal was generated several times.

Real-time signals have queueing semantics and can retain multiple pending instances. A sufficiently large signalfd read buffer can return multiple records corresponding to queued real-time signals.

This boundary matters when a signal is used as a counter. A standard signal cannot reliably encode the number of producer actions merely because signalfd exposes it as a record. The loss can already have occurred in pending-signal state before the descriptor becomes readable.

Thread-directed state limits what a reader can consume

Linux distinguishes process-directed signals from signals targeted at a specific thread. A thread reading signalfd can accept signals directed to itself and process-directed signals available to the thread group. It cannot use that read to consume a signal directed specifically to another thread.

Signal masks are also per-thread. In a multithreaded process, blocking a signal only in the thread that owns the event loop leaves other threads able to receive process-directed signals according to normal signal delivery rules.

A design that centralizes selected signals in one signalfd event loop therefore commonly establishes the blocking mask before creating worker threads, so those threads inherit the blocked state. Later mask changes remain thread-local unless applied separately.

Shared descriptor references do not create independent queues

After fork(), the child inherits the signalfd descriptor, but reads in the child concern signals queued to the child. Passing a signalfd descriptor to another process over a UNIX domain socket similarly makes reads in the receiving process concern signals queued to that receiving process.

The file descriptor is consequently not a portable mailbox containing signal records generated for the original process. Its read behavior remains tied to Linux signal pending state for the process and thread performing the read.

Across execve(), a signalfd remains open unless close-on-exec is set. Pending blocked signals can also survive the image replacement, so a descriptor intentionally retained across execve() can continue exposing eligible pending state to the new program image.

Synchronous fault signals remain outside the model

signalfd is not a replacement path for synchronously generated fault signals such as SIGSEGV from an invalid memory access or SIGFPE from an arithmetic fault. Linux documents these as outside signalfd’s acceptance model; such faults require the traditional signal-handler path when they are to be caught.

That boundary separates externally generated or asynchronously pending signal coordination from faults coupled to the execution of a specific instruction. Turning the latter into ordinary event-loop records would not preserve the execution semantics required for fault delivery.

Descriptor consumption changes event-loop structure, not signal identity

The main effect of signalfd is structural. Selected blocked signals can participate in the same readiness wait and read phase as other Linux descriptors, while their identity and queueing properties remain those of signals.

The descriptor mask selects candidates, the thread signal mask prevents ordinary delivery, pending state determines readability, and read() consumes records from that state. Standard-signal coalescing, real-time queueing, thread targeting, and synchronous-fault boundaries still apply.

That combination makes signalfd useful where signal-driven control events must share an event loop without asynchronous handler execution. The contract remains narrower than a general message queue: descriptor reads expose and consume signal state; they do not redefine it.