A signal included in a signalfd mask can become readable state on a file descriptor instead of invoking an asynchronous signal handler, provided that the signal is blocked from ordinary delivery in the relevant thread. This changes the interface boundary: signal arrival can participate in the same descriptor-oriented event loop as sockets, pipes, and other pollable objects.

The mechanism does not replace Linux signal semantics. Signal generation, process and thread signal masks, pending state, standard-signal coalescing, real-time signal queuing, and delivery rules still apply. signalfd changes the consumption interface for signals selected by its mask.

Blocking establishes the consumption path

A typical setup first blocks a set of signals with sigprocmask() or pthread_sigmask(), then creates a descriptor carrying the same set:

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

pthread_sigmask(SIG_BLOCK, &mask, NULL);

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

Blocking is not an optional optimization. If a selected signal remains unblocked in a thread eligible to receive it, ordinary signal delivery can consume that signal before a read from the descriptor observes it. In a multithreaded process, mask policy therefore belongs to the process design rather than only to the thread that owns the event loop.

Signal masks are per-thread. New threads inherit a copy of the creating thread’s mask, but later mask changes are independent. A program that intends to centralize process-directed signals through signalfd commonly establishes blocking before creating worker threads, then keeps those signals blocked in the workers.

Readiness represents pending selected signals

A signalfd descriptor is readable when one or more signals in its configured mask are pending for consumption through that descriptor. It can be monitored with poll(), epoll, or similar descriptor readiness interfaces.

A successful read() returns one or more struct signalfd_siginfo records. Each record includes the signal number and additional fields whose meaning depends on the signal source. The structure is not the same type as siginfo_t, even though several fields carry related information.

With SFD_NONBLOCK, a read that has no matching pending signal fails with EAGAIN. Without that flag, the read can block. This makes backpressure and scheduling behavior explicit at the descriptor API rather than inside an asynchronous handler.

The mask selects signals, not a private queue

The descriptor’s mask identifies signals that may be accepted through that descriptor. It does not create a separate signal namespace. Pending signals remain part of the process or thread signal model until consumed.

That distinction is visible with standard signals. Multiple instances of a standard signal can coalesce while the signal is pending, so an application cannot infer an arrival count from the number of reads. Real-time signals have queuing semantics defined by the signal subsystem and can preserve multiple pending instances subject to system limits.

signalfd therefore should not be modeled as a generic message queue whose writes correspond one-for-one with reads. The observable records inherit the queuing rules of the underlying signal class.

Updating a descriptor changes its selection mask

Calling signalfd() with an existing descriptor replaces the signal mask associated with that descriptor:

sigaddset(&mask, SIGUSR1);

if (signalfd(fd, &mask, 0) == -1) {
    /* handle error */
}

The call changes which pending signals the descriptor can consume. It does not itself alter the calling thread’s blocked-signal mask. Code that updates one side without the other can create a mismatch between signals blocked for ordinary delivery and signals accepted through the descriptor.

This separation is useful but demands an explicit contract. The thread mask controls ordinary delivery eligibility; the descriptor mask controls descriptor consumption eligibility.

Process-directed and thread-directed signals retain their scope

Linux distinguishes signals directed at a process from signals directed at a particular thread. signalfd does not erase that distinction.

A thread can consume signals that are pending and eligible under the kernel’s signal rules. A descriptor read is not a mechanism for one arbitrary thread to steal every thread-directed signal from another thread. APIs such as pthread_kill() still target a specific thread, and signal mask placement remains relevant to the resulting pending state.

This matters for event-loop architectures that centralize termination or reload signals. Centralization works cleanly when the selected signals and their generation scope match the masking policy across threads.

Handler restrictions disappear, signal semantics do not

Traditional asynchronous handlers execute at interruption points and are constrained to async-signal-safe operations. A signalfd event is consumed by ordinary code after a descriptor becomes readable, so that code executes in the normal control flow of the event loop and is not limited to the async-signal-safe function set merely because the event originated as a signal.

Other constraints remain. The event loop must drain records correctly, handle EAGAIN in nonblocking mode, and account for descriptor lifetime. Closing the descriptor does not automatically unblock signals in any thread. If the program later expects ordinary delivery, it must update thread masks deliberately.

Likewise, SIGKILL and SIGSTOP cannot be blocked and therefore cannot be consumed through signalfd; including them in the supplied mask has no effect for descriptor delivery.

Descriptor lifetime and fork preserve separate concerns

After fork(), the child inherits the file descriptor in the usual way, but signal pending state and delivery rules across a fork boundary have their own semantics. An inherited descriptor should not be treated as a transferable snapshot of the parent’s pending signals.

Descriptor inheritance across execve() is controlled by close-on-exec state. SFD_CLOEXEC requests that state atomically at creation, avoiding a separate fcntl() window in multithreaded code.

These properties place signalfd at the intersection of two kernel models: file-descriptor lifetime and signal delivery. Correct use requires both models to remain explicit.

A descriptor interface narrows asynchronous control flow

The main structural effect of signalfd is not that signals become ordinary bytes. They remain signals with kernel-defined pending and queuing behavior. The descriptor supplies a synchronous read boundary for selected blocked signals and a readiness surface compatible with descriptor multiplexing.

That boundary can simplify event-loop control flow because termination, reload, and similar process events can be dispatched beside other readable descriptors. The resulting design is precise only when signal blocking, descriptor selection, thread scope, and descriptor lifetime are treated as one coordinated contract.