#io-uring #io #networking #uring

rio

GPL-3.0对io_uring的优雅绑定。对于spacejam的GitHub赞助者,可提供MIT/Apache-2.0许可。

25个版本 (6个重大更新)

0.9.4 2020年8月21日
0.9.3 2020年5月6日
0.9.2 2020年1月30日
0.2.0 2018年5月4日

⚠️ 报告了问题

#132异步

Download history 3216/week @ 2024-03-14 3556/week @ 2024-03-21 3742/week @ 2024-03-28 4043/week @ 2024-04-04 2920/week @ 2024-04-11 3142/week @ 2024-04-18 4292/week @ 2024-04-25 3230/week @ 2024-05-02 3865/week @ 2024-05-09 3457/week @ 2024-05-16 2547/week @ 2024-05-23 4561/week @ 2024-05-30 5954/week @ 2024-06-06 2810/week @ 2024-06-13 3391/week @ 2024-06-20 3656/week @ 2024-06-27

16,933 每月下载量
10 个Crate中 使用(7个直接使用)

GPL-3.0 许可证

90KB
2K SLoC

rio

io_uring的绑定,这是Linux IO很长时间以来的热门技术。

稳定性状态

rio旨在利用Rust的编译时检查,与其它语言中的io_uring接口相比具有抗误用的特性,但用户在使用rio时应注意,即使在没有unsafe的情况下,仍然可能出现use-after-free错误。 Completion 会借用请求中涉及的缓冲区,并且其析构函数会阻塞,以延迟释放这些缓冲区直到相应的请求完成;但在Rust中,这被认为是安全的,因为对象的生存期和借用在析构函数运行之前结束,这可以通过多种方式发生,包括通过 std::mem::forget。请小心不要以这种方式泄漏完成,如果您认为Rust的稳定性保证很重要,可能想避免这个Crate。

创新

  • 只依赖于libc,不需要c/bindgen来复杂化问题,没有人想要那样
  • 完成操作与线程或异步运行时(Completion 实现 Future)配合得很好
  • 使用Rust标记特质来确保缓冲区只有在是可写内存时才会写入。(防止您尝试将数据写入静态只读内存)
  • 不需要直接与IoSlice / libc::iovec打交道。rio会为您在后台维护这些。
  • 如果io_uring被放任自流,它将允许你提交比完成队列实际能容纳更多的I/O操作,这可能导致完成操作被丢弃,并导致任何等待完成的用户空间事物的泄漏。当正在进行的请求数量达到这个阈值时,rio会对提交者施加反压力,以确保不会因为完成队列溢出而丢弃任何完成操作。
  • rio将自动处理提交队列的提交。如果你开始等待一个Completion,rio将确保我们已经向内核提交了至少这个请求。其他io_uring库强迫你手动处理,这是误用的另一个可能来源。

这将是sled写路径的核心。它是针对特定的高级应用构建的:一个高性能存储引擎和复制系统。

什么是io_uring?

io_uring是Linux内核很长时间来发生的最大事件。它将改变一切。现在使用epoll的任何东西都将是io_uring的改写,如果它想要保持相关性。它最初是为了在没有使用O_DIRECT的情况下执行真正的异步磁盘I/O而开始的,但它的范围已经扩大,并且随着时间的推移,它将因其能够批量处理大量不同的syscalls而继续支持越来越多的内核功能。在5.5内核中,添加了对更多网络操作的支持,如accept(2)sendmsg(2)recvmsg(2)。在5.6内核中,正在添加对recv(2)send(2)的支持。io_uring已被测量出在基于epoll的网络中具有显著的性能优势,在更重的负载下,io_uring的表现优于基于epoll的设置。我开始开发rio,以便能够尽早深入理解这个令人惊叹的新接口,以便我能够尽快并负责任地使用sled

