Are you disappointed with select, poll, epoll or AIO? Try out the best I/O promise in the Linux landscape.
您对 select、poll、epoll 或 AIO 感到失望吗?尝试 Linux 环境中最佳的 I/O 承诺。
Linux has a rich history in managing I/O operations. Some mechanisms, like select and poll, which are part of POSIX, were developed in the 1980s and inherited from Unix systems. Kernel 2.5 introduced epoll in 2002, enhancing the performance of I/O operations, and it is still widely used today. The subsequent addition of AIO did not fully address the challenges in the asynchronous I/O domain. Until 2019, files could not be opened or stated truly asynchronously. The new I/O Ring (uring) seems to be a game-changer, reshaping how the system interacts with user applications.
Linux 在管理 I/O 操作方面有着丰富的历史。一些机制,如 select 和 poll,是 POSIX 的一部分,是在 20 世纪 80 年代开发的,并继承自 Unix 系统。内核2.5在2002年引入了epoll,增强了I/O操作的性能,至今仍被广泛使用。随后添加的 AIO 并没有完全解决异步 I/O 领域的挑战。直到 2019 年,文件还无法真正异步打开或读取。新的 I/O 环 (uring) 似乎改变了游戏规则,重塑了系统与用户应用程序的交互方式。
The story isn’t about using liburing or io-uring from tokio. We are going to skip all given abstractions and talk directly to the kernel via system calls to discover and learn how the newest I/O non-blocking mechanism works. We will write two sample apps. The first one will display “Hello, World!”. The second one, a bit more complex, will print a file to the standard output. Are you ready to enjoy it?
这个故事与使用 tokio 的 liburing 或 io-uring 无关。我们将跳过所有给定的抽象并通过系统调用直接与内核对话,以发现和了解最新的 I/O 非阻塞机制是如何工作的。我们将编写两个示例应用程序。第一个将显示“Hello, World!”。第二个稍微复杂一些,会将文件打印到标准输出。你准备好享受它了吗?
自下而上可能很难理解。在这个故事中,我们将从查看第一个演示应用程序开始。它只会打印一条消息,但其复杂性已经足以让人迷失。
use crate::uring::*;
pub struct HelloCommand {
pub msg: &'static [u8],
}
pub enum HelloCommandExecute {
Succeeded(),
Failed(&'static [u8]),
}
impl IORingSubmitBuffer for &'static [u8] {
fn extract(self) -> (*const u8, usize) {
(self.as_ptr(), self.len())
}
}
fn fail(msg: &'static [u8]) -> HelloCommandExecute {
HelloCommandExecute::Failed(msg)
}
impl HelloCommand {
const IORING_INVALID_DESCRIPTOR: &'static [u8] = b"I/O Ring Init failed: Invalid Descriptor.\n";
const IORING_SETUP_FAILED: &'static [u8] = b"I/O Ring Init failed: Setup Failed.\n";
const IORING_MAPPING_FAILED: &'static [u8] = b"I/O Ring Init failed: Mapping Failed.\n";
const IORING_SUBMISSION_FAILED: &'static [u8] = b"I/O Ring entry submission failed.\n";
const IORING_SUBMISSION_MISMATCHED: &'static [u8] = b"I/O Ring entry submission mismatch.\n";
const IORING_COMPLETION_FAILED: &'static [u8] = b"I/O Ring entry completion failed.\n";
const IORING_COMPLETION_ERRORED: &'static [u8] = b"I/O Ring completed with failure.\n";
const IORING_SHUTDOWN_FAILED: &'static [u8] = b"I/O Ring shutdown failed.\n";
}
impl HelloCommand {
pub fn execute(&self) -> HelloCommandExecute {
let mut ring = match IORing::init(32) {
IORingInit::Succeeded(value) => value,
IORingInit::InvalidDescriptor(_) => return fail(HelloCommand::IORING_INVALID_DESCRIPTOR),
IORingInit::SetupFailed(_) => return fail(HelloCommand::IORING_SETUP_FAILED),
IORingInit::MappingFailed(_, _) => return fail(HelloCommand::IORING_MAPPING_FAILED),
};
match ring.submit([IORingSubmitEntry::write(2, self.msg, 0, 0)]) {
IORingSubmit::SubmissionFailed(_) => return fail(HelloCommand::IORING_SUBMISSION_FAILED),
IORingSubmit::SubmissionMismatched(_) => return fail(HelloCommand::IORING_SUBMISSION_MISMATCHED),
IORingSubmit::Succeeded(_) => (),
};
let entry = loop {
match ring.complete() {
IORingComplete::Succeeded(entry) => break entry,
IORingComplete::UnexpectedEmpty(_) => continue,
IORingComplete::CompletionFailed(_) => return fail(HelloCommand::IORING_COMPLETION_FAILED),
}
};
if entry.res < 0 {
return HelloCommandExecute::Failed(HelloCommand::IORING_COMPLETION_ERRORED);
}
if let IORingShutdown::Failed() = ring.shutdown() {
return fail(HelloCommand::IORING_SHUTDOWN_FAILED);
}
HelloCommandExecute::Succeeded()
}
}
The code represents a command that can be executed. The main function creates a new I/O Ring, then submits a buffer with a message for printing to the standard output. This is followed by an event processing loop, which is expected to loop only once. Finally, we check if we printed at least one character and close the I/O Ring to release all resources. When executed, it simply prints “Hello, World!”.
代码代表可以执行的命令。主函数创建一个新的 I/O 环,然后提交带有消息的缓冲区以打印到标准输出。接下来是事件处理循环,预计仅循环一次。最后,我们检查是否至少打印了一个字符并关闭 I/O 环以释放所有资源。执行时,它只是打印“Hello, World!”。
You’ve already learned that the I/O Ring must be created before using it. And probably noticed that we can submit I/O operations and loop to receive their completions. Finally, you are aware that the I/O Ring is a resource that needs to be cleaned up.
您已经了解到,在使用 I/O 环之前必须先创建它。并且可能注意到我们可以提交 I/O 操作并循环以接收它们的完成情况。最后,您知道 I/O 环是需要清理的资源。
But what actually is an I/O Ring? You might think of it as having two queues. The first one accepts outgoing I/O requests, and the second one delivers the results of previously scheduled requests. The general idea is that submitting a request is a non-blocking operation, while looping and waiting for completion may block until something is received. You may notice in the following visualization that the Completion Queue (CQ) doesn’t deliver events in the same order as the Submission Queue (SQ).
但 I/O 环到底是什么?您可能会认为它有两个队列。第一个接受传出 I/O 请求,第二个传递先前安排的请求的结果。总体思路是,提交请求是一个非阻塞操作,而循环和等待完成可能会阻塞,直到收到某些内容。您可能会在下面的可视化中注意到,完成队列 (CQ) 不会按照与提交队列 (SQ) 相同的顺序传递事件。
+------------------------------------------------+
| Submission Queue (SQ) |
| +-------+ +-------+ +-------+ +-------+ |
| | SQE 1 | | SQE 2 | | SQE 3 | | SQE 4 | .. |
| +-------+ +-------+ +-------+ +-------+ |
+------------------------------------------------+
|
|
V
+------------------------------------------------+
| Completion Queue (CQ) |
| +-------+ +-------+ +-------+ +-------+ |
| | CQE 3 | | CQE 1 | | CQE 4 | | CQE 2 | .. |
| +-------+ +-------+ +-------+ +-------+ |
+------------------------------------------------+
When we discuss rings, buffers, or queues, we are referring to a memory structure with multiple slots, each having its own location. The advantage of the I/O Ring is that the memory is shared between our application and the kernel. To work with the I/O Ring, we need to initialize the ring and create a shared memory mapping. We will use the io_uring_setup
system call (425) to create a new file descriptor for an I/O Ring.
当我们讨论环、缓冲区或队列时,我们指的是具有多个槽的内存结构,每个槽都有自己的位置。 I/O 环的优点是内存在我们的应用程序和内核之间共享。要使用 I/O 环,我们需要初始化环并创建共享内存映射。我们将使用 io_uring_setup
系统调用 (425) 为 I/O 环创建新的文件描述符。
pub fn sys_io_uring_setup(entries: u32, params: *mut io_uring_params) -> isize;
#[repr(C)]
pub struct io_uring_params {
pub sq_entries: u32,
pub cq_entries: u32,
pub flags: u32,
pub sq_thread_cpu: u32,
pub sq_thread_idle: u32,
pub features: u32,
pub wq_fd: u32,
pub resv: [u32; 3],
pub sq_off: io_sqring_offsets,
pub cq_off: io_cqring_offsets,
}
#[repr(C)]
pub struct io_cqring_offsets {
pub head: u32,
pub tail: u32,
pub ring_mask: u32,
pub ring_entries: u32,
pub overflow: u32,
pub cqes: u32,
pub flags: u32,
pub resv1: u32,
pub user_addr: u64,
}
#[repr(C)]
pub struct io_sqring_offsets {
pub head: u32,
pub tail: u32,
pub ring_mask: u32,
pub ring_entries: u32,
pub flags: u32,
pub dropped: u32,
pub array: u32,
pub resv1: u32,
pub user_addr: u64,
}