CodeRigor

FREE LESSON

Signals & descriptor-based notifications

Distinguish signal delivery from queued work; consume blocked signals with signalfd and aggregate wakeups with eventfd without allocating or logging in a signal handler.

Signals notify a process or thread that something happened. They are not an arbitrary application-message queue. A signal has a disposition, and each thread has a mask of blocked signals. A blocked selected signal may remain pending until ordinary code consumes it through signalfd or a synchronous waiting API.

Pending does not mean counted

Standard signals can coalesce: two sends while the same signal is pending may still leave one pending signal. Real-time signals have different queueing rules. Do not use a standard signal's arrival count as the number of jobs to process. Store work in a queue and use a notification only to wake its consumer.

Two standard-signal sends can become one pending signal
While SIGUSR1 is blocked, repeated sends can coalesce. A signalfd read consumes the pending signal in ordinary code.
Two standard-signal sends can become one pending signal
While SIGUSR1 is blocked, repeated sends can coalesce. A signalfd read consumes the pending signal in ordinary code.
C++
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <unistd.h>
#include <cerrno>
#include <cstdint>
#include <sys/eventfd.h>

int main() {
    int fd=::eventfd(0,EFD_NONBLOCK|EFD_CLOEXEC);assert(fd>=0);std::uint64_t a=2,b=5,value=0;
    assert(::write(fd,&a,8)==8);assert(::write(fd,&b,8)==8);assert(::read(fd,&value,8)==8);
    auto n=::read(fd,&a,8);std::printf("%llu %d\n",static_cast<unsigned long long>(value),n==-1&&errno==EAGAIN);::close(fd);
}

The first runnable example instead uses eventfd, whose uint64 counter adds notification values. Without EFD_SEMAPHORE, one read returns the sum and resets the counter. With that flag, reads have a different one-at-a-time contract. An empty nonblocking read reports EAGAIN; writing UINT64_MAX is invalid.

eventfd aggregates notification counts
Without EFD_SEMAPHORE, writes of two and three produce a read of five and reset the counter to zero. Store actual work separately.
eventfd aggregates notification counts
Without EFD_SEMAPHORE, writes of two and three produce a read of five and reset the counter to zero. Store actual work separately.

Keep ordinary C++ work outside handlers

An asynchronous handler interrupts whatever code was running. Allocation, formatting, logging and locking may reenter library state or deadlock. A very small handler can use only appropriate async-signal-safe operations, preserving errno if necessary. A signalfd-based application instead consumes records in ordinary event-loop control flow and can handle shutdown alongside I/O readiness. SIGKILL and SIGSTOP cannot be intercepted this way.

Dispositions and masks have different scope
Create a deliberate signal-ownership policy before starting workers. Creating signalfd does not install masks across existing threads.
Dispositions and masks have different scope
Create a deliberate signal-ownership policy before starting workers. Creating signalfd does not install masks across existing threads.

Choose signal ownership before starting worker threads: new threads inherit the creator's mask, and signalfd creation alone does not block signals in every thread. A thread-directed signal can only be consumed by the appropriate thread. The build accepts a borrowed, already-configured nonblocking signalfd so it does not silently change its caller's masks or dispositions.

C++
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>

int main() {
    sigset_t set,old,pending;::sigemptyset(&set);::sigaddset(&set,SIGUSR1);assert(::sigprocmask(SIG_BLOCK,&set,&old)==0);
    assert(::raise(SIGUSR1)==0);assert(::raise(SIGUSR1)==0);assert(::sigpending(&pending)==0);int before=::sigismember(&pending,SIGUSR1);
    timespec zero{};assert(::sigtimedwait(&set,nullptr,&zero)==SIGUSR1);
    assert(::sigpending(&pending)==0);int after=::sigismember(&pending,SIGUSR1);
    while(::sigtimedwait(&set,nullptr,&zero)>=0){} assert(::sigprocmask(SIG_SETMASK,&old,nullptr)==0);
    std::printf("%d %d\n",before,after);
}

Teardown matters too

If an owned signal is still pending when you restore an unblocked mask, its default action may happen immediately. The test fixture masks only its selected signals, raises them in the current thread, drains remaining records on cleanup, and restores the previous mask. This keeps a failed learner assertion from leaving an unrelated signal-delivery hazard for the next test.

Before the fill, distinguish three states: a positive counter value, an empty counter reporting EAGAIN, and an invalid descriptor reporting EBADF. Then consider two different pending signals, which a one-record drain must not silently leave behind when the caller's limit allows both. Continue to epoll.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Learn signal delivery and dispositions first. The lesson then uses blocked signals with signalfd and ordinary eventfd reads to keep logging and allocation out of asynchronous handlers.