Linux System Programming · intermediate · ~20 min
Feed signals, timers, and inter-thread wakeups into a single epoll loop.
Linux offers three special file descriptors that turn asynchronous events into ordinary readable fds:
All three work cleanly with epoll, so a single event loop can watch them alongside sockets.
Modern Linux C servers feed everything into one epoll loop. These three fds are how that becomes possible:
volatile sig_atomic_t flag = 1 handler dance.An eventfd holds a 64-bit counter.
write(fd, &n, 8) adds n to the counter.read(fd, &out, 8) returns the counter and resets it to zero.EFD_SEMAPHORE mode, each read instead decrements the counter by 1.Create the timer with timerfd_create, then arm it (one-shot or periodic) with timerfd_settime.
read blocks until the timer fires.signalfd delivers signals as readable data.
sigprocmask.signalfd so those signals become readable as struct signalfd_siginfo records.Because you read them in normal code, there are no async-signal-safety constraints.
A signalfd that does not first block the signals via sigprocmask will race: the default handler still runs.
Audit checklist: the correct pattern is block, then signalfd. Anything else is a bug.
_CLOEXEC flag so they do not leak across exec.#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 turn normally-asynchronous events into readable file descriptors:
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 */
read() on an eventfd with a buffer smaller than 8 bytes. The call errors out.List a process's open fds with ls -l /proc/PID/fd/*. Each entry shows its target.
These synthetic fds appear as anonymous inodes, for example anon_inode:[eventfd].
EINVAL.These fds are used by systemd, kubelet, and essentially every modern Linux daemon written in C, C++, or Go that integrates timers and signals into an epoll loop.
SIGINT inside the loop._CLOEXEC flag and always read the full 8-byte payload.sigprocmask before creating the fd.