Blocked on the wrong socket
The HTTP server from the last lesson has one loop: accept, then
recv until the request is complete, then respond. Picture two clients.
Client A connects and sends nothing. Your server is now inside recv(A),
asleep, waiting for bytes that are not coming. Client B connects, sends a perfect
request, and waits. Nobody is calling accept. Nobody is calling
recv(B). B's bytes sit in a kernel receive buffer that your program
never looks at.
The problem is not speed. The problem is that a blocking call on one fd makes the whole thread deaf to every other fd. You saw one fix in the systems track: a thread per client, each free to block on its own socket. That works, and costs a stack per client plus the locking you learned to fear. There is a second fix that costs almost nothing: never block on a socket. Block on the question instead.
Ask the kernel which fds are ready
The kernel already knows which receive buffers hold data and which send buffers have
room. poll lets you hand it a list of fds and sleep until at least one
of them is ready. One call, one thread, any number of sockets.
You fill in fd and events. The kernel fills in
revents. POLLIN means a recv would return
something right now — data, or a 0 for EOF. POLLOUT means a
send would accept bytes without blocking. POLLHUP and
POLLERR show up in revents whether you asked or not: the
peer hung up, or the socket is broken. A listening socket reports POLLIN
when a connection is waiting in the backlog, because accept would not block.
recv(A), you sleep inside poll,
which wakes the moment any watched fd changes state. The thread still sleeps
most of the time — it just never sleeps on the wrong socket.
The loop
Step through the visualization. The table is your pollfd array. Each
iteration: call poll, walk the array, act on every entry whose
revents is nonzero, go back to poll. New connections add a
row. Hang-ups remove one. The fourth row is a file descriptor that is not a socket
at all — hold that thought for lesson 12.
Readiness is a hint, not a promise
POLLIN told you a recv would not block. Usually true. Not
always: between poll returning and your recv, the kernel can
discard the data (a bad checksum caught late, a reset), or another thread can read it.
If your socket is blocking, that rare case freezes the whole loop — exactly the bug
you were fixing.
So an event loop puts every socket in non-blocking mode. A non-blocking
recv with nothing to read does not sleep; it returns -1 with
errno == EAGAIN (also spelled EWOULDBLOCK). You treat that as
"nothing to do here" and move on. The same applies to accept on a
non-blocking listener and send on a socket whose send buffer is full.
EAGAIN is not an error. Treat it as "try later".
New event-loop code often does if (n < 0) close(fd). On a non-blocking
socket that drops healthy connections the first time readiness lies. Check
errno: EAGAIN means go back to poll; anything
else is a real error.
A complete echo server in one thread
This server handles as many clients as the pollfd array holds, with no
threads and no client able to stall another. Index 0 is always the listening socket.
When it reports POLLIN, accept a client, make it
non-blocking, append it. For every other ready entry, recv and echo
back. On EOF or hang-up, close and remove the row by swapping in the last one.
Connect three terminals with nc localhost 7 and type in any of them.
Each line comes back on the terminal that sent it. Leave one idle for an hour; the
others are unaffected. Kill one; its row disappears and the loop continues. That is
the behaviour the sequential server could not give you, and it still has one thread
and one stack.
poll.
Every socket is O_NONBLOCK. Every recv, send, and
accept is prepared to get EAGAIN and give up until the next
iteration. The moment one call can sleep, every other client waits for it.
Why a VPN daemon is an event loop
The echo server watches sockets. But poll does not care what kind of fd
it is given — a pipe, a terminal, a device file all work. A mesh VPN daemon has
exactly two fds that matter: a UDP socket carrying encrypted packets to and from other
peers, and a TUN device — a file descriptor the kernel hands IP packets
through. The daemon's entire life is one poll over those two fds: UDP
readable → decrypt, write to the TUN; TUN readable → encrypt, sendto the
peer. Lesson 12 opens that second fd. The loop you just wrote is the one it runs in.
poll scans the whole array every call. epoll does not.
With ten thousand connections, copying ten thousand pollfds into the kernel
and scanning them back is the cost that dominates. Linux's epoll and BSD's
kqueue keep the interest list inside the kernel and return only the ready
fds. Same model — a loop over readiness — different data structure. Learn poll
first; everything about epoll is the same idea with a registration step.
An event loop never blocks on a socket — it blocks in poll, which wakes when any fd is ready, then touches only those fds with non-blocking calls that treat EAGAIN as "not yet".