How epoll Wakes a Waiting Thread in the Linux Kernel

Most explanations of epoll stop at two data structures: a red-black tree for registered descriptors and a ready list for events. That leaves the interesting questions unanswered.
Who puts a socket on the ready list? How does epoll_ctl(ADD) connect an ordinary socket to an epoll instance? Where does a thread in epoll_wait() sleep? What happens when another event arrives while the kernel is copying results to user space?
This article follows the normal readiness path in Linux 6.10. Device drivers, NAPI, protocol processing, and busy polling have additional branches; the focus here is the boundary where a pollable file meets epoll.
For a useful complexity model, suppose one epoll instance watches N descriptors. One epoll_wait() examines R ready-list candidates and returns K valid events, with K limited by both R and maxevents:
EPOLL_CTL_ADDinserts into the interest tree, primarily O(log N);- readiness invokes a callback registered on the target file’s wait queue;
epoll_wait()rechecks R candidates and copies at most K results.
The win is not “epoll is O(1).” The win is that a persistent interest set turns repeated scans of N descriptors into work focused on the candidates that received readiness notifications.
The interest set, ready list, and two wait queues
epoll_create1() creates an anonymous file whose private_data points to struct eventpoll. In Linux 6.10, the important fields include:
struct eventpoll {
struct mutex mtx;
wait_queue_head_t wq;
wait_queue_head_t poll_wait;
struct list_head rdllist;
rwlock_t lock;
struct rb_root_cached rbr;
struct epitem *ovflist;
struct file *file;
};rbr is the persistent interest set. rdllist contains items that have received a readiness notification. The lock coordinates rdllist with ovflist, which catches events that arrive while a delivery scan is in progress.
Each watched (file, fd) pair gets an epitem. It carries both an RB-tree node and a ready-list node, the event mask and user data, and a pointer back to its eventpoll.
The missing bridge is eppoll_entry. It contains a wait_queue_entry_t installed on a wait queue supplied by the target file. Its callback is ep_poll_callback(), and its base pointer leads back to the owning epitem.

There are therefore two separate waiting relationships:
- the target file’s wait queue turns file readiness into an
epitemon the epoll ready list; eventpoll.wqcontains user threads sleeping inepoll_wait().
Confusing these two layers causes most incorrect explanations of epoll wakeups.
What EPOLL_CTL_ADD really installs
For this call:
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);the kernel’s ep_insert() performs four relevant jobs.
First, it allocates and initializes an epitem, stores the file, fd, mask, and user data, and inserts the item into eventpoll.rbr while holding the instance mutex. The tree manages the interest set; packet arrival does not scan it.
Second, epoll calls the target file’s poll() method with a poll_table whose queue callback is ep_ptable_queue_proc(). For a TCP socket, sock_poll() reaches the protocol’s polling logic. That logic both computes the socket’s current readiness and calls poll_wait() for the wait queues that can signal future changes.
Third, ep_ptable_queue_proc() allocates an eppoll_entry, initializes its wait callback to ep_poll_callback(), points it at the epitem, and attaches it to the target wait queue. Sockets commonly use the wait queue reachable through sk->sk_wq; pipes, eventfd, timerfd, and other pollable files expose their own queues.
Finally, the same poll() invocation returns the target’s current readiness. A socket may already contain data when it is added. If the returned mask matches the interest, epoll immediately puts the epitem on rdllist and wakes a waiter.
The order matters: install the wait relationship, then evaluate current state. That closes the window in which existing readiness or a concurrent state change might otherwise be missed.
From a TCP packet to ep_poll_callback()
For a conventional TCP receive path, a NIC DMA-writes packets to a receive ring and raises an interrupt. The driver schedules NAPI, NAPI polling feeds packets through IP and TCP processing, and TCP finds the destination struct sock. Data admitted in order enters the socket receive queue.
TCP then calls its data-ready hook. The default sock_def_readable() wakes the socket wait queue with a poll mask containing readable events. The wait-queue machinery invokes matching callbacks, including the ep_poll_callback() installed by EPOLL_CTL_ADD.

