Introducing Sd-Event: The Event Loop API of Libsystemd
0pointer.net
0pointer.net
If anything, it only further convinces me what a leaky abstraction epoll(7) is, plus all the myriad of *fd calls that really just canonize things like the self-pipe trick in the completely wrong layers. I'd probably use something like libkqueue if I wanted a minimal event loop.
[1] http://www.slideshare.net/bcantrill/illumos-lx
1) http://www.bsdnow.tv/episodes/2015_08_19-ubuntu_slaughters_k... around 55:59
The most error-prone part of using epoll in a multi-threaded environment is free-ing of the per-descriptor state. You never quite know when the last epoll_wait that still knew about that descriptor has fired. There are multiple approaches for dealing with this problem, including RCU-like waiting for each thread to pass epoll_wait once, recycling descriptor states by including an anti-race generation counter in the unused bits of the event user data, or garbage collection.
In contrast, with IOCP or other completion-based programming models, you can reference count the active I/O operations and free once the count reaches zero.
After a quick glance I'm not convinced that it is battle-tested enough, and the Android port is not building, Solaris port not really working, etc.
* https://www.freebsd.org/cgi/man.cgi?query=kevent
* http://www.openbsd.org/cgi-bin/man.cgi/OpenBSD-current/man2/...
* http://man.netbsd.org/HEAD/usr/share/man/html2/kevent.html
* https://developer.apple.com/library/mac/documentation/Darwin...
This most definitely has been "battle tested" and it long since has provided a means for programs to use a single mechanism to wait for I/O events, directory modifications, signals, child processes, and timers. And it has some interesting abilities, too, such as the ability to keep track of grand-child processes forked off by a child, which one can use to good advantage to handle poorly written daemons that erroneously fork-child-then-exit-parent.
libkqueue should not be conflated with this. It provides a workalike API. But there are differences. Some of these are of necessity. Because of the way that Linux signalfds work, for starters, it has been quietly re-specified (the alteration to the wording of the manual page being a subtle one that isn't marked in any way) so that EVFILT_SIGNAL code has to be written differently when using libkqueue on Linux to when using real kevent/kqueue on a BSD. Others are accidents of implementation. One can get spurious wakeups from libkqueue that one doesn't get from real BSD kevent().
Its Linux portion also contains a number of bugs. Internal unexposed file descriptors, for all of the signalfds, timerfds, epolls, inotifys, and whatnot that it opens, are not marked close-on-exec, meaning that there's a significant security concern in any program that uses libkqueue in a privileged process and then forks and executes unprivileged children. The implementation of EVFILT_VNODE/NOTE_WRITE for directories is simply wrong. There are pitfalls when using EVFILT_READ with terminal devices. And its inotify usage doesn't account for the fact that notifications are not always the same number of bytes long.
The first two of those bugs are fairly easy to fix. I never got to the bottom of the third. The fourth is more complex, but is addressable I think. The main problem is not fixing these bugs, but getting those fixes available across a multiplicity of Linux distributions, some of which are operating from years-old versions of the library. So even though I wanted (and still want as a matter of fact) to get libkqueue made better, I had to go with dropping libkqueue and implementing my own kqueue()/kevent() workalikes for Linux.
* https://www.mail-archive.com/supervision@list.skarnet.org/ms...
* https://www.mail-archive.com/supervision@list.skarnet.org/ms...
* https://www.mail-archive.com/supervision@list.skarnet.org/ms...
epoll isn't an abstraction at all. It was never designed to be. epoll was designed as a far more scalable replacement for poll and select.
In particular, epoll wasn't designed to be a "higher-level" event loop than poll or select; while more powerful, it's arguably lower-level. Hence my comment that it isn't an abstraction.
Leaking the lower-level interface is a terrible "abstraction".
> least common denominator across all kernel
That's one approach. A better approach is to stop leaking the lower level interfaces and providing the higher-level API you want in a way that isolates you from the details.
When porting to another platform, this may require re-implementing features that are not available on the new platform. This is one of the reasons porting can be difficult.
> allows you to make use of Linux-specific APIs
We never had a problem using those features in the past. This is one of the reasons autotools/cmake exists.
Anywhere I would want to port a library that uses epoll it is trivial to re-implement the functionality using poll(2)/select(2) or even simply setting O_NONBLOCK and busy-waiting while manually polling read(2). Other platforms may have other non-standard features that are full or partial replacements. Performance may not be the same, but that's a common problem when porting software.
I'm sure it's possible to find various pathological cases where this is difficult. In the common cases, proper abstraction is worth it simply for the complexity and maintenance costs.
edit:
None of this should be taken as a recommendation for the epoll interface. vezzy-fnord is right - there are much better solutions available.
In most cases, the latter is fully feasible. You just generalize by categorizing the functionality of a source kernel and finding the equivalent taxonomy in a target kernel. It's not like any commodity OS kernel these days doesn't have a feature to multiplex I/O on ports, fds, handles or whatever that it's that prohibitive to shim a syscall, or more often than not, substitute it at build-time or just use an existing library/framework that encapsulates the details.
Not all kernels allow you to do that. Even among those that do, you'd have to have some level of privilege to install it. (I know I'd look very skeptically at any program or library that wanted an out-of-tree kernel module installed.)
And even if you manage to get that code submitted upstream, and they accept it (not at all a given), now you depend on the most recent version of that target OS, and you don't run on older versions of it. Which starts you down the same non-portable road you're complaining about.
That said, I absolutely think making such changes in upstream kernels/OSes is the right approach. Make your kernel/OS better to accommodate your userspace software, rather than making your userspace software worse to accommodate your kernel/OS.
How about prctl's PR_SET_CHILD_SUBREAPER, which makes a process act like PID 1, as the target for reparenting child processes?
Or fanotify/inotify/etc? (A polling-based interface doesn't work here: it misses things due to races, and wastes power by waking up too often.)
Or mount namespaces, network namespaces, and other unshare/clone flags?
You could reimplement those things in another kernel, but almost certainly not in userspace unless a feature with the same semantics already exists in the target kernel.
The point is they do. For instance, Solaris has contracts (direct corollary to cgroups, actually predating cgroups). In Solaris, orphans are adopted by the kernel and init(1M) can be restarted upon a failure condition. Some BSD flavors have procctl(2).
Polling-based interfaces can work, since a port does not have to be completely equivalent in terms of benchmarking, it must conform well by a cost-benefit analysis. File system event notifications are something many platforms reliably have, so that even devices with strict power management policy can incur them (or more often than not they'll incur the cost of polling regardless).
Most Unixes have converged on rfork(2)-like primitives that can customize process contexts to share reasonably well.
I'm not panning systemd for anything, mind you. I'm speaking in general terms. systemd is decidedly unportable, I'm more than well aware and don't have trouble with that aspect.
Neither of which provides the same semantics as PR_SET_CHILD_SUBREAPER.
Unless your desired target platform already has equivalent functionality, "porting" can't happen. It's easy enough to look at individual features and say "Solaris has something kinda like that" or "FreeBSD has something kinda like that". But Linux has quite a bit of functionality with no functional equivalent in other OSes. (And, conversely, some other OSes have functionality with no equivalent in Linux.) And I don't think it's unreasonable to make software that uses the full functionality of a target platform, rather than a less capable subset that can be made to work on other platforms.
> Polling-based interfaces can work, since a port does not have to be completely equivalent in terms of benchmarking,
Race conditions that cause you to miss events aren't OK, though. And even in terms of benchmarking, once people are done complaining about portable software, they complain that it doesn't run well on their platform. And most of the time they don't blame the platform for that.
You also seem to have intentionally ignored the general point in favor of picking apart a specific example. The solution to all uses of non-portable interfaces isn't either "you can implement a compatibility layer using features of your target OS" or "you don't really want that code anyway, just rip it out".
But forcing the use of epoll instead of an existing abstraction like libevent means that now epoll is added to the list of things that must be implemented.
I could go through my code and change all the open(O_CREAT) calls to open(0x200) because that's the same thing. And then none of that code would work on linux, where O_CREAT is 0x40. Would that be an improvement?
Only if you care about being compatible with Linux. There's a 100% Linux-compatible kernel already available, though. :) And any platform that wants 100% compatibility with Linux could always run it through virtualization.
I'm not saying portability is always a bad thing; far from it. It's also not always a good thing, though. And there's something to be said for the mindset that any time spent on portability is time not spent making the best possible program for the only platform you care about.
Some programs should be portable. It's awesome that Firefox, Libreoffice, and other user-level software runs on Windows, for instance, to introduce people to FOSS and make it easier to switch platforms later.
On the other hand, quite a bit of low-level or server software just doesn't make sense to run elsewhere, especially now that virtualization exists. At some point, it seems like the simplest portability layer is a copy of a Linux kernel and a way to run it on your target platform.
> I could go through my code and change all the open(O_CREAT) calls to open(0x200) because that's the same thing. And then none of that code would work on linux, where O_CREAT is 0x40. Would that be an improvement?
No; that wouldn't be an improvement even for code that only needs to run on one platform, because O_CREAT produces more self-documenting code.
On the other hand, you could write all your code using openat and friends, and never call chdir, which can make software that otherwise had to worry about the working directory much simpler. That may be an improvement, even though that code then won't run on arbitrary POSIX OSes.
I don't think Red Hat had all that much of a stake initially. Kay Sievers was actually a SUSE employee at the time systemd was conceived for public release, IIRC.
Red Hat's interests seem far more directed at cloud and containers (see: Project Atomic) than workstations, and indeed the direction systemd has been heading reflects that.
[1] http://homepage.ntlworld.com./jonathan.deboynepollard/FGA/sy...
GUIs and the web in particular are the perfect example. Every GUI that's not specifically designed for a platform is always worse than the native solution. Additionally the web is laughably behind. Trivial WebGL experiments that you could do natively decades ago regularly hit the HN frontpage. On a lower level you have C which is bad for many reasons but one of them is trying to be portable and so you end up with not being able to assume 2's complement.
Whenever you can do without portability, you're probably better off ignoring it.
That said, nothing truly innovative is being done with systemd to begin with.
— John Carmack If portability is essential to you, sd-event is not your best option.
sd-event is a wrapper around Linux-specific APIs ...
Also, "allows us to access"? Why the implication that epoll and timerfd were inaccessible?With high level, portable, abstractions, it may very well be impossible to expect the event loop to know how to deal sensibly with, eg, timerfd.
It's quite hostile.