最近看到OVS用户态的代码,在接收内核态信息的时候,使用了Epoll多路复用机制,对其十分不解,于是从网上找了一些资料,学习了一下《UNIX网络变成卷1:套接字联网API》这本书对应的章节,网上虽然关于该主题的博文很多,并且讲解的很详细,但是在这里还是做一个学习笔记,记录一下自己的想法。
IO模型
在《UNIX网络变成卷1:套接字联网API》这本书中,提到了五种I/O模型,分别为:阻塞式I/O、非阻塞式I/O、I/O复用(Epoll、select都是一种I/O复用机制),信息驱动式I/O、异步I/O,下面具体的一一介绍。
阻塞式I/O模型
阻塞,顾名思义,当进程在等待数据时,若该数据一直没有产生,则该进程将一直等待,直到等待的数据产生为止,这个过程中进程的状态是阻塞的。
如上图所示,在linux中,用户态进程调用recvfrom系统调用接收数据,当前内核中并没有准备好数据,该用户态进程将一直在此等待,不会进行其他的操作,待内核态准备好数据,将数据从内核态拷贝到用户空间内存,然后recvfrom返回成功的指示,此时用户态进行才解除阻塞的状态,处理收到的数据。
从上述过程可以看出,用户态接收内核态数据的时候,主要有两个过程:内核态获得数据-->将数据从内核态的内存空间中复制到用户态进程的缓冲区中
非阻塞式I/O模型
在非阻塞式I/O模型中,当进程等待内核的数据,而当该数据未到达的时候,进程会不断询问内核,直到内核准备好数据。
如上图,用户态进程调用recvfrom接收数据,当前并没有数据报文产生,此时recvfrom返回EWOULDBLOCK,用户态进程会一直调用recvfrom询问内核,待内核准备好数据的时候,之后用户态进程不再询问内核,待数据从内核复制到用户空间,recvfrom成功返回,用户态进程开始处理数据。
需要注意的是,当数据从内核复制到用户空间中的这一段时间中,用户态进程是处于阻塞的状态的。
非阻塞式I/O模型,个人觉得这个名字可能有点混淆,并不是和阻塞式模型是完全对立的,不是说进程等不到数据,就去做别的事情,恰恰进程这个时候一直在原地等待数据的到来,与阻塞式模型不同的是,非阻塞相当于进程一直在敲门问“数据好了么,快给我”,然后房门后的人说“没有准备好,请稍后!”,这个过程是一种轮询的状态,而阻塞式是佛系的态度,敲了一次门,房门后的人没有给任何回应,于是就去睡觉,啥都不做,直到房门后的人做出响应叫醒他,进程才去做下一步动作。
I/O复用模型
在ovs的用户态源码里,就用到了I/O复用模型,在计算机网络里面,有很多关于“复用”的用法,比如多路复用,意思就是本来一条链路上一次只能传输一个数据流,如果要实现两个源之间多条数据流同时传输,那就得需要多条链路了,但是复用技术可以通过将一条链路划分频率,或者划分传输的时间,使得一条链路上可以同时传输多条数据流。
套用到I/O复用模型上,可以对应到如下应用场景:如果一个进程需要等到多种不同的消息,那么一般的做法就是开启多条线程,每个线程接收一类消息,如果每个线程都是采用阻塞式I/O模型,那么每个线程在消息未产生的时候就会阻塞,也就是说在多线程中使用阻塞式I/O。I/O复用就是基于上述的场景中,无需采用多线程监听消息的方式,进程直接监听所有的消息类型,这其中就涉及到select、poll、epoll等不同的方法。
如上图所示,用户态进程采用select的方法,通过select可以等待多个不同类型的消息,如果其中有一个类型的消息准备好,则select会返回信息,然后用户态进程调用recvfrom接收数据。
可以将select复用机制看作是一个描述符集合的管理,进程通过向这个集合中放入不同的描述符,用来等待不同的消息产生,然后通过select统一的进行管理,让其可以同时等待这个集合中任意一个事件的产生。
I/O复用和阻塞式I/O很相似,不同的是,I/O复用等待多类事件,阻塞式I/O只等待一类事件,另外,在I/O复用中,会产生两个系统调用(如上图,select和recvfrom),而阻塞式I/O只产生一个系统调用。那么这就涉及到具体的性能问题,当只存在一类事件的时候,使用阻塞式I/O模型的性能会更好,当存在多种不同类型的事件时,I/O复用的性能要好的多,因为阻塞式I/O模型只能监听一类事件,所以这个时候需要使用多线程进行处理。
信号驱动式I/O模型
在信号驱动式I/O模型中,与阻塞式和非阻塞式有了一个本质的区别,那就是用户态进程不再等待内核态的数据准备好,直接可以去做别的事情。
如上图所示,当需要等待数据的时候,首先用户态会向内核发送一个信号,告诉内核我要什么数据,然后用户态就不管了,做别的事情去了,而当内核态中的数据准备好之后,内核立马发给用户态一个信号,说”数据准备好了,快来查收“,用户态进程收到之后,立马调用recvfrom,等待数据从内核空间复制到用户空间,待完成之后recvfrom返回成功指示,用户态进程才处理别的事情。
通过上面的图,可以看出信号驱动式I/O模型有种异步操作的赶脚,但是在将数据从内核复制到用户空间这段时间内用户态进程是阻塞的
异步I/O模型
异步I/O模型相对于信号驱动式I/O模型就更彻底了。
如上图,首先用户态进程告诉内核态需要什么数据(上图中通过aio_read),然后用户态进程就不管了,做别的事情,内核等待用户态需要的数据准备好,然后将数据复制到用户空间,此时才告诉用户态进程,”数据都已经准备好,请查收“,然后用户态进程直接处理用户空间的数据。
在复制数据到用户空间这个时间段内,用户态进程也是不阻塞的
同步I/O
《UNIX网络变成卷1:套接字联网API》这本书中,并没有把同步I/O作为一种单独的I/O模型来说明,在没有阅读这些资料之前,我一直认为阻塞式I/O等同于同步I/O,非阻塞式I/O等同于异步I/O,可见不能单纯的通过字面意思就进行判断。
通过对上述几种I/O模型的描述中,可以得到一个结论:阻塞式I/O、非阻塞式I/O、I/O复用模型是同步I/O模型,因为在等待数据的过程中,这三种模型中的进程都没有去做别的事情,即便是非阻塞式的轮询,也可以看作是一种同步。
同时书中也认为信号驱动式I/O模型是同步I/O,书中说到:POSIX将同步IO操作定义为“导致请求进程阻塞,直到I/O操作完成”,而书中认为在信号驱动式I/O模型中等待数据的那段时间不算是真正的I/O操作(因为没有调用I/O相关的系统调用),而数据从内核复制到用户空间才是真正的I/O操作(这个时候调用了recvfrom系统调用)。
I/O模型比较
书中的这张图表述的非常清楚,从等待数据和数据复制这两个时间段,指出了不同I/O模型的区别,这里不再赘述。
总结
从网上看了很多资料,不同的博主对这五个模型总结的情况不同,无一例外,基本都采用一个生活场景来描述他们的不同,但是我个人觉得有些场景描述太过简单,没有将不同模型的区别描述完全,在这里我也举一个生活中的场景作为总结,当然这只是我自己的想法,不妥之处评论区可以指出。
我们去餐厅吃饭,会经过以下几个步骤:首先根据菜单点菜,然后等待厨房准备好,接着服务员上菜。在这个场景中,等待厨房准备菜肴等同于等待数据,服务员上菜等同于将数据从内核复制到用户空间,你就是用户态进程了,服务员和饭店看作是内核态的进程。
阻塞式I/O模型:只点一个菜,然后在餐桌上开始等待,在这个过程中什么事都不干,等服务员把菜上到桌子上之后才开始大快朵颐。
非阻塞式I/O模型:只点一个菜,然后开始等待,啥事都不做,等了一会儿然后就去问服务员,“我的菜好了吗?”,没好接着等待,过了一会儿然后又跑去问....重复这个过程,直到服务员说“亲,你的菜好了,我现在给您送桌上去”,然后你坐在桌子上,等待服务员把饭菜送到你的餐桌上,才开始吃饭。
I/O复用模型:你点了很多菜,然后开始等待,某个时刻其中一个菜或者多个菜厨房里同时好了,服务员跑过来说,“亲,您的有些菜好了,要现在上桌么?”, 你回答,现在就上,于是服务员上一个菜(服务员一次只能上一个菜),你就吃完一个,上一个你就吃完一个。。。
信号驱动式I/O模型:只点一个菜,然后给服务员留下手机,告诉他菜准备好了打个电话给你,先不要上菜,然后你就出去玩耍了,等到菜好了,服务员手机通知你,你立马回到了餐厅,对服务员说“你现在可以上菜了”,于是你在餐桌上等待服务员把菜送上来,然后吃饭。
异步I/O模型:只点一个菜,然后给服务员留下手机,告诉他菜准备好了先上菜,菜上桌了打电话给你,然后你就出去玩耍了,等到菜上桌了,服务员手机通知你,你立马回到了餐桌,开始吃饭。
参考资料
UNIX网络变成卷1:套接字联网API
网络IO之阻塞、非阻塞、同步、异步总结
IO - 同步,异步,阻塞,非阻塞 (亡羊补牢篇)