aboutsummaryrefslogtreecommitdiffstats
skalibs: the iopause library interface

libstddjb
libskarnet
skalibs
Software
skarnet.org

The iopause library interface

The following functions are declared in the skalibs/iopause.h header, and implemented in the libskarnet.a or libskarnet.so library.

General information

iopause is the skalibs API for event loop selection. It's a wrapper around the system's ppoll() or poll() (if available) or select() (if neither ppoll() nor poll() is available) function. It works around some system-dependent quirks; also it works with absolute dates instead of timeouts. This is a good thing: see below.

iopause is a derived work from Dan J. Bernstein's iopause library, but the skalibs implementation is subtly different.

Data structures

An iopause_fd structure is similar to a struct pollfd structure, and must be filled the same way. Usually, the user declares an array of iopause_fd and fills it, one element per descriptor to select on. If x is an iopause_fd:

  • x.fd must be set to the fd number to select on.
  • x.events must be a disjunction of the following flags:
    • IOPAUSE_READ if the fd is to be selected for reading.
    • IOPAUSE_WRITE if the fd is to be selected for writing.
  • When the selection function returns, x.revents contains a disjunction of the following flags:
    • IOPAUSE_READ if the fd is readable (this includes reception of an EOF).
    • IOPAUSE_WRITE if the fd is writable.
    • IOPAUSE_EXCEPT if an exception (such as EOF or an error) occurred on the fd.

Unlike poll() or select(), which use a timeout argument, the iopause() function uses a deadline argument, i.e. an absolute time at which it must return 0 if no event has happened so far, as well as a stamp argument, i.e. an absolute time meaning now. Those arguments are stored in tains. Here is why:

The event loop pattern is mostly used to multiplex several asynchronous events that can happen independently, at the same time or not. Let's say you have 3 events, x, y and z. Each of those has a separate timeout: if x happens before x-timeout milliseconds, you call the x-event-handler function, but if x-timeout milliseconds elapse without x happening, you call x-timeout-handler function. And similarly with y and z.

But the selection function returning does not mean x has happened or that x has timed out. It might also mean that y has happened, that y has timed out, that z has happened, that z has timed out, or something else entirely. In the post-selection part of the loop, the proper handler is called for the event or timeout that has happened; then the loop is executed again, and in the pre-selection part of the loop, the array describing the events is filled, and the selection timeout is computed.

How are you going to compute that global selection timeout? Easy: it's the shortest of the three. But we just spent some amount of time waiting, so the individual timeouts must be recomputed! This means:

  • You need a way to know the time spent in a selection primitive, which basically means getting a timestamp before the selection and once again after the timestamp.
  • You need to recompute every individual timeout everytime you enter the loop.

That is really cumbersome. A much simpler way of doing things is:

  • Always keep a reasonably accurate estimation of the current time in a stamp variable. That means getting the current time at the start of the process, and updating it right after any action that takes a significant amount of time. When to update stamp can be difficult to estimate in CPU-bound processes; fortunately, most processes using an event loop are IO-bound, and the only actions that take a non-negligible amount of time in an IO-bound process are the blocking primitives. So, stamp must be updated right after a selection function returns, and if the program has been correctly written and cannot block anywhere else, it's the only place where it needs to be.
  • For every event, compute the deadline of that event: x-deadline is x-timeout added to the current stamp value when x enters the loop. This is done only once per event: you never have to recompute event deadlines - unlike timeouts, which diminish over time, deadlines do not change.
  • At every iteration, the selection deadline is the earliest of all the available event deadlines.
  • As an added bonus: after the selection function returns and stamp has been updated, it is easy to check which events have timed out and which have not: x has timed out iff x-deadline is earlier than stamp.

Maintaining a global timestamp and using absolute times instead of relative times really is the right way to work with event loops, and the iopause interface reflects that. Of course, you need a reliable, bug-free time library and a monotonic, constant system clock to handle absolute times correctly; that is why iopause relies on the tai library.

Functions

int iopause (iopause_fd *x, unsigned int len, tain const *deadline, tain const *stamp)
Blocks until one of the events described in the x array, of length len, happens, or until the absolute date *deadline is reached. deadline may be null, in which case the function blocks indefinitely until an event happens. If deadline is not null, then stamp must not be null, and must contain an accurate estimation of the current time. The function returns the number of events that have happened, 0 for a timeout, or -1 (and sets errno) for an error.

int iopause_stamp (iopause_fd *x, unsigned int len, tain const *deadline, tain *stamp)
Like iopause(), but if stamp is not null, it is updated right before the function returns. This helps the user always keep a reasonably accurate estimation of the current time in stamp; it is recommended to use this function instead of the lower-level iopause().

Underlying implementations

iopause is an alias to one of iopause_ppoll, iopause_poll or iopause_select. It is always aliased to iopause_ppoll if the ppoll() function is available on the system; else, it's aliased to iopause_poll by default, and users can alias it to iopause_select instead if they configure skalibs with the --enable-iopause-select option.

poll() has a more comfortable API than select(), but its maximum precision is 1 millisecond, which might not be enough for some applications; using select() instead incurs some CPU overhead for the API conversion, but has a 1 microsecond precision. ppoll() gets the best of both worlds with the same interface model as poll() and a 1 nanosecond precision, which is why skalibs always uses it when available.