linux对于io_uring的应用

1、内核支持io_uring

最近发现linux新增了2个补丁,关于支持io_uring,事实上linux在5.1版本就引入了io_uring,有点好奇这次的补丁是实现什么功能

https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/commit/?id=71f0dd5a3293d75d26d405ffbaedfdda4836af32

netdev: add io_uring memory provider info - kernel/git/netdev/net-next.git - Netdev Group’s -next networking tree

看了一下文档,这个补丁通过 io_uring 提供了零拷贝接收功能,使得硬件接收到的数据可以直接传输到用户空间,避免了内核和用户空间之间的数据拷贝。具体是内核会预先配置了一个页面池,这些页面是由用户空间应用程序申请的,且当硬件接收到数据时,会通过 DMA 直接将数据传输到这些用户空间页面中,比起常规的数据接收,减少了一次数据拷贝。不过有个问题是,不知道会不会影响tcpdump抓包,毕竟整个流转发生了变化。

2、liburing

目前流行的是liburing这个库,对整个io_uring做了一层封装,github有3k+⭐

3、nginx支持io_uring

nginx目前还没有支持io_uring,io_uring会对nginx的性能有一些提升,尤其是零拷贝,大数据量下肯定会有很大的提升

4、内核补丁

4.1、描述

io_uring 零拷贝 rx

此补丁集包含新 io_uring 请求所需的 net/ 补丁,该请求将零拷贝 rx 实现到用户空间页面,从而消除了内核到用户的复制。

我们配置了一个页面池,驱动程序使用它来填充 hw rx 队列以分发用户页面而不是内核页面。因此,任何最终到达此 hw rx 队列的数据都将直接通过 dma 进入用户空间内存,而无需通过内核内存反弹。从套接字中“读取”数据反而成为一种_通知_机制,内核会告诉用户空间数据在哪里。总体方法类似于 devmem TCP 提案。

这依赖于 hw 标头/数据拆分、流控制和 RSS,以确保数据包标头保留在内核内存中,并且只有所需的流才会到达配置为零拷贝的 hw rx 队列。配置它超出了此补丁集的范围。

我们与 devmem TCP 共享 netdev 核心基础设施。主要区别在于 io_uring 用于 uAPI,并且所有对象的生命周期都绑定到 io_uring 实例。使用新的 io_uring 请求类型“读取”数据。完成后,数据通过新的共享重新填充队列返回。零拷贝页面池直接从此重新填充队列重新填充 hw rx 队列。当然,这些数据缓冲区的生命周期由 io_uring 而不是网络堆栈管理,具有不同的引用计数规则。

此补丁集是添加基本零拷贝支持的第一步。我们将使用新功能迭代扩展它,例如动态分配的零拷贝区域、THP 支持、dmabuf 支持、改进的复制回退、一般优化等。

在 netdev 支持方面,我们首先针对 Broadcom bnxt。补丁不包括在内,因为 Taehee Yoo 已经在 [1] 中发送了一个更全面的补丁集来添加支持。 Google gve 应该已经支持此功能,
而 Mellanox mlx5 支持仍在进行中,有待驱动程序更改。

========================================== Performance ==========================================

注意:与 epoll + TCP_ZEROCOPY_RECEIVE 的比较尚未完成。

测试设置:

  • AMD EPYC 9454
  • Broadcom BCM957508 200G
  • 内核 v6.11 基础 [2]
  • liburing fork [3]
  • kperf fork [4]
  • 4K MTU
  • 单个 TCP 流

将应用程序线程 + net rx softirq 固定到 不同 核心:

使用应用程序线程 + net rx softirq 固定到 不同 核心:

epoll io_uring
82.2 Gbps 116.2 Gbps (+41%)

固定到 相同 核心:

epoll io_uring
62.6 Gbps 80.9 Gbps (+29%)

Diffstat

