使用 io_uring 改进 ioctl

标题:Revamping ioctl with io_uring

日期:2023/05/26

作者:Kanchan Joshi & Anuj Gupta

链接:https://www.youtube.com/watch?v=iRPpeUIdHjA

注意:此为 AI 翻译生成 的中文转录稿,详细说明请参阅仓库中的 README 文件。


欢迎来到这次演讲,它关于去年在内核 5.19 版本中引入的一项新功能。 它被称为 Uring Command。虽然标题也说这是关于改造 ioctl 的,但实际上它的应用范围比这更通用一些。 如果你因为任何原因正在寻找构建一个新的用户接口的方法,这或许是你想要考虑的东西。 我是 Kanchan,Anos 是联合演讲者,但不幸的是他没能来到现场。

那么,让我们开始吧,或许可以粗略地将用户与内核的通信分为两类。 第一类是我们通过明确定义的系统调用(system calls)进行的通信,我将其标记为结构化通信。 而其他所有不适合那些明确定义的系统调用的通信,都归入非结构化通信的范畴。 我们之所以有这种分类,是因为构建系统调用通常需要创建通用的抽象。 所以,你可能正在编写某个内核组件,它可能是一个驱动程序,一个文件系统,或者其他一些内核组件。 有时你可能想与用户空间应用程序进行通信。 如果你正在做的事情是众所周知的或以前做过的,你可以使用一个系统调用,这样事情就解决了。 但是,如果你正在做一个稍微特殊的操作,你可能会发现将其变得通用有些困难。 这就是为什么创建新的系统调用会变得有点困难。

ioctl 曾被用作一种方式,来表示那些难以塑造成通用 API 的通信。 有些人因为某些原因喜欢它,也有人因为其他原因而不喜欢它。 但这或许不是我们今天将要讨论的重点。 如果你看一下内核层面的通信, 你首先需要将你的接口编码成一个命令。你为它定义一个操作码(opcode),这只是一个数字。 你的内核组件应该实现一个名为 unlocked_ioctl 的回调函数。 你可以在右侧看到它。然后,你在其中所做的任何事情,都将是你特定于命令的操作。 就应用程序而言,它可以使用这个特定的操作码。 并且它可以通过 ioctl(这里是一个系统调用)来提供这个操作码。 一旦这完成了,一旦整个执行结束,你的结果也就可用了。 通常,这被视为一个阻塞的、同步的操作。

现在,如果我们看看它目前被使用的广泛程度。 如果你尝试搜索 unlocked_ioctl 这个关键词,你会发现目前大约有 303 个提供者。 这包括了各种驱动程序、文件系统等等。 当然,这只是提供者的数量。 如果你想计算 ioctl 的数量,目前大约是 6384 个。所以,看起来虽然我们有系统调用,但总是存在以一种前所未有的方式进行通信的需求。 如果这是真的,我的意思是,如果未来也依然如此,你可能会觉得需要考虑使用 ioctl。 但是,如果你这样做,你就不能期望高效率。 这是因为它在语义上一直被视为一个阻塞调用。 还有一件事会发生,就是当你这样做时,你最终会执行 copy_from_usercopy_to_user。 是的,这两件事正是 io_uring 极力想要摆脱的。

io_uring 基础

那么,在我们讨论 io_uring 中的替代方案之前,让我们先看看它的一些基本原理。 io_uring,如果你看这里的这张图,左手边,你在这里看到一张图。 io_uring 在用户空间和内核空间的边界上运行。所以,你可以把它想象成类似于 VFS。 它在那个边界上提供了可扩展的异步基础设施。它适用于存储,也适用于网络 IO。 这里的通信骨干是,你通过让内核创建与用户空间应用程序共享的环形缓冲区(ring buffers)来进行通信。 这就是你进行通信的方式。而效率是这件事的核心部分。 所以,是的,它在减少系统调用数量和那些拷贝操作方面下足了功夫。 io_uringio_uring_setup, io_uring_enter, io_uring_register 等几个系统调用,但你可能需要了解的调整选项非常多。

