chaowen.← Back to archive

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.

Linux deep dive · Event notification and IPC

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:

ProducerConsumerMeaning
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 independentlyOne reader drains once or decrements one at a timeThe count records notification quantity, not message contents.
Core invariant: an eventfd is a counter with a readiness interface. It is not a queue of values. In the default mode, one successful 8-byte read returns the entire current count and resets it to zero.
producer A ── write(2) ──┐ │ producer B ── write(5) ──┼──▶ eventfd_ctx.count = 7 ──▶ epoll: EPOLLIN │ │ kernel completion ──────┘ ▼ read(8 bytes) → 7, count = 0 EFD_SEMAPHORE changes only the read rule: read(8 bytes) → 1, count = 6

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.

FlagEffectTypical use
EFD_CLOEXECSet close-on-exec atomically.Prevent accidental inheritance across execve().
EFD_NONBLOCKEmpty reads and full writes return EAGAIN instead of sleeping.Required when the descriptor belongs to a non-blocking event loop.
EFD_SEMAPHOREA 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.

eventfd2(initval, flags) │ ├─ eventfd_file_create() │ └─ kzalloc(struct eventfd_ctx), init_waitqueue_head(), count = initval └─ anon_inode_getfile("eventfd", &eventfd_fops, ctx, flags) └─ fd_install() → userspace fd write(fd, &n, 8) eventfd_signal(ctx) │ │ ├─ eventfd_ctx_fdget(fd) ├─ lock ctx->wqh.lock ├─ lock ctx->wqh.lock ├─ saturate or add count ├─ reject ULLONG_MAX / wait if full └─ wake waiters with EPOLLIN ├─ count += n; wake readable waiters └─ unlock read(fd, &n, 8) │ ├─ lock ctx->wqh.lock ├─ if count == 0: sleep or return EAGAIN ├─ semaphore? n = 1, count-- : n = count, count = 0 └─ unlock; copy 8 bytes to userspace poll/epoll eventfd_poll() └─ waitqueue registration ────────────── count > 0 → EPOLLIN count < MAX-1 → EPOLLOUT count == ULLONG_MAX → EPOLLERR

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

SymbolRoleWhy 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

PrimitiveBest fitWhat eventfd does differently
PipeTransport bytes or framed records.Eventfd transports only a counter, so there is no payload or pipe-buffer capacity to manage.
FutexLow-contention synchronization on a shared memory word.Eventfd is naturally selectable/epollable and can cross a Unix descriptor handoff.
Condition variableThreads sharing one process-level mutex and predicate.Eventfd integrates with an I/O loop, but does not replace the predicate or data lock.
signalfdConsume signals through a descriptor.Eventfd is an application-controlled count and avoids signal disposition semantics.
timerfdTime-based expirations.Eventfd has no clock or deadline; a separate producer must signal it.

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

  1. 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.
  2. 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 observed EPOLLIN, and whether the handler drained the fd.
  3. 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.
  4. 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.
  5. 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.
  6. If creation fails, distinguish ENOSYS or EPERM from 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

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.