> 写完 [Go 与异步 IO - io_uring 的思考](http://icebergu.com/archives/go-iouring#submissionqueueentry) 后还是决定将学习 `io_uring` 时粗略翻译的两篇文章发出来
> 尽量可以帮助到对 `io_uring` 感兴趣的朋友
> 由于英文水平有限,可能有些地方有一些问题,遇到语句不通顺的地方(记得联系我),可以结合原文对照查看
> 英文原文:
> [Efficient IO with io_uring](http://kernel.dk/io_uring.pdf)
> [What’s new with io_uring](https://kernel.dk/io_uring-whatsnew.pdf)
> 本文并没有将两篇文章内容进行整合,可以跳转至 [What's new with io_uring](http://icebergu.com/archives/linux-iouring#whats-new-with-io-uring)
>
> ---
> 推荐阅读:
> [liburing](https://git.kernel.dk/cgit/liburing/)
> [Lord of the io_uring](https://unixism.net/loti/index.html)
本文的目的是介绍最新的 Linux 异步 IO 接口 `io_uring` ,并将其与现有产品进行比较。
我们将探讨其存在的原因,它的内部工作原理以及开放给用户的接口。
本文不会讨论特定命令之类的细节,这些都可以查看相关 [man](https://github.com/axboe/liburing/tree/master/man) 文档或者 [lord of the io_uring](https://unixism.net/loti/),我们会介绍 `io_uring` 及其工作原理,希望读者可以更深刻的理解。
本文和 [man](https://github.com/axboe/liburing/tree/master/man) 之间会有一些重叠,如果不提供这些细节就无法提供对 `io_uring` 的描述
# 介绍
Linux 中有很多方法可以执行基于文件的 IO,最古老和基本的是 [`read`](https://www.man7.org/linux/man-pages/man2/read.2.html) 和 [`write`](https://www.man7.org/linux/man-pages/man2/write.2.html) 系统调用。后来又添加了允许传入偏移量的 [`pread`](https://man7.org/linux/man-pages/man2/pread.2.html) 和 [`pwrite`](https://www.man7.org/linux/man-pages/man2/pwrite.2.html),然后又引入了他们的矢量版本 [`preadv`](https://man7.org/linux/man-pages/man2/preadv.2.html) 和 [`pwritev`](https://man7.org/linux/man-pages/man2/pwritev.2.html),但是这依然无法满足,所以进一步扩展,出现了允许修饰符标志的系统调用 [`preadv2`](https://man7.org/linux/man-pages/man2/preadv2.2.html) 和 [`pwritev2`](https://man7.org/linux/man-pages/man2/pwritev2.2.html)。
这些系统调用存在一些差异,但是他们相同的特征是都是同步接口。这意味着只有当数据准备就绪或者写入完成了,这些系统调用才会返回。在一些使用场景中需要使用异步接口。POSIX 提供了 [`aio_read`](https://man7.org/linux/man-pages/man3/aio_read.3.html) 和 [`aio_write`](https://www.man7.org/linux/man-pages/man3/aio_write.3.html) 来满足异步需求,但是,他们的实现通常都乏善可陈,性能也很差。
linux 有一个原生的异步 IO 接口,简称 `AIO`,但是它收到了许多的限制:
* 最大的限制是它仅支持 `O_DIRECT`(无缓冲)访问,由于`O_DIRECT`(绕过缓冲和大小/对齐限制)的限制,导致原生 `AIO` 接口在大多数情况下都不可行,对于正常(缓冲) IO 来说,接口依然以同步的方式运行
* 即使满足了 IO 异步的所有限制,有时它依然不是异步的。有很多方式会导致 IO 提交时被阻塞,如果需要元数据来执行 IO,那么提交就会被阻塞等待。对于存储设备,有固定数量的可用请求槽,如果这些插槽都在使用中,提交将阻塞等待一个可用的。这些不确定性意味着依赖于异步提交的程序依然被迫阻塞
* API 不好,每个 IO 提交需要复制 64 + 8 个字节,每次 IO 完成复制 32 个字节。而对于一些不需要内存拷贝的 IO 来说,依然会带来 104 字节的内存拷贝。根据 IO 的大小,带来的损耗可能会很明显。公开的完成事件缓冲区导致 IO 完成变慢,并且在应用中很难使用。而且 IO 总是需要两次系统调用才能完成提交和等待完成,而且在内核修复了 Intel 漏洞([spectre](https://en.wikipedia.org/wiki/Spectre_(security_vulnerability))/[meltdown](https://en.wikipedia.org/wiki/Meltdown_(security_vulnerability)))后,系统调用带来的代价更大了
多年来,人们为了消除上述第一个限制做出了各种努力,但是依然没有成功。就效率而言,支持 10 毫秒以下延迟和非常高 IOPS 的设备的出现,`AIO`接口开始显得有些力不从心 了,对于这些设备来说,缓慢和不确定的提交延迟是一个很大的问题,因为无法从单个核心中获取足够的性能。最重要的是由于上述的限制,可以肯定的说原生的 Linux `AIO` 无法在很多场景下使用。他被丢到了应用的角落,同样也伴随着所有随之而来的问题(长期未发现的 bug 等等)
此外,**普通应用不使用 `AIO` 也意味着 Linux 仍然没有提供给他们想要的功能。绝对没有理由让应用或者库去使用私有的 IO 线程池来模仿异步 IO, 特别是当这些事情可以在内核中更加高效的完成**
# 改善现状
最初的努力集中在改进 `AIO` 接口上,并且进行了相当长的时间,选择这个最初的方向有很多个原因
* 如果可以扩展和改进现有的接口,肯定要比提供一个新的接口更好,采用新的接口需要花费时间,并且审核和批准新接口是一项漫长而艰巨的任务
* 一般来说,工作量会少很多,作为开发人员,总是希望用最少的代价完成最多的工作,扩展现有接口在已有的测试基础架构上会带来很多优势
现有的 `AIO` 接口主要有三个系统调用
* [`io_setup`](https://man7.org/linux/man-pages/man2/io_setup.2.html) 用于设置 aio 上下文
* [`io_submit`](https://man7.org/linux/man-pages/man2/io_submit.2.html) 提交 IO
* [`io_getevents`](https://man7.org/linux/man-pages/man2/io_getevents.2.html) 获取或者等待 IO 完成
由于需要对多个系统调用的行为进行修改,所以我们需要添加新的系统调用来传递这些信息。这样就为相同的代码创建了多个入口点,并在其他地方新建快捷接口。最终在代码的复杂性和可维护性上来说结果并不好,而且只是修复了原有 `AIO` 的一个比较突出问题而已。最重要的事,他实际上使另外的问题变得更糟了,因为现有 API 会变的更加复杂,难以理解和使用
放弃一项工作,然后从头开始总是很难的,不过很明显我们需要一个全新的东西,能够提供我们所需要的东西,需要他具有高性能和可扩展性,而且方便使用,并具有现有接口所没有的特性
# 新接口设计目标
尽管从头开始设计不是容易的事情,但确实使我们在创作是有了充分的艺术自由来创造新的东西
按照重要性从高到低的顺序,主要的设计如下:
* **易于使用,难以滥用(Easy to use, hard to misuse)**。任何用户/应用可见的接口都以此为目标,接口应该易于理解和直观实用
* **可扩展的(Extandable)**。虽然我的背景更多的与存储相关,但我希望该接口不仅仅用于面向块的 IO。这意味着 io_uring 很快会添加网络和非块存储接口。
* **功能丰富(Feature rich)**。Linux `AIO` 满足应用需求的子集,我不想再创建一个接口仅覆盖某些应用的需求,或者需要应用自己来一次次创建相同的接口功能(例如 IO 线程池)
* **效率(Efficiency)**。尽管存储 IO 大部分依然是基于块的,因此大小至少为 512b 或者 4 kb,但在这些大小上的效率对于某些应用仍然是至关重要的。此外,某些请求甚至没有携带数据(有效荷载),对于每次请求的开销而言,新接口必须高效,这一点很重要
* **可扩展性(Scalability)**。尽管效率和低延迟非常重要,但是在峰值端提供最佳的性能也很关键。特别是对于存储,我们一直努力提供可扩展的基础架构,一个新的接口能够将这种可扩展性公开给应用
上述某些目标似乎是互斥的。高效和可扩展性的接口通常很难使用,并且更重要的是,很难被正确使用
丰富又高效的功能也很难实现,不过这些就是我们设定的目标
# io_uring
尽管设计目标定的很高,但是最初的设计还是围绕效率进行的
效率不能是以后才想要去做的事情,它必须从一开始就进行设计,一旦接口被固定,将无法再把一些东西剔除掉
无论是操作请求的提交还是完成,我都不想有任何的内存副本和间接的内存访问
之前基于 `AIO` 的设计时,效率和可扩展性都受到了明显的危害
## 协调应用与内核的共享内存
**由于不需要副本,因此内核和程序必须优雅的共享定义 IO 自身的结构和完成的事件。**
如果打算采用这种共享方式,那么拥有共享数据的协调也应该驻留在程序和内核之间的共享内存中
一旦实现了这种方式,就必须以某种方式来管理两者的同步
一个程序在不调用系统调用的情况下无法和内核共享锁定,并且系统调用肯定会降低与内核通信的速度。这与实现效率的目标不符
**一个可以满足我们需求的数据结构应该是单个生产者和单个消费者的环形缓冲区。
使用共享的环形缓冲区,我们就可以消除在应用和内核之间具有共享锁定的需要,而无需一些巧妙的内存顺序和屏障**
与异步接口相关的基本操作有两个:**提交请求的操作**和**请求完成后的完成事件**
* 对于提交 IO 请求,应用是生产者,内核是消费者
* 对于完成请求而言,情况恰恰相反,内核会生成`完成事件`,而应用会使用`完成事件`
因此我们需要一对环来提供程序和内核之间的有效的通信通道
**这对环(ring) 便是新接口 io_uring 的核心,并构成了新接口的基础,他们被适当的命名为 `提交队列(SQ,SubmissionQueue)` 和 `完成队列 (CQ, CompletionQueue)`**
## 数据结构 (Data Structures)
有了适当的通信基础后,就该着手定义用于描述`请求`和`完成事件`的数据结构了
`完成事件`比较直观,他需要携带与操作结果相关的信息,以及以某种方式将该完成链接回请求的来源
对于 io_uring 使用以下结构:
```c
struct io_uring_cqe {
__u64 user_data;
__s32 res;
__u32 flags;
};
```
`_cqe` 的后缀代表着这个结构是`完成队列事件(Completion Queue Event)`,本文其余部分统称为 `cqe`
* `user_data` 字段来自`提交的请求` 并且可以包含程序识别该请求所需的任何信息
一种常见的使用场景是使其成为指向`请求`的指针
内核不会修改这个字段,只是简单的直接从`提交(submission)`传递给 `完成事件(completion event)`
* `res` 保留了请求的结果,可以认为他就像系统调用返回的值,对于正常的读写操作,会类似于 [`read`](https://www.man7.org/linux/man-pages/man2/read.2.html) 和 [`write`](https://www.man7.org/linux/man-pages/man2/write.2.html) 的返回值,对于成功的操作,他会包含传输的字节数,如果出现异常,他会包含一个负的错误值
例如,发生了 IO error,`res` 将会包含 `-EIO`
* `flags` 可以携带此操作相关的元数据,现在这个字段还未使用
`请求(requst)` 的类型定义会更加复杂, 不仅需要描述比 `完成事件` 更多的信息,他还需要考虑到 `io_uring` 的未来对请求类型的扩展
```c
struct io_uring_seq {
__u8 opcode;
__u8 flags;
__u16 ioprio;
__u32 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];
};
};
```
> 该结构已经更新,可以查看 [Submission Queue Entry](https://unixism.net/loti/ref-liburing/sqe.html)
类似 `完成事件`,提交侧的结构被称为`提交队列项/条目(Submission Queue Entry)` 简称 `sqe`
* `opcode` 字段描述了本次请求的`操作码`,表示当前的请求的操作,例如一个矢量读取的操作码 `IORING_OP_READV`
* `flags` 包含了跨命令类型的常见修饰符标志
* `ioprio` 代表请求的优先级,对于正常的读写,这个字段遵循了 [`ioprio_set`](https://man7.org/linux/man-pages/man2/ioprio_set.2.html) 系统调用的定义
* `fd` 与请求相关联的描述符,并且 `off` 保存了操作的偏移量
* `addr` 有很多意义:
如果 `操作码`描述的是传输数据的操作,`addr` 包含了该操作执行相应IO 的地址
如果操作是某种向量读写,`addr` 就是 `preadv`系统调用 所使用的指向 `iovec` 数组结构的指针
对于非向量 IO 传输,`addr`必须直接包含地址
* `len` 表示非向量 IO 传输的字节数或者对于向量 IO 传输,表示 addr 指向的向量个数 (iovecs 数组的长度)
* 下边的 union 是针对特定操作码的 `flags` 集合,例如对于矢量读取(`IORING_OP_READV`), 这些描述符跟随 `preadv2` 系统调用
* `user_data` 可以适用于所有操作码,并且不会被内核修改。当该请求的完成事件发布时(请求完成),复制到完成事件 `cqe` 中
* `__pad2[3]` 的目的是确保 `seq` 在内存中以 64 个字节大小来对齐,**也用与将来需要包含更多数据来描述请求的场景**
## 通讯通道(Communication Channel)
通过数据结构的描述,让我们来详细介绍一下`环(rings)`的工作原理吧
`提交(submission)`和`完成(completion)` 虽然是对称的,但是两者的使用却有些不同
先从结构简单的 `completion ring` 开始
**`cqes` 被组织成一个数组,该数组的内存可以被内核和应用看到和修改
但是由于 `cqes` 是由内核生成的,因此实际上只有内核在修改 cqes 的条目(entries).**
通信由`环形缓冲区(ring buffer)`管理。
**每当内核将新事件发布到 `CQ 环`,他就会更新与之相关的`环尾(ring tail)`, 当程序消费一个条目时,就会更新`环头(ring head)`**
因此,如果环头和环尾不同,应用就知道有一个或多个事件可以消费
环行计数器(ring counter) 是自由流动的 32 位整数,并且当完成事件数超过环的容量时会自然计算环项索引
这种方法的优势之一是我们可以利用环的完整大小,而无需另外管理环已满的标志,因此要求环必须是 2 的幂等
为了找到`完成事件`在数组中的索引,应用必须使用环的大小掩码来标记当前的尾部索引
```c
unsigned head;
head = cqring->head;
read_barrier();
// 头不等于尾,环未满
if (head != cqring->tail) {
struct io_uring_cqe *cqe;
unsigned index;
// 使用掩码来计算出正确的索引位置
index = head & (cqring->mask);
cqe = &cqing->cqes[index];
head++;
}
cqring->had = head;
write_barrier();
```
ring->cqes[] 是 `io_uring_cqe` 结构的共享数组,后续我们深入探讨如何设置和管理此共享内存以及神奇的读写屏障调用
**对于`提交请求`,扮演的角色正好相反。应用添加条目到环尾,内核从环头消耗条目**
有一个重要的区别是,尽管 CQ 环直接索引共享数组 `cqes`,但是`提交请求`在他们直接有一个间接数组,因此`提交`操作的环形缓冲区是此数组的索引,而该数组又包含 `sqes` 的索引
刚开始可能看起来很奇怪而且令人困惑,但是这背后是有原因的
一些应用可能将请求单元嵌到他们的内部数据结构中,这样可以使他们可以灵活的在一次操作中即可提交多个 `sqes`,继而使程序更容易转换成 `io_uring` 的接口
增加一个供内核消费的 `sqe` 差不多和从内核中获取 `cqe` 是相反的操作
```c
struct io_uring_seq *sqe;
unsigned tail, index;
tail = sqring->tail;
index = tail & (*sqring->ring_mask);
sqe = &sqring->specs[index];
init_io(sqe);
sqring->array[index] = index;
tail++
write_barrier();
sqring->tail = tail;
write_barrier();
```
与 `CQ ring` 一样,后续会说明读写屏障
上边简化的例子,假设 `SQ 环`当前为空或者至少有空间可以再添加一个
一旦一个 `sqe` 被内核消费了,应用就可以自由复用该 `sqe` 条目,即使相应的 `sqe` 尚未完全完成
如果内核在消费该条目后确实需要再次访问它,它会一个稳定的副本。为什么会发生这种情况并不重要,重要的是会对应用产生一些副作用。
**通常,应用会要求指定大小的环,并且会假设改大小会直接对应于应用在内核中有多少个等待的请求。但是,由于 `sqe` 生命周期仅是其实际提交,所以应用可能使用比`SQ`环 尺寸更高的等待请求数量**
应用应该尽量不要这么做,因为可能会有 `CQ 环`溢出的风险。
默认情况下,`CQ 环`的大小是 `SQ 环`的两倍。这允许应用程序在管理这个方面时具有一定的灵活性,但并不能完全消除这样做的需要
> 现在内核提供了保证 `CQ 环` 中事件不丢失的能力 [CQ 环大小](http://icebergu.com/archives/linux-iouring#cq-%E7%8E%AF%E5%A4%A7%E5%B0%8F)
**`完成事件`可以随机到达,在请求提交和相应的完成之间没有排序。SQ 环和 CQ 环相互独立运行**
**但是完成事件始终对应给定的提交请求,因此完成时间始终与相应的提交请求相关联**
# io_uring 接口
和 `AIO` 一样,`io_uring` 具有相应的多个系统调用,这些系统调用定义了它们的操作
## io_uring_setup
第一个是设置 `io_uring` 实例的系统调用
```c
int io_uring_setup(unsigned entries, struct io_uring_params *params);
```
应用程序必须提供条目的数量`entries`给 io_uring 实例,并且提供相关的参数 `params`
* `entries` 表示与 io_uring 相关联的 sqe 数量的平方数,他必须是 2 的幂,[1,4096]
* `params` 结构会被内核读取和写入
```c
struct io_uring_params {
__u32 sq_entries;
__u32 cq_entries;
__u32 flags
__u32 sq_thread_cpu;
__u32 sq_thread_idle;
__u32 resv[5];
struct io_sqring_offsets sq_off;
struct io_cqring_offsets cq_off;
}
```
* `sq_entries` 会被内核填充,让应用程序知道这个环支持多少 `sqe` 条目。
* `cq_entries` 会告诉应用程序,`CQ 环`的大小
* `sq_off` 和 `cq_off` 是通过 io_uring 和内核建立基本通信所必须的
成功调用 `io_uring_setup` 后,内核会返回一个指向 io_uring 实例的文件描述符
这时 `sq_off` 和 `cq_off` 便会排上用场。
鉴于 `sqe` 和 `cqe` 结构是内核和应用程序共享的,应用程序需要一种访问这个内存的方法
这会通过`mmap`的方式映射到应用的内存空间,应用程序使用 `sq_off` 来找出各个环成员的偏移量
```c
struct io_sqing_offsets {
__u32 head; // 环头的偏移量
__u32 tail; // 环尾的偏移量
__u32 ring_mask; // 环 mask 值
__u32 ring_entries; // 环的 entries 值
__u32 flags; // 环 flags
__u32 dropped; // 没有提交的 sqe 数量
__u32 array; // sqe 索引数组
__u32 resv1;
__u32 resv2;
}
```
为了访问这块内存,应用程序必须使用 `io_uring 文件描述符`和`SQ ring` 关联的内存偏移量来调用 `mmap`
`io_uring` API 定义了下列 `mmap` 偏移量,以供应用使用
```c
#define IORING_OFF_SQ_RING OULL
#define IORING_OFF_CQ_RING 0x8000000ULL
#define IORING_OFF_SQES 0x10000000ULL
```
* `IORING_OFF_SQ_RING` 用于将 SQ 环映射到应用程序空间
* `IORING_OFF_CQ_RING` 用于 CQ 环
* `IORING_OFF_SQES` 映射 sqes 数组
**对于 `CQ 环`,`cqes 数组`是 `CQ 环`的一部分,而 `SQ 环`记录了 `sqes 数组`的索引值,所以 `sqes 数组`必须应用单独映射进来**
应用程序可以自己定义指向这些变量的结构,比如
```c
// 自己定义的结构
struct app_sq_ring {
unsinged *head;
unsigned *tail;
unsigend *ring_mask;
unsigned *ring_entries;
unsigned *flags;
unsinged *dropped;
unsigned *array;
};
```
一个典型的安装案例:
```c
struct app_sq_ring app_setup_sq_ring(int ring_fd, struct io_uring_param *p){
struct app_sq_ring sqing;
void *ptr;
ptr = mmap(NULL, p->sq_off.array + p->sq_entries * sizeof(__u32), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, ring_fd, IORING_OFF_SQ_RING);
sqring->head = ptr + p->sq_off.head;
sqring->tail = ptr + p->sq_off.tail;
sqring->ring_mask = ptr + p->sq_off.ring_mask;
sqring->ring_entries = ptr + p->sq_off.ring_entries;
sqring->flags = ptr + p->sq_off.flags;
sqring->dropped = ptr + p->sq_off.dropped;
sqring->array = ptr + p->sq_off.array;
return sqring
}
```
使用 `IORING_OFF_CQ_RING` 和 `cq_offset` 可以同样映射 `CQ 环`
最后使用 `IORING_OFF_SQES` 映射 `sqe` 数组
由于这些是可以在应用之间复用的代码,所以 [liburing](https://github.com/axboe/liburing) 库提供了一组帮助函数完成安装和内存映射
完成以上操作后,应用程序就可以通过 `io_uring` 实例进行通信了
## io_uring_enter
应用程序需要一种方式来通知内核,有请求需要处理
```c
int io_uring_enter(unsigned int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t sig);
```
* `fd` 指 `io_ursing_setup` 返回 `io_uring`的文件描述符
* `to_submit` 告诉内核准备消费的提交的`sqe`数量
* `min_complete` 要求内核等待请求完成数量
**只需要一次调用就完成了提交和等待完成,也就是说应用程序可以通过一个系统调用来提交并等待指定数量的请求完成**
* `flags`包含用来修改调用行为的标识符
最重要的一个 `flags`: `IORING_ENTER_GETEVENTS`
```c
#define IORING_ENTER_GETEVENTS (1U << 0)
```
如果 `flags` 设置了 `IORING_ENTER_GETEVENTS`, 那么内核将主动等待 `min_completes` 个`完成事件`
一般如果设置 `min_completes`,并且需要等待完成时,那么就也要设置 `IORING_ENTER_GETEVENTS`
> 需要注意,并发情况下,如果有多个 `IORING_ENTER_GETEVENTS` 在等待,同时满足等待数量条件的话,只有一个会返回,其他的会继续等待
> 超时请求除外,超时请求会唤醒所有的 `IORING_ENTER_GETEVENTS` 等待者
基本上覆盖了 io_uring 的基本 API
`io_uring_setup` 用来根据提供的 size 来创建 io_uring 实例
然后,应用程序可以向 `sqes` 填充并使用 `io_uring_enter` 来提交请求,同时也可以等待完成,或者稍后单独调用 `io_uring_enter` 来等待完成
除非应用想要等待有请求完成,否则也可以去检查 `CQ 环`是否有可用的事件
**内核可用改变 `CQ 环尾`,因此应用程序可以直接使用环中的`完成事件`,而不需要调用 `io_uring_enter` + `IORING_ENTER_GETEVENTS`**
可以通过 `io_uring_enter` [man page](https://github.com/axboe/liburing/tree/master/man) 或者 [lord of the io_uring](https://unixism.net/loti/ref-iouring/io_uring_enter.html) 来查看可用的命令和如何使用
## sqe 排序
通常 `sqe` 会被单独的异步执行,也就是说一个`sqe`的相关执行不会影响环中后续`sqe`的执行和顺序
这使操作具有充分的灵活性,并且能够并行的执行和完成以获得最大效率和性能
**可能需要排序的场景是为了保证数据完整写入。**
一个常见的例子是一系列写操作,然后是调用 fsync/fdatasync
只要我们允许写入以任意顺序完成,我们只关心在所有写入完成后执行数据同步
通常,应用程序可能将其转换为 写-等待 的操作,然后在底层存储确认所有写操作后发出同步
**`io_uring` 支持将 `sqe` 的 `flags` 字段设置 `IOSQE_IO_DRAIN`,然后将 sqe 提交到 io_uring 中,可以保证在所有之前的请求完成前是不会开始执行的**
需要注意,`IOSQE_IO_DRAIN` 相当于添加了一个请求屏障,这会暂停后续请求的执行
根据特定的应用来选择如何使用这个功能, 因为这可能引入比预期更大的执行管道(不会并发执行请求)
**如果这种类型的消耗很常见,那么应用程序应该针对完整性写入使用独立的 `io_uring` 实例,以允许更好的执行其他命令**
## 链式 sqes
虽然 `IOSQE_IO_DRAIN` 包括了完整的流水线屏障,io_uring 还支持更精细的 `sqe` 序列控制
**链式`sqes`提供了一种描述`提交环`中一些 `sqes` 之间的依赖性。其中每个 `sqe` 执行都依赖于前一个 `sqe` 成功完成**
使用`链式 sqes` 可以实现必须按照顺序执行一系列写操作或者是类似的复制操作,比如其中一个文件中读取,然后写入另一个文件,共享两个 `sqes` 的缓冲区
**通过 `sqe->flags` 字段中设置 `IOSQE_IO_LINK` 来使用链式请求功能,设置后,下一个 `sqe` 将不会在前一个 `sqe `执行成功之前启动**
**如果前一个 `sqe` 没有完全完成(执行失败),那么链就会断开,链中的 `sqe` 会被取消,`-ECANCELED` 作为错误码**
**链式请求中,完全完成是指请求完成成功完成。任何错误或者潜在的读写问题都会中断链,请求必须完全完成**
只要在 `flags `字段中设置了 `IO_SQE_LINK`, `sqes 链`会一直继续,直到第一个没有设置 `IO_SQE_LINK` 的 sqe,支持任意长度的链
## 超时命令
尽管 io_uring 支持的大部分命令都与数据相关,例如 `read/write` 这类直接操作或者 `fsync` 这类间接操作,但是 `timeout` 命令却略有不同
**`IORING_OP_TIME` 会按照触发方式在完成环上的生成相应的 `完成事件`,而不是对数据进行操作**
超时命令支持两种不同的触发方式,他们可以一起在单个命令中使用
**一种触发方式是经典`超时`,调用者传递一个具有非零秒/纳秒值的 `timespec`**
为了保持 32 位和 64 位的兼容性,必须使用以下格式
```
struct __kernel_timespec {
int64_t tv_sec;
long long tv_nsec;
}
```
用户空间也应该有一个 `timespec64` 的结构来匹配内核中的描述(__kernel_timespec)
**如果超时触发,`sqe->addr` 字段必须指向该类型的结构,到达指定的时间后超时命令将会完成**
**第二种触发方式对`完成`计数,将`完成计数值(completion count value)`应该填充到 `seq->offset` 字段中,`完成事件`到达指定次数后就会完成超时命令**
**可以在一个超时命令中指定两种触发方式。如果一个请求中有两个超时,那么最先触发的条件将生成超时完成时间**
**发布超时完成事件时,所有完成事件的等待者都会被唤醒,无论他们要求的完成量是否满足**
# 内存排序
**通过 `io_uring` 实例进行安全高效通信的一个重要方面就是正确使用内存排序原语(memory ordering)**
本文并不会介绍各种体系结构的内存排序,如果愿意使用 [liburing](https://github.com/axboe/liburing) 库公开的简化 io_uring API,那么就可以安全的进行通信,可以忽略 该章节
如果对使用原始接口感兴趣,那么了解本章是很重要的
为了简单起见,我们简化为两个简单的内存排序操作,为了保持简短,会简化解释
* `read_barrier()` 确保在进行后续的内存读取之前,先前的写入是可见的
* `write_barrier()` 在之前的写入之后再执行写入
根据不同的体系架构,这两个函数之一或者两个都是无操作(no-ops,没有任何操作)的。
使用 io_uring 时这没关系,重要的是我们在某些体系机构上将需要他们,所以应用开发这需要了解如何做到这一点
需要 `write_barrier()` 来确保写入的顺序
比如应用需要填充一个 `sqe`,并通知内核可以消费,这个可以分成两个阶段来做
1. 首先填充 `sqe` 中的字段,然后将 `sqe` 的索引放到 `SQ` 环型数组中
2. 然后更新 `SQ` 环尾来通知内核有新的条目可以用
在没有任何顺序要求的情况下,处理器完全可以按照他认为的最优顺序来重新排序这些写操作
可以看一下下边的例子,每一个数字都代表一个内存操作
```c
1: sqe-opcode = IORING_OP_READV;
2: sqe->fd = fd;
3: sqe->off = 0;
4: sqe->addr = &iovec;
5: sqe->len = 1;
6: sqe->user_data = some_value;
7: sqring->tail = sqring->tail + 1
```
*无法保证写入操作 7(`更新环尾使 sqe 内核可见`)会在最后执行写入**
重要的是,在 7 之前的写入操作都要在 7 之前可见,否则内核可能看到写入一半的 `sqe`
**从应用程序的角度来看,通知内核新的 `sqe` 可用前,需要使用写屏障来保证正确的顺序**
由于实际的 `sqe` 字段的以任何顺序写入都没有关系,只要他们在环尾更新前写入完成就行
这样写入顺序就会变成下边这样
```c
1: sqe-opcode = IORING_OP_READV;
2: sqe->fd = fd;
3: sqe->off = 0;
4: sqe->addr = &iovec;
5: sqe->len = 1;
6: sqe->user_data = some_value;
write_barrier() // 确保更新环尾前,之前的操作都已经写入
7: sqring->tail = sqring->tail + 1
write_barrier() // 确保队尾更新成功
```
**内核在读取 `SQ 环尾`前,也会使用 `read_barrier()`,来保证可以读取应用程序对环尾的更新**
**从 `CQ 环`来看,由于`消费者`/`生产者`是相反的,因此应用只需要在读取 `CQ 环尾`前执行一次 `read_barrier()`,来确保 看到内核的任何写操作**
尽管内存排序类型已经被简短成两种特定类型了(读屏障和写屏障),但是架构的实现还是会有所不同,具体取决于正在运行代码的机器
事实上应用直接使用 `io_uring` 而不是 [liburing](https://github.com/axboe/liburing) 帮助函数,依然需要体系架构特定的屏障类型
[liburing](https://github.com/axboe/liburing) 库中提供这些屏障函数
# liburing
了解了 `io_uring` 的内部细节后,现在可以学习一种更简单的方式来完成上边的大部分操作了
[liburing](https://github.com/axboe/liburing) 有两个目的
* 为基本的使用场景提供了简化的 api
* 不需要用重复代码来创建 `io_uring` 实例
简化的 api 确保了应用程序不需要担心内存屏障,也不需要自己去管理环形缓冲区。这使 API 更易于理解和使用,并且不需要去了解内部工作细节
如果只是提供 [liburing](https://github.com/axboe/liburing) 的实例,那么本文会短很多,但应该了解一些内部工作原理,这样可以让应用获得更大的性能
[liburing](https://github.com/axboe/liburing) 当前的目的是较少重复代码,并为标准场景提供基本的帮助,暂时还无法通过 [liburing](https://github.com/axboe/liburing) 暂时还没有提供一些更高级的功能
使用 [liburing](https://github.com/axboe/liburing) 并不意味着不能将这两种混合使用
在底层他们都是使用相同的结构来操作,即使应用是使用原始的系统调用接口,也推荐使用 [liburing](https://github.com/axboe/liburing) 的 `setup` 帮助函数
## liburing setup
让我们从一个例子开始,[liburing](https://github.com/axboe/liburing) 提供了一个基本的帮助函数,来完成 `io_uring_setup`的调用和三个必须的 `mmap`
```c
struct io_uring ring;
io_uring_queue_init(ENTRIES, &ring, 0);
```
该 `io_uring` 结构保存了 `SQ 环`和 `CQ 环`,并且调用了他们的设置逻辑,对于这个例子我们将 `flags` 设置成了 0
应用使用完 `io_uring` 结构后,可以调用 `io_uring_queue_exit`
```c
io_uring_queue_exit(&ring)
```
拆卸 ring,和应用分配的其他资源一样,一旦一样用退出,他们就会自动被内核回收。
对于应用已经创建的任何 `io_uring` 实例都是这样的
## liburing 提交和完成
一个非常基本的使用场景就是提交一个请求然后等待他完成,使用 liburing 就是下边这个样子
```c
struct io_uring_sqe sqe;
struct io_uring_sqe cqe;
// 获取 sqe,并填充 READV 操作
sqe = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe, fd, &iovec, 1, offset);
// 提交请求,通知内核可以消费 sqe
io_uring_submit(&ring);
// 等待完成事件
io_uring_wait_ce(&ring, &cqe);
app_handle_cqe(cqe);
io_uring_cqe_seen(&ring, cqe);
```
注意,如果还有其他提交的 `sqe`,那么等待的可能不是刚才提交的 `sqe`
如果应用仅希望查看完成情况,而不希望等待完成事件,可以调用 `io_uring_peek_cqe`
对于这两种场景,应用都必须使用`完成事件 cqe`来调用 `io_uring_cqe_seen`
否则重复调用 `io_uring_peek_cqe` 或者 `io_uring_wait_cqe` 会返回同样的事件
这种函数上的功能分隔是有必要的,以避免内核可能在应用完成之前覆盖现有的`完成事件`
`io_uring_cqe_seen` 会增加 `CQ 环头`,使内核可以在同一槽位可以上填充新的事件
[liburing](https://github.com/axboe/liburing) 也提供了很多填充 `sqe` 的函数,比如 `io_uring_prep_readv`
我们推荐应用尽量使用 [liburing](https://github.com/axboe/liburing) 提供的函数
[liburing](https://github.com/axboe/liburing) 仍处于起步阶段,并且正在不断开发以扩展受支持的功能和可用的助手
# 高级用例和特性
上面的例子和使用场景适用于各种类型的 IO,基于 `O_DIRECT 的文件IO`,`有缓冲的文件 IO`,`socket IO` 等等
不需要特别的操作去保证异步性,不过 `io_uring` 的确给应用提供了更多的功能,以下小节描述了大多数功能
## 固定文件和固定缓冲区
### 注册文件
每次将`文件描述符`填充到 `sqe`,然后提交给内核时,内核都必须检索对`文件描述符`的引用
当 IO 完成后,会再次删除文件引用,由于文件引用的原子性,这样对高 IOPS 的工作场景而言,速度会明显下降。
**为了缓解此问题,io_uring 提供了一种对 `io_uring 实例`预注册文件集的方法**
```c
int io_uring_register(unsigned int fd, unsigned int opcode, void *arg, unsigned int nr_args);
```
* `fd` 是 `io_uring 实例`的文件描述符
* `opcode` 执行的注册类型。
对于注册文件集来说,必须是 `IORING_REGISTER_FILES`。
* `arg` 必须指向应用准备打开的文件描述符数组
* `nr_args` 便是数组的大小
**一旦 [`io_uring_register`](https://unixism.net/loti/ref-iouring/io_uring_register.html) 成功将文件集注册后,应用就可以将`文件集数组的索引`(而不是使用实际的文件描述符)赋值给 `sqe->fd` 了,并设置 `sqe->flags` 字段为 `IOSQE_FIXED_FILE` 来标记 `sqe->fd` 是一个文件集索引**
应用可以继续使用未注册的文件,即使是注册过的文件也可以通过`文件描述符`赋值 `sqe->fd`,`sqe->flags`不设置 `IO_FIXED_FILE` 来正常使用`文件描述符`
**当 `io_uring 实例`被移除后,注册的文件集会自动释放,或者使用 `IORING_UNREGISTER_FILES` opcode 来调用 [`io_uring_register`](https://unixism.net/loti/ref-iouring/io_uring_register.html)**
### 注册缓冲区
不仅仅可以注册文件集,还可以注册一组`固定 IO 缓冲区(fixed IO buffers)`
使用 `O_DIRECT` 时,内核在真正执行 IO 前,必须映射应用内存页(pages) 到内核中,并且当 IO 完成后取消对这些页的映射。
这些操作的开销可能是昂贵的。如果应用可以复用 IO 缓冲区,那么总共只需要进行一次映射和取消映射,而不是每次 IO 操作都需要
**要注册一组固定缓存区,[`io_uring_register`](https://unixism.net/loti/ref-iouring/io_uring_register.html) 必须使用 `IORING_REGISTER_BUFFERS` 的 `opcode` 来调用,`args` 必须包含填充好每个 `iovec` 的地址和长度字段的 `iovec` 数组,`nr_args` 则是 `iovec` 数组的大小**
成功注册`固定缓冲区`后,应用可以使用 `IORING_OP_READ_FIXED` 和 `IORING_OP_WRITE_FIXED` 在 IO 中利用这些缓冲区。
当使用 `固定操作码(fixed op-codes)` 时,`sqe->addr` 必须包含了那些固定缓冲区之一的索引,并且 `sqe->len`为请求的字节长度。
**应用可能会注册大于 IO 操作的缓冲区,一个固定的读/写只是一个固定缓冲区的子集是完全合法的。**
## 轮询 IO
> 由于对 轮询 IO 不了解,导致本节翻译可能会有一些问题,可以查看 [Efficient IO with io_uring](http://kernel.dk/io_uring.pdf)
对于低延迟的应用来说,io_uring 提供了对文件轮询的 IO 的支持。
在这种情况下,轮询是指在不依赖硬件中来发出完成事件信号的情况下执行 IO,轮询 IO 后,应用将反复向硬件驱动询问已提交的 IO 请求的状态。
这和应用进入休眠状态然后等待硬件中断来唤醒的非轮询 IO 是不同的。
对于延迟非常低的设备和 IOPS 很高的情况,轮询可以显著提高性能,高中断率会导致非轮询的应用具有更高的开销。
在等待时间和总体 IOPS 速率上,轮询是否有意义取决于应用,IO 设备和机器的性能
要利用 IO 轮询,就必须在调用 [`io_uring_setup`](https://unixism.net/loti/ref-iouring/io_uring_setup.html) 时将 `io_uring_params->flags` 设置 `IORING_SETUP_IOPOLL`,或者使用 [liburing](https://github.com/axboe/liburing) 的 `io_uring_queue_init`。
使用轮询后,应用不能通过 `CQ 环尾`来检查可用的`完成事件`了,因为不会自动触发异步硬件的完成事件了。
相反,应用必须主动去查询,通过设置 `IORING_ENTER_GETEVENTS` 和 `min_complete` 来调用 `io_uring_enter` 获取到完成事件。可以设置 `IORING_ENTER_GETEVENTS` 和 `min_complete`=0。
对于轮询 IO,这可以要求内核简单的检查驱动上的完成事件,而不是不断的循环执行
在使用 `IORING_SETUP_IOPOLL` 注册为轮询 `io_uring` 实例上,只有对轮询的完成事件有意义的 `opcodes` 才可以被使用。
这些包括任何的读写命令:`IORING_OP_READV`, `IORING_OP_WRITEV`,`IORING_OP_READ_FIXED`, `IORING_OP_WRITE_FIXED`
在已注册为轮询的 `io_uring` 实例上使用非轮询的`操作码`是不合法的。这样会导致 `io_uring_enter` 返回 `-EINVAL`。
背后的原因是,当使用 `IORING_ENTER_GETEVENTS` 来调用 `io_uring_enter` 时内核无法知道是否可以完全的进入睡眠状态来等待事件或者是否应该主动轮询事件
## 内核测轮询
虽然 `io_uring` 可以通过更少的系统调用来高效的发布和完成更多的请求,在某些情况下我们可以通过进一步减少系统调用的数量来提高执行 IO 的效率
**这种功能之一就是`内核侧轮询`,启用该功能后,应用将不再刻意通过 [`io_uring_enter`](https://unixism.net/loti/ref-iouring/io_uring_enter.html) 来提交 IO 了。当应用更新 `SQ 环`,并且提交新的 `sqe` 时,内核会自动发现一个或多个新的 `sqe` 并且提交他们。**
这是通过特定于 `io_uring` 的内核线程来完成的
**使用这个功能,io_uring 实例必须使用 `IORING_SETUP_SQPOLL` 作为 `io_uring_params->flags` 来注册 io_uring 实例,或者传递给 `io_uring_queue_init` 函数。**
**如果应用希望线程为特定的 cpu,那么使用 `IORING_SETUP_SQ_AFF` flag,并且设置 `io_uring_params->sq_thread_cpu` 为所需 cpu。**
注意使用 `IORING_SETUP_SQPOLL` 来设置 io_uring 实例是特权操作,如果用户没有足够权限,那么 `io_uring_setup` /`io_uring_queue_init` 会以 `-EPERM` 失败
为了避免在 io_uring 实例处于非活动状态时浪费过多的 CPU。当内核侧线程空闲一段时间后,它将自动进入睡眠状态。
发生这种情况时,内核线程会设置 `IORING_SQ_NEED_WAKEUP` 到 `SQ 环` 的 flags。
设置该值后,应用将无法依赖内核自动查找新条目,并且必须调用使用 `IORING_ENTER_SQ_WAKEUP` 来调用 `io_uring_enter`。
应用逻辑看起来想下边这样
```c
// 添加新的 sqe 条目
add_more_io();
// 如果可轮询并且线程已经睡眠,需要调用 io_uring_enter() 使内核发现一个新的 IO
if ((*sqring->flags) & IORING_SQ_NEED_wAKEUP)
io_uring_enter(ring_fd, to_submit, to_wait, IORING_ENTER_SQ_WAKEUP);
```
只要应用一直提交请求,就永远不会设置 `IORING_SQ_NEED_WAKEUP`,我们可以有效的执行 IO,无需执行单个系统调用
**可以通过设置 `io_uring_params->sq_thread_idle` 字段来配置空闲前的特定宽限期。**
值是以毫秒为单位,如果未设置此值,那么内核默认将线程置为睡眠状态前的空闲时间为1秒
对于正常的中断驱动的 IO,应用可以直接查看 `CQ 环`来找到完成事件。
如果使用 `IORING_SETUP_IOPOLL` 设置的 io_uring 实例,内核将会负责获取完成事件
对于这两种情况,除非应用希望等待 IO 发生,否则可以简单的查看 `CQ 环`来查找事件
# 性能
最后, io_uring 达到了他的设计目标
我们有了一个内核和应用之间非常有效的交付机制——通过两个不同的环
虽然在应用程序中正确使用原始系统调用接口需要注意一些问题,但主要的复杂性实际上是需要显式内存排序原语。
它们只涉及发布和处理事件的提交和完成方面的一些细节,并且在应用程序之间通常遵循相同的模式。
随着 [liburing](https://github.com/axboe/liburing) 的不断成熟,希望大多数应用都可以对 他的接口满意
尽管本文的目的不是详细介绍 io_uring 的性能和可扩展性,但本节会介绍在这方面的一些优势
更多的细节可以看 `https://lore.kernel.org/linux-block/20190116175003.17880-1-axboe@kernel.dk/`
注意,由于在阻塞方面的进一步改进,这些结果可能有些过时了
例如,在我的测试机中,使用 io_uring 的每核心性能峰值是 1700k 4k IOPS 而不是 1620k。
注意,这些值没有绝对的之意义,不过在衡量相对上的优化还是很有用的
现在,通过 io_uring 可以发现应用和内核之间的通信不再是较低的延迟和较高的峰值性能的瓶颈了
## 原始性能,真实性能
有很多方法可以查看接口的原始性能。大多是测试都会设计内核的其他部分
上边的数字就是这样的一个例子,我们通过从块设备或文件中随机读取来测量性能。对于最高性能, `io_uring` 帮助我们通过轮询获得 1.7M 4k IOPS。而 `AIO` 只能达到 608k 。这里其实有点不太公平,因为 `AIO` 不支持轮询 IO
如果我们禁用轮询,`io_uring` 在相同的测试用例中也可以达到 1.2M 的 IOPS。这样看来 `AIO` 的局限性就非常的明显,在相同工作负载下,`io_uring` 的 IOPS 是 `AIO` 的 两倍
`io_uring` 还支持 `no-op` 命令,该命令主要检查接口的原始吞吐量。根据所使用的的系统,观察到的消息从每秒 1200 万条到每秒2000 万条不等。实际结果会因具体的测试用 例而异,而且主要受到必须执行的系统调用数量的限制
在其他方面,原始接口是和内存绑定的,并且提交和完成事件消息都很小并且在内存中是线性的,因此每秒实现的消息率可能非常高
## 缓冲的异步性能
我之前说过,内核内缓存的 `AIO` 实现可能会比用户空间实现更高效。一个主要原因是和缓存和非缓冲数据有关。
当进行缓冲 IO 时,应用通常严重依赖内核的`页缓冲(page cache)` 来提供更好的性能。用户空间的应用无法指导他解析奥莱要请求的数据是否已经缓存。当然也可以查询这些信 息,但是这需要更多的系统调用,不过现在缓存的东西可能几秒后就不再缓存了了。因此具有 IO 线程池的应用通常必须将请求交给异步上下文中,从而导致至少两次上下文的切换。如果请求的数据已经在页面缓存中,这将导致性能急剧下降。
`io_uring` 处理这种情况就像处理其他可能阻塞应用的资源一样。
更重要的是,对于不会阻塞的操作,会以内联的方式提供数据。这时 io_uring 对于页面缓冲中已经存在的 IO 来说,可以和常规同步接口一样高效。
一旦 IO 提交调用返回,应用将在 `CQ环`中有一个完成事件在等待他并且数据已经被复制了
# What's new with io_uring
距离第一个支持 io_uring 的 内核(5.1) 发布已经 6 个月了
和任何新的 API 和功能特性一样,初始版本只是一个起点
一旦人们开始将现有的应用转换为API,或者开始根据 API 编写新应用时,不可避免的就会产生新的功能需求
本文将尝试介绍一些自推出以来更重要的补充。
## 新命令
大多数功能都不可避免的使用 `io_uring` 的`新操作码`。增加了新的核心功能,其中大多数只是常规同步系统调用的镜像版本。
对于实际的命令定义,我希望读者使用 [liburing] 的帮助函数来设置这些。
clone 地址:`git://git.kernel.dk/liburing`
重要性不分先后,新命令为
* `IORING_OP_SYNC_FILE_RANGE` 这个命令增加了对异步方式执行 `sync_file_range` 的支持。它支持同步的系统调用的所有功能
* `IORING_OP_SENDMSG` 和 `IORING_OP_RECVMSG`。之前可以在套接字上常规的执行 `IORING_OP_READV` 和 `IORING_OP_WRITEV`,而且这也是使用 `io_uring` 来做网络 IO 的唯 一方法。
现在我们支持了 `sendmsg` `recvmsg` 的异步版本。如果可能的话,他们会内联执行,如果他们阻塞了提交的应用,那在后台运行
* `IORING_OP_ACCEPT` 和 `send/recvmsg` 调用一样,为 `accept4` 系统调用提供了了异步支持。
这是 `io_uring` 支持的第一个创建新的文件描述符的系统调用
* `IORING_OP_TIMEOUT` 该命令的特殊之处在于他没有参考现有的系统调用,而是增加了对触发CQ环中的超时条件来唤醒在事件上睡眠的应用。
超时有两种方式,一种是**事件完成次数**或者**特定的超时(绝对或相对)**。无论哪种事件先触发,都会将 CQ 中增加一个完成事件,并唤醒等待者
`liburing` 使用超时提供了 `io_uring_wait_cqe_timeout()` ,但是应用也可以根据需要来使用。
* `IORING_OP_TIME_REMOVE` 可以删除现有的超时
* `IORING_OP_ASYNC_CANCEL` 可以取消已有的异步工作
熟悉的 `AIO`/`libaio` 的人可能会说 `io_cancel` 系统调用已经存在很长时间了,不是过一直都没有实现。而且他仅仅和 `AIO` 的 poll 命令一起工作。
在 io_uring 中,这适用于任何读写操作,accept ,send/recvmsg 等等。这里使用不同的命令会有一个重要的区别。
**`读写常规文件`时会以不间断状态等待 IO。这意味着它将忽略任何信号或者尝试取消,也就意味着无法取消。**
**如果他们还没有开始,那么 io_uring 就可以取消他,如果已经启动,那么取消就会失败**
**`网络IO` 通常会处于等待的中断等待,因此可以随时取消。**
如果成功取消,那么 `IORING_OP_ASYNC_CANCEL` 请求的`完成事件`结果(`cqe->res`)就是 0, `-EALREADY` 表示取消操作已经在进行中,如果指定的原始请求找不到了就会返回 `-ENOENT`
对于取消请求的返回 `-EALREADY`,io_uring 可能会也可能不会导致请求提前终止
* 对于阻塞 IO,原始请求会按照原先的请求完成
* 对于可取消的 IO,它会在所有可能的情况下尽早终止
## 其他
### eventfd
现在支持在 `io_uring` 中支持 `eventfd` 通知,应用可以使用 `eventfd` 通知完成事件
### 文件描述符注册
`注册文件集`的支持被扩展了很多,现在不在局限于 1024 个文件。
而支持 64k 注册的文件。而且还支持稀疏文件集,也就是说一个巨大的文件集可以有 fd == -1 的 集/文件。
**这一点很重要,因为我们现在还支持文件集更新,应用可以在表中特定偏移位置显示的更新大量文件。**
在此更改前,更新/更改文件集的唯一方法是取消现有文件集的注册,然后注册一个新文件集
### CQ 环大小
默认情况下,`io_uring` 会将 `CQ 环`的尺寸设置为 `SQ 环`的大小的两倍。之所以这样是因为 `sqe` 的生存周期非常短,一旦内核看到他们们就会被消耗掉。
意味着应用可以使用比SQ环大小更高的请求数量。为了避免轻易溢出 `CQ 环`,我们将 `CQ 环`加倍容纳更多的`完成事件`
有一些用例需要一个比 `SQ 环`大的多的 `CQ 环`,以前他们必须使用一个大的 `SQ 环` 来设置,但这在内存利用方面效率很低。io_uring 现在支持独立调整 `CQ 环`的大小,这样就可以有一个 128 条目的 `SQ 环`,而 `CQ环` 大小是 32k。
如果应用想独立设置 `CQ 环` 大小,则必须用 `IORING_SETUP_CQSIZE`设置 `io_uring_prarams->flags` 来创建 io_ring 实例,并且设置 `io_uring_params->cq_entries` 来指定大小。
`CQ 环` 的尺寸必须至少和 `SQ 环 相同,和 `SQ 环`一样也必须是 2 的幂
io_uring 现在也和`内核工作队列`基础架构脱离了。这是纯粹的内部变化,无法通过 API 看到。
为何有必要这样做,对详细信息感兴趣的人可以看这两个提交 [io-wq: small threadpool implementation for io_uring
](http://git.kernel.dk/cgit/linux-block/commit/?h=for-5.5/io_uring&id=771b53d033e8663abdf59704806aa856b236dcdb) 和 [io-wq: small threadpool implementation for io_uring](https://git.kernel.dk/cgit/linux-block/commit/?h=for-5.5/io_uring&id=771b53d033e8663abdf59704806aa856b236dcdb)。
重要的是通过它支持了文中提到的几个特性
通过 `io_uring_params->features` 可以查看是否设置`IORING_FEAT_NODROP` ,它可以防止 `CQ 完成事件`的溢出问题,保证不丢消息
稍微高一些的 Linux 版本支持该 feature
过多的创建 io_uring 实例可能导致 `ENAOMEM` 错误,可以看 `https://github.com/spacejam/sled/issues/899`
【译】高性能异步 IO —— io_uring (Effecient IO with io_uring)