-rw-r–r– Documentation/netlink/specs/netdev.yaml 15
-rw-r–r– include/net/netmem.h 21
-rw-r–r– include/net/page_pool/memory_provider.h 45
-rw-r–r– include/net/page_pool/types.h 4
-rw-r–r– include/uapi/linux/netdev.h 7
-rw-r–r– net/core/dev.c 16
-rw-r–r– net/core/devmem.c 93
-rw-r–r– net/core/devmem.h 49
-rw-r–r– net/core/netdev-genl.c 11
-rw-r–r– net/core/netdev_rx_queue.c 69
-rw-r–r– net/core/page_pool.c 51
-rw-r–r– net/core/page_pool_user.c 7
-rw-r–r– net/ipv4/tcp.c 7
-rw-r–r– tools/include/uapi/linux/netdev.h 7

4.2、代码解析

一、Netlink 及 UAPI 接口扩展

1.1 修改 Documentation/netlink/specs/netdev.yaml

  • 新增属性集 “io-uring-provider-info”
    • 在属性集定义中增加了一个名为 io-uring-provider-info 的空属性集,作为嵌套属性使用的模板。
  • 扩展 page-pool 属性
    • 在 page-pool 的属性中,除了原来的 dmabuf 属性,还增加了一个嵌套属性 io-uring,其嵌套属性集就是前面定义的 io-uring-provider-info
    • 同样,在队列属性中也增加了 io-uring 字段,这样用户空间通过 Netlink 获取设备信息时,可以看到和配置 io_uring 内存提供者相关的信息。

1.2 修改 UAPI 头文件 tools/include/uapi/linux/netdev.h 和 include/uapi/linux/netdev.h

  • 新增枚举值
    • 分别为 page-pool 和队列属性增加了 NETDEV_A_PAGE_POOL_IO_URINGNETDEV_A_QUEUE_IO_URING 枚举值,以及 NETDEV_A_IO_URING_PROVIDER_INFO_MAX
    • 这些修改使得内核在通过 Netlink 通知用户空间时,能传递与 io_uring 内存提供相关的配置信息。

二、内存向量与页面池相关改动

2.1 修改 include/net/netmem.h

  • 更新 net_iov 结构体
    • 原来 net_iov 结构体中 owner 字段由指向 dmabuf_genpool_chunk_owner 修改为指向新定义的 net_iov_area
  • 新增结构体 net_iov_area
    • 用于管理一片区域内的 net_iov 数组,包含:
      • niovs:指向 net_iov 数组的指针;
      • num_niovs:数组中 net_iov 的数量;
      • base_virtual:表示该区域在 dma-buf 中起始的虚拟偏移。
  • 新增内联辅助函数
    • net_iov_owner() 用于获取 net_iov 的所属区域;
    • net_iov_idx() 用于计算 net_iov 在所属区域内的索引。

2.2 修改 include/net/page_pool/types.h

  • 整合内存提供者接口
    • pp_memory_provider_params 结构中增加了一个指向 memory_provider_ops 的指针。
    • 同时,在 struct page_pool 中也新增了同样的指针。这样页面池在初始化、分配、释放、销毁等操作时,可以通过统一的接口调用不同内存提供者的实现。

2.3 新增内存提供者接口定义(include/net/page_pool/memory_provider.h)

  • 定义结构体 memory_provider_ops
    • 包含一系列函数指针,主要操作包括:
      • alloc_netmems:分配 netmem;
      • release_netmem:释放 netmem;
      • initdestroy:初始化和销毁内存提供者;
      • nl_fill:用于通过 Netlink 填充内存提供者相关的信息;
      • uninstall:在设备注销时卸载内存提供者。
  • 辅助函数声明
    • 声明了设置 DMA 地址、关联/解除页面池与 net_iov 的函数,以及打开/关闭 RX 队列时的接口。

三、核心代码中的内存提供者支持

