likes
comments
collection
share

Rust通过绑定使用 io_uring本文翻译自 https://www.thespatula.io/rust/rust

作者站长头像
站长
· 阅读数 24

本文翻译自 www.thespatula.io/rust/rust_i…

介绍

这是一个系列的一部分,我正在努力从零开始构建一个游戏。您可以在这里找到所有内容的存储库,这里是我执行WebSockets的系列的最后一部分。

回溯一下,如果你错过了之前的任何或所有内容,我试图只使用Rust标准库构建所有内容。到目前为止,这并不太困难,但后来我决定我真的希望我的WebSocket服务器是可扩展的,这就是一切都走到了一边。

 侧身

不是电影那种侧身,当我想到它我得到一个坏的味道在我的嘴里。这个侧面是意识到,如果我想只使用标准库来执行JavaScript,我将需要在epoll或io_uring之上构建它。这些API允许在Linux上处理输入和输出操作,例如文件和网络。mio库是建立在epoll之上的,而最受欢迎的aprc库时雄也是建立在Mio之上的。Mio本身构建在libc上,使用系统调用与epoll进行通信,epoll是Linux内核的一部分。你可以看到他们使用系统调用! macro(用于Linux),可在此处找到。

Rust通过绑定使用 io_uring本文翻译自 https://www.thespatula.io/rust/rust

我们的起点和终点

这对我们来说意味着,如果我们想要apec,我们需要使用epoll并构建一个最小的mio,在此基础上我们将构建一个最小的时雄apec运行时。或者,我们可以选择另一个API io_uring,并在此基础上进行构建。对我来说,这听起来更有趣,因为io_uring是闪亮的和新的。

公平地说,使用io_uring的决定花了它和epoll的一点时间,虽然后者肯定有更多的代码示例和广泛的文档,但前者可能是未来的发展方向。但在我们开始之前,让我们先来看看这两个。

 To epoll or liburing?

在Linux上,创建JavaScript运行时的可靠方法是使用epoll。虽然时雄的tokio-uring是建立在io_uring之上的,但大多数人使用的时雄库是建立在epoll之上的。但让我们倒回一个时刻,并得到我们的轴承。

您可能听说过epoll、kqueue、IOCP或io_uring,但它们是操作系统处理并发I/O操作的核心,例如阅读和写入文件以及TCP连接。如果您正在处理CDN、文件服务器、数据库系统等,那么当访问文件或数据时,它可能会使用这些队列之一。当你打开一个网站的连接并请求一个页面时,也会发生同样的事情。也就是说,它并不总是必须使用这些队列,但在高度并发的阅读和写数据的情况下可能会使用。

在Linux上,从网络连接到实际文件的所有内容都与文件描述符相关联,由于我们关注的是Linux,因此我们将从epoll和以下定义开始:

epoll API(特定于Linux)允许应用程序监视多个文件描述符,以确定哪些描述符准备好执行I/O。该API旨在更有效地替代传统的select()和poll()系统调用。

 引自lwn.net

它是怎么做到的?阅读这篇文章的简介给出了一个好主意。他们也有一个方便的图表,我重新做了,因为我需要美学的一致性:

Rust通过绑定使用 io_uring本文翻译自 https://www.thespatula.io/rust/rust

 从这里重新绘制

要分解这个,会发生什么:

  • ·创建一个内核级epoll实例来保存我们的文件描述符。
  • ·将文件描述符添加到我们的兴趣列表中。
  • ·操作系统监视这些I/O。
  • ·询问操作系统是否准备好了(如果没有,则阻止)。

所以,epoll,作为操作系统的一部分,是软件,和所有软件一样,如果某个东西成功了,那么人们总是想创建自己的,新的和改进的版本。输入io_uring,这是一个API,它可以做与epoll类似的事情,尽管我前面的句子暗示了我的尖刻,但它实际上在许多方面都是一种改进。

有许多不同之处,但两个是io_uring是异步的,而不是事件驱动的,这意味着它不会阻塞上面的步骤(4),并且它还允许重复操作。这个插件可以减少系统调用的数量,也就是对操作系统的调用,如果你不知道的话,系统调用是昂贵的,而我又不是有钱人。

当然,还有许多其他的差异,从io_uring的队列与epoll的列表的不同,以及每个API可以做的事情的广度和深度,但这是我们将在构建P2P运行时的过程中展开的。