如果你看一下通信协议,应用程序首先通过设置一个环(ring)开始。 当它设置一个环时,你基本上会得到一个叫做提交队列 (submission queue) 的环形缓冲区,同时你也会得到一个叫做完成队列 (completion queue) 的环形缓冲区。 在这个环内,现在你想要提交你的命令。 你所做的是获取一个 SQE (Submission Queue Entry)。你用你特定于命令的操作来填充它。 它可以是读,也可以是写,或者任何其他操作。 你可以重复这个过程。你可以再取一个 SQE,如果你这样做,你实际上是在尝试进行批处理 (batching)。 然后在某个时候,你想告诉内核,我已经准备好了这么多命令或一个命令。 当你这样做时,你将调用一个叫做 io_uring_submit 的操作。 这基本上对应于代码中的第三步。

然后,是的,你已经提交了。此时,内核,或者说 io_uring 会做它的工作。 它会去调用任何实现了读、写或那个特定操作的组件。 作为一个应用程序,你不需要关心它。在某个你关心结果的时刻,你会想要查看完成情况。 所以,你会说现在我想看看完成情况。 你会从环中获取一个 CQE (Completion Queue Entry)。所以,第四步,虽然看起来是串行的,但它不必是串行的。 你选择何时去做这件事。当我们得到一个 CQE 时,我们在这里所做的是等待一个完成事件。 当我们得到一个 CQE 的那一刻,我们查看结果。 然后我们就知道它发生了什么。 在这里,cqe->res 告诉我们读操作是失败了还是成功了。 一旦完成,你需要做一个叫做 io_uring_cqe_seen 的操作。 通过这个操作,你是在告诉内核,我已经处理完这个特定的 CQE 了,我不再需要它了。 这就是现有的通信协议。你可以认为这对于任何 io_uring 操作都是通用的。 顺便说一下,如果此时你有问题,我很乐意回答。

伪异步与真异步

那么,继续讲基础知识。让我们谈谈将同步操作转为异步操作的两种可能方式。 我把它们标记为,第一种是伪异步 (pseudo-async),另一种是真异步 (true-async)。 我这里的意思是,第一种方式是关于你有一个同步操作, 为了把它变成异步,你决定去做这件事。 你决定把这个操作卸载到一个单独的线程中。当你这样做时,你就开始给人一种整个事情已经变成异步的印象。 实际上,从某些角度来看,确实如此。 这个方案的优点是,大多数操作都可以被转换成异步的。 比如任何现有的同步系统调用,都可以被转换成异步。 但问题在于,这种方式是无法扩展的。因为,是的,你需要… 每次调用基本上都需要一个线程。这只是在转移责任。

当我们谈论真异步时,这是指不使用任何工作线程(worker or thread)。 这实际上是关于将提交与完成解耦。在这种情况下,我们必须以一种提交者不必等待的方式进行提交。 这很快,可扩展,但是,是的,如果你走这条路,需要做更多的工作。

那么,io_uring 实现了这两种模型中的哪一种呢?答案是两种都实现了。 第一种不是默认模式。我的意思是,io_uring 首先尝试做真异步。 如果在某些情况下行不通,它可以回退到伪异步。 对于已知的阻塞操作,比如,想象一下如果你想把 mkdir 这样的操作变成异步,是的。 如果这是一个已知的阻塞操作,那么从一开始,它就会使用一个工作线程来执行那个操作。 这是现有的部分。现在,当我们谈论异步 ioctl 时,我们可以选择其中一种模型。 如果你选择伪异步,是的,会容易一些。 但那些问题依然会存在。所以,在这一点上,我们尝试的目标是效率,并选择真异步模型。

Uring Command:一种新的接口

而这正是 Uring Command 试图做的事情。它也被称为 Uring pass-through(Uring 直通)。 这是一个通用的设施,可以将任何 io_uring 的能力附加到任何底层的命令上。 “任何 io_uring 的能力” 这点很重要。虽然我一直在谈论异步性,但这并不是 io_uring 拥有的唯一能力。 还有其他能力。我列出了一些:提交批处理 (submission batching)、提交卸载 (submission offload)、完成轮询 (completion pooling)、注册文件 (registered file)、注册缓冲区 (registered buffer)。 所有这些都是真正可能的。它的目的就是将所有这些能力附加到任何命令上。 当然,命令提供者保持不变。我们看到了那么多 ioctl 的提供者。 所以,在这里,我的意思是,那些都是非常合格的。 无论命令提供者是什么,它都需要与 io_uring 协作,以实现非阻塞的提交和完成。

用户接口