3.1 在 net/core/dev.c 中

  • 新增 dev_memory_provider_uninstall()
    • 遍历设备的所有 RX 队列,检查各队列是否已绑定内存提供者(通过 mp_ops 字段),如果绑定,则调用对应的 uninstall 回调函数。
  • 修改注销流程
    • 在注销设备时,用 dev_memory_provider_uninstall() 替代了原来的 dev_dmabuf_uninstall,确保所有内存提供者资源得到正确清理。

3.2 在 net/core/devmem.c 中

  • 调整对 net_iov 的访问
    • 将原来直接使用 owner->niovs 改为通过 owner->area.niovs,并更新 num_niovsowner->area.num_niovs
  • 更新绑定与释放逻辑
    • 修改了函数 net_devmem_bind_dmabuf_to_queue、net_devmem_alloc_dmabuf、net_devmem_free_dmabuf 等,使其使用新结构来处理内存提供者信息。
  • 实现 dmabuf_devmem_ops
    • 定义了一个静态的 memory_provider_ops 结构 dmabuf_devmem_ops,将初始化、销毁、分配、释放、Netlink 填充(nl_fill)和卸载函数关联起来,为 dmabuf 类型的内存提供者提供实现。

3.3 在 net/core/netdev-genl.c 中

  • 调整 Netlink 填充逻辑
    • 修改 netdev_nl_queue_fill_one 函数,使其在处理 RX 队列时,如果队列绑定了内存提供者,则调用内存提供者的 nl_fill() 回调来填充 Netlink 消息。

3.4 在 net/core/netdev_rx_queue.c 中

  • 新增 RX 队列内存提供者管理函数
    • 实现了 net_mp_open_rxq 和 net_mp_close_rxq 接口,用于在特定 RX 队列上绑定和解绑内存提供者。这包括对 mp_params 的设置和队列重启的调用。

3.5 在 net/core/page_pool.c 与 page_pool_user.c 中

  • 整合内存提供者接口
    • 在页面池初始化、分配、释放和销毁过程中,均通过检查 pool->mp_ops 是否设置,来决定是否调用内存提供者的相关接口。
    • 新增辅助函数 net_mp_niov_set_dma_addr、net_mp_niov_set_page_pool、net_mp_niov_clear_page_pool,分别用于设置 DMA 地址、关联和解除页面池与 net_iov 之间的关系。
  • 更新 Netlink 填充逻辑
    • 在 page_pool_nl_fill 函数中,调用内存提供者的 nl_fill() 回调,以将相关信息填入消息。

3.6 在 net/ipv4/tcp.c 中

  • 修改 TCP 接收逻辑
    • 在 tcp_recvmsg_dmabuf 函数中,将原来调用 net_iov_binding_id 改为 net_devmem_iov_binding_id,以使用新接口获取绑定 ID,从而适配新的内存提供者机制。

四、总结

这份补丁的主要改动可以总结为以下几点:

  1. 扩展 Netlink/UAPI 接口
    • 在文档和 UAPI 头文件中新增了 io_uring 内存提供者相关的属性,方便用户空间配置与监控。
  2. 引入统一的内存提供者接口
    • 通过新增 memory_provider_ops 接口,统一管理各种内存提供者(如 dmabuf 和未来的 io_uring 零拷贝方案)的分配、释放、初始化、销毁和卸载等操作。
    • 修改页面池(page pool)和 netmem 相关数据结构,引入 net_iov_area 来代替原来的直接指针,并新增辅助函数。
  3. 修改核心网络代码以支持新接口
    • 在网络设备注销、RX 队列绑定、页面池操作和 TCP 接收逻辑中,均调用新的内存提供者接口,从而实现对内存提供者(包括未来的 io_uring 零拷贝)的统一支持。

总体来说,这套改动为内核网络子系统引入了更灵活、统一的内存管理机制,为未来支持基于 io_uring 的零拷贝网络接收铺平了道路。通过这一改动,数据在网络接收过程中可以直接从硬件 DMA 到用户空间内存(零拷贝),减少了不必要的数据复制,从而在高吞吐量、低延迟的场景下提升系统性能。

