select / poll / epoll
Three generations of multiplexed I/O — waiting on multiple file descriptors simultaneously without blocking on any one of them.
select(2)
The original POSIX interface. Takes a bitmask (fd_set) of up to FD_SETSIZE (typically 1024) FDs. Returns which are ready.
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(fd1, &rfds);
FD_SET(fd2, &rfds);
select(maxfd + 1, &rfds, NULL, NULL, NULL);
if (FD_ISSET(fd1, &rfds)) { /* fd1 is readable */ }Limitation: O(n) scan; FD number limit (1024 by default); fd_set must be rebuilt on every call.
poll(2)
Replaces the bitmask with an array of struct pollfd. No FD number limit. Still O(n) scan — the kernel copies the array on every call.
epoll(7) (Linux)
Kernel-side event registration. Add FDs once with epoll_ctl(); retrieve only the ready ones with epoll_wait(). O(1) for the wait regardless of how many FDs are watched. The foundation of every high-performance Linux server (nginx, Redis, Node.js event loop).
int efd = epoll_create1(0);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = fd };
epoll_ctl(efd, EPOLL_CTL_ADD, fd, &ev);
// later:
struct epoll_event events[MAX];
int n = epoll_wait(efd, events, MAX, -1);In the curriculum
Covered in m08-sockets-networking lesson 8-2 (“Handling Multiple Clients — select and poll”) and the PDF ch09 Going Deeper section (fd_set internals, FD_SETSIZE, O(n) scan, poll vs epoll). Also introduced in m09-concurrency as the event-driven alternative to threading.