如果你看一下用户接口,你必须使用一个新的操作码叫做 IORING_OP_URING_CMD,它会进入 SQE。 这就像你使用 IORING_OP_READIORING_OP_WRITE 一样。 这是我们必须使用的新东西。 有趣的一点是,你需要提供的命令, 你实际上不必在外部为它分配空间。你可以直接从这个 SQE 本身获得它。 所以,如果你使用常规的 SQE,你将获得 16 字节的空间。 这是你可以使用的空闲空间。你可以认为你根本不需要做 malloc 就有了一块内存。 如果你想要更多空间,你可以说我想要 Big SQE。 然后你将获得 80 字节的空间。Big SQE 是与此一同添加的另一个设施。 所以,两者是共同开发的。

是的,作为一个应用程序,你会将你的命令放在 SQE 内部。 io_uring 不太关心里面放的是什么。这一点上它和 ioctl 很相似。 你的应用程序将提供者特定的操作码放入 cmd_op 字段中,仅此而已。

至于结果,是的,正如你在例子中看到的,结果通常会到达 CQE。 现在情况也是如此。但是,你看到在 CQE 中,我们只有一个结果的空间。 有些情况下,内核想要通知或告知多个结果。 如果是这种情况,你会说我想要 Big CQE。这个也已经被添加进来了。 整个基础设施都是伴随这个功能一起添加的。所以,如果你想要 Big SQE,你将不得不在设置环的时候提供一个名为 IORING_SETUP_SQE128 的标志。 这就是它的作用。这有助于实现零拷贝提交 (zero copy submission)。 如果你这样做,你就不必做 copy_to_user。 对于 Big CQE,你可以通过使用 IORING_SETUP_CQE32 标志来设置环以请求它。这有助于实现零拷贝完成 (zero copy completion)

如果你看右边的图,我想这就是我们刚才讨论的内容。 这是一种松散的映射。如果我们把 ioctl 转换到这个方案中,fd (文件描述符) 会进入 SQE。 SQE 用绿色表示。操作码将是 IORING_OP_URING_CMD。 提供者特定的操作码,对于 ioctl 来说是 OPX,它会进入 cmd_op 字段。 实际的命令会进入 SQE,正如你在这里看到的。 如果你需要更多空间,你会请求 Big SQE。 我想右边是关于 CQE 的。第一个结果会进入常规的 CQE。 如果你想要更多,你可以请求 Big CQE。

内核接口

那么,以上是应用程序或用户接口部分。 但是,是的,如果你正在编写一个内核空间组件,你可能想知道你将如何与 io_uring 通信。 这是第二部分。如果你是一个内核空间提供者,并且需要与 io_uring 通信以这种方式实现你的操作,这部分就与你相关了。

命令提供者需要实现一个新的回调函数,名为 uring_cmd。 你可以在 struct file_operations 中看到,在我们有 unlocked_ioctl 的地方,现在我们有了一个叫做 uring_cmd 的东西。 当然,当我们谈论提交时,io_uring 将是第一个接收到 SQE 的。 io_uring 所做的是,它准备一个叫做 struct io_uring_cmd 的东西。 这是一个它根据 SQE 准备的内核内部结构。 这个结构被用于 io_uring 和内核提供者之间的所有通信。

如果你看这里的流程图,io_uring 通过 uring_cmd 回调调用了提供者。 现在,你的提供者会做它的工作。它会尝试提交命令或做任何它需要做的事情。 一旦提交完成,它会通过返回一个特定的代码 -EIOCBQUEUED 来表示“我完成了”。 这就是关于提交的全部内容。在某个时候,你完成了操作的完成阶段。 你需要通过调用一个名为 io_uring_cqe_fill(此处疑为口误,应为完成相关的 API)的 API 来通知 io_uring。 在这里,你再次使用 io_uring_cmd_done(此处的 API 名可能不准确,但意指完成通知),并且你能够返回两个结果。

这是简化的通信模型。有时,如果你是一个提供者,你可能希望… 你的完成操作需要在任务上下文 (task context) 中进行。 为此有另一个 API 叫做 io_uring_cmd_complete_in_task,你可以使用它。 你可以提供一个回调函数,并且那个特定的回调函数被保证会在任务上下文中被调用。 所以这有时可能很有用。

内核中的用户