5、接口

  1. io_uring_setup
  • 原型int io_uring_setup(unsigned entries, struct io_uring_params *p);

  • 作用:创建一个 io_uring 实例,初始化提交队列和完成队列,并返回一个文件描述符(fd)来与内核进行后续的 I/O 操作。

  • 参数

    • entries:提交队列和完成队列的条目数。
    • p:一个 io_uring_params 结构体,用于传递内核参数(如队列大小、特性标志等)。
  • 返回值:成功返回一个文件描述符,用于后续操作;失败返回负数错误码。

  1. io_uring_enter
  • 原型int io_uring_enter(int fd, unsigned to_submit, unsigned min_complete, unsigned flags, sigset_t *sig);

  • 作用:提交 I/O 请求并等待完成。它将请求从用户空间提交到内核,并在必要时等待 I/O 请求的完成。

  • 参数

    • fdio_uring 实例的文件描述符。
    • to_submit:提交的 I/O 请求数量。
    • min_complete:最小完成队列条目数,表示内核完成的请求数量。
    • flags:标志位,控制操作的行为。
    • sig:信号掩码,指定在等待时要屏蔽的信号。
  • 返回值:成功返回已提交的条目数;失败返回负数错误码。

  1. io_uring_register
  • 原型int io_uring_register(int fd, unsigned opcode, void *arg, unsigned arg2);

  • 作用:用于注册一些额外的内核行为,例如注册文件描述符、缓冲区等。此函数允许用户为 io_uring 实例注册一些资源,供后续 I/O 操作使用。

  • 参数

    • fdio_uring 实例的文件描述符。
    • opcode:操作码,表示要执行的注册操作类型(如注册文件描述符、缓冲区等)。
    • arg:操作所需的参数(例如文件描述符、缓冲区地址等)。
    • arg2:额外的参数(根据操作类型不同而不同)。
  • 返回值:成功时返回 0,失败时返回负数错误码。

  1. io_uring_unregister
  • 原型int io_uring_unregister(int fd, unsigned opcode);

  • 作用:撤销之前注册的资源,例如取消对某些文件描述符的注册。

  • 参数

    • fdio_uring 实例的文件描述符。
    • opcode:操作码,指定要撤销的注册资源。
  • 返回值:成功时返回 0,失败时返回负数错误码。

  1. io_uring_peek_cqe
  • 原型int io_uring_peek_cqe(int fd, struct io_uring_cqe **cqe);

  • 作用:检查完成队列中是否有完成的 I/O 请求,而不阻塞。如果有,返回一个指向完成队列条目的指针。

  • 参数

    • fdio_uring 实例的文件描述符。
    • cqe:指向指针的指针,用于返回完成队列条目。
  • 返回值:成功时返回 0,失败时返回负数错误码。

  1. io_uring_wait_cqe
  • 原型int io_uring_wait_cqe(int fd, struct io_uring_cqe **cqe);

  • 作用:等待并返回一个已完成的 I/O 请求的完成队列条目。

  • 参数

    • fdio_uring 实例的文件描述符。
    • cqe:指向指针的指针,用于返回完成队列条目。
  • 返回值:成功时返回 0,失败时返回负数错误码。

  1. io_uring_cq_advance
  • 原型void io_uring_cq_advance(int fd, unsigned count);

  • 作用:推进完成队列,通常在用户空间处理完一个或多个完成队列条目后调用,以通知内核已处理。

  • 参数

    • fdio_uring 实例的文件描述符。
    • count:推进的完成队列条目数。
  • 返回值:无返回值。


linux对于io_uring的应用
https://zjfans.github.io/2025/02/12/linux对于io_uring的应用/
作者
张三疯
发布于
2025年2月12日
许可协议