在我们离开epoll去寻找闪亮的新事物之前,我建议看看这篇文章,它有一个很好的解释和epoll的工作示例。理解它是如何工作的,以及为什么它以这种方式工作,将帮助你更好地欣赏它。

但是等等,您可能会说,如果您正在构建WebSocket服务器,请不要离开epoll,因为它仍然可能比io_uring具有更好的性能。是的,你可能是对的,但阅读评论,似乎这些问题将得到解决。此外,io_uring比epoll年轻得多,我只能看到它随着开发的推进而变得更好。另外,我可以学到一些新的和很酷的东西,我可以在晚宴上炫耀,这是最重要的事情。

本文的其余部分。

以下是我们在本文中最终要做的:

  1. 1. 从源代码安装liburing。
  2. 2. 使用bindgen生成绑定。
    1. 修复(2)的bug。
    1. 建立一个简单的安装程序,我们可以。

现在,如果你熟悉bindgen,你可能会认为我要在我的项目中添加一个crate,这意味着我不仅仅像我之前宣传的那样使用标准库。通常情况下,这是真的,如果我的临时变通方法在长期内不奏效,我可能最终会这样做,但现在我只打算使用bindgen-tool,然后调用它。作弊?也许吧,但我们的TOML将像塞缪尔·L·哈钦森(Samuel L.杰克逊的圆顶(我向塞缪尔L。杰克逊)。

 安装Liburing

如果你想自己沿着走,你需要满足几个先决条件:

  • ·使用Linux(我使用基于Mint 21.2 /Ubuntu的)
  • ·使用最新的内核(5.1之后的版本,但最好是6.x)

您可以使用以下命令检查内核:

uname -r

或者,使用一个Linux容器,确保它具有Web访问权限,并使用ssh。解决了这些问题之后,让我们开始安装liburing,这个库将帮助我们使用io_uring。我们要做的第一件事是检查它是否已经安装:

ldconfig -p | grep liburing

如果你得到了一些东西,你可以尝试使用这些现有的库,或者继续下一节,我们从源代码安装最新的库。我发现这是最可靠的,因为默认的apt仓库有一个旧版本。如果需要的话,你也可以参考原始资料。

 来源

如果您已经安装了库,但希望安装最新版本,则可以执行以下操作来删除它:

sudo apt remove liburing-dev liburing2
sudo apt autoremove
ldconfig -p | grep liburing

这里的最后一行只是证明它已经消失了。如果在运行时您仍然收到某些内容,那么您可能需要转到文件夹 /usr/lib/usr/include并手动删除所有与liburing关联的 .so文件。如果您使用的是不同风格的Linux,那么您的文件也可能位于另一个文件夹中,因此请确保查看ldconfig输出。

接下来,确保你有构建工具,以便从源代码编译:

sudo apt install build-essential

完成后,您可以克隆存储库,配置和制作。在repo中有更具体的方向,但我摆脱了以下情况,它工作得很好:

git clone https://github.com/axboe/liburing
cd liburing
./configure
make
sudo make install
sudo ldconfig

ldconfig的最后一行是更新共享库(.so - shared object)缓存并使其识别liburing的存在。在它运行之后,你可以通过转到 /usr/lib/usr/include来检查它是否都在那里,或者运行以下命令:

ls /usr/lib | grep "liburing"
ls /usr/include | grep "liburing"

您应该在每个文件夹中看到多个文件。如果没有,您需要检查它是否在另一个文件夹中,或者在此过程中沿着错误。

 生成绑定

我们将使用bindgen,这是一个命令行工具:

自动生成FFI绑定到C和C++库。

如果你不熟悉FFI(外部函数接口),这是一种调用其他语言库的方法。很多东西都是用C/C++写的,并且已经过广泛的测试,所以我们不是在它的基础上重新发明轮子。像Python这样的语言大量使用C库和Numpy和Pandas这样的包。这主要是出于性能原因,当涉及到CPU密集型任务时,Python会调用它们来进行大量繁重的工作。

从本质上讲,我们最终想要的是一些Rust函数的shell,它们包装了底层的库函数。当一切都说了和做了之后,它们会看起来像这样:

extern "C" {
    pub fn sendmsg(
        __fd: ::std::os::raw::c_int,
        __message: *const msghdr,
        __flags: ::std::os::raw::c_int,
    ) -> isize;
}

让我们安装bindgen并开始:

cargo install bindgen-cli

一旦我们有了这个,我们将以两种方式生成are绑定:

  • ·手动,在命令行中,然后将它们复制到我们的项目中。
  • ·以编程方式,作为构建过程的一部分。

后一种方法是正确的,但我们将使用第一种方法,这样我们就可以看到它是如何工作的。

 手工装订

你需要做的第一件事是在项目的根目录中创建一个名为wrapper. h的文件。名称并不重要,您可以将其称为headers. h或任何您想要的名称。您的项目文件夹看起来像这样:

├── Cargo.lock
├── Cargo.toml
├── wrapper.h 
├── src
│   └── main.rs
└── target

wrapper.h中,我们将列出我们想要绑定的liburing C头文件:

#include "/usr/include/liburing.h"

还有其他的头文件,但是bindgen应该递归地检查所有也包含在liburing. h中的文件,比如io_uring. hbarrier. h。添加wrapper.h后,现在可以从项目文件夹中运行bindgen:

bindgen wrapper.h –output bindings.rs

运行后,您可以打开bindings.rs,它应该在wrapper.h旁边,并看到大约10k行代码。不幸的是,它不会捕捉到解放的一切。你可以通过打开原始的liburing. h并找到这个函数来检查这一点:

IOURINGINLINE void io_uring_cq_advance(struct io_uring *ring, unsigned nr)

你不会在生成的文件中找到它,原因是它是一个内联函数,由IOURINGINLIEN表示。我们需要显式地告诉bindgen包含内联函数。你可能想知道为什么它不会默认绑定它们?根据文档:

这些函数通常不会在对象文件或共享库中结束,我们可以可靠地链接到符号,因为它们会内联到每个调用站点。因此,我们不生成到它们的绑定,因为这会创建链接错误。

在我们的例子中,我们不应该有这个问题,或者至少到目前为止我还没有,所以我们将通过运行以下命令来强制bindgen包含它们:

bindgen wrapper.h --experimental --wrap-static-fns --output bindings.rs

现在,您可以搜索bindings.rs,您应该会发现包含了io_uring_cq_advance函数。要使用这些绑定,您可以将bindings.rs文件移动到src/ 文件夹中,然后在main.rs中添加:

pub mod bindings;

use bindings::*;

fn main() {
    // Nothing here yet
}

在那之后,你会得到大量关于命名的错误。要抑制这些,请在bindings.rs文件的顶部添加以下内容:

#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]

虽然在使用这些函数时,您不会通过编辑器获得任何可见的错误,但当您尝试构建时,您将获得它们,这是因为您尚未链接实际的库。我建议尝试构建,这样你就可以看到错误是什么样子的。

要解决此问题,请在项目文件夹中与wrapper.h一起创建一个build.rs文件,并向其中添加以下内容:

fn main() {
    println!("cargo:rustc-link-search=native=/usr/lib");
    println!("cargo:rustc-link-lib=dylib=uring");
}

build.rs脚本,你可以在这里阅读更多,是一个专门命名的脚本,运行它是为了链接到外部库,如liburing。我找不到一个令人满意的使用println的理由 但是似乎确实有围绕构建过程和使用打印的选择的讨论和RFC。

这些print语句将 /usr/lib添加到搜索路径,然后链接到liburing。奇怪的是,print语句实际上说的是链接到uring,而库被称为liburing。Linux上的约定是在libraries前面加上lib,所以我想,就照这样吧。

print语句的dylib部分意味着我们是动态链接而不是静态链接,这意味着如果我们要按原样分发它,其他人需要安装这个库。至此,一切就绪,您应该能够运行cargo build, 而只会出现一些关于未使用的导入的警告。

注意:此时您将能够使用非内联的函数和类型,但内联函数仍可能失败。我们将在下一节中解决这个问题。

 构建过程

如前所述,有一种更好的方法,那就是扩展我们build.rs脚本。使用构建脚本而不是手动方法的原因归结为:

  • ·如果我们更新库,它会自动更新绑定。
  • ·我们可以调整脚本以适应不同的平台(OS)。

还有其他原因,但我们只能说,就目前而言,这是一个更可持续的方法,并继续与节目。

Bindgen

让我们首先修改build.rs脚本,如下所示:

use std::env;
use std::path::PathBuf;
use std::process::Command;

fn main() {
    println!("cargo:rustc-link-search=native=/usr/lib");
    println!("cargo:rustc-link-lib=dylib=uring");
    println!("cargo:rerun-if-changed=wrapper.h");

    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());

    // Generate bindings using command-line bindgen
    let bindgen_output = Command::new("bindgen")
        .arg("--experimental")
        .arg("--wrap-static-fns")
        .arg("wrapper.h")
        .arg("--output")
        .arg(out_path.join("bindings.rs"))
        .output()
        .expect("Failed to generate bindings");

    if !bindgen_output.status.success() {
        panic!(
            "Could not generate bindings:\n{}",
            String::from_utf8_lossy(&bindgen_output.stderr)
        );
    }
}

