当前位置: 首页 > Linux

原生Linux异步文件操作,io_uring尝鲜体验

时间:2023-04-06 19:59:05 Linux

Linux异步IO历史异步IO一直是Linux系统的痛点。Linux很早就有POSIXAIO异步IO实现,但是是通过在用户空间开用户线程来模拟的,效率极低。后来Linux2.6引入了真正内核级支持的异步IO实现(Linuxaio),但是只支持DirectIO,只支持磁盘文件读写,还有文件大小的限制,各种麻烦。到目前为止(2019年5月),libuv还在以pthread+preadv的形式实现异步IO。随着Linux5.1的发布,Linux终于有了自己简单易用的异步IO实现,并且支持大部分文件类型(磁盘文件、套接字、管道等),这就是本文的主角:io_uringIOCP和IO多路复用modelepoll不同,io_uring的思想更类似于Windows上的IOCP。以快递为例:同步模式是你先在你家楼下等,然后在电商平台下单,直到快递公司把货送到楼下,然后你再把东西带到楼上。epoll类似于你下单,快递公司送货到楼下,通知你可以下楼取货,然后你下楼拿上来。虽然用户还是需要下楼取货(同时读写有一段时间),但是因为不用再等快递员上门了,效率大大提高了.但是epoll不适用于磁盘IO,因为磁盘文件总是可读的。而且IOCP是一步到位,直接送货上门,甚至不用下楼取货。整个过程是完全非阻塞的。io_uring的简单使用io_uring是一个系统调用接口。虽然一共只有3个系统调用,但是实际使用起来非常复杂。这里直接介绍liburing打包,方便用户使用。在尝试之前,请确保您的Linux内核版本高于5.1(uname-r)。liburing需要自己编译(后期可能会被各大linux发行版以软件包的形式收录),gitclone后直接./configure&&sudomakeinstall即可。io_uring结构体初始化liburing提供了自己的核心结构体io_uring,内部封装了io_uring自身的文件描述符(fd)以及与内核通信所需的其他变量。结构io_uring{结构io_uring_sq平方;结构io_uring_cqcq;intring_fd;};使用前需要初始化,使用io_uring_queue_init来初始化这个结构体。externintio_uring_queue_init(无符号项,结构io_uring*环,无符号标志);正如函数名所示,io_uring是一个循环队列(ring_buffer)。第一个参数entries表示队列的大小(实际空间可能比用户指定的大);第二个参数ring是指向需要初始化的io_uring结构的指针;第三个参数flags是标志参数,没有特殊需要可以传0。例如#includestructio_uringring;io_uring_queue_init(32,&ring,0);提交读写请求,首先使用io_uring_get_sqe获取sqe结构体。externstructio_uring_sqe*io_uring_get_sqe(structio_uring*ring);一个sqe(submissionqueueentry)代表一个IO请求,在循环队列中占据一个空位。当io_uring队列满时,io_uring_get_sqe会返回NULL,注意错误处理。注意这里的队列是指未提交的请求,已提交(但未完成)的请求不占用空间。结构io_uring_sqe*sqe=io_uring_get_sqe(&ring);然后使用io_uring_prep_readv或io_uring_prep_writev来初始化sqe结构。staticinlinevoidio_uring_prep_readv(structio_uring_sqe*sqe,intfd,conststructiovec*iovecs,unsignednr_vecs,off_toffset);静态内联voidio_uring_prep_writev(structio_uring_sqe*sqe,intfd,conststructiovec*iovecs,unsignednr_vecs,off_toffset);第一个参数sqe是前面得到的sqe结构体指针;fd是需要读写的文件描述符,可以是磁盘文件,也可以是socket;数组元素个数,offset是文件操作的偏移量。可以看出,这两个函数完全是按照preadv和pwritev设计的,语义相同,很容易上手。需要注意的是,如果需要顺序读写文件,offset偏移量需要由程序自己维护。structioveciov={.iov_base="Helloworld",.iov_len=strlen("Helloworld"),};io_uring_prep_writev(sqe,fd,&iov,1,0);初始化sqe后,可以使用io_uring_sqe_set_data传入你自己的数据,一般是malloc获取的指针,这个在C++中可以直接传入。staticinlinevoidio_uring_sqe_set_data(structio_uring_sqe*sqe,void*data);注意prep_*中会有memset(0),所以一定要先prep_*再set_data。笔者为此纠结了两个小时。一旦sqe准备就绪,就可以使用io_uring_submit提交请求。externintio_uring_submit(structio_uring*ring);可以初始化多个sqe,然后一次提交。io_uring_submit(&ring);完成IO请求io_uring_submit是一个异步操作,不会阻塞当前线程。那么你怎么知道提交的操作何时完成呢?liburing提供了函数io_uring_peek_cqe和io_uring_wait_cqe来获取当前完成的IO操作。externintio_uring_peek_cqe(structio_uring*ring,structio_uring_cqe**cqe_ptr);externintio_uring_wait_cqe(structio_uring*ring,structio_uring_cqe**cqe_ptr);第一个参数是io_uring结构指针;第二个参数是输出参数,cqe_ptr是cqe指针变量的地址。cqe(completionqueueentry)标记一个完成的IO操作,同时也记录之前传入的用户数据。每个cqe对应于前面的sqe。这两个函数,io_uring_peek_cqe如果没有完成IO操作会立即返回,cqe_ptr会被清空;而io_uring_wait_cqe将阻塞线程并等待IO操作完成。对于(;;){io_uring_peek_cqe(&ring,&cqe);如果(!cqe){放(“等待...”);//接受新连接,做其他事情}else{puts("Finished.");休息;}}为了简单起见,上面以忙等待为例。在实际应用场景中,应该是事件循环。浏览器和nodejs在内部为我们隐藏了事件循环的实现,写C/C++语言只能自己完成。之前为sqe设置的用户数据可以通过io_uring_cqe_get_data获取。staticinlinevoid*io_uring_cqe_get_data(structio_uring_cqe*cqe);默认情况下,IO完成事件不会从队列中清除,导致io_uring_peek_cqe得到相同的事件,使用io_uring_cqe_seen标记该事件已经处理完毕staticinlinevoidio_uring_cqe_seen(structio_uring*ring,structio_uring_cqe*cqe);io_uring_cqe_seen(&ring,cqe);清除io_uring,释放资源并清除io_uring结构useio_uring_queue_exitexernvoidio_uring_queue_exit(structio_uring*ring);io_uring_queue_exit(&ring);文件/home/carter/test.txt并写入字符串Helloworld#include#include#include#includeintmain(){structio_uring戒指;io_uring_queue_init(32,&ring,0);结构io_uring_sqe*sqe=io_uring_get_sqe(&ring);intfd=open("/home/carter/test.txt",O_WRONLY|O_CREAT);structioveciov={.iov_base="Helloworld",.iov_len=strlen("Helloworld"),};io_uring_prep_writev(sqe,fd,&iov,1,0);io_uring_s提交(&ring);结构io_uring_cqe*cqe;对于(;;){io_uring_peek_cqe(&ring,&cqe);如果(!cqe){放(“等待...”);//接受新连接,做其他事情}else{puts("Finished.");休息;}}io_uring_cqe_seen(&ring,cqe);io_uring_queue_exit(&ring);}可见C语言的异步操作还是比同步操作复杂的多。libuv(nodejs的底层IO库)已经表示会引入io_uring。如果要自己使用,就必须使用协程库来简化异步操作。这是我使用自己的协程库Cxx-yield实现的一个简单的文件服务器演示。可以看出,经过简单的封装,异步文件读写可以简化为一行:https://github.com/CarterLi/C...。这是在JavaScript中编写async和await的快感