epoll, eventfd, timerfd, signalfd

The set libwayland's event loop is made of, in kernel/sys/sys_event.c:
four kinds of descriptor with no inode, FD_EVENT, that read() drains
and poll() reports on. They were table entries returning ENOSYS that
read as done.

epoll registers (descriptor, file) pairs, so a closed descriptor
leaves the set; level- and edge-triggered, EPOLLONESHOT. Waiting is
poll()'s, a tick at a time. The old eventfd and signalfd numbers (284,
282) were aliased to the new calls and read flags from a garbage
register; they are their own entries now, with epoll_create and
epoll_pwait.

cmd/evtest checks each, then all four in one set. The plans are
brought up to date: everything Wayland was waiting on is built except
setsockopt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-24 10:59:52 +02:00
commit a4fbdb7437
12 changed files with 974 additions and 37 deletions

128
cmd/evtest/main.c Normal file
View file

@ -0,0 +1,128 @@
// evtest: eventfd, timerfd, signalfd and epoll, each checked for the one
// property an event loop depends on, and then all four in one epoll set --
// which is how libwayland's event loop uses them.
//
// A musl program, linked dynamically, so it goes through libc's wrappers
// exactly as a ported program would.
#include <errno.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
#include <sys/signalfd.h>
#include <sys/timerfd.h>
#include <time.h>
#include <unistd.h>
static int bad;
#define CHECK(cond, ...) do { \
if (!(cond)) { printf("evtest: FAIL " __VA_ARGS__); printf("\n"); bad++; } \
} while (0)
static long
ms_since(struct timespec *t0)
{
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return (t.tv_sec - t0->tv_sec) * 1000 + (t.tv_nsec - t0->tv_nsec) / 1000000;
}
int
main(void)
{
uint64_t v;
struct timespec t0;
// eventfd: writes add, a read takes the sum and empties it, and an empty
// nonblocking one says EAGAIN rather than blocking.
int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
CHECK(efd >= 0, "eventfd: %d", errno);
v = 3; write(efd, &v, 8);
v = 4; write(efd, &v, 8);
v = 0;
CHECK(read(efd, &v, 8) == 8 && v == 7, "eventfd sum %llu", (unsigned long long)v);
CHECK(read(efd, &v, 8) < 0 && errno == EAGAIN, "eventfd empty: errno %d", errno);
int sem = eventfd(2, EFD_SEMAPHORE);
CHECK(read(sem, &v, 8) == 8 && v == 1, "semaphore eventfd gave %llu", (unsigned long long)v);
// timerfd: a one-shot 50 ms timer is not ready at once, is after it, and a
// blocking read waits for it rather than returning.
int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
CHECK(tfd >= 0, "timerfd_create: %d", errno);
struct itimerspec its = { { 0, 0 }, { 0, 50 * 1000000 } };
clock_gettime(CLOCK_MONOTONIC, &t0);
CHECK(timerfd_settime(tfd, 0, &its, 0) == 0, "timerfd_settime: %d", errno);
v = 0;
CHECK(read(tfd, &v, 8) == 8 && v == 1, "timerfd expirations %llu", (unsigned long long)v);
long waited = ms_since(&t0);
CHECK(waited >= 40 && waited < 1000, "timerfd fired after %ld ms", waited);
// signalfd: a blocked SIGUSR1 sent to ourselves is read back as data.
sigset_t m;
sigemptyset(&m);
sigaddset(&m, SIGUSR1);
sigprocmask(SIG_BLOCK, &m, 0);
int sfd = signalfd(-1, &m, SFD_NONBLOCK);
CHECK(sfd >= 0, "signalfd: %d", errno);
kill(getpid(), SIGUSR1);
struct signalfd_siginfo si;
memset(&si, 0, sizeof(si));
CHECK(read(sfd, &si, sizeof(si)) == sizeof(si) && si.ssi_signo == SIGUSR1,
"signalfd read signo %u", si.ssi_signo);
// epoll: all of it in one set. Nothing ready -> times out; then the
// timer, the eventfd and the signal each arrive as their own event,
// carrying the data they were registered with.
int ep = epoll_create1(EPOLL_CLOEXEC);
CHECK(ep >= 0, "epoll_create1: %d", errno);
struct epoll_event ev;
ev.events = EPOLLIN; ev.data.u64 = 1; epoll_ctl(ep, EPOLL_CTL_ADD, efd, &ev);
ev.events = EPOLLIN; ev.data.u64 = 2; epoll_ctl(ep, EPOLL_CTL_ADD, tfd, &ev);
ev.events = EPOLLIN; ev.data.u64 = 3; epoll_ctl(ep, EPOLL_CTL_ADD, sfd, &ev);
CHECK(epoll_ctl(ep, EPOLL_CTL_ADD, efd, &ev) < 0 && errno == EEXIST, "double add");
struct epoll_event out[8];
clock_gettime(CLOCK_MONOTONIC, &t0);
CHECK(epoll_wait(ep, out, 8, 30) == 0, "an empty set was ready");
CHECK(ms_since(&t0) >= 20, "epoll_wait timed out after %ld ms", ms_since(&t0));
its.it_value.tv_nsec = 20 * 1000000;
timerfd_settime(tfd, 0, &its, 0);
int n = epoll_wait(ep, out, 8, 1000);
CHECK(n == 1 && out[0].data.u64 == 2, "timer through epoll: n=%d data=%llu",
n, n > 0 ? (unsigned long long)out[0].data.u64 : 0ULL);
read(tfd, &v, 8);
v = 1; write(efd, &v, 8);
kill(getpid(), SIGUSR1);
n = epoll_wait(ep, out, 8, 1000);
int seen = 0;
for (int i = 0; i < n; i++)
seen |= 1 << out[i].data.u64;
CHECK(n == 2 && seen == ((1 << 1) | (1 << 3)), "eventfd+signal: n=%d mask=%x", n, seen);
// Level-triggered: still ready until drained. Then edge-triggered: once.
n = epoll_wait(ep, out, 8, 0);
CHECK(n == 2, "level-triggered repeat gave %d", n);
read(efd, &v, 8);
read(sfd, &si, sizeof(si));
ev.events = EPOLLIN | EPOLLET; ev.data.u64 = 1;
epoll_ctl(ep, EPOLL_CTL_MOD, efd, &ev);
v = 1; write(efd, &v, 8);
CHECK(epoll_wait(ep, out, 8, 0) == 1, "edge-triggered first report");
CHECK(epoll_wait(ep, out, 8, 0) == 0, "edge-triggered reported twice");
// A closed descriptor leaves the set.
close(efd);
CHECK(epoll_wait(ep, out, 8, 0) == 0, "closed fd still in the set");
CHECK(epoll_ctl(ep, EPOLL_CTL_DEL, tfd, 0) == 0, "EPOLL_CTL_DEL");
printf("evtest: %s\n", bad ? "FAILED" : "ok");
return bad != 0;
}