虽然前两行是熟悉的,但我们添加了相当多的内容,但如果你仔细观察,你会发现除了out_path之外,bindgen语句正在做我们之前在命令行中做的事情。让我们看看不熟悉的部分。

  • 首先,有out_path,这是一个由cargo在编译时创建的环境变量。它告诉我们构建的位置,我们将其用于bindings.rs
  • ·第二,Command语句运行我们的bindgen语句,就像它在命令行一样,然后如果失败就会抛出异常。
  • ·第三,如果语句返回为不成功,我们会吐出一条错误消息,沿着从Command返回的Output对象中提供的任何错误。

不过,这并不是100%有效,因为那些内联函数仍然会给我们带来困难。我们在这里所做的就是采用命令行版本并在构建脚本中展开它。

 内联函数

这就是事情变得有点困难的地方,好像他们还没有达到这一点。当我们运行**-wrap-static-fns时,它将为我们创建内联函数的包装器,但它们是用C完成的,并输出到extern.c**文件。实际上,如果您自己在 /tmp/bindgen中找到此文件并将其删除,则可以使用前面部分中的命令重新生成它:

bindgen wrapper.h --experimental --wrap-static-fns --output bindings.rs

由于我们不能在程序中直接使用extern.c,我们需要编译它,以便我们可以链接到对象(so)文件。最简单的方法是将cc crate放入crate中,然后执行以下操作:

    cc::Build::new()
        .file(&extern_c_path)
        .include("/usr/include")
        .include(".") // To find wrapper.h in current directory
        .compile("extern");

