4. Event Notification

epoll Readiness List

midconcurrencydata-structures

epoll's core data structure is a "ready list" (rdllist in the kernel): a queue of file descriptors that currently have a pending event. epoll_wait() is, underneath, just "drain (some of) the ready list."

What makes epoll interesting is the level-triggered vs. edge-triggered distinction, controlled per-fd by the EPOLLET flag:

  • level-triggered (the default) -- an fd is reported EVERY time you poll, for as long as its underlying condition (e.g. "socket has unread data") stays true.
  • edge-triggered -- an fd is reported exactly ONCE per transition from not-ready to ready. Polling again won't re-report it until the condition goes false and then becomes true again.

register(fd, edge_triggered) and set_condition(fd, ready) (given) simulate an fd being watched and its underlying readiness condition changing (e.g. data arriving on a socket). set_condition(fd, True) only enqueues a NEW notification on a false->true transition -- exactly like how ep_poll_callback() only fires on a real wakeup, not on every check.

Implement EpollInstance.poll(): drain the current ready list and return the fds that fired, in the order they became ready. Apply the level/edge-triggered rule to decide which fds go back onto the ready list for the NEXT poll() call:

  • level-triggered: stays on the ready list if its condition is STILL true.
  • edge-triggered: never goes back on the ready list from this poll -- it only reappears via a fresh false->true transition through set_condition.

The missing link: closing a watched fd

Also implement unregister(fd) (EPOLL_CTL_DEL): the fd is being closed and must stop being watched entirely -- including being pulled off the ready list if it's currently sitting there pending report. Without this, a later poll() could hand the caller back a file descriptor that's already been closed, which is exactly the kind of use-after-close bug real event loops have to guard against explicitly (closing a socket doesn't automatically un-register it from every epoll instance watching it).

Example

ep = EpollInstance()
ep.register(1, edge_triggered=False)
ep.register(2, edge_triggered=True)
ep.set_condition(1, True)
ep.set_condition(2, True)
ep.poll()   # -> [1, 2]  (both fire)
ep.poll()   # -> [1]     (1 is LT and still ready; 2 is ET and already reported)
ep.unregister(1)
ep.poll()   # -> []      (1 is gone, 2 already reported)

Constraints

  • Only report an fd whose condition is CURRENTLY true at poll() time -- don't report a stale entry whose condition was cleared (via set_condition(fd, False)) since it was queued.
  • Preserve the order fds appear in self.ready_list for the returned list.
  • unregister on an fd that was never registered (or already unregistered) is a no-op, not an error.

Real-world grounding

  • The ready list and its level-triggered re-queueing behavior: fs/eventpoll.c (see rdllist, ep_poll_callback(), and the EPOLLET bit in EP_PRIVATE_BITS).

Real epoll's ready list is protected by a spinlock (since ep_poll_callback() can fire from interrupt context when a device signals readiness) and uses a second overflow list (ovflist) to handle events that arrive while a scan of rdllist is already in progress -- both concerns this exercise sidesteps by construction, since nothing here runs concurrently. The reporting rule itself -- report while level-triggered and still true, report once per edge for edge-triggered -- is exactly what you implemented.

hints
epoll_readylist.pysign in to run
Loading...
test result

This is a preview. Sign in to run this exercise against the real tests.