// lesson: event-io
Waiting for Input โ poll and select
An interactive program spends nearly all of its life doing nothing,
and doing nothing well is a genuine engineering problem. Your editor's
main loop wants to say: "wake me when the user types, but no later
than 100 ms from now, because I might also have a resize to handle or
a status message to expire." A plain blocking read can't express
"no later than"; a VMIN=0 polling read can, but only by burning CPU
in a spin loop or by committing the whole program to VTIME's
tenth-of-a-second granularity on one fd.
The general answer โ the one every event loop from vim to nginx to your terminal emulator is built on โ is readiness notification: ask the kernel to block for you, on a set of fds, with a deadline.
poll()
#include <poll.h>
struct pollfd {
int fd; /* which descriptor */
short events; /* what you care about (POLLIN, ...) */
short revents; /* what actually happened (kernel fills) */
};
int poll(struct pollfd *fds, nfds_t nfds, int timeout_ms);
You hand poll an array of fds and what you're waiting for
(POLLIN โ readable; POLLOUT โ writable without blocking), plus a
timeout in milliseconds (-1 = forever, 0 = just check). It returns:
- > 0: that many entries have nonzero
revents. Check each. - 0: the timeout expired; nothing happened.
- -1: error โ and yes,
EINTRagain: a signal (SIGWINCH!) woke the process. For us that's a feature, but the caller must decide whether to retry, and with how much of the deadline left.
The crucial semantic: poll tells you a read won't block, not how
much data there is. The contract is "one read will return
something" โ maybe 1 byte. Readiness, then short read, then loop:
the two lessons compose.
POLLHUP (hang-up: the other end closed) and POLLERR can appear in
revents unrequested. Treat them as "readable" โ the read will
return 0 or an error, which is exactly the news you need.
select(), for the record
select(2) is poll's older sibling: same idea, clunkier interface
(fd bitmasks you rebuild every call, a struct timeval the kernel may
scribble on, and a hard FD_SETSIZE ceiling of 1024 fds). You'll read
it in older code โ kilo's tutorial era used it โ but there is no
reason to write new select code. For thousands of fds there's
epoll (Linux) / kqueue (BSD); a terminal watching one or two fds
needs none of that.
Deadlines that survive EINTR
There's a subtle bug lurking in the obvious retry loop:
while (poll(&p, 1, timeout_ms) < 0 && errno == EINTR)
; /* BUG: restarts the FULL timeout after every signal */
If signals arrive steadily (a user leaning on the resize handle), the
deadline recedes forever. Correct code computes the deadline once,
against a monotonic clock, and re-arms poll with whatever remains:
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
long deadline_ms = ts.tv_sec * 1000 + ts.tv_nsec / 1000000 + timeout_ms;
/* after EINTR: remaining = deadline_ms - now_ms; if <= 0, timed out */
CLOCK_MONOTONIC, not time() or CLOCK_REALTIME: wall clocks jump
(NTP, DST, a sysadmin); the monotonic clock only marches forward.
โบ Poll with a Deadline
15 ptsTwo functions that will sit at the heart of your event loop:
wait_readable(fd, timeout_ms)โ block until fd is readable or the deadline passes. Return 1 (readable), 0 (timeout), -1 (error).timeout_ms < 0means wait forever. EINTR must not restart the full timeout โ recompute the remainder from a monotonic clock.read_byte_timeout(fd, out, timeout_ms)โ the composition: wait, then read exactly one byte. Return 1 (got a byte), 0 (timeout), -1 (error or EOF).
Tests: data already waiting returns immediately; a writer that shows up 150 ms in is caught within the deadline; an empty fd times out in roughly the right amount of time (not instantly, not forever); and a single interrupting signal partway through a wait must not restart the full timeout โ the original deadline is honored, not reset.
Log in to submit a solution and earn points.