现在,让我们看看在内核中,目前有哪些人在使用它。

  1. NVMe 驱动 正如我所说,这功能是去年在内核中添加的。NVMe 驱动程序是…它被改变了, 我应该说,它被扩展以使用 Uring Command 以及整个接口。 NVMe 驱动已经有了基于 ioctl 的直通操作。所以,你可以看到,我在右边展示了 nvme_ns_char_fops。 如果你看 .unlocked_ioctl,这就是以前直通操作的实现方式。 在这里,你看到它现在也有了 uring_cmd 回调。这就是 NVMe 的 Uring Command 直通。 这使得任何 NVMe 命令,可能超越了读和写,都可以被高效地使用。 特别是在 NVMe 中,如果你们了解的话,你们知道 NVMe 是与存储交互的新方式。 所以,创造新系统调用的问题在那里是非常突出的。

  2. UBLK 驱动 第二个合作者或第二个用户是 UBLK (Userspace Block) 驱动。这也是去年开发的。 它从头开始就使用了 Uring Command。如果你看右边的图,你在这里看不到任何 unlocked_ioctl。 它从未使用过 ioctl。它所有的通信都是通过 Uring Command 完成的。

  3. 未来的用户 在目前这个时间点,FUSE 正在考虑使用它。 在网络方面,Sockets 正在考虑使用它。所以,相关的讨论正在进行中。 我们未来可能会看到一些进展。

代码示例:ioctl vs Uring Command

这里有一个快速的转换示例,这是针对 NVMe 的。 在左边,你看到了一个 ioctl 直通的简单例子。我们打开 NVMe 设备句柄。 然后我们调用 ioctl。在这里我们使用了一个名为 NVME_IOCTL_IO64_CMDioctl 代码,就是这样。 很简单,对吧?

对于 Uring 直通,是的,代码多了一点。你同样打开 fd。 你设置一个环。这一次,你通过指定这些标志来请求 Big SQE 和 Big CQE。 然后你获取一个 SQE。我认为重要的一点是,在 SQE 内部,我在这里写了一条注释,你从 SQE 中提取命令。 对吧?你根本不需要分配它。而这个用于 NVMe 直通的命令,它需要 72 字节的空间。 是的,它在这种情况下就像是免费可用的。你准备它,提交它。 一旦你在调用 io_uring_wait_cqe 后收到了完成事件,你就可以检查两个结果。 所以,NVMe 确实需要 Big SQE 和 Big CQE 才能高效地完成这项工作。

其他 io_uring 能力

正如我所说,io_uring 中还有其他可用于其他操作的能力。 Uring Command 也能够利用这些能力。

  • 固定缓冲区 (Fixed Buffer) 有一个设施叫做 io_uring 中的固定缓冲区。 这个设施旨在降低每次 I/O 映射和取消映射缓冲区的成本。 如果你看右边,有一个叫做 io_uring_register_buffers 的东西。 你可以预先注册你的缓冲区。如果你有 N 个缓冲区要做 IO,你可以预先注册它们。 如果你这样做了,在 IO 期间,你实际上可以使用那些缓冲区。 你可以为任何一个缓冲区指定索引。 结果将是,你将获得稍好或好得多的效率和更低的 CPU 使用率,这取决于你的工作负载。 这对于常规的读写操作是可用的。对于 Uring Command,这个功能也已经添加了。 接口是,第一部分保持不变。 应用程序将以同样的方式注册缓冲区。那部分没有改变。 新的部分是在 SQE 的 uring_cmd_flags 中,你可以指定一个叫做 IORING_URING_CMD_FIXED 的标志。 如果你这样做,你就会开始使用这个特定的能力。 然后你通过缓冲区索引(某个数字)来指定缓冲区,就这样。 就你的提供者而言,你的提供者可以通过调用一个名为 io_uring_cmd_import_fixed 的新 API 来使用这个预先映射好的缓冲区。 这是关于…这就像一个内核空间 API。 目前它被命名为这个,但你知道,未来它可能会被改变。 如果你想看例子,我添加了 FIO(用户空间示例)和内核空间的 NVMe 示例。

  • IO 轮询 (IOPOL) 另一个能力是 IOPOL。 IOPOL 是指不等待完成,而是轮询完成。 我的意思是,如果你的操作将非常快地完成,你可能真的不想去等待它。 通过轮询它,你可能会获得更好的效率。 你决定基本上不睡眠,不经历上下文切换等等。 这个功能在常规的读写操作中是可用的。 对于 Uring Command,现在也可以使用。 应用程序只需要设置 IORING_SETUP_IOPOL,仅此而已。 这是应用程序本来就会做的事情。就内核而言,如果你正在编写它的提供者,提交部分是一样的,没有变化。 对于完成部分,是的,你可能需要实现一个新的回调函数叫做 uring_cmd_iopoll。 同样,例子可以在这里找到。

