A file-descriptor event loop can wait efficiently for sockets, pipes, timers, and other kernel objects. A common problem appears when work originates somewhere that is not already represented by a file descriptor: another thread changes shared state and needs the loop to wake immediately.
Polling shared state on a timer adds latency or wastes wakeups. A condition variable can wake a thread, but it cannot be placed directly in the same poll() or epoll wait set as a socket. A pipe can bridge the two models, but using a pipe only as a wakeup signal means maintaining a read end, a write end, and byte-buffer semantics that the application may not actually need.
Linux provides eventfd for this case. eventfd() creates a file descriptor backed by a kernel-maintained 64-bit counter. A producer writes an integer to increment the counter. The descriptor becomes readable while the counter is nonzero, so an event loop can wait for it alongside ordinary I/O.
The useful mental model is:
worker thread changes shared state
|
| write 1
v
eventfd counter
|
| counter > 0
v
poll/epoll reports readable
|
| read counter
v
event loop processes shared stateeventfd is Linux-specific. It is a good fit when the notification itself is small and the real data lives somewhere else, such as a thread-safe queue.
Start with the counter, not with the wakeup
Create an eventfd with an initial counter value and optional flags:
#include <sys/eventfd.h>
int wake_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (wake_fd == -1) {
/* handle error */
}The initial value is zero, so the descriptor starts out not readable.
EFD_CLOEXEC prevents the descriptor from accidentally surviving an execve(). EFD_NONBLOCK makes reads and writes fail with EAGAIN instead of blocking when they cannot proceed immediately. Nonblocking mode is usually appropriate inside an event loop because readiness can change between observing an event and performing the I/O.
The key detail is that an eventfd carries a counter, not a byte stream. Each successful write supplies exactly eight bytes containing an unsigned 64-bit integer in the host’s native byte order. The kernel adds that value to the counter.
For example:
#include <stdint.h>
#include <unistd.h>
uint64_t increment = 1;
ssize_t n = write(wake_fd, &increment, sizeof increment);
if (n != sizeof increment) {
/* handle error */
}If three producers each write 1 before the consumer reads, the counter becomes 3.
Without EFD_SEMAPHORE, one successful read returns the current counter value and resets the counter to zero:
uint64_t pending;
ssize_t n = read(wake_fd, &pending, sizeof pending);
if (n != sizeof pending) {
/* handle error */
}If the counter was 3, pending becomes 3.
That aggregation is one reason eventfd works well as a wakeup mechanism. Several notifications can collapse into one readable state while still leaving a count that says how much was accumulated.
Wait for eventfd with ordinary I/O
An eventfd is readable when its counter is greater than zero. That means poll() can wait for it exactly as it waits for a socket or pipe.
Here is a small complete example. A worker thread signals the event loop three times. The event loop wakes, drains the counter, and keeps going until it has observed all three notifications:
#define _GNU_SOURCE
#include <errno.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/eventfd.h>
#include <poll.h>
#include <unistd.h>
struct worker_args {
int wake_fd;
};
static void *worker_main(void *arg)
{
struct worker_args *args = arg;
for (int i = 0; i < 3; i++) {
uint64_t one = 1;
if (eventfd_write(args->wake_fd, one) == -1) {
perror("eventfd_write");
return (void *)1;
}
}
return NULL;
}
int main(void)
{
int wake_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if (wake_fd == -1) {
perror("eventfd");
return EXIT_FAILURE;
}
pthread_t worker;
struct worker_args args = { .wake_fd = wake_fd };
int err = pthread_create(&worker, NULL, worker_main, &args);
if (err != 0) {
errno = err;
perror("pthread_create");
close(wake_fd);
return EXIT_FAILURE;
}
uint64_t total = 0;
while (total < 3) {
struct pollfd pfd = {
.fd = wake_fd,
.events = POLLIN,
};
int ready = poll(&pfd, 1, -1);
if (ready == -1) {
if (errno == EINTR) {
continue;
}
perror("poll");
break;
}
if (pfd.revents & POLLIN) {
eventfd_t count;
if (eventfd_read(wake_fd, &count) == -1) {
if (errno == EAGAIN) {
continue;
}
perror("eventfd_read");
break;
}
total += count;
printf("received %llu notification(s), total=%llu\n",
(unsigned long long)count,
(unsigned long long)total);
}
}
void *worker_result;
err = pthread_join(worker, &worker_result);
if (err != 0) {
errno = err;
perror("pthread_join");
close(wake_fd);
return EXIT_FAILURE;
}
close(wake_fd);
return worker_result == NULL && total == 3
? EXIT_SUCCESS
: EXIT_FAILURE;
}The exact grouping is deliberately unspecified by the program. Depending on scheduling, the loop might read 3 once, or it might observe smaller counts across multiple reads. The guarantee that matters is that successful writes add to the counter and a normal read returns the accumulated nonzero value before resetting it to zero.
Do not make application correctness depend on each write producing a separate poll() wakeup. Readiness is a state: the descriptor is readable while its counter is nonzero.
Put the data in a queue and use eventfd only as the doorbell
The counter can represent a quantity, but many applications need to transfer structured work: callbacks, messages, file paths, or completed jobs.
Do not encode that payload into the eventfd. Instead, keep the payload in a synchronization-safe queue and treat the eventfd as a doorbell:
producer thread:
lock queue
enqueue work
unlock queue
write 1 to eventfd
event-loop thread:
read eventfd
lock queue
remove available work
unlock queue
process workThe ordering is important. Enqueue the work before signaling. If the producer writes first and enqueues second, the event loop can wake, find no work, and go back to sleep before the item becomes visible.
The queue still needs its own synchronization. eventfd does not make arbitrary shared memory safe. Its job is notification, not mutual exclusion and not storage of structured data.
There are two common policies for the value written to the eventfd.
Write once per queued item
Writing 1 for every item makes the counter approximate the number of notifications that have not yet been drained.
This can be useful for accounting, but the queue remains the source of truth. A consumer can read a count of five and then drain five or more items if its queue policy allows producers to add more work concurrently.
Signal only the transition from empty to nonempty
A queue can also notify only when it changes from empty to nonempty. This reduces eventfd writes during bursts because the event loop needs only one doorbell ring to know that work exists.
This optimization requires careful queue synchronization. The producer must not miss the empty-to-nonempty transition, and the consumer must coordinate clearing or rearming the notification state with draining the queue. If that protocol is unnecessary for the workload, writing once per enqueue is easier to reason about.
Drain readiness instead of assuming one read per loop iteration
With normal eventfd semantics, one successful read resets the counter to zero. In a level-triggered poll() or epoll loop, that usually drains the current readiness state in one operation.
Nonblocking mode still matters. Another consumer could read the counter first, or a more complicated program could race with another operation. In that case, eventfd_read() can fail with EAGAIN; an event loop should treat that as “nothing remains to drain” rather than as a fatal error.
If a program registers the eventfd with edge-triggered epoll (EPOLLET), follow the usual edge-triggered rule: perform nonblocking I/O until it would block. For a normal, non-semaphore eventfd, a successful read normally takes the counter to zero, so a following read will return EAGAIN unless another writer raced in.
The important distinction is between readiness and messages. An eventfd being readable does not promise that there was exactly one producer action since the previous call to epoll_wait().
Understand the counter limits and write failures
The eventfd counter is unsigned 64-bit state, but not every 64-bit value is a valid stored counter value. The maximum ordinary counter value is UINT64_MAX - 1.
A write adds its value to the existing counter. If the result would exceed that maximum, a blocking eventfd write waits for a read to reduce the counter. With EFD_NONBLOCK, the write fails with EAGAIN instead.
Writing UINT64_MAX itself is invalid and fails with EINVAL.
For a wakeup descriptor where producers usually write 1, reaching the limit through ordinary user-space writes is unrealistic in healthy software. Even so, the failure path matters. If the application treats notification as required for correctness, silently ignoring eventfd_write() errors can leave queued work sleeping indefinitely.
Choose a policy explicitly. Depending on the program, that may mean logging and terminating, retrying under a bounded policy, or relying on another already-pending notification only when the queue protocol proves that doing so is safe.
Use EFD_SEMAPHORE only when one unit per read is the desired contract
Creating the descriptor with EFD_SEMAPHORE changes read behavior:
int slots_fd = eventfd(0, EFD_CLOEXEC | EFD_SEMAPHORE);When the counter is nonzero, each successful read returns 1 and decrements the counter by one instead of returning the entire accumulated value and resetting it to zero.
If the counter contains 3, three reads can each consume one unit.
This can model semaphore-like permits, but it changes the event-loop behavior substantially. A normal eventfd is attractive for wakeups because one read coalesces accumulated notifications. Semaphore mode intentionally does the opposite.
Use EFD_SEMAPHORE when consumers need to claim units individually. Do not enable it merely because multiple threads are involved.
Know what eventfd guarantees across process boundaries
After fork(), the child inherits a duplicate descriptor referring to the same eventfd object. Reads and writes through those descriptors operate on the same counter.
Across execve(), the descriptor is preserved unless close-on-exec is set. That is why EFD_CLOEXEC is a sensible default when the descriptor is an internal implementation detail rather than an intentionally inherited IPC channel.
This behavior makes eventfd usable between related processes as well as threads. However, it does not provide message framing, peer identity, half-close semantics, or payload transport. A pipe, Unix-domain socket, shared-memory protocol, or another IPC mechanism may be a better fit when those properties matter.
Compare eventfd with the usual alternatives
eventfd is not a universal replacement for pipes or condition variables.
A pipe is portable across POSIX-like systems and can carry bytes. It is often the right choice when the notification and the payload naturally belong in the same byte stream. When a pipe is used only to wake an event loop, eventfd has a simpler counter model and uses one descriptor instead of a read/write pair.
A condition variable is a natural primitive when threads are already waiting under a mutex and do not need integration with file-descriptor readiness. Adding eventfd just to imitate a condition variable can make the design harder to understand.
A mutex-protected atomic flag plus periodic polling may be sufficient when latency requirements are loose and the program already wakes frequently for other reasons. Avoid introducing a Linux-specific system call when the simpler mechanism meets the requirements.
eventfd is strongest when all of these are true:
- the program runs on Linux;
- one thread or subsystem must wake a file-descriptor event loop;
- the payload can live in shared state or another channel;
- coalescing notifications is acceptable or useful.
Avoid common notification bugs
The most important mistakes are protocol mistakes rather than API syntax mistakes.
Signaling before publishing the work. Put shared state into a consumer-visible state before writing to the eventfd.
Treating a read value as queue truth. The counter tells you what was written to the eventfd. It does not automatically equal the current queue length when producers and consumers run concurrently.
Ignoring write errors. A missed required wakeup can turn into an indefinite stall.
Expecting one readiness event per write. Readiness reports that the counter is nonzero; multiple writes can be coalesced.
Using blocking operations inside an event loop. EFD_NONBLOCK avoids turning a stale readiness observation into an unexpected blocking read.
Forgetting descriptor lifetime. Close the eventfd when it is no longer needed, and use EFD_CLOEXEC unless inheritance across execve() is intentional.
Conclusion
eventfd turns a small piece of shared counter state into ordinary file-descriptor readiness. That makes it a precise bridge between thread or process notifications and Linux event loops.
For the common worker-to-event-loop pattern, keep the real work in a properly synchronized queue, publish that work before signaling, write a small counter increment, and let the event loop drain the eventfd before consuming queued work. Treat readiness as a state rather than as one message per wakeup, handle nonblocking failures explicitly, and reserve EFD_SEMAPHORE for designs that truly need one-unit-at-a-time consumption.
When the problem is only “wake this Linux event loop because shared work is ready,” that narrow model is exactly what makes eventfd useful.