A signal included in a signalfd mask can make a file descriptor readable instead of invoking an asynchronous handler, provided that signal is blocked from ordinary delivery. A successful read() then consumes pending signal state and returns one or more signalfd_siginfo records.
This changes the interface used to receive selected signals, but it does not replace Linux signal semantics. Signal masks, process-directed versus thread-directed delivery, standard-signal coalescing, and the special status of SIGKILL and SIGSTOP still define the boundary around the descriptor.
Blocking establishes the descriptor path
signalfd() associates a signal set with a file descriptor. In normal use, the same signals are blocked with sigprocmask() in a single-threaded process or pthread_sigmask() in a multithreaded process.
Blocking matters because a signal that remains eligible for ordinary delivery can follow its configured disposition instead of remaining pending for consumption through the descriptor. The descriptor is therefore not a global interception layer placed ahead of signal delivery. It is a read interface for matching pending signals.
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGHUP);
pthread_sigmask(SIG_BLOCK, &mask, NULL);
int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);In a multithreaded process, blocking only the thread that creates the descriptor is insufficient for process-directed signals. Another thread with the signal unblocked can be selected for ordinary delivery. A common design blocks the selected set before worker threads are created so the mask is inherited.
Readiness means a matching signal is pending
The descriptor is readable when at least one signal from its configured mask is pending and available to the reading thread. poll(), select(), and epoll() can therefore place signal reception beside sockets, pipes, timerfd, and other descriptor-backed event sources.
A read returns complete signalfd_siginfo structures. If the supplied buffer can hold several records, one call can return several pending signals.
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++) {
dispatch_signal(&info[i]);
}
}Consuming a record also consumes that pending signal occurrence. It is no longer pending for a handler or for an interface such as sigwaitinfo().
With SFD_NONBLOCK, a read with no matching pending signal fails with EAGAIN. Without nonblocking mode, the read waits until a matching signal becomes available.
The record carries signal metadata
signalfd_siginfo contains the signal number plus fields analogous to parts of siginfo_t, including sender identifiers and signal-specific data. Which fields are meaningful depends on the signal and its ssi_code.
The structure is a Linux ABI record, not a serialized form of siginfo_t. Code should inspect the fields defined for the event being handled rather than assuming every member is valid for every signal source.
This representation is useful in an event loop because dispatch occurs in ordinary control flow. The callback is not executing under asynchronous signal-handler restrictions merely because the event originated as a signal.
Standard signals still coalesce
signalfd does not turn standard signals into an unlimited queue. Linux standard signals retain their normal pending semantics: if several instances of the same standard signal are generated while that signal is already pending, only one instance remains pending.
That property creates an important distinction between readiness and event counting. Ten rapid SIGHUP generations do not imply ten signalfd_siginfo records. Applications that require per-occurrence queueing need a mechanism with queueing semantics appropriate to that requirement, such as real-time signals where suitable, or a different IPC channel.
Real-time signals follow their own queueing rules; signalfd exposes pending signal state without redefining those rules.
Thread-directed signals keep their target
Signal targeting also remains intact. A thread reading a signalfd can consume signals directed to itself and process-directed signals available to it. It cannot use that descriptor read to take a signal specifically directed to another thread.
This matters when a process combines pthread_kill() or tgkill() with a central event-loop thread. A signal targeted at a worker does not become a generic process event merely because the process owns a signalfd.
For a central signal loop, process-directed control signals and a consistently blocked mask across threads fit the model more directly.
SIGKILL and SIGSTOP remain outside the mechanism
Attempts to include SIGKILL or SIGSTOP in the signalfd mask are ignored. Linux does not permit these signals to be blocked, caught, or redirected into a descriptor-driven acceptance path.
Synchronous fault signals are another boundary. Faults such as an invalid memory access producing SIGSEGV are tied to the faulting execution context and are not a practical replacement target for signalfd; Linux documents synchronously generated fault signals as requiring a signal handler when they are to be caught.
The descriptor model is strongest for asynchronous control and notification signals whose delivery can be deliberately blocked and accepted in normal program flow.
Descriptor lifecycle follows file-descriptor rules
A newly created signalfd can use SFD_CLOEXEC and SFD_NONBLOCK at creation time. The former prevents unintended inheritance across execve(); the latter fits event loops that drain readiness without blocking.
After fork(), the child inherits the descriptor, but signal pending state and targeting still follow signal rules. A descriptor can also be passed over a UNIX domain socket. Reading it in the receiving process concerns signals pending for that receiving process, not a portable stream of signal events captured from the sender.
The object is therefore descriptor-shaped at the API boundary while remaining attached to signal semantics at the delivery boundary.
The useful boundary is synchronous acceptance
The central effect of signalfd is not that signals become ordinary bytes. Selected asynchronous notifications become synchronously consumable records, and descriptor readiness tells an event loop when such records are available.
That arrangement can remove asynchronous handlers from control-signal paths and consolidate dispatch under epoll(). It does not erase signal masks, targeting, coalescing, or non-catchable signals. Designs that preserve those boundaries get a descriptor-oriented control path without assigning stronger guarantees to signalfd than Linux provides.