性能表现

现在,这是一个效率的例子。 我在这里展示的是,对于 NVMe,NVMe 直通被转换成了基于 Uring Command 的直通。 之后它的性能如何,这正试图展示这些数字。 但是,我们并没有与 ioctl 进行比较。在 ioctl 的情况下,我们知道它并不能真正扩展。 所以,这不是最好的例子。但这里,它显示了基于 io_uring 的直通比基于 io_uring 的块 IO 或直接 IO 稍微好一些。 所以,我们是在与一个更强大的竞争对手进行比较,而不是与同为直通的 ioctl 比较。 你在这里看到的是,也许是最后一个。 如果你看最后一张图,它显示峰值性能提升了 11%。 这些词,如果你说 “base”,”base” 的意思是没有任何附加功能调整时的性能。 “base + FB” 是基础性能加上固定缓冲区 (fixed buffer) 能力。固定缓冲区能力就像是提升了这两者的速度。 “base + poll” 是我提到的 IOPOL。同样,IOPS 也因此提高了。 最后一个是结合所有功能调整,看看峰值性能是多少。 最终,它达到了 500 万 IOPS,这是在这个特定情况下的设备极限。 这个设备对于 512 字节的随机读操作,能够提供 500 万 IOPS。 而这一点在目前是可能达到的。

上游状态和总结

如果你看一下上游的状态,io_uring_cmd 的初始支持是在去年的 5.19 版本中添加的。 与此同时,作为用户和合作者的 NVMe Uring 直通也一同被开发出来。 Big SQE 和 Big CQE 设施也在那时被添加。 第二个用户,即 UBLK,是在 6.0 版本中添加的。 我谈到的效率调整功能,我只提到了两个,但还有更多。 我想我只提了那些需要一些用户空间改变的功能。那些是在 6.1 版本中添加的。 而且,我想我们谈到过还有其他用户正在开发中。

那么,这就是我所有的内容。如果你有任何问题,我很乐意回答。谢谢。

问答环节

观众提问 1 (关于系统调用转换): 我是说,没有 6.0 内核的开销和使用 io_uring 缓冲区。 所以,你在 io_uring 缓冲区中请求,内核在某些 sqe 中执行,然后在 cqe 中完成。 所以,我想知道,并非每个系统调用都能转换成 io_uring。 那么,什么样的系统调用可以被 io_uring 替代呢?

演讲者回答: 目前,io_uring 支持的操作有很多。 我认为数量还在不断增长。只是有些操作可能更高效,因为它们是使用真异步方式实现的。 而另一些操作可能没那么高效,因为它们……你知道,它们很难被转换成真正的异步模型。 所以,是的,你有很多选择。 如果你有新的东西,或者某些东西不适合现有的系统调用,这就是我谈到的方法。 这可能对构建很有用。 所以,你可以考虑,你知道,使用常规模型将你现有的系统调用改为异步。 或者你可以考虑使用这个新方法,看看哪个可能更适合。

观众提问 2 (关于同步/异步模型切换): 我认为传统的 ioctl 模型总是同步的,对吧?但现在 io_uring 是异步的。 那么,假设某些子系统采纳了 io_uring,让 ioctl 通过 io_uring 工作。 但是当用户切换到使用 io_uring 时,他们基本上是从同步模型切换到异步模型。 所以,调用它的应用层必须进行适配。是这样吗?

演讲者回答: 是的。 我想如果我理解你的问题正确的话,是的。我想如果你考虑使用 io_uring,你就是在考虑从同步模型转向异步模型,对吧? 对。只是我们之前在 io_uring 的情况下,并没有任何能够映射到 ioctl 的东西。 如果你是一个现有的内核提供者,并且你之前有通过 ioctl 做的事情,对吧? 但现在,如果你关心效率,那么你可以考虑使用这个。 我想,这就是我想要说的。 否则,总的来说,io_uring,如果我理解你的问题正确,它就是关于从同步模型转向异步模型。

