Linux System Programming · intermediate · ~20 min
Feed signals, timers, and inter-thread wakeups into a single epoll loop.
eventfd is a per-process counter with a fd interface. timerfd is a periodic or one-shot timer that delivers via a fd. signalfd lets signals arrive on a fd instead of via async-unsafe handlers. All three are epoll-friendly.
Modern Linux C servers feed everything into one epoll loop. eventfd is how worker threads wake the loop. timerfd handles timeouts. signalfd replaces the entire volatile sig_atomic_t flag = 1 dance.
eventfd: maintains a 64-bit counter. write(fd, &n, 8) adds n; read(fd, &out, 8) returns the counter and zeroes it (or, in EFD_SEMAPHORE mode, decrements by 1).
timerfd_create + timerfd_settime: arm a one-shot or periodic timer. read blocks until it fires; returns the number of expirations.
signalfd: block the signals you care about with sigprocmask first, then signalfd makes them readable as struct signalfd_siginfo records. NO async-signal-safety constraints — you handle them in normal code.
Pentester mindset. A signalfd that doesn't first block the signals via sigprocmask will race — the default handler still runs. Audit checklist: block-then-signalfd is the pattern; anything else is a bug.
Defensive coding habit. Always create with _CLOEXEC flag so fds don't leak across exec. Always read the 8-byte payload — a short read errors out.
#include <sys/eventfd.h>
int eventfd(unsigned init, int flags);
#include <sys/timerfd.h>
int timerfd_create(int clockid, int flags);
int timerfd_settime(int fd, int flags, const struct itimerspec *new, struct itimerspec *old);
#include <sys/signalfd.h>
int signalfd(int fd, const sigset_t *mask, int flags);
Three Linux-specific syscalls let you turn ordinarily-async things (a
timer firing, a signal arriving, another thread saying 'hey wake up') into
file-descriptor-readable events. Combined with epoll this gives you one
unified event loop — no signal handlers, no pselect, no condition variables.
int ev = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
/* In thread A: */ uint64_t n = 1; write(ev, &n, 8);
/* In epoll loop: */ uint64_t got; read(ev, &got, 8); /* clears the counter */
/* One-shot timer in 100 ms: */
int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
struct itimerspec spec = {0};
spec.it_value.tv_sec = 0;
spec.it_value.tv_nsec = 100 * 1000 * 1000;
timerfd_settime(tfd, 0, &spec, NULL);
/* In epoll loop, when tfd is readable: */
uint64_t exp; read(tfd, &exp, 8); /* exp = number of times the timer fired */
ls -l /proc/PID/fd/* shows each fd's target. eventfd/timerfd appear as anon_inode:[eventfd] etc.
Each of these is a real fd; close it. Always read exactly 8 bytes — short reads are EINVAL.
systemd, kubelet, every modern Linux daemon written in C/C++/Go that integrates timers and signals into an epoll loop.
Turn timers, signals, and wakeups into fds. One epoll loop owns them all.