Linux event notification / eventfd
Linux eventfd: a counter-shaped wakeup primitive for event loops
When one execution context needs to wake another, a pipe carries bytes and a signal carries a coarse condition. eventfd carries something in between: a kernel-owned 64-bit counter exposed as a file descriptor, so producers can add notifications and an epoll loop can consume them without inventing a byte protocol.
2026-08-15
The concrete problem
Imagine a worker pool where background threads finish jobs and one event-loop thread owns the result queue. The workers need to say “there is work,” but the event loop already waits in poll() or epoll_wait(). A busy loop wastes CPU; a condition variable does not naturally join a descriptor-based loop; a signal has awkward coalescing and handler rules; and a pipe requires a framing convention even when the payload is only a wakeup.
eventfd solves the narrow notification problem by combining two interfaces:
| Producer | Consumer | Meaning |
|---|---|---|
write(fd, &value, 8) | read(fd, &value, 8) | Add to, then retrieve, a 64-bit counter. |
Kernel subsystems call eventfd_signal() | poll()/epoll_wait() | Turn completion or state changes into descriptor readiness. |
| Several writers add independently | One reader drains once or decrements one at a time | The count records notification quantity, not message contents. |
What the API actually promises
The syscall is eventfd(initval, flags); the modern entry point is eventfd2. initval is a 32-bit unsigned initial count. The returned descriptor refers to a kernel object whose counter is an unsigned 64-bit value. User space must read and write exactly 8 bytes. Short transfers fail with EINVAL; this is not a byte-stream API.
| Flag | Effect | Typical use |
|---|---|---|
EFD_CLOEXEC | Set close-on-exec atomically. | Prevent accidental inheritance across execve(). |
EFD_NONBLOCK | Empty reads and full writes return EAGAIN instead of sleeping. | Required when the descriptor belongs to a non-blocking event loop. |
EFD_SEMAPHORE | A successful read returns 1 and decrements by 1, even when the count is larger. | Represent one wake token per read rather than drain the whole count. |
Without EFD_SEMAPHORE, writing 3 and then reading once yields 3. With it, the same counter permits three reads that each yield 1. In both modes the descriptor is readable while the count is nonzero. The flag changes consumption semantics, not the storage type or the epoll interface.
A successful user-space write adds its 64-bit unsigned value. The maximum representable value, UINT64_MAX, is reserved for kernel overflow notification and cannot be written by user space; a user write of that value fails with EINVAL. A blocking write sleeps if adding the value would exceed UINT64_MAX - 1. With EFD_NONBLOCK, it returns EAGAIN.
Kernel data and control flow
The central object is struct eventfd_ctx in fs/eventfd.c:
struct eventfd_ctx {
struct kref kref;
wait_queue_head_t wqh;
__u64 count;
unsigned int flags;
int id;
};
The wait-queue head contains the lock used to protect the counter and the waiters that poll(), read(), and kernel producers share. The descriptor's private data points at this context; the file operations keep the context alive until the last reference is released.
The important split is between two kinds of producer. User space enters through eventfd_write(), which validates the 8-byte value and may sleep for space. A kernel subsystem that already has an eventfd_ctx can call eventfd_signal() directly, avoiding a user/kernel copy. That is why eventfd appears in completion APIs such as io_uring and KVM even when no process explicitly calls write().
Waking a waiter is not data delivery. The wakeup says that a subsequent read may make progress. The reader still owns the operation that consumes the count, and multiple readers can race to consume it. A design that needs the associated job or result must store that data in a queue or shared structure and use an appropriate memory-ordering protocol around the eventfd notification.
Important functions and source-level details
| Symbol | Role | Why it matters |
|---|---|---|
eventfd2() | Creates the anonymous inode and its eventfd_ctx. | Use the flags at creation time; fcntl() cannot retrofit EFD_SEMAPHORE. |
eventfd_ctx_fdget() | Resolves an fd and takes a reference to the eventfd context. | Used by kernel-facing registration APIs such as io_uring and KVM. |
eventfd_read() | Implements the blocking/non-blocking read and counter reset/decrement. | Returns exactly one 64-bit counter result per successful call. |
eventfd_write() | Validates and adds a user-supplied value. | Rejects ULLONG_MAX; full-counter behavior depends on EFD_NONBLOCK. |
eventfd_signal() | Increments from kernel code and wakes readers. | Kernel overflow saturates at ULLONG_MAX, which makes EPOLLERR observable. |
eventfd_poll() | Maps counter state to poll bits. | Readable means count > 0; writable means there is room for a user write. |
eventfd_release() | Drops the context reference when the file is closed. | Closing the last fd destroys the object; a copied or passed fd keeps it alive. |
In the current implementation, the wait-queue lock serializes changes to count, reads, writes, and readiness checks. That gives eventfd atomic counter operations, not a general memory-ordering contract for arbitrary application data. The eventfd operation should be paired with C11 atomics, a mutex, or another explicit synchronization scheme when the notification accompanies a shared queue.
Readiness, edge-triggering, and the lost-notification trap
For level-triggered epoll, a nonzero counter remains readable until a reader consumes it. This is forgiving: if the handler processes one job but leaves count at 6 in semaphore mode, epoll will report the fd again. In default mode, one read drains all seven notifications, so the handler must understand that the single value is a batch count, not one event.
Edge-triggered epoll changes the burden. An edge is generated when the readiness state changes, such as a transition from count 0 to count > 0. The handler must drain the eventfd until read() returns EAGAIN; otherwise it can leave the fd readable without receiving another edge. This is the same drain rule as other edge-triggered descriptors, but eventfd makes the mistake especially easy because one default-mode read may consume a whole batch.
for (;;) {
uint64_t count;
ssize_t n = read(event_fd, &count, sizeof count);
if (n == sizeof count) {
process_up_to_count(count); /* or just treat it as “wake up” */
continue;
}
if (n < 0 && errno == EAGAIN)
break; /* drained for EPOLLET */
if (n < 0 && errno == EINTR)
continue;
handle_unexpected_eventfd_error();
break;
}
There is also a semantic race if the eventfd is treated as the queue itself. A producer can increment the counter before it publishes the corresponding item, or a consumer can drain the counter while another thread is still adding work. The fix is not another read(); it is a protocol: publish the item with release semantics, increment the eventfd, then acquire before consuming the queue, with a loop that rechecks the queue until it is empty.
Tradeoffs and failure modes
| Primitive | Best fit | What eventfd does differently |
|---|---|---|
| Pipe | Transport bytes or framed records. | Eventfd transports only a counter, so there is no payload or pipe-buffer capacity to manage. |
| Futex | Low-contention synchronization on a shared memory word. | Eventfd is naturally selectable/epollable and can cross a Unix descriptor handoff. |
| Condition variable | Threads sharing one process-level mutex and predicate. | Eventfd integrates with an I/O loop, but does not replace the predicate or data lock. |
| signalfd | Consume signals through a descriptor. | Eventfd is an application-controlled count and avoids signal disposition semantics. |
| timerfd | Time-based expirations. | Eventfd has no clock or deadline; a separate producer must signal it. |
- Counter overflow: user writes cannot create
UINT64_MAX, but kernel producers can saturate the counter there. Poll reportsEPOLLERR; a read returns the overflow value and clears the condition. Treat this as a diagnostic that notifications were not being consumed quickly enough, not as an ordinary count. - Wrong read size: reading 4 bytes, or writing 4 bytes, fails with
EINVAL. Useuint64_tandsizeof(value). - Unintended blocking: a blocking read in an event-loop callback can stall every other descriptor. Use
EFD_NONBLOCKand handleEAGAINas normal. - Coalescing surprise: default mode turns many writes into one returned sum. If each token must be processed separately, use semaphore mode or maintain a separate queue.
- Close and inheritance: a forked child or an fd passed over
SCM_RIGHTSkeeps the same kernel object alive. SetEFD_CLOEXECand audit every deliberate handoff. - Multiple consumers: reads compete for the same count. The kernel does not assign a notification to a particular worker, so use a work-stealing or queue protocol if ownership matters.
Security and isolation implications
An eventfd is a capability-shaped reference to a kernel counter. Possession of the descriptor grants the ability to add notifications, consume them, and observe whether the counter is nonzero. It is not an authorization system: anyone who receives the fd can perturb the protocol unless the surrounding process boundary prevents it.
Descriptor leakage can cause both confidentiality and availability problems. A leaked reader can drain wakeups and starve the intended consumer; a leaked writer can keep an event loop busy or drive a kernel-backed counter toward overflow. Use EFD_CLOEXEC, close unused copies after fork(), restrict Unix-socket fd passing, and count inherited descriptors in service-manager setups.
Seccomp profiles may deny eventfd2 with ENOSYS or EPERM, and a container shares the host kernel's eventfd implementation. The syscall is not a virtualization boundary: the fd's lifetime and wakeups are local to the kernel object, while the process and namespace policy around that fd comes from the application and runtime.
Debugging method
- Record the fd, flags, producer identity, and the counter value returned by each read. A log that says only “woke up” cannot distinguish a coalescing bug from a missing wakeup.
- Trace the boundary with
strace -f -tt -e trace=eventfd2,read,write,poll,ppoll,epoll_wait,epoll_ctl. Check whether the producer wrote, whether the waiter observedEPOLLIN, and whether the handler drained the fd. - Inspect
/proc/<pid>/fd/<fd>and/proc/<pid>/fdinfo/<fd>on Linux. The fdinfo entry exposes eventfd-specific identity and semaphore information, which helps find an unexpected duplicate. - For an epoll edge-triggered loop, temporarily switch to level-triggered mode. If the problem disappears, the likely bug is a handler that did not read until
EAGAIN. - Test the protocol with writes of 1, then 3, then 0. A zero write is a no-op; a default-mode read after 1 + 3 returns 4, not two records. Test a semaphore fd separately because changing the flag changes the read result.
- If creation fails, distinguish
ENOSYSorEPERMfrom application errors. The former usually indicates an old kernel or seccomp policy; an available header does not guarantee an allowed syscall.
# Linux tracing strace -f -tt -e trace=eventfd2,read,write,poll,ppoll,epoll_wait,epoll_ctl ./eventfd-lab # Inspect an eventfd while the process is paused readlink /proc/$PID/fd/$EVENTFD cat /proc/$PID/fdinfo/$EVENTFD # The descriptor is not a text file; read its protocol through the syscall API. dd if=/proc/$PID/fd/$EVENTFD bs=1 count=8 2>&1 || true
Compact reproducible lab
Goal
Show that a child can add a count through an inherited eventfd, that poll() wakes when the count becomes nonzero, and that default mode drains the count while semaphore mode consumes one token per read.
Prerequisites
Linux 2.6.30 or newer, a C compiler, and standard Linux headers. The program is intentionally Linux-specific because macOS has no eventfd syscall; the semaphore flag used by the second run was added in Linux 2.6.30.
Exact commands
cc -O2 -Wall -Wextra -o /tmp/eventfd-lab -x c - <<'C'
#define _GNU_SOURCE
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/eventfd.h>
#include <sys/poll.h>
#include <sys/wait.h>
#include <unistd.h>
static void die(const char *what) { perror(what); exit(EXIT_FAILURE); }
int main(int argc, char **argv) {
int semaphore = argc > 1 && argv[1][0] == 's';
int fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK |
(semaphore ? EFD_SEMAPHORE : 0));
if (fd < 0) die("eventfd");
pid_t child = fork();
if (child < 0) die("fork");
if (child == 0) {
usleep(100000);
uint64_t n = 3;
if (write(fd, &n, sizeof n) != (ssize_t)sizeof n) _exit(2);
_exit(0);
}
struct pollfd pfd = {.fd = fd, .events = POLLIN};
int rc = poll(&pfd, 1, 50);
if (rc < 0) die("poll before write");
printf("before-write: poll=%d revents=0x%x\n", rc, pfd.revents);
pfd.revents = 0;
rc = poll(&pfd, 1, 1000);
if (rc <= 0) die("poll after write");
printf("after-write: revents=0x%x (POLLIN=0x%x)\n", pfd.revents, POLLIN);
for (int i = 0; i < (semaphore ? 4 : 2); i++) {
uint64_t n;
ssize_t got = read(fd, &n, sizeof n);
if (got == (ssize_t)sizeof n)
printf("read[%d]: %llu\n", i, (unsigned long long)n);
else if (got < 0 && errno == EAGAIN)
printf("read[%d]: EAGAIN (counter empty)\n", i);
else
die("read");
}
if (waitpid(child, NULL, 0) < 0) die("waitpid");
close(fd);
return 0;
}
C
/tmp/eventfd-lab
/tmp/eventfd-lab semaphore
# Optional: observe the syscalls and the inherited fd.
strace -f -tt -e trace=eventfd2,read,write,poll,wait4 /tmp/eventfd-lab
Expected result
Default mode should look like this, with a platform-dependent readiness bitmask:
before-write: poll=0 revents=0x0 after-write: revents=0x1 (POLLIN=0x1) read[0]: 3 read[1]: EAGAIN (counter empty)
Semaphore mode should return one for each of the first three reads, then EAGAIN:
before-write: poll=0 revents=0x0 after-write: revents=0x1 (POLLIN=0x1) read[0]: 1 read[1]: 1 read[2]: 1 read[3]: EAGAIN (counter empty)
The first poll times out because the counter is zero. The child writes 3, which makes the descriptor readable. In default mode the first read returns the whole batch; in semaphore mode it consumes exactly one token.
Revealing variations
Change the write value from 3 to 0. The write succeeds but the second poll still times out because adding zero does not make the counter readable. Then remove EFD_NONBLOCK and leave the final read in place: the program will block after the counter is drained, demonstrating why an event-loop fd should normally be nonblocking.
Environment result: this host is macOS (Darwin 24.6, Apple clang 17), and its SDK has no <sys/eventfd.h> or eventfd syscall. The Linux compile command therefore cannot run here. The corrected procedure is to save the C block as eventfd-lab.c, then run it on Linux or in a Linux container with docker run --rm -v "$PWD":/lab -w /lab debian:bookworm bash -lc 'apt-get update && apt-get install -y build-essential strace && cc -O2 -Wall -Wextra eventfd-lab.c -o eventfd-lab && ./eventfd-lab'. Containers share the host kernel, so a restrictive seccomp profile can still deny eventfd2.
Production example: completion notification around a queue
A practical worker-pool design keeps the payload in a mutex-protected or lock-free queue and uses eventfd only as the event-loop doorbell. A worker publishes a completed-job record, then writes 1 to the eventfd. The loop receives EPOLLIN, reads the counter once, and drains the queue until it is empty. The count can be treated as a hint or batch size; the queue is the source of truth.
Kernel-backed systems use the same shape. io_uring can register an eventfd so completion queue entries wake an external poller. KVM exposes irqfd so a userspace eventfd write can inject a virtual interrupt. In both cases, the important abstraction is not “a pipe with a funny payload”; it is a reference-counted kernel object whose counter and wait queue bridge a producer to a descriptor-oriented consumer.
Choose eventfd when the signal is naturally a count and the consumer already lives in a poll/epoll loop. Choose a pipe or socket when the wakeup must carry data. Choose a futex or condition variable when shared-memory synchronization is the primary problem. The primitive is small; the surrounding predicate and ownership protocol are where correctness lives.
Primary sources
- Linux
eventfd(2)manual - Linux source:
fs/eventfd.c - Linux source: kernel eventfd API declarations
- Linux UAPI: eventfd flags
- Linux
epoll(7)manual, including edge-triggered drain rules - Linux
io_uring_register(2), eventfd completion notification - KVM API:
KVM_IRQFDand eventfd-backed virtual interrupts
Remember: eventfd is a count plus a wakeup, not a message queue. Use the counter for notification, a separate queue for data, nonblocking reads in event loops, and explicit synchronization for the memory those notifications describe.