“Why an event loop, why not use threads?”
With the advent of light-weight processes (threads) programmers these days have a golden hammer they often swing without consideration. Event loops and non-blocking I/O is often a far easier approach, as well as less error prone.
The purpose of many applications is, with a little logic sprinkled on top, to act on network packets entering an interface, timeouts expiring, mouse clicks, or other types of events. Such applications are often very well suited to use an event loop.
Applications that need to churn massively parallel algorithms are more suitable for running multiple (independent) threads on several CPU cores. However, threaded applications must deal with the side effects of concurrency, like race conditions, deadlocks, live locks, etc. Writing error free threaded applications is hard, debugging them can be even harder.
Sometimes the combination of multiple threads and an event loop per thread can be the best approach, but each application of course needs to be broken down individually to find the most optimal approach. Do keep in mind, however, that not all systems your application will run on have multiple CPU cores -- some small embedded systems still use a single CPU core, even though they run Linux, with multiple threads a program may actually run slower! Always profile your program, and if possible, test it on different architectures.
libuEv is a simple event loop in the style of the more established libevent, libev and the venerable Xt(3) event loop. The u (micro) in the name refers to both the small feature set and the small size overhead impact of the library. The primary target of libuEv is a modern Linux system.
Experienced developers may appreciate that libuEv is built on top of modern Linux APIs: epoll, timerfd and signalfd. Note, a certain amount of care is needed when dealing with APIs that employ signalfd. For details, see this article at lwn.net.
“Event driven software improves concurrency” -- Dave Zarzycki, Apple
The C interface to libuEv is listed in uev.h
. It handles three
different types of events: I/O (files, sockets, message queues, etc.),
timers, and signals. With a slight caveat on signals detailed below
in the Summary.
/*
* Callback example, arg is passed from watcher's *_init()
* w->fd holds the file descriptor, events is set by libuEv
* to indicate if any of UEV_READ and/or UEV_WRITE is ready.
*/
void callback (uev_t *w, void *arg, int events);
/* Event loop: Notice the use of flags! */
int uev_init (uev_ctx_t *ctx);
int uev_exit (uev_ctx_t *ctx);
int uev_run (uev_ctx_t *ctx, int flags); /* UEV_NONE, UEV_ONCE, and/or UEV_NONBLOCK */
/* I/O watcher: fd is non-blocking, events is UEV_READ and/or UEV_WRITE */
int uev_io_init (uev_ctx_t *ctx, uev_t *w, uev_cb_t *cb, void *arg, int fd, int events);
int uev_io_set (uev_t *w, int fd, int events);
int uev_io_start (uev_t *w);
int uev_io_stop (uev_t *w);
/* Timer watcher: timeout and period in milliseconds */
int uev_timer_init (uev_ctx_t *ctx, uev_t *w, uev_cb_t *cb, void *arg, int timeout, int period);
int uev_timer_set (uev_t *w, int timeout, int period); /* Change timeout or period */
int uev_timer_start (uev_t *w); /* Restart a stopped timer */
int uev_timer_stop (uev_t *w); /* Stop a timer */
/* Signal watcher: signo is the signal to wait for, e.g., SIGTERM */
int uev_signal_init (uev_ctx_t *ctx, uev_t *w, uev_cb_t *cb, void *arg, int signo);
int uev_signal_set (uev_t *w, int signo); /* Change signal to wait for */
int uev_signal_start(uev_t *w); /* Restart a stopped signal watcher */
int uev_signal_stop (uev_t *w); /* Stop signal watcher */
To monitor events the developer first creates an event context, this
is achieved by calling uev_init()
with a pointer to a (thread) local
uev_ctx_t
variable.
uev_ctx_t ctx;
uev_init(&ctx);
For each event to monitor, be it a signal, timer or file descriptor, a
watcher must be registered with the event context. The watcher, an
uev_t
, is registered by calling the event type's _init()
function
with the uev_ctx_t
context, the callback, and an optional argument.
Here is a signal example:
void cleanup_exit(uev_t *w, void *arg, int events)
{
/* Graceful exit, with optional cleanup ... */
uev_exit(w->ctx);
}
int main(void)
{
uev_t sigterm_watcher;
.
.
.
uev_signal_init(&ctx, &sigterm_watcher, cleanup_exit, NULL, SIGTERM);
.
.
.
}
When all watchers are registered, call the event loop with uev_run()
and the argument to the event context. The flags
parameter can be
used to integrate libuEv into another event loop.
In this example we set flags
to none:
uev_run(&ctx, UEV_NONE);
With flags
set to UEV_ONCE
the event loop returns as soon as it has
served the first event. If flags
is set to UEV_ONCE | UEV_NONBLOCK
the event loop returns immediately if no event is available.
Note: libuEv handles many types of errors, stream close, or peer shutdowns internally, but also lets the callback run. This is useful for stateful connections to be able to detect EOF.
- Set up an event context with
uev_init()
- Register event callbacks with the event context using
uev_io_init()
,uev_signal_init()
oruev_timer_init()
- Start the event loop with
uev_run()
- Exit the event loop with
uev_exit()
, possibly from a callback
Note 1: Make sure to use non-blocking stream I/O! Most hard to find bugs in event driven applications are due to sockets and files being opened in blocking mode. Be careful out there!
Note 2: When closing a descriptor or socket, make sure to first stop your watcher, if possible. This will help prevent any nasty side effects on your program.
Note 3: As mentioned above, a certain amount of care is needed when
dealing with signalfd. If your application use system()
you replace
that with fork()
, and then in the child, unblock all signals blocked
by your parent process, before you run exec()
. This because Linux
does not unblock signals for your children, and neither does most (all?)
C-libraries. See the finit project's implementation of run()
for
an example of this.
LibuEv is by default installed as a library with a few header files, you should only ever need to include one:
#include <uev/uev.h>
The output from the pkg-config
tool holds no surprises:
$ pkg-config --libs --static --cflags libuev
-I/usr/local/include -L/usr/local/lib -luev
The prefix path /usr/local/
shown here is only the default. Use the
configure
script to select a different prefix when installing libuEv.
Here follows a very brief example to illustrate how one can use libuEv to act upon joystick input.
#include <err.h>
#include <errno.h>
#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <uev/uev.h>
struct js_event {
uint32_t time; /* event timestamp in milliseconds */
int16_t value; /* value */
uint8_t type; /* event type */
uint8_t number; /* axis/button number */
} e;
static void joystick_cb(uev_t *w, void *arg, int events)
{
read(w->fd, &e, sizeof(e));
switch (e.type) {
case 1:
printf("Button %d %s\n", e.number, e.value ? "pressed" : "released");
break;
case 2:
printf("Joystick axis %d moved, value %d!\n", e.number, e.value);
break;
}
}
int main(void)
{
int fd;
uev_t watcher;
uev_ctx_t ctx;
fd = open("/dev/input/js1", O_RDONLY, O_NONBLOCK);
if (fd < 0)
errx(errno, "Cannot find a joystick attached.");
uev_init(&ctx);
uev_io_init(&ctx, &watcher, joystick_cb, NULL, fd, UEV_READ);
puts("Starting, press Ctrl-C to exit.");
return uev_run(&ctx, 0);
}
To build the example, follow installation instructions below, then save
the code as joystick.c
and call GCC
gcc `pkg-config --libs --static --cflags libuev` -o joystick joystick.c
Alternatively, call the Makefile
with make joystick from
the unpacked libuEv distribution.
More complete and relevant example uses of libuEv is the FTP/TFTP
server uftpd, and the Linux /sbin/init
replacement finit.
Both successfully employ libuEv.
Also see the bench.c
program (make bench from within the
library) for reference benchmarks against libevent and
libev.
The library is built for and developed on GNU/Linux systems, patches to support *BSD and its kqueue interface are most welcome.
libuEv use the GNU configure and build system. To try out the bundled
examples, use the --enable-examples
switch to the configure
script.
./configure
make all
make test
make install
The resulting .so file is ~14 kiB (make install-strip).
libuEv was originally based on LibUEvent by Flemming Madsen but has been completely rewritten with a much clearer API. Now more similar to the famous libev by Mark Lehmann. Another small event library used for inspiration is the very small Picoev by Oku Kazuho.
libuEv is developed and maintained by Joachim Nilsson.