观众提问 3 (关于同步方式使用 io_uring): 我只是在想,如果你切换了,然后你只做了些简单的事情,比如你调用 io_uring,然后让同一个线程等待那个完成事件,那你其实什么也没省下来。 你必须在应用层面上做一个更高级别的重构。

演讲者/其他观众补充: 演讲者: 你是说我之前谈到的第一种方法,用一个工作线程把同步操作变成异步吗? 你能再说一遍吗?我想你刚才说的是,将同步操作转换为异步的一个简单方法就是使用一个工作线程。 你可以在应用层做,或者内核里有人可以为你做。 它可以创建一个内部内核线程,然后把你的操作变成异步。你可以这样做。 但这有可扩展性问题。你的线程数量是有限的,对吧?

另一位观众: 我想回答一下我理解的你问题的另一部分,如果你以一种本质上同步的方式使用 io_uring,就像你说的,排队一个操作然后等待它,那么,是的,你会得到本质上同步的行为,因为你仍然写了一个同步的程序。 所以,我的意思是,你必须用一种不同的方式来编写应用程序才能使用 io_uring。 你提到了将 ioctl 切换到 io_uring。当然,内核里不会有任何东西会以破坏用户空间的方式进行这种切换,因为那是不被允许进入内核的。 你可以为它添加新功能。我也不同意 ioctl 本质上是同步的这种说法,因为内核中有大量的异步 ioctl 调用。 它不必是那样的。

演讲者: 是的。我想有时它被视为一种 hack。 但是,是的,在某个点上,事情就是那样的。 我想现在更清楚了,如果你使用 Uring Command,意图非常明确,我们真的想要一个异步模型。 我想你是对的。我想在 ioctl 操作的情况下,io_uring 正是被以一种尝试进行异步操作的方式使用。 是的。是的。如果你看看像 Video for Linux 接口,那全是异步的。 而且全是用 ioctl 完成的。好的。好的。

观众提问 4 (关于等待多个环): 我看到了给用户的 io_uring 接口。 你会有一个等待…我不记得函数名了。 基本上,当数据可用时你会等待,那会是阻塞的,对吧? 所以你提交一个任务,然后你可以做任何你想做的事。 但最终,你必须等待内核的通知,说“我为你准备好数据了”。对。 让我看看。好的。到 API 那里。也许是第四步。第四步。是的。 那么,如果我的应用程序需要多个环呢?在这种模型下,我必须搞清楚我应该先等待哪个队列,哪个环。 有没有像 epoll 那样的东西?如果一个环准备好了?

演讲者/其他观众回答:
演讲者: 在这种情况下,你只有一个环。通常你不用为此烦恼。 我想如果我理解你的问题正确的话。是的。你的点是,你必须在某个时候调用 io_uring_wait_cqe。 如果我需要两个环怎么办?如果你真的不想要…抱歉? 观众: 我知道这是一个环的接口。 我在想,在当前的 poll 系统调用中,你可以等待多个文件描述符。对。 所以我在想,在某些场景下,也许我的应用需要通过多个环与内核通信。 那种情况下,我应该如何做等待?谢谢。 演讲者: 如果你有一个不同的环,你会使用 io_uring_wait_cqe 并且你会把那个特定的环作为输入传给它。 所以,如果因为某些原因,你决定使用多个环,是的,你会那么做。 但它是这样的。我假设在这个例子里,是的,这看起来像一个顺序的步骤。 但你实际上可以做多次提交。 我在这里没有展示的是 SQE 中的一个叫做 user_data 的操作。 在 SQE 中,有一个 user_data 字段。你用它来确定你得到的是哪个完成事件。 所以,当你调用 io_uring_wait_cqe 时,这意味着你愿意等待。 如果你不想等待,io_uring 提供了 API,基本上允许你只是去…你知道,戳一下或者看一眼,是否有任何 CQE 可用。 如果有任何 CQE 可用,你会在不等待的情况下得到它。你不会被阻塞。 如果 CQE 不可用,你无论如何都会立即拿回控制权。 所以等待的问题在那里解决了。但在某个时间点,是的,你总想知道你是否收到了你提交的任何东西的完成通知,对吧? 否则就没必要提交了,对吧?现在,如果你看 CQE,你会想看 CQE 的 user_data 来知道对应的提交是哪一个。 你映射它,然后你就知道,好的,我现在收到了这个特定 io_uring 操作的完成通知。 现在,这件事,你可以应用在单个环或多个环上。 我认为那关系不大。这回答你的问题了吗?