The callback can reach the correct epoll instance without scanning every watched descriptor:
wait_queue_entry
-> eppoll_entry
-> epitem
-> eventpoll
-> rdllistIts main path is conceptually:
read_lock_irqsave(&ep->lock, flags);
if (disabled_or_unmatched(epi, pollflags))
goto out;
if (ep->ovflist != EP_UNACTIVE_PTR)
chain_into_ovflist(epi);
else if (!ep_is_linked(epi))
list_add_tail(&epi->rdllink, &ep->rdllist);
if (waitqueue_active(&ep->wq))
wake_up(&ep->wq);
out:
read_unlock_irqrestore(&ep->lock, flags);Three details matter in production.
The callback queues an interest item, not a byte count. If the same epitem is already linked on rdllist, repeated notifications do not add duplicate list nodes. User space must still call read(), write(), or accept() and trust the operation’s actual return value.
Waking a wait queue only makes a task runnable. It does not execute user code inside the callback. The scheduler chooses when and where the task runs.
Finally, this callback can execute in a context that cannot sleep. It performs short locked list operations; copying to user memory waits until epoll_wait() runs in process context.
Where epoll_wait() sleeps
When epoll_wait() finds no event and is allowed to block, the regular path prepares a wait entry for the current task, sets TASK_INTERRUPTIBLE, rechecks for events while coordinating with ep->lock, joins ep->wq as an exclusive waiter, and calls the scheduler with the requested timeout.
The recheck before sleeping closes another race. An event may arrive after the first empty test. The waiter and callback coordinate through the ready-list lock and wait queue so an event cannot quietly appear between “nothing ready” and “now asleep.”
When a callback wakes the task, epoll_wait() resumes and begins delivery.
Why ovflist exists
Delivering an event means re-running the target’s poll() method and copying an epoll_event to user memory. That copy can fault and sleep. The kernel cannot hold epoll’s spin-style ready-list lock throughout the operation, but releasing it while iterating a list that callbacks can modify would corrupt the scan or lose events.
Linux 6.10 solves this with a private transfer list and ovflist:
ep_start_scan()briefly takes the write lock, movesrdllistinto a privatetxlist, activatesovflist, and drops the lock.- The waiter scans
txlist, rechecks each target, and copies valid results without holdingep->lock. - Concurrent callbacks see that
ovflistis active and append newly signaled items to that separate single-linked list. ep_done_scan()merges new items and unconsumedtxlistentries back intordllist.

This is why epoll_wait() does not simply pop ready entries under one lock. The design separates short non-sleeping notification work from potentially sleeping user-memory delivery.
LT, ET, and ONESHOT diverge after delivery
For each candidate, epoll calls the target’s poll() again. If the resulting mask is valid, it copies the event and decides what happens to the item:
- Level-triggered: if the file remains ready, put the
epitemback onrdllist. A later wait can return it again. - Edge-triggered: do not automatically requeue after delivery. The application normally uses nonblocking I/O and drains the condition until
EAGAIN. - EPOLLONESHOT: disable the item’s public event bits after one delivery. User space re-arms it with
EPOLL_CTL_MODafter finishing the current work.
ET does not mean “the kernel reports every byte transition.” It changes how readiness is re-delivered. The application must consume and interpret the underlying file operation correctly.
Two multi-worker designs, two wakeup problems
“Several workers use epoll” can mean two different architectures.
Several threads can wait on one epoll fd. Their wait entries live on the same eventpoll.wq and are exclusive. A wakeup normally selects one exclusive waiter; if events remain, the delivery path can wake another. This is the case described by epoll(7) when one edge-triggered fd wakes one of several threads waiting on the same epoll instance.
Alternatively, each worker can own its own epoll instance while every instance watches the same listening socket. The socket’s wait queue then holds multiple eppoll_entry callbacks, one per epoll instance. A single connection can notify multiple instances and wake multiple workers.
EPOLLEXCLUSIVE, added in Linux 4.5, targets this second layer. Epoll registers its callback as an exclusive entry on the target file’s wait queue, reducing the number of epoll instances notified for one event. Ordinary nonexclusive registrations on the same target still receive notifications.

SO_REUSEPORT moves distribution earlier. Each worker creates its own listening socket bound to the same address, and the network stack selects a socket before epoll is involved. This removes the shared listening socket from the normal accept path and can be customized with reuseport BPF.
Where epoll is fast—and where the cost moved
Epoll is strongest when the interest set is stable, N is large, and only a small fraction of descriptors is active at once. It persists registrations and lets target-file callbacks identify ready candidates.
The costs are still real:
- frequent add/delete operations allocate objects, update the tree, and attach or detach wait queues;
- callbacks contend on shared ready-list state when many CPUs deliver to one instance;
- delivery rechecks R candidates and copies K events;
- LT loops can spin on an unconsumed condition;
- ET loops can strand buffered work if they stop before
EAGAIN.
That context also clarifies why io_uring is not automatically faster. Avoiding some readiness and syscall coordination helps only when those costs dominate the complete event loop.
The red-black tree manages the interest set. The ready list collects candidates. Two layers of wait queues connect target files to sleeping tasks. ovflist protects delivery against concurrent notifications. Once those pieces are visible, LT spin, ET stalls, shared-instance contention, and accept-side thundering herds are no longer unrelated folklore; they are consequences of one mechanism.
References
- Linux 6.10,
fs/eventpoll.c - Linux 6.10,
net/socket.c - Linux 6.10,
net/core/sock.c - Linux 6.10,
net/ipv4/tcp_input.c - Linux kernel documentation, NAPI
- Linux man-pages,
epoll(7) - Linux man-pages,
epoll_ctl(2) - Linux man-pages,
socket(7)


