Io_submit and Linux AIO – An epoll alternative
blog.cloudflare.com
blog.cloudflare.com
There needs to be more openness to providing general interfaces that solve whole classes of interface problems all at once, and without the technical conservatism that confronts people trying to contribute new interfaces.
My personally least favourite syscall design misstep is how VFS flags work. AT_ flags were supposed to be generic but we now have AT_STATX_, O_ flags are overused, and some at(2) syscalls like renameat2(2) have their own brand of flags (RENAME_). Internally VFS has uniform flags for lookup and other various things, so it's a bit silly that these flags are all basically incompatible.
(Also, the fact that O_RDWR != O_RDONLY|O_WRONLY is just silly. It takes up just as many bytes but makes life needlessly difficult when designing new extensions to openat(2).)
A while ago, I proposed a kernel supplied libc add-on that would provide the syscall wrappers that glibc won't. Maybe we could declare that all applications must make new system calls through this interface and maintain syscall compatibility only for the current legacy set of system calls.
There is absolutely nothing about the Go language or runtime environment that excuses them calling into libc to do platform things. Nothing. Every other managed language environment does it, and Go can too.
With Rust I usually end up having to compile a bunch of build-time dependencies from source on my build images because of this, instead of just using a version of Debian etc. new enough to have the right dependencies in the package repo.
However I don’t think Go ‘s approach makes sense on some other platforms (e.g. macOS) that don’t suffer from this issue. In those cases you sacrifice forward compatibility for no real benefit.
This is how we've done it for something like 10 years now, and I've never thought about it since it works - but is there another approach we could use to link against an older libc version from a newer Ubuntu version?
The solution is to just tell people to update their code. Or accept that some bad APIs will be baked in for eternity, and deal with it.
If you don't mind me asking, what are you thinking about, syscalls aside? The way they handle stack?
Thankfully, they changed that in Go 1.11 (https://golang.org/doc/go1.11#runtime)
libc as the boundary doesn't make sense either as glibc is not the Linux kernel, by design, and there are other libcs in common production, like in a billion Android phones.
And no, you still can't use NTDLL on Windows - not unless you're doing (OS-specific) drivers. It might work in practice, but it also breaks sometimes, because there's no API stability guarantee.
By the way Windows CRT ships new versions with Visual Studio out of devdiv... So Windows doesn't even work that way if you draw the boundary at libc. This is part of my comment, I don't understand which part of it is MS doing it right? Ntdll abstracting sysenter? Kernel and advapi abstracting ntdll? I don't think the latter example is one of great technical execution, it just sort of happened to promote compatibility with 9x and they're stuck with it.
Although as of Win10, the actual CRT is also an API-stable OS-level component now, not a VC++-specific thing: https://blogs.msdn.microsoft.com/vcblog/2015/03/03/introduci...
The right thing here is the general notion of abstracting away core kernel APIs in userspace. It lets you refactor the whole thing significantly without breaking everything. And userspace calls are easier to maintain long-term for legacy support reasons, as well (at some point you can make it into a DLL that doesn't even have to be distributed with the OS, and just ship that with apps that need it).
Abstractions are good but they need some basis in reality. The situation in Windows is in many cases layers atop layers without an actual need.
The only place that'd have any bearing is for modified syscalls. Newly added obviously aren't an issue, And looking at syscalls removed, it's because they removed whole subsystems that never had anyone using it. Those would probably not even get upstreamed into the mainline kernel to being with.
I'm not seeing any evidence of what you're saying while looking at how the NT syscall layer has changed over the past couple decades.
sync_file_range and then sync_file_range2, because sync_file_range's API is literally impossible on most architectures that aren't x86.
> There was a problem with that… I don’t remember what exactly, but it was like, the structures you fill did not match how Redis stored the data or something. I need to try again soon or later because the speedup in Redis would be huge.
The FreeBSD sendfile() syscall is async (mostly; there is a potential to block reading metadata in VOP_BMAP). It launches a request to read disk blocks into pages, and attaches the pages to the socket buffer via mbufs, but marks them M_NOTREADY so that the protocol layer cannot send them. When the disk I/O completes, the mbufs are marked ready in the context of the disk interrupt thread, and then the proto output routine (eg, tcp_output()) is called.
This is more-or-less fire and forget for nginx. (yes, it does check the status via kqueue, but it does not block)
This was a huge performance win for us at Netflix, and the not-ready concept is the basis for our kernel-tls layer (mbufs are left not-ready, and encrypted after the disk read, and then the crypto thread calls the proto output).
> (2) Pre-warm the disk cache with posix_fadvise(2) and hope for the best.
Just as an aside, there is also the option of mlock(2) to improve this technique as implemented in the flash webserver (https://www.usenix.org/legacy/events/usenix99/full_papers/pa...). Also some people use vmtouch (https://hoytech.com/vmtouch/) for similar things.
Regarding this new interface, the LWN article says:
> AIO poll operations always operate in the "one-shot" mode; once a poll notification has been generated, a new IOCB_CMD_POLL IOCB must be submitted for that file descriptor if further notifications are needed.
Does this mean that in the (I think fairly common) case of wanting to persist a poll indefinitely until (for example) a socket is closed, with this new interface you will need to re-issue a poll after every IO operation? If so would that mitigate some/any of the performance benefit you described?
Lastly just wanted to point out the following research in case you haven't seen it before (and the various other designs it cites):
https://www.usenix.org/legacy/event/atc11/tech/final_files/S...
Cheers!
Also, epoll is good enough for me, my tasks. So no actual need to jump on the code to replace my epoll event loops.
I think we just need I/O readiness like epoll for disk I/O. AIO for disk seems to use the completion model ("do it and tell me when you're done" like Windows), which has higher maximum performance, but is more difficult to use / integrate. So you can now do both disk and network I/O with AIO... but it's going to be readiness based for network and completion based for disk. What the actual fuck.
Edits:
Thinking about it, readiness based reading from disk only works if you tell the system up front where to read and how much (not necessary for network sockets), which is new API that you have to use. Reading must be done when the disk is ready and can't wait for you, so it needs another buffer and memcpy. Readiness based disk writing also kind of works and it's easier - the system tells you when the write buffer has room again (threshold selectable or fixed), but submitted data is not going to be written right away. This might be the semi-reasonable explanation for the strange O_DIRECT limitation of AIO - if you need async buffered writes, just let the write cache do its work (and, you know, randomly block sometimes - never mind reading).
So readiness-based disk I/O could work with reasonably user-friendly new API for reading and not much worse performance loss than readiness-based network I/O. No idea about how much work it would take in the kernel.
It would also be nice to have completion-based network I/O with AIO for symmetry reasons and in case you really need the best performance. Though I have never worked on something where completion-based network I/O would have made a meaningful performance difference.
I'm not defending the suckiness of the AIO situation on Linux, but I understand why it is to some extent.
> It would also be nice to have completion-based network I/O with AIO for symmetry reasons
I.e., you want something like NT IO completion ports, introduced in 1994.
Yes, that's why I mentioned Windows.
Almost all the performance benefits that accrue from Linux AIO for disk access is that it allows you to replace the operating system cache and scheduler with something that is better designed for your application. And large gains are demonstrably possible. If you just wanted asynchronous disk calls, there are other APIs that use thread pools for that.
> So readiness-based disk I/O could work with reasonably user-friendly new API for reading and not much worse performance loss than readiness-based network I/O.
Off the cuff that new API sounds exactly like a completion-signaled read. You tell the system up from where to read and how much (the async read call), it reads when the disk is ready and buffers for you (your passed buffer and/or the cache if you're clever), and then it signals "readiness" which is the same as completion in this case.
Do you see this as distinct in any other way?
Using Linux AIO well entails writing a complete disk I/O implementation stack including your own disk cache infrastructure, I/O scheduler, etc with a high degree of technical competency and APIs to match. If you actually do all of this, Linux AIO is powerful and pleasant to use for high-performance disk access, if somewhat poorly documented. It works even better with raw block devices instead of files in filesystems -- you can pin some of its quirks on annoying filesystem behavior.