Asio Uring
Introduction
Asio Uring adapts the io_uring
functionality added in the Linux kernel 5.1 to the model of Boost.Asio with the framework laid for future adaptation to the model of the Networking TS. This enables native asynchronous file I/O within the context of Boost.Asio.
Important Notes
Proactor- vs. Reactor-Style I/O
io_uring
provides two broad ways to interact with files asynchronously:
- Proactor-style I/O using asynchronous analogs to
preadv2
andpwritev2
- Reactor-style I/O using an asynchronous analog to
poll
As a user of this library you don't need to concern yourself with the details of this (that's the point of the library) other than to be aware that certain classes of file descriptors work with one but not the other.
File descriptors which represent actual files in the filesystem only work with the former (and should be referred to asio_uring::asio::async_file
) whereas file descriptors which don't represent actual files (e.g. sockets) only work with the latter (and should be referred to asio_uring::asio::poll_file
).
You'll know you've done it wrong if your application hangs and the stack trace indicates it's stuck submitting work to the io_uring
.
Multithreading
It is not safe to simultaneously surrender multiple threads to asio_uring::asio::execution_context
to use to run work. Put formally:
If two threads A and B include calls to any of the following member functions of asio_uring::asio::execution_context
:
restart
run
run_one
poll
poll_one
And it is not the case that the call in A inter-thread happens before the call in B, or vice versa, then the behavior is undefined.
Note that the Executor
concept requires certain functions be thread safe (i.e. that they "not introduce data races as a result of concurrent calls to those functions from different threads") and asio_uring::asio::execution_context::executor_type
abides by this. Only the execution context itself is not thread safe.
Usage
As a user of the library you will interact directly with the following classes:
asio_uring::asio::execution_context
: A model of theExecutionContext
concept implemented on top of anio_uring
asio_uring::fd
: An RAII wrapper over a Linux file descriptor which has a unary constructor which checks its argument against-1
and throws astd::system_error
encapsulatingerrno
if the argument is-1
(simplifies interoperating with system calls that generate file descriptors)asio_uring::asio::async_file
: An I/O object which encapsulates a file descriptor for which proactor-style I/O is appropriate (models the Boost.Asio conceptsAsyncRandomAccessReadDevice
andAsyncRandomAccessWriteDevice
)asio_uring::asio::poll_file
: An I/O object which encapsulates a file descripctor for which reactor-style I/O is appropriate (models the Boost.Asio conceptsAsyncReadStream
andAsyncWriteStream
)asio_uring::asio::connect_file
: Addsconnect
support toasio_uring::asio::poll_file
asio_uring::asio::accept_file
: Wraps a file descriptor for the sole purpose of performingaccept4
calls
Note that unlike Boost.Asio you will interact directly with file descriptors (via the owning wrapper asio_uring::fd
) and that for reactor-style I/O you are expected to provide file descriptors which are already in non-blocking mode (the library cannot be expected to do this for you).
The expectation that your file descriptors will already be in non-blocking mode is motivated by the fact that:
fcntl
is a system call- Minimizing system calls is desirable (for performance)
- Many system calls can set the non-blocking flag as part of another system call (for example calls to
socket
andaccept4
withSOCK_NONBLOCK
)
Accordingly the library gives the user the opportunity to minimize system calls (by fusing setting the non-blocking flag with another system call) and as a consequence expects file descriptors to be in non-blocking mode when they are presented to it.
Note that asio_uring::asio::accept_file
uses accept4
with SOCK_NONBLOCK
and therefore provides a non-blocking file descriptor while only making a single system call.
Examples
File I/O
#include <iostream>
#include <string_view>
#include <utility>
#include <asio_uring/asio/execution_context.hpp>
#include <asio_uring/asio/async_file.hpp>
#include <asio_uring/fd.hpp>
#include <boost/asio/buffer.hpp>
#include <fcntl.h>
// ...
// Open file using system call, the
// fd wrapper class will automatically
// throw from its constructor if this
// fails
asio_uring::fd file(::open("/path/to/file",
O_RDONLY));
// Creates the io_uring and the context
// for performing asynchronous I/O, the
// argument to the constructor is the size
// of the ring
asio_uring::asio::execution_context ctx(100);
// Creates an I/O object which can be used
// to read from and write to the file
// (although since we opened with O_RDONLY
// we can't actually read), note how we
// surrender ownership of the file descriptor
// to the I/O object
asio_uring::asio::async_file io(ctx,
std::move(file));
// We setup a read of up to 1024 bytes, note
// that the read itself may proceed in the
// background but the completion handler (i.e.
// the lambda) won't be actually invoked until
// we surrender a thread to the execution_context
// to use to run some work
char buffer[1024];
auto read_handler = [&](auto ec,
auto bytes_transferred)
{
if (ec) {
std::cerr << "ERROR: " << ec.message() << std::endl;
return;
}
std::string_view sv(buffer,
bytes_transferred);
std::cout << "Read " << bytes_transferred << " bytes:\n"
<< sv << std::endl;
};
io.async_read_some_at(0,
boost::asio::buffer(buffer),
read_handler);
// Here we actually surrender the current thread
// to the execution_context to use to run some
// work, this particular call will block until
// there's no work left
ctx.run();
Socket I/O
There is an included fully worked example program which demonstrates interoperating with Boost.Beast: src/asio/example/beast_example.cpp
.
Dependencies
- Boost (tested against 1.70.0)
- C++17 compiler (tested against GCC 8.3.0)
- CMake >=3.8 (tested against 3.13.4)
- liburing
- Linux kernel >=5.1 (tested against 5.1 on Ubuntu 19.04)
The following are optional:
Building
cmake .. -DBOOST_ROOT=/path/to/boost -DURING_INCLUDE_DIR=/path/to/liburing/src -DURING_LIBRARY=/path/to/liburing.a
cmake --build .
If Catch2 was provided you can then run the tests via:
ctest
If Doxygen was provided you can build the documentation via:
cmake --build . --target doc
Installation
cmake --build . --target install