Linux signals normally interrupt a thread through asynchronous delivery. signalfd() offers a different boundary for selected signals: keep them blocked in the relevant threads, then consume pending instances by reading a file descriptor.
The signal mechanism itself does not become a byte stream. The kernel still maintains signal state and normal process- or thread-directed delivery rules. signalfd adds a file-descriptor interface for accepting signals from a configured set.
Blocking and the descriptor mask are separate state
A signalfd has a signal-set mask that selects which signals it can accept. That mask does not block those signals for a thread. Normal use therefore pairs descriptor creation with a signal mask operation.
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);The two masks serve different roles. The thread signal mask prevents ordinary asynchronous delivery. The signalfd mask selects pending signals eligible to be returned by read().
This distinction matters in multithreaded programs because each thread has its own signal mask. Blocking a signal in one thread does not automatically block it in every other thread. A process that centralizes process-directed signals through one signalfd commonly arranges the mask before creating worker threads so they inherit the blocked set.
A read consumes pending signal state
When a selected signal is pending, read() returns one or more struct signalfd_siginfo records that fit in the supplied buffer. Consumed signals are no longer pending and cannot later be accepted again through another signal-wait interface.
struct signalfd_siginfo si;
ssize_t n = read(sfd, &si, sizeof(si));
if (n == sizeof(si)) {
if (si.ssi_signo == SIGTERM) {
/* begin shutdown */
}
}The record carries fields such as the signal number and, where applicable, sender or queued-signal information. Its layout is specific to the signalfd API rather than being a raw siginfo_t object.
With SFD_NONBLOCK, a read made when no selected signal is pending fails with EAGAIN. Without nonblocking mode, the read can wait for an eligible signal.
Readiness lets signals share an event loop
A signalfd becomes readable when at least one signal from its configured set is pending and available to the caller. The descriptor can therefore participate in poll(), select(), and epoll alongside sockets, timers, pipes, and other descriptor-backed event sources.
epoll
|
+-- listening socket
+-- timerfd
+-- signalfd
|
+-- SIGTERM
`-- SIGHUPThis changes the control-flow boundary. Instead of transferring control into a signal handler at an arbitrary instruction boundary, the program can process selected signal notifications at a normal event-loop point.
That does not make signal semantics identical to socket semantics. Standard signals retain their normal pending behavior, and the descriptor exposes signal acceptance rather than an unrestricted queue of every generation event.
Process-directed and thread-directed signals keep their scope
Reading a signalfd follows Linux signal targeting rules. A thread can read signals directed to itself and process-directed signals available to the thread group. It cannot use the descriptor to consume a signal directed specifically to another thread.
That boundary is easy to miss in designs that treat one signalfd as a process-wide mailbox. The file descriptor is process-accessible like other descriptors, but eligibility for a particular signal still follows signal scope.
The per-thread mask is equally important. If another thread leaves a selected process-directed signal unblocked, the kernel may deliver that signal normally to that thread instead of leaving it pending for the signalfd consumer.
Multiple descriptors compete for overlapping signals
A process may create several signalfd objects with different masks. If the same signal appears in more than one descriptor mask, one pending occurrence can be consumed through one eligible descriptor; reading it does not create copies for every descriptor.
signalfd A: SIGTERM, SIGHUP
signalfd B: SIGTERM
pending SIGTERM
|
+--> readable through an eligible descriptor
consumption removes the pending occurrenceOverlapping masks therefore express multiple acceptance paths, not multicast delivery.
A descriptor’s mask can also be replaced by calling signalfd() with that existing descriptor as the first argument. Changing the descriptor mask and changing thread signal masks remain separate operations.
SIGKILL and SIGSTOP stay outside the interface
SIGKILL and SIGSTOP cannot be blocked. If either is placed in the mask supplied to signalfd(), Linux silently ignores it for this interface.
The restriction follows the stronger signal rule: these signals cannot be intercepted or deferred by ordinary userspace signal handling. A file-descriptor acceptance path cannot override that property.
Synchronous faults are not a signalfd replacement case
Signals generated synchronously by execution faults, such as a SIGSEGV caused by an invalid memory access or a SIGFPE caused by an arithmetic fault, are not a suitable signalfd path. Linux documents these as signals that must be handled through a signal handler when recovery or diagnostics require interception.
This creates a clear architectural boundary. signalfd fits externally generated or otherwise asynchronously accepted control signals such as termination or reload notifications. It does not turn fault handling into ordinary event-loop I/O.
fork and epoll have a specific edge case
A child created by fork() inherits file descriptors and signal-mask state. A child can read signals sent to it through an inherited signalfd. There is, however, a narrower epoll interaction: an inherited signalfd that was registered with an epoll instance before the fork does not cause the child’s epoll_wait() to report readiness for signals sent to the child.
A child that needs this arrangement can create its own signalfd after the fork and register that descriptor with its event loop.
This is an implementation-facing boundary worth separating from the general descriptor inheritance rule. The inherited descriptor can still be read; the inherited pre-fork epoll registration is the part with the special readiness behavior.
Descriptor state is observable through procfs
Linux exposes the signal mask associated with a signalfd through the corresponding /proc/<pid>/fdinfo/<fd> entry. The sigmask field provides the descriptor’s configured mask in hexadecimal form.
That state is useful when a process appears to have blocked a control signal but the event loop never observes it. Debugging then has at least three distinct questions: which threads block the signal, which signals the descriptor accepts, and whether the event loop monitors the intended descriptor.
signalfd is therefore not a replacement for Linux signal semantics. It is a bridge between those semantics and descriptor-driven control flow: selected signals remain governed by signal masks and targeting rules, while their acceptance becomes a readable event that can occupy the same scheduling point as other event-loop work.