// 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, EINTR again: 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 pts

Two 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 < 0 means 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.