io_uring的机理和跟epoll的对比

在高性能网络编程的领域,我们一直在追求更低的延迟、更高的吞吐量。从 select、poll 到 epoll,每一次演进都带来了巨大的性能提升。而今天,io_uring 的出现,正引领着一场更深刻的变革。它不仅仅是 epoll 的简单升级,而是一种从根本上改变应用与内核交互方式的全新范式。

深入 io_uring 的内部:一套高效的"零拷贝"物流系统

io_uring 的高性能并非魔法,而是源于其精巧的内部设计。它在你的应用程序和内核之间,建立了一套基于共享内存的高效"物流系统"。

这套系统由两个核心组件构成:

  1. 提交队列 (Submission Queue - SQ): 你的"发货单"暂存区。

  2. 完成队列 (Completion Queue - CQ) : 内核送回的"收货回执"暂存区。

这两个队列存在于通过 mmap 创建的共享内存中,这意味着你的应用和内核可以像访问自家内存一样读写它们,彻底消除了传统系统调用中昂贵的内存拷贝开销

整个工作流程如下:

  1. 打包任务 (构建 SQE) : 你需要发的每一个 I/O 请求,都会被打包成一个"任务单元",即 io_uring_sqe (Submission Queue Entry)。这个结构体里详细记录了任务的所有信息:要操作的 fd、数据要读/写到哪个 buffer (addr)、buffer 的多长 (len)、具体的操作类型 (opcode) 等。

  2. "零拷贝"入队: 你的应用直接在共享内存的 SQ 中填写这个 SQE。这并非传统意义的拷贝,而是直接写入,效率极高。

  3. 通知内核: 通过一次 io_uring_submit() 系统调用(这是一个非常轻量的调用),直接批量把整个队列里的请求直接提交到系统里面。

  4. 内核异步处理: 内核的工作线程被唤醒,它直接从共享内存的 SQ 中取出任务,然后发起真正的异步 I/O 操作。在此期间,你的应用程序完全是自由的。

  5. 结果返回 : I/O 操作完成后,内核会在共享内存的 CQ 中放入一个"完成回执",即 io_uring_cqe (Completion Queue Entry),里面包含了操作的结果(如成功读取的字节数)。(注意此时想读写缓存里的内容已经被读进到咱们预先设置好的buffer 里面了)

  6. 应用获取结果: 你的应用通过检查 CQ 来获取已完成的任务结果,然后直接进行业务处理。

这套机制的核心优势在于:批量化、全异步、零拷贝

qps测试对比:

100W个包每个包 2 * 64Bytes

100W个包每个包 4 * 64Bytes

cpp 复制代码
#include <stdio.h>
#include <liburing.h>      // 包含了所有 io_uring 的函数和数据结构定义
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>


// 定义三种不同的事件类型,用于在请求完成时区分是哪种操作
#define EVENT_ACCEPT   	0  // 接受新连接事件
#define EVENT_READ		1  // 读数据事件
#define EVENT_WRITE		2  // 写数据事件

/**
 * @brief 连接信息结构体
 * 这个结构体作为上下文信息,会被附加到每一个提交给 io_uring 的请求 (SQE) 上。
 * 当请求完成后,内核会原封不动地将它返回到完成队列 (CQE) 中。
 * 这样,我们就能知道完成的这个事件是关于哪个 fd、属于哪种类型的操作。
 */
struct conn_info {
	int fd;     // 事件关联的文件描述符
	int event;  // 事件的类型 (EVENT_ACCEPT, EVENT_READ, or EVENT_WRITE)
};


/**
 * @brief 初始化TCP服务器
 * 这是一个标准的网络编程函数,用于创建、绑定和监听一个套接字。
 * @param port 服务器监听的端口号
 * @return 创建的监听套接字文件描述符
 */
int init_server(unsigned short port) {	

	int sockfd = socket(AF_INET, SOCK_STREAM, 0);	
	struct sockaddr_in serveraddr;	
	memset(&serveraddr, 0, sizeof(struct sockaddr_in));	
	serveraddr.sin_family = AF_INET;	
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);	
	serveraddr.sin_port = htons(port);	

	if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))) {		
		perror("bind");		
		return -1;	
	}	

	listen(sockfd, 10);
	
	return sockfd;
}



