A Linux process can block selected signals and receive them by reading a file descriptor instead of running an asynchronous handler. signalfd() makes those pending signals visible through the same readiness interfaces used for sockets, pipes, and other descriptors, including poll() and epoll.
That conversion is not a replacement for signal masking. The descriptor has its own signal set, while each thread retains a signal mask that controls ordinary delivery. A robust design depends on both states remaining aligned.
The descriptor observes pending signals from a selected set
A new signalfd is created with a signal set:
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGINT);
pthread_sigmask(SIG_BLOCK, &mask, NULL);
int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);The mask passed to signalfd() identifies signals eligible to be returned through that descriptor. In normal use, those same signals are blocked first so they become pending rather than being dispatched according to their dispositions.
This produces two distinct controls. The thread signal mask governs asynchronous delivery. The signalfd mask governs which pending signals the descriptor can consume. Changing one does not implicitly rewrite the other.
SIGKILL and SIGSTOP cannot be received through signalfd. Linux silently ignores them if they appear in the descriptor mask, matching their special status in the signal model.
Readiness represents pending state, not handler execution
A signalfd becomes readable when at least one signal covered by its mask is pending for the reading context. A successful read() returns one or more signalfd_siginfo records, limited by the supplied buffer.
Reading has a semantic effect beyond copying metadata. The returned signal occurrences are consumed and are no longer pending. They cannot subsequently be accepted by sigwaitinfo() or delivered to a handler as the same pending occurrences.
This makes readiness level-sensitive to consumable signal state. If several eligible signals are pending and one read leaves some unconsumed, the descriptor can remain readable. With SFD_NONBLOCK, a read with no eligible pending signal fails with EAGAIN rather than waiting.
The event-loop consequence is precise: readiness announces that a read can consume signal state at that instant. It does not reserve a particular occurrence for a particular waiter.
Standard and real-time signals retain different queue semantics
signalfd changes the acceptance interface, not the underlying Linux signal semantics. Standard signals generally do not queue multiple instances while blocked. If the same standard signal is generated repeatedly before it is accepted, the pending state can represent only one occurrence.
Real-time signals are queued and preserve multiple generated instances, subject to the applicable resource limits. A signalfd reader can therefore receive multiple records for queued real-time signals where repeated standard signals may collapse into one pending indication.
This distinction matters when an application treats signal count as data. Moving reception into an event loop does not make standard signals into a reliable counter. If every occurrence must remain distinct, the chosen signal class and its queue semantics remain part of the contract.
Thread masks define the multithreaded boundary
Signal masks are per-thread on Linux. A process-directed signal can be delivered to any thread that does not block it and is eligible for delivery. Blocking a signal only in the thread that owns the event loop is therefore insufficient if another thread leaves that signal unblocked.
A common signalfd arrangement blocks the selected signals before creating worker threads. New threads inherit a copy of the creating thread’s signal mask, so the blocked state propagates through normal thread creation. The event-loop thread can then consume process-directed pending signals through signalfd.
Thread-directed signals have a narrower boundary. A thread reading signalfd can accept signals directed to itself and process-directed signals, but not signals specifically directed to another thread. The descriptor does not merge every thread’s private pending set into one process-wide queue.
This constraint prevents signalfd from acting as a universal signal mailbox for arbitrary thread-directed events.
Multiple signalfd instances compete for overlapping occurrences
A process may create several signalfd descriptors with different masks. That can separate classes of signals into different readiness sources. The separation is clean only when the masks are disjoint.
If two signalfd masks include the same signal, a pending occurrence is not duplicated for both descriptors. It can be consumed once through either eligible descriptor. Readiness observed on both descriptors must not be interpreted as two independent copies of the event.
The same competition exists with other synchronous signal-acceptance interfaces. A signal consumed through signalfd is removed from pending state, so another consumer cannot accept that same occurrence later.
This is an ownership boundary created by consumption rather than by registration. Overlapping consumers share access to pending state; they do not receive fan-out delivery.
File-descriptor lifetime differs from signal disposition
A signalfd object has normal descriptor lifetime rules. Duplicated descriptors refer to the same underlying signalfd object, and its resources remain until the last associated descriptor is closed. SFD_CLOEXEC can prevent the newly created descriptor from surviving a successful execve().
Signal masks and pending signals follow their own process and thread rules. A signal mask is preserved across execve(), and pending blocked signals can remain pending. If a signalfd is intentionally inherited across an exec boundary, eligible pending signals can still be available to the new program.
That combination can be useful, but it also makes descriptor inheritance and signal-mask inheritance one joint interface boundary. Accidentally inheriting only one side can produce a process with blocked signals but no intended consumer, or an inherited signalfd whose mask no longer matches the new program’s event policy.
Synchronous faults remain outside the model
signalfd is suited to asynchronously generated signals that can be blocked and accepted later. It is not a substitute for handling synchronous hardware-fault signals such as a SIGSEGV caused by an invalid memory access or a SIGFPE caused by a faulting arithmetic operation.
Those signals arise as part of executing the faulting instruction and require the signal-handling path when an application chooses to catch them. Routing ordinary control signals through a descriptor does not turn synchronous execution faults into event-loop messages.
The boundary keeps signalfd narrow: it integrates selected pending signal state with file-descriptor multiplexing, while the broader signal model still governs generation, targeting, masking, queueing, and fault delivery.