timerfd, signalfd, eventfd: every event source is a file descriptor now
Published by RodHat

The self-pipe trick is at least thirty years old. You create a pipe, install a signal handler that writes one byte to the write end, then select/poll/epoll on the read end. When a signal arrives, your event loop wakes up, reads the byte, and dispatches. Every tutorial still teaches it. Every event loop library either implements it or wraps a variant.
It is garbage. It was always garbage. Signal handlers that touch global state are undefined behavior waiting for a sufficiently creative interrupt window, write(2) is not async-signal-safe in all implementations despite what the man page says about the specific case, and you still have to coordinate the handler with the main thread to avoid losing signals during a race between the write and the read. The whole thing is a workaround for the fact that select/poll could not block on a signal. It was never a design.
Linux 2.6.22 added signalfd(2). Linux 2.6.25 added timerfd_create(2) and eventfd(2). Together they close the loop: every event source that used to require a workaround is now a file descriptor you can hand to epoll and forget.
signalfd: signals as readable bytes
Block the signals you want to handle. Block them in every thread, or at least the threads you don’t want interrupted. Then create a signalfd against that same signal set.
#include <sys/signalfd.h>
#include <signal.h>
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGHUP);
/* Block these signals in the calling thread (and all threads it spawns). */
if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0)
err(1, "sigprocmask");
int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
if (sfd < 0)
err(1, "signalfd");
Pass -1 as the first argument to create a new fd. Pass an existing signalfd to update its mask without creating a new descriptor.
When a signal in the mask is delivered to the process, sfd becomes readable. Read one struct signalfd_siginfo per read call, or loop until you get EAGAIN:
struct signalfd_siginfo info;
ssize_t n = read(sfd, &info, sizeof(info));
if (n != sizeof(info))
err(1, "read signalfd");
switch (info.ssi_signo) {
case SIGTERM:
case SIGINT:
shutdown_requested = 1;
break;
case SIGHUP:
reload_config();
break;
}
The struct gives you ssi_signo, ssi_errno, ssi_code, ssi_pid, ssi_uid, ssi_fd, ssi_tid, ssi_band, ssi_int, ssi_ptr, ssi_utime, ssi_stime, and ssi_addr. That is the full siginfo_t equivalent, including sender PID and UID. Your signal handler never had any of that.
No handler. No global flag. No pipe. No race. The signal sits in the kernel’s queue until your event loop gets around to reading it, which is exactly what you want.
timerfd: timers as readable counters
SIGALRM + setitimer is the old way. Interval timer fires, signal arrives, handler runs at an arbitrary point in your code, you check a global. timerfd_create replaces all of it with a file descriptor that becomes readable when the timer fires.
#include <sys/timerfd.h>
int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
if (tfd < 0)
err(1, "timerfd_create");
struct itimerspec its = {
.it_interval = { .tv_sec = 5, .tv_nsec = 0 }, /* repeat every 5s */
.it_value = { .tv_sec = 5, .tv_nsec = 0 }, /* first fire in 5s */
};
if (timerfd_settime(tfd, 0, &its, NULL) < 0)
err(1, "timerfd_settime");
Use CLOCK_MONOTONIC for intervals. Use CLOCK_REALTIME only when you need to fire at a wall-clock time, and pair it with TFD_TIMER_ABSTIME to set it_value as an absolute timestamp rather than a relative offset. CLOCK_REALTIME timers can go wild after adjtime(3) or settimeofday(2) calls; CLOCK_MONOTONIC does not care.
When the timer fires, the fd is readable. Read an unsigned 64-bit integer: it counts how many expirations occurred since the last read. If your event loop runs slow and the timer fires three times between reads, you get 3 back. You can detect missed ticks rather than silently losing them, which SIGALRM cannot do.
uint64_t expirations;
ssize_t n = read(tfd, &expirations, sizeof(expirations));
if (n != sizeof(expirations))
err(1, "read timerfd");
if (expirations > 1)
log_warn("timer fired %lu times between reads (loop is slow)", expirations);
do_periodic_work();
Stop the timer by setting it_value to zero:
struct itimerspec stop = { 0 };
timerfd_settime(tfd, 0, &stop, NULL);
eventfd: a counter you can share
eventfd(2) is a file descriptor backed by a 64-bit unsigned integer in the kernel. Write to add to the counter; read to consume it. When the counter is nonzero, the fd is readable. When the counter is at UINT64_MAX - 1, it is not writable. That is the entire API.
#include <sys/eventfd.h>
int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (efd < 0)
err(1, "eventfd");
The initial value (here 0) seeds the counter.
To wake a waiting epoll loop from another thread:
/* producer thread */
uint64_t one = 1;
if (write(efd, &one, sizeof(one)) != sizeof(one))
err(1, "write eventfd");
/* consumer/event loop */
uint64_t count;
if (read(efd, &count, sizeof(count)) != sizeof(count))
err(1, "read eventfd");
/* count is however many writes happened since the last read */
process_pending_work(count);
Pass EFD_SEMAPHORE at creation time to change the read semantics: each read decrements the counter by exactly 1 and returns 1, instead of draining the entire counter at once. Use this when multiple readers compete for individual “tokens” rather than a batch of work. Without the flag, the default drain-on-read is usually what you want in a single-reader event loop.
eventfd works across fork too. The child gets a copy of the fd referencing the same kernel object, so parent and child can coordinate through it. The SCM_RIGHTS tip covers the related pattern of passing live fds between unrelated processes.
Putting it together: an epoll loop over all three
#include <sys/epoll.h>
#include <sys/signalfd.h>
#include <sys/timerfd.h>
#include <sys/eventfd.h>
#include <signal.h>
#include <stdint.h>
#include <unistd.h>
#include <err.h>
#define MAX_EVENTS 16
int main(void)
{
/* signals */
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGINT);
sigprocmask(SIG_BLOCK, &mask, NULL);
int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
/* timer: 10s interval */
int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
struct itimerspec its = {
.it_interval = { .tv_sec = 10 },
.it_value = { .tv_sec = 10 },
};
timerfd_settime(tfd, 0, &its, NULL);
/* event fd for worker thread wake-up */
int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
int ep = epoll_create1(EPOLL_CLOEXEC);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sfd; epoll_ctl(ep, EPOLL_CTL_ADD, sfd, &ev);
ev.data.fd = tfd; epoll_ctl(ep, EPOLL_CTL_ADD, tfd, &ev);
ev.data.fd = efd; epoll_ctl(ep, EPOLL_CTL_ADD, efd, &ev);
struct epoll_event events[MAX_EVENTS];
for (;;) {
int n = epoll_wait(ep, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
int fd = events[i].data.fd;
if (fd == sfd) {
struct signalfd_siginfo info;
read(sfd, &info, sizeof(info));
if (info.ssi_signo == SIGTERM || info.ssi_signo == SIGINT)
goto done;
} else if (fd == tfd) {
uint64_t exp;
read(tfd, &exp, sizeof(exp));
do_periodic_work(exp);
} else if (fd == efd) {
uint64_t count;
read(efd, &count, sizeof(count));
drain_work_queue(count);
}
}
}
done:
return 0;
}
No signal handlers. No self-pipe. No global flags checked in multiple places. The loop blocks in epoll_wait and every wake-up comes from a readable fd with data that tells you exactly what fired and how many times.
Error handling is stripped for clarity; in real code, check every return value, and consider EINTR on epoll_wait if you have signals that are intentionally not blocked (realtime signals, SIGCHLD for waitpid, etc.).
FreeBSD
These three syscalls are Linux-only. FreeBSD’s kqueue(2) covers the same ground with different mechanics: EVFILT_TIMER for timers, EVFILT_SIGNAL for signals, EVFILT_USER for the eventfd-equivalent pattern. The concepts map cleanly. The code does not.
If you’re targeting both, abstract the event loop behind a thin layer, or use libuv/libevent, which implement kqueue and epoll backends with a common interface. If you’re writing portable systems code and care about getting it right on both, W. Richard Stevens covered the portability traps in detail in the TCP/IP Illustrated Vol. 1 appendices, though kqueue postdates the book; the relevant reference now is the kevent(2) man page on any modern FreeBSD install.
The SO_REUSEPORT tip and splice and tee tip pair with this one: between the three of them you have the kernel primitives for connections, data movement, and event dispatch, all as file descriptors.