#define ENTRIES_LENGTH		1024  // io_uring 队列的深度,表示可以同时处理多少个在途请求
#define BUFFER_LENGTH		1024  // 每个连接的读写缓冲区大小

/**
 * @brief 准备一个异步接收数据的请求
 * @param ring io_uring 实例的指针
 * @param sockfd 要接收数据的文件描述符 (连接 fd)
 * @param buf 接收数据的缓冲区
 * @param len 缓冲区的长度
 * @param flags 接收操作的标志,通常为0
 * @return int 
 */
int set_event_recv(struct io_uring *ring, int sockfd,
				      void *buf, size_t len, int flags) {
    
    // 1. 从提交队列 (Submission Queue, SQ) 中获取一个可用的空槽位 (Submission Queue Entry, SQE)
	struct io_uring_sqe *sqe = io_uring_get_sqe(ring);

    // 2. 创建本次请求的上下文信息
	struct conn_info read_info = {
		.fd = sockfd,
		.event = EVENT_READ, // 标记这是一个读事件
	};
	
    // 3. 使用 liburing 提供的帮助函数来填充 SQE,告诉内核这是一个 recv 操作
	io_uring_prep_recv(sqe, sockfd, buf, len, flags);

    // 4. 将我们的上下文信息拷贝到 SQE 的 user_data 字段中
	memcpy(&sqe->user_data, &read_info, sizeof(struct conn_info));
    return 0;
}


/**
 * @brief 准备一个异步发送数据的请求
 */
int set_event_send(struct io_uring *ring, int sockfd,
				      void *buf, size_t len, int flags) {
    
    // 步骤同 set_event_recv
	struct io_uring_sqe *sqe = io_uring_get_sqe(ring);

	struct conn_info write_info = {
		.fd = sockfd,
		.event = EVENT_WRITE, // 标记这是一个写事件
	};
	
	io_uring_prep_send(sqe, sockfd, buf, len, flags);
	memcpy(&sqe->user_data, &write_info, sizeof(struct conn_info));
    return 0;
}


/**
 * @brief 准备一个异步接受新连接的请求
 */
int set_event_accept(struct io_uring *ring, int sockfd, struct sockaddr *addr,
					socklen_t *addrlen, int flags) {

    // 步骤同上
	struct io_uring_sqe *sqe = io_uring_get_sqe(ring);

	struct conn_info accept_info = {
		.fd = sockfd,        // 这里是监听的 sockfd
		.event = EVENT_ACCEPT, // 标记这是一个 accept 事件
	};
	
	io_uring_prep_accept(sqe, sockfd, (struct sockaddr*)addr, addrlen, flags);
	memcpy(&sqe->user_data, &accept_info, sizeof(struct conn_info));
    return 0;
}




int main(int argc, char *argv[]) {

	unsigned short port = 9999;
	int sockfd = init_server(port);

	struct io_uring_params params;
	memset(&params, 0, sizeof(params));

	struct io_uring ring;
    // 初始化 io_uring 实例。这是最关键的一步。
    // 内核会创建提交队列 (SQ) 和完成队列 (CQ) 所需的共享内存。
	io_uring_queue_init_params(ENTRIES_LENGTH, &ring, &params);
	
    // 准备并提交第一个 accept 请求。
    // 服务器必须先提交一个 accept 请求,才能接收客户端的连接。
	struct sockaddr_in clientaddr;	
	socklen_t len = sizeof(clientaddr);
	set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len, 0);
	
	char buffer[BUFFER_LENGTH] = {0};

    // 进入主事件循环
	while (1) {
        
        // 1. 提交请求
        // 调用 io_uring_submit(),这是一个系统调用。
        // 它会通知内核去检查提交队列 (SQ) 中是否有新的请求需要处理。
        // 我们可以一次性准备多个请求,然后通过一次 submit 全部提交。
		io_uring_submit(&ring);

        // 2. 等待完成事件
		struct io_uring_cqe *cqe;
        // io_uring_wait_cqe() 会阻塞程序,直到完成队列 (Completion Queue, CQ) 中至少有一个完成事件 (CQE)。
        // 这类似于 epoll_wait() 的作用。
		io_uring_wait_cqe(&ring, &cqe);

        // 3. 批量获取完成事件
		struct io_uring_cqe *cqes[128];
        // io_uring_peek_batch_cqe() 尝试从 CQ 中一次性取出多个已完成的事件,非常高效。
        // 它返回实际获取到的事件数量。
		int nready = io_uring_peek_batch_cqe(&ring, cqes, 128);

		int i = 0;
        // 4. 遍历并处理所有已完成的事件
		for (i = 0; i < nready; i++) {

			struct io_uring_cqe *entries = cqes[i];
			struct conn_info result;
            // 从 CQE 的 user_data 中拷贝出我们之前存入的上下文信息
			memcpy(&result, &entries->user_data, sizeof(struct conn_info));

            // 根据事件类型,执行不同的逻辑
			if (result.event == EVENT_ACCEPT) {
                
                // 一个 accept 请求完成了,意味着一个新客户端连接进来了。
                // 我们需要立刻提交下一个 accept 请求,以便能继续接收其他客户端。
				set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len, 0);
				
                // entries->res 字段存放了 I/O 操作的结果。
                // 对于 accept 操作,它就是新连接的文件描述符 connfd。
				int connfd = entries->res;

                // 为这个新连接提交一个读请求,准备接收客户端发来的数据。
				set_event_recv(&ring, connfd, buffer, BUFFER_LENGTH, 0);

			} else if (result.event == EVENT_READ) {  // 一个读请求完成了

                // 对于读写操作,entries->res 是成功读/写的字节数。
				int ret = entries->res;

				if (ret == 0) { // ret 为 0 表示客户端关闭了连接
					close(result.fd);
				} else if (ret > 0) { // 成功读取到数据
                    // 提交一个写请求,将刚刚读取到的数据原样发回 (Echo Server)
					set_event_send(&ring, result.fd, buffer, ret, 0);
				}
			}  else if (result.event == EVENT_WRITE) { // 一个写请求完成了
				
                // 数据已经成功发回给客户端。
                // 继续为这个连接提交一个读请求,等待客户端的下一次数据。
				set_event_recv(&ring, result.fd, buffer, BUFFER_LENGTH, 0);
			}
		}

        // 5. 标记事件已消费
        // io_uring_cq_advance() 告诉内核,我们已经处理完了 nready 个完成事件。
        // 内核可以安全地重用这部分完成队列的空间了。
        // 这一步非常重要,必须调用!
		io_uring_cq_advance(&ring, nready);
	}

}

结论:

epoll 已经足够优秀,但它仍然遵循"应用查询就绪态,应用执行I/O"的两步走模式。而 io_uring 则通过 Proactor 模式和基于共享内存的无锁队列,将这一过程简化为"应用提交任务,内核完成任务"的一步到位模式。极致的性能: 大幅减少了系统调用次数和用户态/内核态的切换,消除了内存拷贝,最大化地利用了CPU。

相关推荐
猷咪2 小时前
C++基础
开发语言·c++
IT·小灰灰2 小时前
30行PHP,利用硅基流动API,网页客服瞬间上线
开发语言·人工智能·aigc·php
快点好好学习吧2 小时前
phpize 依赖 php-config 获取 PHP 信息的庖丁解牛
android·开发语言·php
秦老师Q2 小时前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
烟锁池塘柳02 小时前
解决Google Scholar “We‘re sorry... but your computer or network may be sending automated queries.”的问题
开发语言
是誰萆微了承諾2 小时前
php 对接deepseek
android·开发语言·php
vx_BS813302 小时前
【直接可用源码免费送】计算机毕业设计精选项目03574基于Python的网上商城管理系统设计与实现:Java/PHP/Python/C#小程序、单片机、成品+文档源码支持定制
java·python·课程设计
2601_949868362 小时前
Flutter for OpenHarmony 电子合同签署App实战 - 已签合同实现
java·开发语言·flutter
星火开发设计2 小时前
类型别名 typedef:让复杂类型更简洁
开发语言·c++·学习·算法·函数·知识
qq_177767372 小时前
React Native鸿蒙跨平台数据使用监控应用技术,通过setInterval每5秒更新一次数据使用情况和套餐使用情况,模拟了真实应用中的数据监控场景
开发语言·前端·javascript·react native·react.js·ecmascript·harmonyos