使用Go进行io_uring的动手实践( 二 )

  • 地址 :对于我们的 readv 调用 , 它将创建一个缓冲区(或向量)数组以将数据读入其中 。因此 , 地址字段包含该数组的地址 。
  • Length :向量数组的长度 。
  • 用户数据 :一个标识符 , 用于将我们的请求从完成队列中移出 。请记住 , 不能保证完成结果的顺序与SQE相同 。那会破坏使用异步API的全部目的 。因此 , 我们需要一些东西来识别我们提出的请求 。这达到了目的 。通常 , 这是指向一些保存有请求元数据的结构的指针 。
  • 在完成方面 , 我们从CQ获得完成队列事件(CQE) 。这是一个非常简单的结构 , 其中包含:
    • 结果 : 的返回值 readv syscall。如果成功 , 它将读取字节数 。否则 , 它将具有错误代码 。
    • 用户数据 :我们在SQE中传递的标识符 。
    这里只需要注意一个重要的细节:SQ和CQ在用户和内核之间共享 。但是 , 尽管CQ实际上包含CQE , 但对于SQ而言却有所不同 。它本质上是一个间接层 , 其中SQ数组中的索引值实际上包含保存SQE项的实际数组的索引 。这对于某些在内部结构中具有提交请求的应用程序很有用 , 因此允许它们在一个操作中提交多个请求 , 从本质上简化了 的采用 io_uring API。
    这意味着我们实际上在内存中映射了三件事:提交队列 , 完成队列和提交队列数组 。下图应使情况更清楚:
    使用Go进行io_uring的动手实践文章插图
    现在 , 让我们重新访问 的 flags 之前跳过 字段 。正如我们所讨论的 , CQE条目可能完全不同于队列中提交的条目 。这带来了一个有趣的问题 。如果我们要一个接一个地执行一系列I / O操作怎么办? 例如 , 文件副本 。我们想从文件描述符中读取并写入另一个文件 。在当前状态下 , 我们甚至无法开始提交写入操作 , 直到看到CQ中出现读取事件为止 。那就是 的地方 flags 进来。
    我们可以 设置 IOSQE_IO_LINK 在 flags 现场 以实现这一目标 。如果设置了此选项 , 则下一个SQE将自动链接到该SQE , 直到当前SQE完成后它才开始 。这使我们能够按所需方式对I / O事件执行排序 。文件复制只是一个示例 。从理论上讲 , 我们可以 链接 任何 彼此 系统调用 , 直到在未设置该字段的情况下推送SQE , 此时该链被视为已损坏 。
    系统调用通过对 简要概述 io_uring 操作方式的, 让我们研究实现它的实际系统调用 。只有两个 。
    1. int io_uring_setup(unsigned entries, struct io_uring_params *params);
    的 entries 表示SQEs的数量为该环 。params 是一个结构 , 其中包含有关应用程序要使用的CQ和SQ的各种详细信息 。它向该 返回文件描述符 io_uring 实例。
    1. int io_uring_enter(unsigned int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t sig);
    该调用用于向内核提交请求 。让我们快速浏览以下重要内容:
    fdto_submitmin_complete精明的读者会注意到 ,中具有 to_submit 和 min_complete 在相同的调用 意味着我们可以使用它来仅提交 , 或仅完成 , 甚至两者! 这将打开API , 以根据应用程序工作负载以各种有趣的方式使用 。
    轮询模式对于延迟敏感的应用程序或具有极高IOPS的应用程序 , 每次有可用数据读取时让设备驱动程序中断内核是不够高效的 。如果我们要读取大量数据 , 那么高中断率实际上会减慢用于处理事件的内核吞吐量 。在这些情况下 , 我们实际上会退回轮询设备驱动程序 。要将轮询与一起使用 io_uring, 我们可以 设置 IORING_SETUP_IOPOLL 在 标志 io_uring_setup 呼叫中, 并将轮询事件与 的 保持一致 IORING_ENTER_GETEVENTS 设置 io_uring_enter 呼叫中。
    但这仍然需要我们(用户)拨打电话 。为了提高性能 ,,io_uring 它还具有称为“内核侧轮询”的功能 通过该功能 , 如果将 设置为 IORING_SETUP_SQPOLL 标志 io_uring_params, 内核将自动轮询SQ以检查是否有新条目并使用它们 。这基本上意味着我们可以继续做所有的I /我们想?不执行甚至一个 单一的系统打电话。这改变了一切 。
    但是 , 所有这些灵活性和原始功率都是有代价的 。直接使用此API并非易事且容易出错 。由于我们的数据结构是在用户和内核之间共享的 , 因此我们需要设置内存屏障(神奇的编译器命令以强制执行内存操作的顺序)和其他技巧 , 以正确完成任务 。
    幸运的是 , 的创建者Jens Axboe io_uring 创建了一个包装器库 ,liburing 以帮助简化所有操作 。使用 liburing, 我们大致必须执行以下步骤: