linux对于io_uring的应用
1、内核支持io_uring
最近发现linux新增了2个补丁,关于支持io_uring,事实上linux在5.1版本就引入了io_uring,有点好奇这次的补丁是实现什么功能
看了一下文档,这个补丁通过 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%) |
-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 内存提供者相关的信息。
- 在 page-pool 的属性中,除了原来的
1.2 修改 UAPI 头文件 tools/include/uapi/linux/netdev.h 和 include/uapi/linux/netdev.h
- 新增枚举值
- 分别为 page-pool 和队列属性增加了
NETDEV_A_PAGE_POOL_IO_URING
与NETDEV_A_QUEUE_IO_URING
枚举值,以及NETDEV_A_IO_URING_PROVIDER_INFO_MAX
。 - 这些修改使得内核在通过 Netlink 通知用户空间时,能传递与 io_uring 内存提供相关的配置信息。
- 分别为 page-pool 和队列属性增加了
二、内存向量与页面池相关改动
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 数组,包含:
- 新增内联辅助函数
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;init
和destroy
:初始化和销毁内存提供者;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_niovs
为owner->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,从而适配新的内存提供者机制。
四、总结
这份补丁的主要改动可以总结为以下几点:
- 扩展 Netlink/UAPI 接口
- 在文档和 UAPI 头文件中新增了 io_uring 内存提供者相关的属性,方便用户空间配置与监控。
- 引入统一的内存提供者接口
- 通过新增 memory_provider_ops 接口,统一管理各种内存提供者(如 dmabuf 和未来的 io_uring 零拷贝方案)的分配、释放、初始化、销毁和卸载等操作。
- 修改页面池(page pool)和 netmem 相关数据结构,引入 net_iov_area 来代替原来的直接指针,并新增辅助函数。
- 修改核心网络代码以支持新接口
- 在网络设备注销、RX 队列绑定、页面池操作和 TCP 接收逻辑中,均调用新的内存提供者接口,从而实现对内存提供者(包括未来的 io_uring 零拷贝)的统一支持。
总体来说,这套改动为内核网络子系统引入了更灵活、统一的内存管理机制,为未来支持基于 io_uring 的零拷贝网络接收铺平了道路。通过这一改动,数据在网络接收过程中可以直接从硬件 DMA 到用户空间内存(零拷贝),减少了不必要的数据复制,从而在高吞吐量、低延迟的场景下提升系统性能。
5、接口
io_uring_setup
原型:
int io_uring_setup(unsigned entries, struct io_uring_params *p);
作用:创建一个
io_uring
实例,初始化提交队列和完成队列,并返回一个文件描述符(fd
)来与内核进行后续的 I/O 操作。参数
:
entries
:提交队列和完成队列的条目数。p
:一个io_uring_params
结构体,用于传递内核参数(如队列大小、特性标志等)。
返回值:成功返回一个文件描述符,用于后续操作;失败返回负数错误码。
io_uring_enter
原型:
int io_uring_enter(int fd, unsigned to_submit, unsigned min_complete, unsigned flags, sigset_t *sig);
作用:提交 I/O 请求并等待完成。它将请求从用户空间提交到内核,并在必要时等待 I/O 请求的完成。
参数
:
fd
:io_uring
实例的文件描述符。to_submit
:提交的 I/O 请求数量。min_complete
:最小完成队列条目数,表示内核完成的请求数量。flags
:标志位,控制操作的行为。sig
:信号掩码,指定在等待时要屏蔽的信号。
返回值:成功返回已提交的条目数;失败返回负数错误码。
io_uring_register
原型:
int io_uring_register(int fd, unsigned opcode, void *arg, unsigned arg2);
作用:用于注册一些额外的内核行为,例如注册文件描述符、缓冲区等。此函数允许用户为
io_uring
实例注册一些资源,供后续 I/O 操作使用。参数
:
fd
:io_uring
实例的文件描述符。opcode
:操作码,表示要执行的注册操作类型(如注册文件描述符、缓冲区等)。arg
:操作所需的参数(例如文件描述符、缓冲区地址等)。arg2
:额外的参数(根据操作类型不同而不同)。
返回值:成功时返回 0,失败时返回负数错误码。
io_uring_unregister
原型:
int io_uring_unregister(int fd, unsigned opcode);
作用:撤销之前注册的资源,例如取消对某些文件描述符的注册。
参数
:
fd
:io_uring
实例的文件描述符。opcode
:操作码,指定要撤销的注册资源。
返回值:成功时返回 0,失败时返回负数错误码。
io_uring_peek_cqe
原型:
int io_uring_peek_cqe(int fd, struct io_uring_cqe **cqe);
作用:检查完成队列中是否有完成的 I/O 请求,而不阻塞。如果有,返回一个指向完成队列条目的指针。
参数
:
fd
:io_uring
实例的文件描述符。cqe
:指向指针的指针,用于返回完成队列条目。
返回值:成功时返回 0,失败时返回负数错误码。
io_uring_wait_cqe
原型:
int io_uring_wait_cqe(int fd, struct io_uring_cqe **cqe);
作用:等待并返回一个已完成的 I/O 请求的完成队列条目。
参数
:
fd
:io_uring
实例的文件描述符。cqe
:指向指针的指针,用于返回完成队列条目。
返回值:成功时返回 0,失败时返回负数错误码。
io_uring_cq_advance
原型:
void io_uring_cq_advance(int fd, unsigned count);
作用:推进完成队列,通常在用户空间处理完一个或多个完成队列条目后调用,以通知内核已处理。
参数
:
fd
:io_uring
实例的文件描述符。count
:推进的完成队列条目数。
返回值:无返回值。