I/O 环是 Windows 预览版中的一项新功能,这是环形缓冲区的 Windows 实现,即一个循环缓冲区,这是一个固定大小的缓冲区,它被定义成一个环形,当缓冲区满了后,新来的数据会覆盖掉旧的数据。在本文的示例中用于同时排队多个 I/O 操作,以允许用户模式应用程序执行大量 I/O 操作在一个操作中完成而不是转换从用户到内核,然后返回每个单独的请求。
这个新功能增加了很多新功能和内部数据结构。
I/O 环使用
I/O 环的当前实现仅支持读取操作,并且允许一次最多排队 0x10000 个操作。对于每个操作,调用者都需要提供目标文件的句柄、输出缓冲区、文件的偏移量和要读取的内存量。这一切都是在稍后讨论的多个新数据结构中完成的。但首先,调用者需要初始化其 I/O 环。
创建和初始化 I/O 环
为此,系统提供了一个新的系统调用——NtCreateIoRing。这个函数创建一个新的 IoRing 对象类型的实例,这里描述为 IORING_OBJECT:
NtCreateIoRing 接收一个新结构作为输入参数——IO_RING_STRUCTV1。此结构包含有关当前版本的信息,目前只能为 1、必需和建议标志(目前均不支持除 0 以外的任何值)以及提交队列和完成队列的请求大小。
该函数接收此信息并执行以下操作:
1.验证所有输入和输出参数——它们的地址、大小对齐等。
2.检查请求的提交队列大小并根据请求的条目数计算提交队列所需的内存量。如果 SubmissionQueueSize 超过 0x10000,则会返回一个新的错误状态 STATUS_IORING_SUBMISSION_QUEUE_TOO_BIG。
3.检查完成队列大小并计算它所需的内存量。完成队列限制为 0x20000 个条目,如果请求更大的数量,则返回错误代码 STATUS_IORING_COMPLETION_QUEUE_TOO_BIG。
4.创建一个 IoRingObjectType 类型的新对象并初始化所有可以在此时初始化的字段——标志、提交队列大小和掩码、事件等。
5.为队列创建一个section,将它映射到系统空间并创建一个 MDL 来支持它。然后在用户空间中映射相同的section。该section将包含提交空间和完成空间,应用程序将使用该部分将所有请求的 I/O 操作的参数与内核通信并接收状态代码。
使用提交队列地址和其他要返回给调用者的数据初始化输出结构。
6.NtCreateIoRing 成功返回后,调用者可以将其数据写入提供的提交队列。队列将有一个队列头,后跟一个 NT_IORING_SQE 结构数组,每个结构代表一个请求的 I/O 操作。标题描述了此时应该处理哪些条目:
队列头使用 QueueIndex 和 QueueCount 字段描述了哪些条目应该是进程。 QueueIndex 指定要处理的第一个条目的索引,而 QueueCount 指定条目的数量。 QueueIndex + QueueCount 必须低于条目总数。
每个队列条目都包含有关请求操作的数据:文件句柄、文件偏移量、输出缓冲区基数、偏移量和要读取的数据量。它还包含一个 OpCode 字段来指定请求的操作。
I/O 环操作代码
调用者可以请求四种可能的操作类型:
1.IORING_OP_READ:请求系统从文件读取数据到输出缓冲区。文件句柄将从提交队列条目中的 FileRef 字段中读取。这将被解释为文件句柄或预注册的文件句柄数组的索引,具体取决于是否在队列条目标志字段中设置了 IORING_SQE_PREREGISTERED_FILE 标志 (1)。输出将写入条目的 Buffer 字段中提供的输出缓冲区。与 FileRef 类似,如果设置了 IORING_SQE_PREGISTERED_BUFFER 标志 (2),则该字段可以改为包含预注册的输出缓冲区数组的索引。
2.IORING_OP_REGISTERED_FILES:请求预先注册文件句柄以便稍后处理。在这种情况下,队列条目的 Buffer 字段指向文件句柄数组。请求的文件句柄将被复制并放置在队列条目的 FileHandleArray 字段中的新数组中,FilesRegistered 字段将包含文件句柄的数量。
3.IORING_OP_REGISTERED_BUFFERS:请求为要读入的文件数据预先注册输出缓冲区。在这种情况下,条目中的 Buffer 字段应包含一个 IORING_BUFFER_INFO 结构数组,描述将读取文件数据的缓冲区的地址和大小:
缓冲区的地址和大小将被复制到一个新数组中,并放置在提交队列的 BufferArray 字段中。 BuffersRegistered 字段将包含缓冲区的数量。
4.IORING_OP_CANCEL:请求取消文件的挂起操作。就像在 IORING_OP_READ 中一样,FileRef 可以是句柄或文件句柄数组的索引,具体取决于标志。在这种情况下,Buffer 字段指向要为文件取消的 IO_STATUS_BLOCK。
所有这些选项可能有点令人困惑,因此以下是基于请求标志的 4 种不同阅读场景的插图:
标志为 0,使用 FileRef 字段作为文件句柄,使用 Buffer 字段作为指向输出缓冲区的指针:
请求标志 IORING_SQE_PREREGISTERED_FILE (1),因此 FileRef 被视为预注册文件句柄数组的索引,而 Buffer 是指向输出缓冲区的指针:
请求标志 IORING_SQE_PREGISTERED_BUFFER (2),因此 FileRef 是文件的句柄,而 Buffer 被视为预注册输出缓冲区数组的索引:
IORING_SQE_PREREGISTERED_FILE 和 IORING_SQE_PREGISTERED_BUFFER 标志都已设置,因此 FileRef 被视为预注册文件句柄数组的索引,而 Buffer 被视为预注册缓冲区数组的索引:
提交和处理 I/O 环
一旦调用者设置了所有提交队列条目,它就可以调用 NtSubmitIoRing 将其请求提交给内核,以根据请求的参数进行处理。在内部,NtSubmitIoRing 遍历所有条目并调用 IopProcessIoRingEntry,发送 IoRing 对象和当前队列条目。该条目根据指定的 OpCode 进行处理,然后调用 IopIoRingDispatchComplete 以填充完成队列。完成队列与提交队列非常相似,以包含队列索引和计数的头部开始,后面是一个条目数组。每个条目都是一个IORING_CQE结构——它有来自提交队列条目的UserData值,以及来自IO_STATUS_BLOCK的用于操作的状态和信息:
一旦完成所有请求的条目,系统在 IoRingObject->RingEvent 中设置事件。只要不是所有条目都完成,系统将使用从调用者接收到的 Timeout 等待事件,并在所有请求完成时唤醒,导致事件发出信号,或超时到期。
由于可以处理多个条目,返回给调用者的状态将是指示无法处理条目的错误状态或 KeWaitForSingleObject 的返回值。单个操作的状态代码可以在完成队列中找到,所以不要把从 NtSubmitIoRing 接收到 STATUS_SUCCESS 代码与成功的读取操作混淆。
使用 I/O 环——官方方式
像其他系统调用一样,这些新的 IoRing 函数没有文档化,也不打算直接使用。相反,KernelBase.dll 提供了方便的封装函数,这些函数接收易于使用的参数并在内部处理所有需要发送到内核的未记录的函数和数据结构。有创建、查询、提交和关闭 IoRing 的函数,以及为前面讨论的四种不同操作构建队列条目的辅助函数。
CreateIoRing
CreateIoRing 接收有关标志和队列大小的信息,并在内部调用 NtCreateIoRing 并将句柄返回给 IoRing 实例:
这个新的句柄类型实际上是一个指向一个未记录结构的指针,该结构包含从 NtCreateIoRing 返回的结构和管理这个 IoRing 实例所需的其他数据:
所有其他 IoRing 函数都将接收这个句柄作为它们的第一个参数。
创建 IoRing 实例后,应用程序需要为所有请求的 I/O 操作构建队列条目。由于没有记录队列的内部结构和队列条目结构,KernelBase.dll 导出帮助函数以使用调用者提供的输入数据构建这些函数。四个函数如下:
BuildIoRingReadFile BuildIoRingRegisterBuffers BuildIoRingRegisterFileHandles BuildIoRingCancelRequest
每个函数创建都会向提交队列添加一个新的队列条目,其中包含所需的操作码和数据。它们的目的可以从其名称中看出来,但为了清楚起见,让我们一一介绍:
BuildIoRingReadFile
该函数接收由 CreateIoRing 返回的句柄,后跟两个指向新数据结构的指针。这两个结构都有一个 Kind 字段,它可以是 IORING_REF_RAW,表示提供的值是原始引用,也可以是 IORING_REF_REGISTERED,表示该值是预注册数组的索引。第二个字段是值和索引的联合,其中将提供文件句柄或缓冲区。
BuildIoRingRegisterFileHandles 和 BuildIoRingRegisterBuffers
这两个函数创建提交队列条目以预注册文件句柄和输出缓冲区。两者都接收从 CreateIoRing 返回的句柄、数组中预注册文件/缓冲区的计数、要注册的句柄或缓冲区数组以及 UserData。
在 BuildIoRingRegisterFileHandles 中,Handles 是指向文件句柄数组的指针,在 BuildIoRingRegisterBuffers 中,Buffers 是指向包含缓冲区基数和大小的 IORING_BUFFER_INFO 结构数组的指针。
BuildIoRingCancelRequest
就像其他函数一样,BuildIoRingCancelRequest 接收从 CreateIoRing 返回的句柄作为它的第一个参数。第二个参数同样是指向 IORING_REQUEST_DATA 结构的指针,该结构包含指向应取消操作的文件的句柄或文件句柄数组中的索引。第三个和第四个参数是要放入队列条目的输出缓冲区和 UserData。
在使用这些函数构建所有队列条目后,可以提交队列:
SubmitIoRing
该函数接收与用于初始化 IoRing 和提交队列的第一个参数相同的句柄。然后它接收要提交的条目数量、等待操作完成的时间(以毫秒为单位),以及一个指向将接收已提交条目数量的输出参数的指针。
GetIoRingInfo
此 API 使用新结构返回有关 IoRing 当前状态的信息:
它包含IoRing的版本和标志,以及提交和完成队列的当前大小。
一旦完成了对IoRing的所有操作,就需要使用CloseIoRing关闭它,CloseIoRing接收句柄作为其唯一参数,并关闭IoRing对象的句柄,并释放用于该结构的内存。
到目前为止,我在系统上找不到使用此功能的任何内容,但是一旦 21H2 发布,我预计会看到大量I/ o的Windows应用程序开始使用它,可能主要是在服务器和azure环境中。
总结
到目前为止,Windows 中的 I/O中没有关于这个新添加的公共文档,但是当21H2在今年晚些时候发布的时候,我们将看到所有这些都被正式记录下来,并被Windows和第三方应用程序使用。如果使用得当,对于频繁进行读取操作的应用程序,这可能会显著提高性能。像每个新功能和系统调用一样,这也可能产生意想不到的安全影响。hfire0x已经发现了一个漏洞,他是第一个公开提到这个功能的人,并通过向NtCreateIoRing发送一个错误的参数使系统崩溃,这个漏洞后来被修复了。
本文翻译自: https://windows-internals.com/i-o-rings-when-one-i-o-operation-is-not-enough/如若转载,请注明原文地址