另一位观众: 我想可能被忽略的部分是,拥有多个环会很罕见,你通常不会那么做,因为你可以通过一个单独的环与多个设备、多个文件描述符通信。 所以,你可以把一大堆对很多文件或很多设备的操作全部提交到同一个环里,对吧? 它们会进去,然后以不同的顺序返回。你必须追踪它们。 这就是他提到的 user_data 的用处,所以你可以把它们理清楚。 但使用第二个环会有点奇怪。真的没有理由那么做。 你不需要。

演讲者: 对。我想同样的事情也可以在代码里看到。 如果你看 sqe->fd,你只是在整个环里填充了一个 SQE。 你可以再拿一个 SQE。你可以给它一个不同的 fd。 这就是多个设备或多个文件如何能通过同一个环来操作。 我的意思是,同一个环。就像,你只需要创建,用不同的文件句柄填充不同的 SQE。 这就是刚才解释的,对吧?

观众提问 5 (关于 ioctl 的未来): 你期望 ioctl 接口在某个时间点会被废弃,然后我们会完全切换到 Uring 吗? 演讲者回答: 不?我不知道。我只是觉得,是的,如果你需要开发一个 ioctl 并且你还关心效率,也许这是你想要看的东西。 我的意思是,是的。比如,在一些简单的情况下,在内核中给新驱动添加新的 ioctl 是安全的,而不是用 Uring。 好的,明白了。谢谢。

观众提问 6 (关于共享内存的安全性): 谢谢。这更像是关于 io_uring 整体的问题。我想知道,现在我们…你有一个环,然后用户空间和内核共享同一块内存。 是否有任何安全风险,或者也许已经有一些我不知道的方法。 比如,如果有人,用户提供了一个 payload 把它放进共享内存里,然后内核开始取它并处理它,然后用户可以同时改变那个 payload? 这种事会发生吗?

演讲者/其他观众回答: 演讲者: 就内核代码而言,所有的代码都经过了审查,所有的代码,就像,你不能有一个随机的东西。 我的意思是,你知道,所有这些查看 SQE 的提供者,你知道,这…我是说,所有这些都需要被审查,对吧? 人们在审查它。这就是,你知道,事情就是这样被添加进去的。 所以,我的意思是,情况不是说内核会去拿某个会导致它崩溃的东西。

另一位观众: 是的。是的,我的意思是,他说的绝对是真的,但这真的就像任何其他的系统调用接口一样,对吧? 你总是必须注意用户空间传给你的是什么,并且内核总是会把整个 SQE 从环里复制出来。 至少,然后对它进行操作,这样它就脱离了…是的,没错。 所以,对于零拷贝来说…我甚至不知道。SQE 会被拷贝,但那只有 64 字节。 我的意思是,如果你在做一个 I/O 操作,那完全取决于那个 I/O 操作是如何实现的, 但它可能被拷贝也可能不被拷贝。 你知道,如果你在做缓冲 I/O,它会被拷贝到页缓存之类的东西里。 但是命令,如果实现得当,命令本身会被复制出来。

UBLK 开发者: 我可以快速补充一点,对于 UBLK, 有一个独立的共享内存供内核和用户空间用于 I/O。 所以只有描述符进入环,然后 I/O 缓冲区使用独立的共享内存。

观众: 那么,当数据发送到内核时,数据应该被锁定。 否则,当内核正在读取数据时,用户… 另一位观众: 是的,如果用户空间在内核或 UBLK 正在操作内存时改变它,那是不好的。 所以,不要那样做。 另一位观众: 是的。但我的意思是,那是 I/O 数据。你对数据内容没有任何控制。你只是把它移动到别处。所以用户应该知道当我提交了 payload,我就不应该再碰它了。 另一位观众: 是的。在你收到 I/O 完成通知之前不要动它。这就像你往一个文件里写东西一样。 如果你同时从另一个线程改变你正在写入文件的那个缓冲区,这是个坏主意。