Linux signalfd gives selected signals a descriptor-oriented consumption path. Instead of transferring control into an asynchronous handler, a process can block those signals, associate them with a signalfd object, and consume pending instances through read(). The descriptor can also participate in poll(), select(), and epoll, placing signal reception beside sockets, timers, and other readiness sources.
This interface is Linux-specific. The signal mask, pending-signal rules, and descriptor operations come from Linux and POSIX signal semantics where applicable; they are not properties of the C language itself.
Blocking establishes the consumption boundary
The mask passed to signalfd() selects signals that the descriptor may accept. That mask does not by itself block ordinary signal delivery. In normal use, the same signals are blocked in the receiving thread before the descriptor is used:
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGHUP);
pthread_sigmask(SIG_BLOCK, &mask, NULL);
int sfd = signalfd(-1, &mask, SFD_CLOEXEC | SFD_NONBLOCK);A blocked signal can remain pending instead of invoking its normal handler or default action. signalfd then provides a synchronous operation that can consume matching pending state.
This separation matters because two masks participate in the design. The thread signal mask controls asynchronous delivery, while the signalfd mask controls which pending signals are eligible for reads from that descriptor. Treating the descriptor mask as a replacement for signal blocking leaves another delivery path active.
SIGKILL and SIGSTOP cannot be received through signalfd. Including either in the descriptor mask has no effect.
Reads consume signal state rather than copy notifications
A successful read() returns one or more struct signalfd_siginfo records. Each record has a fixed 128-byte layout and carries fields such as the signal number, sender identity where applicable, and signal-specific metadata.
The buffer must be large enough for at least one record. If several eligible signals are pending and the buffer has room, one read can return multiple records. Consuming a record removes that signal instance from pending state; the same instance is not subsequently available to a handler or sigwaitinfo().
That behavior makes the descriptor a consumer of the process signal mechanism, not a mirror of it. An application cannot assume that reading signalfd merely observes an event while leaving the original signal available for another consumer.
Standard and real-time signals also retain their existing queuing semantics. Standard signals generally coalesce while pending, so repeated generation of the same standard signal does not imply one signalfd record per generation. Real-time signals can queue multiple instances. signalfd changes the acceptance interface, not the underlying signal queuing model.
Readiness represents matching pending state
A signalfd descriptor becomes readable when at least one signal selected by its mask is pending and eligible for the caller. A nonblocking read() with no eligible pending signal fails with EAGAIN; a blocking read waits.
This readiness property is what makes signalfd fit a descriptor event loop. The loop can wait on a socket, timerfd, eventfd, and signalfd through one multiplexing interface, then dispatch ordinary control flow after readiness is reported.
Readiness does not mean that signal delivery has become a byte-stream protocol. The pending state still belongs to the kernel signal subsystem, and a read consumes structured signal records according to signal rules. A readiness notification can also become stale before a later read if another thread or another eligible signalfd consumes the pending signal first.
Thread masks remain per-thread state
Each thread has its own signal mask. That creates a critical process-level constraint for multithreaded programs: blocking a signal only in the thread that creates signalfd does not automatically prevent another thread from receiving a process-directed instance asynchronously.
A common architecture blocks the selected signals before creating worker threads. New threads inherit a copy of the creating thread’s signal mask, so the blocked state begins consistently across the thread group. A designated event-loop thread can then consume process-directed signals through signalfd.
This is a coordination rule built from signal-mask inheritance, not a special signalfd guarantee. If another thread later unblocks a selected signal, that thread may again become an asynchronous delivery target according to normal signal selection rules.
Thread-directed signals add another boundary. A thread reading signalfd can consume signals directed to itself and signals directed to the process as a whole. It cannot use that read to consume a signal directed specifically to a different thread.
Descriptor masks can be replaced in place
Passing -1 as the first argument to signalfd() creates a new descriptor. Passing an existing signalfd descriptor replaces the signal set associated with that object.
That operation changes future eligibility for descriptor reads but does not rewrite the process’s thread masks. If an application adds a signal to the signalfd mask without also arranging suitable blocking, asynchronous delivery can still occur. Removing a signal from the descriptor mask likewise does not unblock it.
The two layers therefore need coordinated updates when signal policy changes at runtime. A descriptor configuration and a thread-mask configuration that drift apart can produce signals that remain blocked with no intended consumer, or signals that bypass the event loop.
Multiple descriptors compete for shared pending instances
A process can create several signalfd objects with different or overlapping masks. Distinct masks can separate control signals into different readiness sources. With overlapping masks, however, a pending instance that matches more than one descriptor can be consumed through any one eligible descriptor only once.
This prevents overlapping signalfd objects from acting as independent subscriptions. They are alternative consumers of shared signal state. Designs that require fan-out must perform that fan-out after one component has accepted the signal.
Descriptor duplication has a different shape. Duplicated descriptors refer to the same signalfd object, including its configured mask. Closing one duplicate does not destroy the object while another descriptor still refers to it.
fork and exec preserve different parts of the state
After fork(), the child inherits file descriptors and a copy of the calling thread’s signal mask. Reads from an inherited signalfd in the child concern signals pending for the child, not signals pending for the parent.
There is a narrower interaction with epoll. A signalfd registered in an epoll instance before fork() can be inherited by the child, and the child can read its own signals from that descriptor, but the inherited epoll registration does not report signalfd readiness for signals sent to the child. A child that needs epoll-driven signal readiness can create or register an appropriate signalfd after the fork boundary.
Across execve(), a signalfd remains open unless close-on-exec is set. The signal mask is also preserved across execve(). This can be useful when descriptor inheritance is intentional, but accidental inheritance can impose blocked-signal state on a program that expects conventional signal handling. SFD_CLOEXEC makes the descriptor side of that boundary explicit; signal-mask policy still requires separate handling.
Synchronous fault signals remain outside this mechanism
signalfd is not a general replacement for every signal handler. Linux documents that synchronously generated fault signals, such as a SIGSEGV caused by an invalid memory access or a SIGFPE caused by an arithmetic fault, cannot be received through signalfd in the ordinary blocked-signal pattern. Such faults require the signal-handler path when an application handles them.
That boundary follows from the relationship between a fault and the execution context that caused it. Moving process-control signals such as SIGTERM into an event loop is a different operation from recovering control at a faulting instruction.
Descriptor integration changes control flow, not signal semantics
The main effect of signalfd is structural. Selected asynchronous control events can be accepted through ordinary reads at a controlled point in an event loop, avoiding arbitrary handler entry for those signals.
The kernel still applies signal masks, pending-state rules, standard versus real-time queuing, process-directed versus thread-directed targeting, and inheritance semantics. Robust use therefore depends on treating signalfd as one consumer interface over the signal subsystem, with thread masks defining the boundary that routes eligible signals toward it.