但是,这样做会破坏我们毫发无损的cargo.toml,所以我们将以一种更复杂的方式使用gcc自己来做:

use std::env;
use std::path::PathBuf;
use std::process::Command;

fn main() {
    println!("cargo:rustc-link-search=native=/usr/lib");
    println!("cargo:rustc-link-lib=dylib=uring");
    println!("cargo:rerun-if-changed=wrapper.h");

    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    let extern_c_path = env::temp_dir().join("bindgen").join("extern.c");

    // … previous bindgen code goes here

    // Compile the generated wrappers
    let gcc_output = Command::new("gcc")
        .arg("-c")
        .arg("-fPIC")
        .arg("-I/usr/include")
        .arg("-I.")
        .arg(&extern_c_path)
        .arg("-o")
        .arg(out_path.join("extern.o"))
        .output()
        .expect("Failed to compile C code");

    if !gcc_output.status.success() {
        panic!(
            "Failed to compile C code:\n{}",
            String::from_utf8_lossy(&gcc_output.stderr)
        );
    }

    // Create a static library for the wrappers
    let ar_output = Command::new("ar")
        .arg("crus")
        .arg(out_path.join("libextern.a"))
        .arg(out_path.join("extern.o"))
        .output()
        .expect("Failed to create static library");

    if !ar_output.status.success() {
        panic!(
            "Failed to create static library:\n{}",
            String::from_utf8_lossy(&ar_output.stderr)
        );
    }

    // Tell cargo where to find the new library
    println!("cargo:rustc-link-search=native={}", out_path.display());
    println!("cargo:rustc-link-lib=static=extern");
}

 这就是我们正在做的:

  • ·定义extern.c文件的路径。
  • ·Bindgen运行并创建绑定。
  • ·包装的内联函数放在 **extern.c ** 中。
  • ·我们编译包装器。
  • ·从目标文件创建静态库。
  • ·告诉Rust在哪里可以找到库。

我相信步骤(1)很容易理解,我们已经完成了上面的(2),所以我们将从(4)开始,并将选项传递给编译器:

  • ·-c:编译为对象。
  • ·-fPIC:参见链接。
  • -I/usr/include:在指定路径中查找头文件。
  • ·-I:在当前位置查找头。

