Io_uring is not an event system
despairlabs.com
despairlabs.com
I have to say though, the name io_uring seems unfortunate and I think the author touches on this in the article... the name is really an implementation detail but io_uring's true purpose is a generic asynchronous syscall facility that is currently tailored towards i/o. syscall_queue or async_queue or something else...? A descriptive api name and not an implementation detail would probably go a long way in helping the feature be easier to understand. Even window's IOCP seems infinitely better named than 'uring'.
Where it really shines is disk IO because we don't have an epoll equivalent there. I imagine it would also be great at network requests that go to or from disk in a simple way because you can chain the syscalls in theory.
io_uring does win on more recent versions, but it's not like it blows epoll out of the water - it's an incremental improvement.
Specifically for websockets where you have a lot of idle connections, you don't want a bunch of buffers waiting for reads for long periods of time - this is why Go doesn't perform well for websocket servers as calling Read() on a socket requires tying up both a buffer and a goroutine stack.
I haven't looked into how registering buffers works with io_uring, if it's 1-to-1 mapping or if you can pass a pool of N buffers that can be used for reads on M sockets where M > N. The details matter for that specific case.
Again where io_uring really shines is file IO because there are no good solutions there currently.
"what if, instead of the kernel telling us when something is ready for an action to be taken so that we can take it, we tell the kernel what action to we want to take, and it will do it when the conditions become right."
The difference between select and epoll was that select would keep checking in until the conditions were right while epoll would send you a message. That was gamechanging.
- I'm not really sure why this is seen as such a fundamental change. It's changed from the kernel triggering a callback to... a callback.
io_uring: when any of these descriptors are ready, read into any one of these buffers I've preallocated for you, then let me know when it is done.
Instead of waking up a process just so it can do the work of calling back into the kernel to have the kernel fill a buffer, io_uring skips that extra syscall altogether.
Taking things to the next level, io_uring allows you to chain operations together. You can tell it to read from one socket and write the results into a different socket or directly to a file, and it can do that without waking your process pointlessly at any intermediate stage.
A nearby comment also mentioned opening files, and that's cool too. You could issue an entire command sequence to io_uring, then your program can work on other stuff and check on it later, or just go to sleep until everything is done. You could tell the kernel that you want it to open a connection, write a particular buffer that you prepared for it into that connection, then open a specific file on disk, read the response into that file, close the file, then send a prepared buffer as a response to the connection, close the connection, then let you know that it is all done. You just have to prepare two buffers on the frontend, issue the commands (which could require either 1 or 0 syscalls, depending on how you're using io_uring), then do whatever you want.
You can even have numerous command sequences under kernel control in parallel, you don't have to issue them one at a time and wait on them to finish before you can issue the next one.
With epoll, you have to do every individual step along the way yourself, which involves syscalls, context switches, and potentially more code complexity. Then you realize that epoll doesn't even support file I/O, so you have to mix multiple approaches together to even approximate what io_uring is doing.
(Note: I've been looking for an excuse to use io_uring, so I've read a ton about it, but I don't have any practical experience with it yet. But everything I wrote above should be accurate.)
> The obvious place where BPF can add value is making decisions based on the outcome of previous operations in the ring. Currently, these decisions must be made in user space, which involves potential delays as the relevant process is scheduled and run. Instead, when an operation completes, a BPF program might be able to decide what to do next without ever leaving the kernel. "What to do next" could include submitting more I/O operations, moving on to the next in a series of files to process, or aborting a series of commands if something unexpected happens.
[0] https://github.com/checkpoint-restore/criu/blob/7686b939d155...
[1] https://github.com/checkpoint-restore/criu/blob/7686b939d155...
[2] https://github.com/checkpoint-restore/criu/blob/7686b939d155...
[3] https://www.infradead.org/~tgr/libnl/doc/api/group__qdisc__p...
> Only members inside the chain are serialized. A chain of SQEs will be broken, if any request in that chain ends in error. io_uring considers any unexpected result an error. This means that, eg, a short read will also terminate the remainder of the chain. If a chain of SQE links is broken, the remaining unstarted part of the chain will be terminated and completed with -ECANCELED as the error code.
So it sounds like you would need to decide what your strategy is. It sounds like you can inspect the step in the sequence that had the error, learn what the error was, and decide whether you want to re-issue the command that failed along with the remainder of the sequence. For a short read, you should still have access to the bytes that were read, so you're not losing information due to the error.
There is an alternative "hardlink" concept that will continue the command sequence even in the presence of an error in the previous step, like a short read, as long as the previous step was correctly submitted.
Error handling gets in the way of some of the fun, as usual, but it is important to think about.
[0]: https://manpages.debian.org/unstable/liburing-dev/io_uring_e...
But to answer your actual question, you can link requests and either abort or continue on failure.
https://github.com/golang/go/issues/31908
https://github.com/libuv/libuv/pull/2322
https://github.com/tokio-rs/mio/issues/923
Check out Stefan Metzmacher's talk at SambaXP 2021 (online event) for details:
Looks like the bandwidth went from 3.8 GB/s to 22 GB/s, with the client being the bottleneck.
Maybe someday I’ll get it done :)
Iouring is a huge improvement here.
The problem with the former occurs with large lists of file descriptors. Calling from user to kernel, the kernel needs to copy and examine N file descriptors. When user mode comes back, it needs to scan its list of file descriptors to see what changed. That's 2 O(n) scans at every syscall, one kernel side, one user side, even if only zero or one file descriptors has an event.
epoll and kqueue make it so that the kernel persists the list of interesting file descriptors between calls, and only returns back what has actually changed, without either side needing to scan an entire list.
By contrast, the high level programming model of io_uring seems pretty similar to POSIX AIO or Windows async I/O [away from readiness and more towards "actually do the thing"], but with the innovation being a new data structure that allows reduction in syscall overhead.
DNS resolution is also fiddly to do in a robust, nonblocking, async manner. An in-kernel dns resolution can block its thread for several minutes.
With io_uring, there will be no need to use multithreading or process pools to mitigate these situations. io_uring enables a pure-async programming style that has never before been practical on a mainstream os.
I would guess that the desire for something more "descriptive" reflects the fact that you are not in the weeds with io_uring (et al), and as such a name that's tied to specifics of the terrain (io, urings) feels esoteric and unfamiliar.
However, to anyone who is an immediate consumer of io_uring or its compatriots, "io" obviously implies "syscall", but is better than "syscall", because it's more specific; since io_uring doens't do anything other than io-related syscalls (there are other kinds of syscalls), naming it "syscall_" would make it harder for its immediate audience to remember what it does.
Similarly, "uring" will be familiar to most of the immediate audience, and is better than "queue", because it also communicates some specific features (or performance characteristics? idk, I'm also not in the weeds) of the API that the more generic "_queue" would not.
So, while I agree that the name is mildly inscrutable to us distant onlookers, I think it's the right name, and indeed reflects a wise pattern in naming concepts in complex systems. The less ambiguity you introduce at each layer of indirection or reference, the better.
I recently did a toy project that has some files named in a similar fashion: `docker/cmd`, which is what the CMD directive in my Dockerfile points at, and `systemd/start`, which is what the ExecStart line of my systemd service file points at. They're mildly inscrutable if you're unfamiliar with either docker or systemd, as they don't really say much about what they do, but this is a naming pattern that I can port to just about any project, and at the same time stop spending energy remembering a unique name for the app's entry point, or the systemd script.
Some abstract observations:
- naming for grokkability-a-first-glance is at odds with naming for utility-over-time; the former is necessarily more ambiguous
- naming for utility over time seems like obviously the better default naming strategy; find a nice spot in your readme for onboarding metaphors and make sure the primary consumers of your name don't have to work harder than necessary to make sense of it
- if you find a name inscrutable, perhaps you're just missing some contextIf the BSDs hadn't already claimed the name, it would probably have been fine to call this kqueue or something like that.
My idea was to have a syscall like this:
struct batchvec {
unsigned long batchv_callnr;
unsigned long long batchv_argmask;
};
asmlinkage long sys_batchcall(struct batchvec *batchv, int batchvcnt,
long args[16], unsigned flags);
You were supposed to give in a batchvec a sequence of system call numbers and a little mapping to arguments you provided in args. batchv_argmask is a long long - 64 bit type, this mask is divided to 4 bit fields, every field can address a long from args table. AFAIR Linux syscalls have up to 6 arguments. 6 fields for arguments and one for return value, that gives 7 fields - 28 bits and now I don't remember why I thought I need a long long.It would go like this pseudo code:
int i = 0;
for(; i < batchvcnt; i++) {
args[batchv[i].argmask[6]] = sys_call_table[batchv[i].callnr](args[batchv[i].argmask[0]], args[batchv[i].argmask[1]], args[batchv[i].argmask[2]], args[batchv[i].argmask[3]], args[batchv[i].argmask[4]], args[batchv[i].argmask[5]]);
if(args[batchv[i].argmask[6]] < 0) {
break;
}
}
return i;
It would return a number of successfully run syscalls. It would stop on first failed one. The user would have to pick up the error code out of args table.I would be interested to know why it wouldn't work.
I started implementing it against Linux 4.15.12, but never went to test it. I have some code, but I don't believe it is my last version of the attempt.
It's not really the same, though; readv and writev are still synchronous APIs, they just do more at once.
Exactly. It's such an awesome design. I wonder how many system calls will be supported in the future.
The roundtrip for command buffer submission in GPU is huge by my estimation, around 100µs. On a 10TFLOPS card, which is nowhere near the top of the line, that's 1 billion operations. I don't know exactly where all the time is going, but suspect it's a bunch of process and kernel transitions between the application, the userland driver, and the kernel driver.
My understanding is that games mostly work around this by batching up a lot of work (many dozens of draw calls, for example) in one submission. But it's still a problem if CPU readback is part of the workload.
So my question is: can a technique like io_uring be used here, to keep the GPU pipeline full and only take expensive transitions when absolutely needed? I suspect the programming model will be different and in some cases harder, but that's already part of the territory with GPU.
It’s why graphics APIs are asynchronous until you synchronize f.e. by flipping a frame buffer or reading something back.
APIs like Vulkan are very explicit about this and have fences and semaphores. Older APIs will just block if you do something that requires blocking.
Now Unix is finally, in 2021, getting a syscall queue construct where I can interact with the kernel asynchronously.
I believe both are limited to specific operations, mostly IO, and aren't fully general asynchronous syscall interfaces.
I went through the list of operations supported by `io_uring_enter`. Almost all of them are for IO, the remainder (NOP, timeout, madvise) are useful for supporting IO, though madvise might have some non-IO uses as well. While io_uring could form the basis of a generic async syscall interface in the future, in its current state it most certainly is not.
The article mostly talks about io_uring enabling completion based IO instead of readiness based IO.
AFAIK kqueue also supports completion based IO using aio_read/aio_write together with sigevent.
If you can point to a practical example of a program doing it this way and seeing a performance benefit, I would be curious to see it. I did some googling and didn't really even find any articles mentioning this as possibility.
kqueue is widely considered to be readiness based, just like epoll, not completion based.
What you wrote sounds like an interesting hack, but I'm not sure it counts for much if it is impractical to use.
Now nginx could use threads for file io, and linux file performance became ok as well.
As for your "performance benefit" question and nobody using it over readiness based approaches: Well yes. No idea how it compares with a traditional readiness based socket program. But it's kind of not the point either. AFAIK the scenario where POSIX AIO makes most sense is files on disk.
But as has been pointed out elsewhere, io_uring is also about making other syscalls asynchronous.
https://web.archive.org/web/20110719052845/http://developers...
So at least one UNIX system had them a while ago.
I've written at least two wrapper libraries for I/O readiness, POSIX signal, file event, and user-triggered event polling that encompass epoll, kqueue, and Solaris Event Ports. Supporting all three is relatively trivial from an API perspective because they work so similarly. In fact, notably all three let you poll on the epoll, kqueue, or Event Port descriptor itself. So you can have event queue trees, which is very handy when writing composable libraries.
So, it will probably happen Soon™, but you're probably still ~2 years out before you can reliably depend on it, I'd say.
The kernel wakes up the user space scheduler when it decides to put the process onto a cpu. The user space scheduler decides which user space thread executes in the kernel thread context that it runs in, and does a user space thread switch (not a full context switch) to it. It's a combination of kernel threads and user space (aka "green") threads.
[1] https://www.phoronix.com/scan.php?page=news_item&px=Google-F...
The sort of thing Go and Rust runtimes try to approximate in a hackish way nowadays. They would both by improved by an appropriate scheduler-activation hook.
Back then the idea didn't gain support. It needed a champion, and nobody cared enough. It seemed unnecessary, complicated. What was done instead seemed to be driven by interests that focused on one kind of task or another, e.g. networking or databases.
It doesn't help that the understandings many people have of performance around asynchronous I/O, stackless and stackful coroutines, userspace-kernel interactions, CPU-hardware interactions and so on are not particularly deep. For example I've met a few people who argued that "async-await" is the modern and faster alternative to threads in every scenario, except for needing N threads to use N CPU cores. But that is far from correct. Stackful coroutines doing blocking I/O with complex logic (such as filesystems) are lighter than async-await coroutines doing the same thing, and "heavy" fair scheduling can improve throughput and latency statistics over naive queueing.
It's exciting to see efficient userspace-kernel I/O scheduling getting attention, and getting better over the years. Kudos to the implementors.
But it's also kind of depressing that things that were on the table 20-25 years ago take this long to be evaluated. It's almost as if economics and personal situations governs progress much more than knowledge and ideas...
It's almost as if economics and personal situations
governs progress much more than knowledge and ideas...
That has always been the case and will probably always be the case.More efficient? Yes. Simpler? Not really. A synchronous program would be simpler, everyone who has done enough of these know it.
Let's say your program wants to list a directory, if it has nothing to do during that time then there is no point to using an asynchronous model, that only adds costs and complexity.
And even if your program doesn't have any opportunity for doing IO in parallel, being able to chain a sequence of IO operations together and issue them with at most one syscall may still get you improved latency.
Interesting times.
How do you do that without an async API? Thread pool and synchronous syscalls? That's not simpler.
At no point did I say that these APIs were not useful? I’m literally the person who explained what kind of uses would be simplified by async syscalls.
And you can keep your strawman to yourself. I objected to the statement that it would make all programs faster / simpler, the argument that it would make some simpler or faster is not something I ever argued against.
And I pointed out that even the simplest example you could come up with can in fact be made faster (than implementing sequentially) or simpler (than implementing with threads, which require synchronization) with an async API. So I don't see the straw man.
Pretty much anything that needs to interact with the kernel can benefit from this.
No, you pointed out that a different example could be made faster (and almost certainly at the cost of simplicity, the mention of which you carefully avoided).
> So I don't see the straw man.
That doesn't surprise me.
In general I totally agree that a synchronous program will be way simpler than an equivalent asynchronous one, though.
You can even choose between executing them sequentially and aborting on the first error or trying to complete as many as possible.
Why would it be any more of an issue than calling a blocking exec?
> why on earth you would call exec that way
The same reason as why you would want to do anything else with io_uring? In an async runtime you have to delegate blocking calls to a thread pool. Much nicer if the runtime can use the same execution system as for other IO.
Now you turn everything doing fast I/O into something that's essentially an easier rwx situation WRT memory corruption.
>Much nicer
In other words: the runtime should handle it because the kernel doesn't need to.
io_uring can really be thought of as a generic asynchronous syscall interface.
It uses a kernel thread pool to run operations. If something is blocking on a kernel-level it can just be run as a blocking operation on that thread pool.
The same syscall used to inform the kernel that you're submitting N new operations can also specify that it should block waiting for M completions. One of those completions could be for a timeout command that was previously submitted.
I would call it a message passing system, not an asynchronous syscall facility. A syscall, even one that doesn't block indefinitely, transfers control to the kernel. io_uring, once setup, doesn't. Now that we have multiple cores, there is no reason to context switch if the kernel can handle your request on some other core on which it is perhaps already running.
Which makes me wonder if Mach wasn't right all along.
Is there a middle ground where io_uring doesn't require the mitigations but other syscalls do?
Cached version of the write up https://archive.is/VgHkW
For cancellation there is an API. Example call: https://github.com/axboe/liburing/blob/c4c280f31b0e05a1ea792...
[1] https://elixir.bootlin.com/linux/v5.13-rc6/source/kernel/exi...
Edit: I just looked up at the latest version of the source [1]. Yes, it does clean up io_uring related files.
This is why many of us are excited about io_uring.
https://windows-internals.com/i-o-rings-when-one-i-o-operati...
Looks like they took a lot of the concepts from Winsock RIO and applied them to file I/O. Which is fascinating because with network traffic you can't predict packet boundaries and thus your I/O rate can be unpredictable. RIO helps you get the notification rate under control, which can help if your packet rate is very high.
With files, I would think you can control the rate at which you request data, as well as the memory you allocate for it.
The other thing it saves just like RIO is the overhead of locking/unlocking buffers by preregistering them. Is that the main reason for this API then ?
I would be very interested to hear from people who have actually run into limits with overlapped file reads and are therefore excited about IoRings
Unfortunately Rust went the exact other way.
E.g. with io_uring, the event triggers the kernel to read into a buffer.
With async/await epoll wakes up my process which does the syscall to read the file.
In both cases you still need to read from the device and get the data to the user process?
If the work happens in the kernel process or user process it still costs the same?
Additionally, as noted elsewhere, you can chain syscalls pretty deeply so that the entire operation occurs in the kernel & never schedules your process. This also benefits spatial & temporal locality AND removes the cost of needing to schedule the process in the first place.
A syscall that does literally nothing can still have a latency as observed from userspace that is vastly faster than the hardware latency of a fast SSD, though Spectre and friends have slowed this down a bit.
However, when comparing the latency as observed from userspace of syscalls that actually do significant work and cause stuff to happen, such as shepherding an IO request through the various layers to get down to the actually submitting commands to the hardware, then you usually do start talking about latencies that are on a similar order of magnitude to fast SSDs.
In particular, if you're trying to minimize IO latency, the kernel can no longer afford to put a CPU core to sleep or switch to run another task while waiting for the IRQ signalling that the SSD is done. The interrupt latency followed by the context switch back to the kernel thread that handles the IO completion is a substantial delay compared to spinning in that thread until the SSD responds.
An interesting thing about your second point is that those overhead costs are almost entirely driven by the software scheduler implementation. The actual hardware costs are actually quite low and in theory it should be possible to meaningfully switch if you could just determine what to run next quickly as discussed here [1].
In practice implementation differences can offset that advantage, or it just might not make any difference for the application at all since its not the hotspot.
I think for socket IO a variety of people did some synthetic benchmarks for epoll vs uring, and got all kinds of results from either one being a bit faster to both being roughly the same.
This asymmetry is because the command submitter is able to block and is in control. It even works with elastic ringbuffers that can be expanded as needed. All memory allocation can be on the command submitter side.
This trick works in other domains, not just kernels. For example request-reply ringbuffer pairs can be used to communicate with interrupt handlers, in OSes and also in bare metal embedded systems, and the interrupt handler does not need to block when writing to the reply ringbuffer. It's a nice way to queue things without locking issues on the interrupt side.
Similarly with signal handlers and attached devices (e.g. NVMe storage, GPUs, network cards), those can can always write to the reply ringbuffer too.
On my live machines the kernel is taking 30% CPU copying memory.
I'm waiting for userspace networking... file IO can be offset with async-to-async parallelism.