epoll (Linux) and kqueue (BSD)
/ epoll -> EE-poll; kqueue -> KAY-queue /
Imagine a club with a guest list of fifty thousand names. With poll() the bouncer re-reads the entire list at every door-check. The smarter scheme is to register each guest with the venue ONCE, and then simply be handed a short slip naming exactly who just arrived. epoll on Linux and kqueue on BSD and macOS are that smarter scheme: a kernel-side, persistent interest set you register descriptors into once, after which each wait returns only the descriptors that actually became ready.
Concretely with epoll: you create an epoll instance with epoll_create1(), which gives you back a special descriptor. You add each socket you care about with epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event) - this registration is remembered by the kernel between calls. Then you call epoll_wait(), which blocks and, on return, fills an array with just the ready events, telling you how many in the return value. The kernel maintains a ready list internally as I/O completes, so a wait that finds three ready sockets out of fifty thousand costs O(3), not O(50000). kqueue is the same idea with a single unified kevent() call that both registers interest (changelist) and collects ready events (eventlist), and it can watch not only sockets but timers, signals, file changes, and process events.
Why it matters: epoll and kqueue are what actually solved the C10k problem and underlie essentially every modern high-performance server and async runtime (nginx, Redis, Node.js, libuv, Netty, Tokio, and so on). They are readiness-based, not completion-based: they tell you a socket is ready, and you still do the read() yourself. They are also not portable to each other - epoll is Linux-only, kqueue is the BSD/macOS family - which is why cross-platform libraries wrap both behind one interface.
int ep = epoll_create1(0); struct epoll_event ev = { .events = EPOLLIN, .data.fd = sock }; epoll_ctl(ep, EPOLL_CTL_ADD, sock, &ev); struct epoll_event out[64]; int n = epoll_wait(ep, out, 64, -1); /* n ready events */
Register the socket once with epoll_ctl(); each epoll_wait() then returns only the descriptors that are ready.
epoll scales O(ready), not O(watched), but it is still readiness-based: after a wait you must call read()/accept() yourself, and you must handle EAGAIN correctly. A frequent trap is forgetting to remove a closed descriptor with EPOLL_CTL_DEL, or assuming epoll behaves like select across platforms - it does not exist outside Linux.