剩下的代码处理我们的输出路径,这应该很容易理解。现在,我并不完全确定fPIC的用途,但我觉得它用于动态链接之外并不重要。

在第二部分中,我们创建静态库,我们使用ar,这是一种用于库的归档工具。我们传递给它一个命令,它实际上是四个命令:

  • ·c:创建。
  • ·r:插入替换。
  • ·u:只插入更多当前文件。使用R
  • ·s:将目标文件写入存档。

您可以在这里阅读这些命令的细微差别,但总结一下,它正在构建一个存档( .a),然后是一个静态链接的对象文件( .o)。此时,您应该能够无错误地编译。

 基本日志设置(_O)

现在我们已经创建了绑定,我们将做一个快速而粗略的演示,在下一篇文章中,我们将构建一些功能更全的东西。首先,您需要将以下内容添加到您main.rs

#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]

#[cfg(not(rust_analyzer))]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

use std::io;
use std::mem::zeroed;
use std::ptr::null_mut;

初始宏将忽略我们不关心的错误。后面的两行有点奇怪。它们所做的是确保我们连接到我们的绑定,你可以把它想象成一个pub mod语句,但是要进入build文件夹而不是src来查找文件。在本例中,它使用了前面提到的OUT_EXPERT环境变量来查找bindings.rs文件。这两行中的第一行--include-line上面的宏--只是一种关闭可能出现的错误的方法,这取决于您的编辑器。如果没有,它仍然可以编译得很好,但是你可能会在编辑器中看到一个错误。

如果你没有错误,那么我们已经成功地创建了io_uring的绑定,并准备创建基本的示例,但在此之前,我们应该看看io_uring实际上是什么,以及为什么我们需要它。

 好吧,什么事?

此时,您可能已经意识到,我们一直在忙碌安装库,以至于我们从未越过基本的学习来理解API是如何工作的。我建议你阅读这篇文章和这篇文章,以获得更深入的理解。现在,我将从第二个链接中借用这个图表,以便给出快速描述:

Rust通过绑定使用 io_uring本文翻译自 https://www.thespatula.io/rust/rust

提交和完成队列(从这里重新创建)

使用io_uring, 你有一个提交和完成队列,这类似于epoll的兴趣就绪列表,但有一个主要的区别。使用epoll时,列表由内核在内核空间中维护,而队列则位于使用io_uring的共享内存空间中。这个模型的好处是您不需要进行那么多的系统调用,特别是在更新队列中的项目状态等时。这意味着总体上上下文切换较少。

io_uring的过程基本相同,至少在概念上是如此。您可以向提交队列中添加提交队列条目或SQE,并从完成队列中获取已完成的工作,就像在epoll中添加和检索项一样。通常,这些条目将涉及等待I/O,无论是文件读/写还是网络流量,尽管您也可以将其用于其他事情,为了避免已经很长的文章变得太长,我不会在这里讨论。

添加条目后,您可以选择是使用io_uring_wait_cqe阻塞并等待完成队列条目(CQE),使用超时等待,还是使用io_uring_peek_cqe轮询。无论你做什么,你最终都会得到一个超时的响应,并继续添加和获取项目。

这是一个关于正在发生的事情的手波浪式描述,还有更多的内容,我们将在以后的文章中看到,但现在上面的图像提供了一个很好的可视化。

 回到我们的代码

对于最后一点,我们将使用绑定创建一个简单的队列,并向其抛出一些东西,然后返回一些东西。我们将执行以下操作:

  • ·设置日志并创建队列。
  • ·提交noop(无操作)。
  • ·等待它完成。
  • ·退出队列。

让我们先来看看我们的main函数的流程:

fn main() -> io::Result<()> {
    let queue_depthu32 = 1;
    let mut ring = setup_io_uring(queue_depth)?;

    println!("Submitting NOP operation");
    submit_noop(&mut ring)?;

    println!("Waiting for completion");
    wait_for_completion(&mut ring)?;

    unsafe { io_uring_queue_exit(&mut ring) };

    Ok(())
}

main.rs中,我们首先设置了这个队列:

    let queue_depth: u32 = 1;
    let mut ring = setup_io_uring(queue_depth)?;

查看函数setup_io_uring,我们有以下内容:

