【高性能网络(4)】异步io机制——io_uring


前言

本文将介绍io_uring,在设计上它是真正异步的,支持任何类型的I/O,甚至可以重写Linux的每个系统调用。


一、一些概念

同步与异步:

同步:通常是顺序执行,需要等待函数返回结果。
异步:无需马上进入等待,可以自行选择发送报文或等待。

五种IO模型

阻塞IO、非阻塞IO、信号驱动IO、异步IO、IO复用

环形缓冲区:

也叫循环队列,比如一个队列有5个位置,之前存入5个数据会依次占满队列,那么当第六个数据存入时,会计算6除以5余1,于是第六个数据存入第一个位置并覆盖原有数据。

二、io_uring

io_uring: 直译为,用户能使用的用于输入输出的环形缓冲区。

image.png

2.1 io_uring 的好处

  1. 原来需要多次系统调用(读或写),现在变成批处理一次提交(SQ -->> CQ)

io_uring 将这种批处理能力带给了 storage I/O 系统调用之外的 其他一些系统调用

  1. 零拷贝技术:提交读写请求时,会提交一个buffer,内核会直接从这个buffer中读写数据。减少了复制步骤。

2.2 io_uring 的三个系统调用API

int io_uring_setup(u32 entries, struct io_uring_params *p);
// - 创建一个 SQ 和一个 CQ,
// - queue size 至少 entries 个元素,
// - 返回一个文件描述符,随后用于在这个 io_uring 实例上执行操作。

int io_uring_enter(unsigned int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t *sig);
// 这个系统调用用于初始化和完成(initiate and complete)I/O,使用共享的 SQ 和 CQ。 单次调用同时执行:
// 1. 提交新的 I/O 请求
// 2. 等待 I/O 完成

int io_uring_register(unsigned int fd, unsigned int opcode, void *arg, unsigned int nr_args);
// 注册用于异步IO的文件或用户缓冲区

2.3 io_uring 实现 tcp_server

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_queue_init_params(ENTRIES_LENGTH, &ring, &params);
	// 初始化submission queue 、 completed queue
	// 该函数调用了系统调用API : io_uring_setup
	
#if 0
	struct sockaddr_in clientaddr;	
	socklen_t len = sizeof(clientaddr);
	accept(sockfd, (struct sockaddr*)&clientaddr, &len);
#else

	struct sockaddr_in clientaddr;	
	socklen_t len = sizeof(clientaddr);
	set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len, 0);
	
#endif

	char buffer[BUFFER_LENGTH] = {0};

	while (1) {

		io_uring_submit(&ring);
		// 调用系统调用API: io_uring_enter

		struct io_uring_cqe *cqe;
		io_uring_wait_cqe(&ring, &cqe);
		// 取competed queue 的开始位置

		struct io_uring_cqe *cqes[128];
		int nready = io_uring_peek_batch_cqe(&ring, cqes, 128);  // epoll_wait
		// 从开始位置带出 至多【128】的元素

		int i = 0;
		for (i = 0;i < nready;i ++) {

			struct io_uring_cqe *entries = cqes[i];
			struct conn_info result;
			memcpy(&result, &entries->user_data, sizeof(struct conn_info));

			if (result.event == EVENT_ACCEPT) {

				set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len, 0);
				// 在该函数中调用了io_uring_prep_accept,其中与accept不同的点在多了第一个参数&ring
				// 提交请求到sqe里面
				//printf("set_event_accept\n"); //

				int connfd = entries->res;

				set_event_recv(&ring, connfd, buffer, BUFFER_LENGTH, 0);
				// 在该函数中调用了 io_uring_prep_recv,其中与recv不同的点在多了第一个参数&ring
				// 
				
			} else if (result.event == EVENT_READ) {  //

				int ret = entries->res;
				//printf("set_event_recv ret: %d, %s\n", ret, buffer); //

				if (ret == 0) {
					close(result.fd);
				} else if (ret > 0) {
					set_event_send(&ring, result.fd, buffer, ret, 0);
				}
			}  else if (result.event == EVENT_WRITE) {
  //

				int ret = entries->res;
				//printf("set_event_send ret: %d, %s\n", ret, buffer);

				set_event_recv(&ring, result.fd, buffer, BUFFER_LENGTH, 0);
			}
		}

		io_uring_cq_advance(&ring, nready);
		// 清空已经处理过的cq
	}

}

2.4 一些值得考虑的问题

2.4.1 submission queue 的 entry 与 competed queue 的 entry 有什么区别?

是一个节点,共用的是一块内存。

2.4.2 io_uring | epoll

  1. epoll:每一次设置完,等待事件的触发
  2. io_uring:每一次设置完,需要再次设置

2.4.3 reactor 与 proactor 区别?

  1. Reactor 模式要求 主线程(I/O 处理单元) 只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元)。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成
  2. 与 Reactor 模式不同,Proactor 模式 将所有 I/O 操作都交给主线程和内核来处理工作线程仅仅负责业务逻辑

总结

在本文中主要介绍了io_uring给网络编程带来的改变,这是真正的异步io,适用于更高效proactor模式。

参考链接:
https://github.com/0voice
https://arthurchiao.art/blog/intro-to-io-uring-zh/#2-io_uring
https://blog.csdn.net/ZYZMZM_/article/details/98049471

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值