前言:io_uring是由JensAxboe开发的异步IO框架,在Linux内核5.1引入。这篇文章介绍了什么是异步框架和io_uring的一些基本内容,最后介绍一个关于node.js(Libuv)中io_uring的pr,之前有提到但是还没有合并。1io_uring简介在io_uring之前,Linux并没有成熟的异步IO能力。什么是异步IO?回想一下我们读取资源的过程,我们可以通过阻塞或者非阻塞的方式调用read和readv,或者通过epoll监听文件描述,以字符和事件的方式,在回调中调用read系列函数进行读取。这些API有一个共同点,无论是主动检测还是被动检测资源是否可读,当可读时,进程都需要自己执行读操作。io_uring的强大之处在于不需要进程自己主动进行读操作,而是内核在读完后通知进程。与epoll相比,io_uring更进了一步。类似的能力还有windows的IOCP。2io_uring基本使用2.1初始化io_uring和epoll一样,API不多,但是io_uring比epoll复杂很多。我们首先需要调用io_uring_setup来初始化io_uring并得到一个fd。intring_fd;unsigned*sring_tail,*sring_mask,*sring_array,*cring_head,*cring_tail,*cring_mask;structio_uring_sqe*sqes;structio_uring_cqe*cqes;charbuff[BLOCK_SZ];off_toffset;structio_uring_paramsp;m*sq_ptr,,sizeof(p));//得到io_uring对应的fditring_fd=io_uring_setup(QUEUE_DEPTH,&p);intsring_sz=p.sq_off.array+p.sq_entries*sizeof(unsigned);intcring_sz=p.cq_off.cqes+p。cq_entries*sizeof(structio_uring_cqe);//将ring_fd映射到mmap返回的地址,我们可以通过操作返回地址来操作ring_fd,达到用户和内核共享数据的目的cq_ptr=sq_ptr=mmap(0,sring_sz,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_POPULATE,ring_fd,IORING_OFF_SQ_RING);sqes=mmap(0,p.sq_entries*sizeof(structio_uring_sqe),PROT_READ|PROT_WRITE,MAP_SHARED|MAP_POPULATE,ring_fd,IORING_OFF_SQES);//保存任务队列的地址和完成队列,稍后提交获取任务并完成任务节点,需要使用sring_tail=sq_ptr+p.sq_off.tail;sring_mask=sq_ptr+p.sq_off.ring_mask;sring_array=sq_ptr+p.sq_off.array;cring_head=cq_ptr+p.cq_off.head;cring_tail=cq_ptr+p.cq_off.tail;cring_mask=cq_ptr+p.cq_off.ring_mask;cqes=cq_ptr+p.cq_off.cqes;io_uring不仅实现起来很复杂,使用起来也很复杂,但目前只需要对原理有一个大概的了解即可。上述代码的主要用途如下。1birth之后的io_uring,得到一个io_uring实例对应的fd。2将io_uring对应的fd通过mmap映射到一个内存地址,然后我们就可以通过操作内存地址与内核进行通信了。3保存任务队列和完成队列的地址信息,后面会用到。2.2提交任务我们看到io_uring底层维护了任务队列(sq)和完成队列(completionqueue)两个队列(cq)。相应的节点称为sqe和cqe。当我们需要操作某个资源时,我们可以获取一个seq,填写字段,然后提交给内核。我们来看看sqe的核心字段。structio_uring_sqe{__u8opcode;/*操作类型,如读写*/__s32fd;/*资源对应的fd*/__u64off;/*资源的偏移量(操作起点)*/__u64addr;/*保存数据的第一个内存地址*/__u32len;/*数据长度*/__u64user_data;/*用户自定义字段,通常用于关联请求和响应*/__u8flags;/*Flags*/...};io_uring_sqe的核心字段比较好理解,structure一个请求发出后,插入到内核的请求任务队列中。然后调用io_uring_enter通知内核有任务需要处理。我们可以设置在返回之前调用io_uring_enter时等待多少请求。此外,内核处理轮询模式。此时内核会启动内核线程检测任务是否完成,进程不需要调用io_uring_enter。下面是我们发送读取请求的逻辑。unsignedindex,tail;tail=*sring_tail;//在请求队列中获取空闲位置,是一个环,需要环回index=tail&*sring_mask;structio_uring_sqe*sqe=&sqes[index];//初始化请求structuresqe->opcode=op;//读取fdsqe->fd=fd;//将读取到的数据保存到buff中//响应时关联buff即可找到对应的请求上下文sqe->addr=(unsignedlong)buff;sqe->user_data=(unsignedlonglong)buff;memset(buff,0,sizeof(buff));sqe->len=BLOCK_SZ;sqe->off=offset;//插入请求队列sring_array[index]=index;//updateindextail++;//通知内核有任务需要处理,等待任务完成后返回io_uring_smp_store_release(sring_tail,tail);intret=io_uring_enter(ring_fd,1,1,IORING_ENTER_GETEVENTS);2.3任务完成时当任务完成时,io_uring_enter会返回。但是这里有一个问题。请求任务和响应不对应。内核不保证任务完成的顺序。内核只是告诉我们哪些任务已经完成。我们可以通过user_data关联请求和响应,类似于rpc通信中的seq。user_data字段在request中设置,会在response中返回,让请求者知道response对应的是哪个request。响应对应的结构比较简单。structio_uring_cqe{/*用户自定义字段,通常用于关联请求和响应*/__u64user_data;/*系统调用的返回码,如read*/__s32res;//暂时不用__u32flags;};我们这里假设请求和响应是串行的,所以不需要使用user_data字段来关联请求和响应。从前面的代码可以看出,我们将数据读入了buff变量。我们来看看内核返回后我们的处理逻辑。structio_uring_cqe*cqe;unsignedhead,reaped=0;//获取完成队列的头节点,消费buff中存储的数据head=io_uring_smp_load_acquire(cring_head);cqe=&cqes[head&(*cring_mask)];//更新头部索引head++;io_uring_smp_store_release(cring_head,head);这是io_uring一次读操作的大致流程。我们看到用户层的逻辑还是比较复杂的,作者也想到了,所以封装了Libring库,简化了使用。3Liburing的使用那么我们怎么使用呢,我们来回忆一下epoll的使用。//创建epoll实例intepollfd=epoll_create();//封装fd并订阅事件structepoll_eventevent;event.events=EPOLLIN;event.data.fd=listenFd;//注册到epollpoll_ctl(epollfd,EPOLL_CTL_ADD,listenFd,&event);//等待事件触发intnum=epoll_wait(epollfd,events,MAX_EVENTS,-1);for(i=0;i