fn setup_io_uring(queue_depth: u32-> io::Result<io_uring> {
    let mut ring: io_uring = unsafe { zeroed() };
    let ret = unsafe { io_uring_queue_init(queue_depth, &mut ring, 0) };
    if ret < 0 {
        return Err(io::Error::last_os_error());
    }

    Ok(ring)
}

我们获取队列的深度,在main中设置为1,然后返回一个io_uring类型。这个类型来自我们的绑定,看起来像这样:

pub struct io_uring {
    pub sq: io_uring_sq,
    pub cq: io_uring_cq,
    pub flags: ::std::os::raw::c_uint,
    pub ring_fd: ::std::os::raw::c_int,
    pub features: ::std::os::raw::c_uint,
    pub enter_ring_fd: ::std::os::raw::c_int,
    pub int_flags: __u8,
    pub pad: [__u83usize],
    pub pad2: ::std::os::raw::c_uint,
}

这是通过io_uring_setup函数填充的,该函数从io_uring_queue_init中调用。你可以在这两个文件夹中找到。这将把提交队列(sq)的大小设置为1,然后自动把完成队列(cq)设置为两倍,即2。完成队列的大小需要大于提交队列的大小。还要注意,我们已经将参数清零,将它们设置为默认值。

我们将在整个过程中使用不安全的包装器,这是因为我们正在呼吁非Rust世界,并且无法保证。在这种情况下,我们必须信任库的作者。

回到main,我们有一个环变量,它实际上是由两个环组成的(提交和完成),我们将把它传递给submit_noop,它执行以下操作:

fn submit_noop(ring: &mut io_uring) -> io::Result<()> {
    unsafe {
        let sqe = io_uring_get_sqe(ring);
        if sqe.is_null() {
            return Err(io::Error::new(io::ErrorKind::Other, "Failed to get SQE"));
        }

        io_uring_prep_nop(sqe);
        (*sqe).user_data = 0x88;

        let ret = io_uring_submit(ring);
        if ret < 0 {
            return Err(io::Error::last_os_error());
        }
    }

    Ok(())
}

现在的情况是:

  1. 1. 从初始化的环中获取提交队列。
    1. 检查它是否存在(非空)。
  2. 3. 使用io_uring_prep_nop创建一个noop。
  3. 4. 将一些数据附加到它的一个字段(user_data)。
    1. 提交到我们的队列。
    1. 检查提交失败。

除了io_uring_prep_nop之外,还有大量的其他准备函数,例如从文件或TCP连接中进行阅读和写入。我建议快速搜索一下,看看有什么可用的。我们将在后面的文章中使用其中的一些。

我们要做的最后一件事是等待NOOP完成:

fn wait_for_completion(ring: &mut io_uring) -> io::Result<()> {
    let mut cqe: *mut io_uring_cqe = null_mut();
    let ret = unsafe { io_uring_wait_cqe(ring, &mut cqe) };

    if ret < 0 {
        return Err(io::Error::last_os_error());
    }

    unsafe {
        println!("NOP completed with result: {}", (*cqe).res);
        println!("User data: 0x{:x}", (*cqe).user_data);
        io_uring_cqe_seen(ring, cqe);
    }

    Ok(())
}

这里我们创建一个指向io_uring_cqe的不可变指针,它是一个结构体,看起来像这样:

pub struct io_uring_cqe {
    pub user_data: __u64,
    pub res: __s32,
    pub flags: __u32,
    pub big_cqe: __IncompleteArrayField<__u64>,
}

这是一个完成队列条目,我们将把它传递给io_uring_wait_cqe,在完成之前的提交之后,它将把完成数据存储在指针指示的位置。

现在,我们只关心user_data字段,在检查io_uring_wait_cqe的返回值没有指示失败(即,它实际上是指向某个东西)。

这里发生的最后一件事是我们调用io_uring_cqe_seen来通知io_uring我们已经看到了这个完成。如果您查看文档,则必须完成此操作,否则完成队列的槽将无法重用。

最后,我们返回main,然后退出队列:

unsafe { io_uring_queue_exit(&mut ring) };

此功能将释放资源并将您从戒指管理的负担中解放出来。

 结论

就这样,只需几千字加上一两页代码,就可以创建自己的liburing绑定和io_uring队列。这一点,至少从我在下一节所做的工作来看,是比较容易的部分。

我们离一个开源服务器还有很长的路要走,但我们正在接近!

转载自:https://juejin.cn/post/7403659734258270219
评论
请登录