1

io_uring 阅读笔记

 2 years ago
source link: https://blog.kuangjux.top/2021/10/30/io-uring%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
KuangjuX(狂且)

io_uring 阅读笔记

Created2021-10-30|Updated2021-10-30|技术
Post View:2

io_uring 简述

对于 io_uring 的异步请求有两个重要的操作:提交请求、完成所提交的请求。

对于 IO 事件的提交,应用程序是生产者而内核是消费者;而对于 完成实践来说,内核是生产者而应用程序是消费者。因此我们需要需要一对环(rings) 提供高性能的 channel 用于在内核和应用程序中的通信,这对环就是新的接口的核心: io_uring,它们被命名为 submission queue(SQ), completion queue(CQ),这两个数据结构构造了新接口的基础。

io_uring 的数据结构

首先我们看一下 (completion queue event)CQE 的数据结构定义:

struct io_uring_cqe {
__u64 user_data;
__s32 res;
__u32 flags;
}

首先 io_uring_cqe 有一个 user_data 的域,这个域是被最初的提交的请求时就被携带的,能够携带任何用来表明这是哪个请求的信息,最基础的使用就是使用原始请求的指针,内核将不会修改这个域,它仅仅直接从提交事件中转移到完成事件中。res 指向本次提交事件所返回的结果,就像系统调用返回的结果一样。flags 域将携带依赖于该操作的元数据,但是现在这个域还没有被使用。

对于 submission queue event(SQE) 来说结构定义则更为复杂:

struct io_uring_sqe {
   __u8 opcode;
   __u8 flags;
   __u16 ioprio;
   __s32 fd;
   __u64 off;
   __u64 addr;
   __u32 len;
   union {
   __kernel_rwf_t rw_flags;
   __u32 fsync_flags;
   __u16 poll_events;
__u32 sync_range_flags;
__u32 msg_flags;   
   };
   __u64 user_data;
   union {
   __u16 buf_index;
   __u64 __pad2[3];
   };
};

opcode 域用来描述操作码对于提交的请求,例如对于一个读请求来说则是 IORING_OP_READVflags 包含在命令类型中通用的修饰符标志。ioprio 则用来表示该请求的优先级,对于普通的读写请求来说,将遵循 ioprio_set 系统调用的定义。fd 是与该请求相关的文件描述符,off 表明该操作开始执行的偏移量,addr 包含了内核开始IO操作的地址。对于 non-vectored IO 传输,addr 必须直接包含地址。如果是 non-vectored IO 的话直接携带 len, 如果是 vectored IO 的话则携带 a number of vector(被 addr 所描述)。

接下来是一个 union 用来描述特定的 opcode 的。举例来说,对于 vectored read (IORING_OP_READV),这些标志位这些标志位应当和 preadv2(2) 系统调用的标志位相同。 user_data 是由用户传输进来且不会被内核访问与修改。buf_index 将在高级使用用例中进行描述,最后的 pad 用来做数据结构的填充,是被用于作为64位对齐使用。

(注:vectored IO 是一种 IO 通过一个单生产者顺序地从多个 buffer 中读取数据并写入到一个数据流中;或者从一个 buffer 中读取数据并写入到多个数据流中)

io_uring 通信

在明白了 io_uring 的数据结构后,让我们看看 io_uring 工作的细节。

CQEs 被一个数组来组织,该数组的内存对于内核和应用程序来说都是可见的并且是可修改的。但是,由于 CQE 是由内核生成的,因此实际上只有内核在修改 CQE。通信的方法使用一个 ring buffer 来管理。每当内核将新事件发布到 CQ ring 时,它都会更新与其关联的 tail。当应用程序使用一个 entry 时,它会更新 head。因此,如果 tail 与 head 不同,则应用程序知道它有一个或多个可供消费的事件。环形计数器本身是自由流动的 32 位整数,当完成的事件数量超过环的容量时,依靠自然包装。这种方法的一个优点是我们可以利用环的全尺寸,而不必在侧面管理“环已满”标志,这会使环的管理变得复杂。随之而来的是,环的大小必须是 2 的幂。

为了找到一个事件的索引,应用程序必须给当前的 tail 索引加上一个掩码,如下所示:

  unsigned head;
  head = cqring→head;
read_barrier();
  if (head != cqring→tail) {
struct io_uring_cqe *cqe;
unsigned index;
index = head & (cqring→mask);
cqe = &cqring→cqes[index];
/* process completed cqe here */
...
/* we've now consumed this entry */
head++;
  }
cqring→head = head;
  write_barrier();

ring->cqes[]io_uring_cqe 结构的共享数组。在之后,我们将会介绍共享内存是如何进行启动和管理的。

对于提交事件这一端规则仍然被保留。应用程序更新 tail 同时内核消费 head,一个重要的不同点是 CQ ring 直接索引 CQEs 的共享内存,提交端在它们中有一个 indirection 的 array,因此提交端的 ring buffer 是通过 index 直接访问 array。

一个例子如下所示:

struct io_uring_sqe *sqe;
unsigned tail, index;
tail = sqring→tail;
index = tail & (*sqring→ring_mask);
sqe = &sqring→sqes[index];
/* this call fills in the sqe entries for this IO */
init_io(sqe);
/* fill the sqe index into the SQ ring array */
sqring→array[index] = index;
tail++;
write_barrier();
sqring→tail = tail;
write_barrier();

完成事件可能以任何顺序达到,请求的顺序和完成的顺序没有任何联系,SQ ring 和 CQ ring 独立地运行。然而,一个完成的事件将总是与一个请求的事件相适配。因此,一个完成的事件蒋宗辉he一个特定的提交请求相联系。

io_uring 接口

Efficient IO with io_uring

Vectored I/O


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK