前导知识
同步与异步:在编程中,异步指的是两者之间的关系。这种关系是【无需马上进入等待】。什么意思呢?举个例子,比如服务器和客户端之间,如果是同步关系,那么一般情况是客户端发送报文,服务器处理之后返回应答报文,这期间客户端等待,无所事事,收到返回报文之后再继续发送下一笔报文。异步关系下,客户端不需要等待服务器有没有返回报文,可以自行选择继续发送报文或者等待。当然异步可以不只是客户端和服务器之间的关系,也可以是执行函数与执行函数之间的关系,A函数可以在B函数阻塞时直接接手执行,不等待B函数执行完成。只要能确定两个事物,就能确定他们的关系。
常规IO函数的隐藏步骤:read、write、recv、send都是常规的IO函数,它们底层其实都分了两个步骤,一是发起读写请求,二是进行数据的拷贝(读函数将数据从内核缓冲区拷贝进用户空间,写函数将数据从用户空间拷贝入内核缓冲区),并进行结果返回。所以当第一步执行之后,因为没有数据等原因,无法进行第二部,那么通常会阻塞住,造成资源开销。这两步无法隔离。
异步IO和非阻塞IO:异步IO一定是非阻塞IO,非阻塞IO不一定是异步IO。异步IO是可以在没有数据可以进行IO的时候,用一些方法进行跳转,暂时先不进行读写操作的。非阻塞IO除了异步IO的情形,还可以是在没有读写数据的时候,直接返回异常值。
环形缓冲区:也叫循环队列,比如一个队列有5个位置,之前存入5个数据会依次占满队列,那么当第六个数据存入时,会计算6除以5余1·,于是第六个数据存入第一个位置并覆盖原有数据。
Io_uring的概念与逻辑
Io_uring: Input/Output user ring。可以直译为,用户能使用的用于输入输出的环形缓冲区。而个环形缓冲区的实现和使用,又会涉及到异步等技术栈。
Io_uring逻辑简图:
如图,两个queue都是环形缓冲区。当有任务请求的时候,会先储存到submit queue中。之后调用submit函数会从submit queue里将请求批量取出交给内核执行,内核执行完成之后再将已完成任务放置到complete queue里等待清除。这些任务包括,读取、写入、连接等。
io_uring的优点
异步IO提高程序执行效率。而io_uring会先将io任务存入内核中的submit队列,发起请求后程序可以异步地继续执行其他任务和操作,直到io_uring_sumbit函数一次性将所有请求提交至内核进行处理,内核会异步地执行这些任务,哪怕有些请求暂时没法处理也不会阻塞整个程序的执行。一方面比起多线程技术会减少线程切换、建立、关闭等开销,另一方面比起同步IO省去了阻塞造成的开销。
零拷贝技术。Io_uring中,提交读写请求的时候,还会伴随着提交一个buffer的地址,之后内核处理这些读写请求的时候,会直接从这个buffer里读取数据,或者直接往这个buffer写入数据。相比起以往的常规模型,先将数据存储在内核缓冲区,复制进入应用层,少了复制的步骤(详情可见下文)。并且在创建queue的时候也是直接创建在内核中,用户对队列的操作都是直接对队列进行的,避免了产生复制。
Io_uring构建tcp服务器
构建一个客户端发送什么,就会返回什么的服务器。
导入相关的包与库,设置相关结构体和宏
tcp服务器初始化
-
创建io_uring队列
先初始化一个与io_uring队列的参数相关的结构体。然后用这个参数结构体在内核中去创建并初始化一个长度为ENTRIES_LENGTHD(1024)的io_ring队列,这个队列的引用存储在ring中。也就是说可以通过ring去访问io_uring队列。Io_ring队列是有submit queue和complete queue组成的,它们的长度也都为1024。
提交accept任务
Sqe是submit queue entry,entry是元素的意思。先通过io_uring_get_sqe获取到submit queue的一个元素的指针,然后通过io_uring_prep_accept修改这个struct io_uring_sqe实例的属性,将它变为一个accept请求。Seq->user_Data可以存一些自定义数据。
提交请求并取出执行结果
通过io_uring_submit将submit queue中的请求批量提交。
io_uring_wait_cqe(&ring,&cqe)的作用是阻塞当前线程,直到至少有一个请求完成,这句代码非必要,但在这个小项目中笔者选择了引用。
io_uring_peek_batch_cqe(&ring,cqes,128)获取至多128个已完成的事件,并且存入cqe队列。
遍历cq队列,提交新的请求
提交请求的时候,都会设置user_data,这个user_data在请求完成之后都会带到complete queue的元素中。遍历complete queue的元素,取出其中的user_data,根据这些user_data进行下一步操作。如果完成的是accept事件那就提交recv请求,如果完成的是recv事件那么提交send的请求,send请求在下一轮轮询中随即就会被完成掉。完成了send请求之后那么又继续提交recv请求。最后用io_uring_cq_advance()清空cq队列,advance是前进的意思,通过调整指针指向的方向来表示队列的释放。entries->res是执行结果,accept任务会给entries->res赋值clentfd,send和recv任务则会给它赋值传输字节流的量。