io_uring解锁以下内核功能

  • 为越来越多的syscalls提供完全异步接口
  • 不需要使用O_DIRECT即可执行异步磁盘I/O,就像你使用AIO时必须做的那样
  • 将数百个磁盘和网络I/O操作批量到一个单一的syscall中,这在meltdown/spectre世界之后特别美妙,因为我们的syscalls已经显著减速
  • 如果配置在SQPOLL模式下,可以进行0-syscall I/O操作提交
  • 可配置的完成轮询,以交换CPU和低延迟
  • 允许表达复杂的0复制广播语义,类似于splice(2)或sendfile(2),但可以在不将内存和映射弹跳到用户空间的情况下与许多类似文件的对象一起工作。
  • 允许注册I/O缓冲区和文件描述符以进行廉价的重用(在内核中使用缓冲区和文件描述符的重映射具有显著的成本)。

要了解更多关于io_uring的信息,请参阅

要查看一些有趣的io_uring性能结果的幻灯片,请参阅Jens的演示文稿的第43-53页。

为什么不使用那些其他Rust io_uring库?

  • 他们还没有复制 rio 的特性,但您基本上仍然必须使用它来负责任地使用 io_uring,因为该API的边缘非常锋利。截至2020年1月13日,我所看到的所有库都很容易使完成队列溢出,以及容易表达使用后释放的问题,看起来并不适合异步操作等...

将在未来一天或两天内出现问题的示例

异步TCP回显服务器

use std::{
    io::self,
    net::{TcpListener, TcpStream},
};

async fn proxy(ring: &rio::Rio, a: &TcpStream, b: &TcpStream) -> io::Result<()> {
    let buf = vec![0_u8; 512];
    loop {
        let read_bytes = ring.read_at(a, &buf, 0).await?;
        let buf = &buf[..read_bytes];
        ring.write_at(b, &buf, 0).await?;
    }
}

fn main() -> io::Result<()> {
    let ring = rio::new()?;
    let acceptor = TcpListener::bind("127.0.0.1:6666")?;

    extreme::run(async {
        // kernel 5.5 and later support TCP accept
        loop {
            let stream = ring.accept(&acceptor).await?;
            dbg!(proxy(&ring, &stream, &stream).await);
        }
    })
}

文件读取

let ring = rio::new().expect("create uring");
let file = std::fs::open("file").expect("openat");
let data: &mut [u8] = &mut [0; 66];
let completion = ring.read_at(&file, &mut data, at);

// if using threads
completion.wait()?;

// if using async
completion.await?

文件写入

let ring = rio::new().expect("create uring");
let file = std::fs::create("file").expect("openat");
let to_write: &[u8] = &[6; 66];
let completion = ring.write_at(&file, &to_write, at);

// if using threads
completion.wait()?;

// if using async
completion.await?

快速的O_DIRECT操作(在家试试这个/运行o_direct示例)

use std::{
    fs::OpenOptions, io::Result,
    os::unix::fs::OpenOptionsExt,
};

const CHUNK_SIZE: u64 = 4096 * 256;

// `O_DIRECT` requires all reads and writes
// to be aligned to the block device's block
// size. 4096 might not be the best, or even
// a valid one, for yours!
#[repr(align(4096))]
struct Aligned([u8; CHUNK_SIZE as usize]);

fn main() -> Result<()> {
    // start the ring
    let ring = rio::new()?;

    // open output file, with `O_DIRECT` set
    let file = OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .truncate(true)
        .custom_flags(libc::O_DIRECT)
        .open("file")?;

    let out_buf = Aligned([42; CHUNK_SIZE as usize]);
    let out_slice: &[u8] = &out_buf.0;

    let in_buf = Aligned([42; CHUNK_SIZE as usize]);
    let in_slice: &[u8] = &in_buf.0;

    let mut completions = vec![];

    for i in 0..(10 * 1024) {
        let at = i * CHUNK_SIZE;

        // By setting the `Link` order,
        // we specify that the following
        // read should happen after this
        // write.
        let write = ring.write_at_ordered(
            &file,
            &out_slice,
            at,
            rio::Ordering::Link,
        );
        completions.push(write);

        let read = ring.read_at(&file, &in_slice, at);
        completions.push(read);
    }

    for completion in completions.into_iter() {
        completion.wait()?;
    }

    Ok(())
}

依赖项