From 4743df8c4ab518202b6f40ed165b83b048386526 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sun, 25 Oct 2026 11:59:12 +0100 Subject: [PATCH] epoll: edge-triggered descriptors lost their edges on more than one CPU EPOLLET reported a descriptor again only after a scan had seen it not ready. Xorg watches its clients edge-triggered; when it read part of a client's requests and more arrived before its next scan, the socket never looked not-ready, so it was never reported again -- X waiting on the client, the client waiting on X. One CPU almost never hit it; on a 16-thread laptop the desktop froze within seconds: Window Maker's menu stuck, xterm never mapped, text gone, and Caps Lock dead because X owns the LED. Now a still-ready descriptor is reported again whenever anything has signalled readiness since its last report (poll_generation()), which may be spurious, as EPOLLET permits, and is never missed. Reproduced and verified with QEMU at -smp 8: the root menu works and xterm runs cmatrix. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4 --- include/core/defs.h | 1 + kernel/sys/sys_event.c | 18 ++++++++++++++++-- kernel/sys/sys_poll.c | 8 ++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/include/core/defs.h b/include/core/defs.h index bd604a4..db5fbbd 100644 --- a/include/core/defs.h +++ b/include/core/defs.h @@ -735,6 +735,7 @@ int net_detach(struct device *); int net_attach_late(struct device *); int net_eth_poll(void); void net_eth_link_check(void); +uint poll_generation(void); int net_eth_count(void); void net_eth_counts(int i, uint64 *rx, uint64 *tx); struct netif *net_eth_netif(int i); diff --git a/kernel/sys/sys_event.c b/kernel/sys/sys_event.c index 630ec9c..f4b353f 100644 --- a/kernel/sys/sys_event.c +++ b/kernel/sys/sys_event.c @@ -82,6 +82,7 @@ struct epitem { struct file *f; // checked against the table, never dereferenced alone uint32 events; uint64 data; + uint seq; // EPOLLET: poll_generation() when last reported int was_ready; // for EPOLLET: report on becoming ready, not on being int disabled; // EPOLLONESHOT fired, until EPOLL_CTL_MOD }; @@ -649,7 +650,8 @@ sys_epoll_ctl(void) case EPOLL_CTL_ADD: if (i >= 0) { r = -EEXIST; break; } if (o->ep.n == EP_MAX) { r = -ENOSPC; break; } - o->ep.items[o->ep.n++] = (struct epitem){ fd, tf, ev.events, ev.data, 0, 0 }; + o->ep.items[o->ep.n++] = (struct epitem){ .fd = fd, .f = tf, .events = ev.events, + .data = ev.data }; break; case EPOLL_CTL_MOD: if (i < 0) { r = -ENOENT; break; } @@ -676,6 +678,9 @@ static int ep_ready(struct evobj *o, struct epoll_event *out, int max) { int got = 0; + // Taken before any descriptor is looked at, so a kick that lands during the + // scan is newer than what is recorded and is reported next time. + uint gen = poll_generation(); acquiresleep(&o->lock); for (int i = 0; i < o->ep.n && got < max; ) { @@ -699,12 +704,21 @@ ep_ready(struct evobj *o, struct epoll_event *out, int max) it->was_ready = 0; continue; } - if ((it->events & EPOLLET) && it->was_ready) + // Edge-triggered: an edge is new data, not only a not-ready-to-ready + // change. A descriptor still ready since it was last reported is + // reported again if anything has signalled since -- possibly spuriously, + // which EPOLLET allows; never missed, which it does not. Xorg watches its + // clients this way. Waiting for a not-ready scan in between lost the + // edge whenever X read part of a client's requests and more arrived + // before it looked again: on one CPU that almost never happened, on + // several it hung the desktop -- X waiting on the client, the client on X. + if ((it->events & EPOLLET) && it->was_ready && it->seq == gen) continue; if (out) { out[got].events = (uint32)rev; out[got].data = it->data; it->was_ready = 1; + it->seq = gen; if (it->events & EPOLLONESHOT) it->disabled = 1; } diff --git a/kernel/sys/sys_poll.c b/kernel/sys/sys_poll.c index 44f4df6..8a688a2 100644 --- a/kernel/sys/sys_poll.c +++ b/kernel/sys/sys_poll.c @@ -47,6 +47,14 @@ poll_kick(void) wakeup(&ticks); } +// How many kicks there have been. epoll's edge-triggered mode compares it: +// see ep_ready(). +uint +poll_generation(void) +{ + return __atomic_load_n(&poll_seq, __ATOMIC_ACQUIRE); +} + // Sleep one timer tick, or until a poll_kick, or until killed/signaled. // Returns 1 if a tick went by, 0 if it was a kick -- which must not count // against a timeout, or a timed poll on a busy network would end early --