Linux System Programming · intermediate · ~20 min

eventfd, timerfd, signalfd — synthetic fds

Feed signals, timers, and inter-thread wakeups into a single epoll loop.

Overview

Linux offers three special file descriptors that turn asynchronous events into ordinary readable fds:

  • eventfd is a per-process counter exposed through a file descriptor.
  • timerfd is a one-shot or periodic timer that delivers its expirations through a file descriptor.
  • signalfd lets signals arrive on a file descriptor instead of through an asynchronous, signal-unsafe handler.

All three work cleanly with epoll, so a single event loop can watch them alongside sockets.

Why it matters

Modern Linux C servers feed everything into one epoll loop. These three fds are how that becomes possible:

  • eventfd is how a worker thread wakes the loop.
  • timerfd handles timeouts.
  • signalfd replaces the old, fragile volatile sig_atomic_t flag = 1 handler dance.

Core concepts

eventfd

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.
  • In EFD_SEMAPHORE mode, each read instead decrements the counter by 1.

timerfd

Create the timer with timerfd_create, then arm it (one-shot or periodic) with timerfd_settime.

  • A read blocks until the timer fires.
  • The value returned is the number of expirations since the last read.

signalfd

signalfd delivers signals as readable data.

  1. First block the signals you care about with sigprocmask.
  2. Then call 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.

Pentester mindset

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.

Defensive coding habit

  • Always create these fds with the _CLOEXEC flag so they do not leak across exec.
  • Always read the full 8-byte payload. A short read errors out.

Syntax notes

#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);

Lesson

Three Linux-specific syscalls turn normally-asynchronous events into readable file descriptors:

  • a timer firing
  • a signal arriving
  • another thread saying "wake up"

Combined with epoll, this gives you one unified event loop. No signal handlers, no pselect, no condition variables.

Code examples

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 */

Line by line

/* 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 */

Common mistakes

  • Calling read() on an eventfd with a buffer smaller than 8 bytes. The call errors out.

Debugging tips

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].

Memory safety

  • Each of these is a real file descriptor. Close it when you are done.
  • Always read exactly 8 bytes. A short read returns EINVAL.

Real-world uses

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.

Practice tasks

  1. Use an eventfd as a thread-wakeup primitive.
  2. Set up a one-shot timerfd and drain it.
  3. Use a signalfd to handle SIGINT inside the loop.

Summary

  • eventfd, timerfd, and signalfd turn wakeups, timers, and signals into readable file descriptors.
  • One epoll loop can then own all of them, alongside sockets.
  • Always use the _CLOEXEC flag and always read the full 8-byte payload.
  • For signalfd, block the signals with sigprocmask before creating the fd.

Practice with these exercises