IO多路复用模型

NIO问题

上一章节我们介绍了NIO模型,结尾处简单介绍下NIO的一些不足,下面我们根据这个话题往下讨论

在这里插入图片描述

根据NIO模型,理论上我们可以有无数个client与service建立连接,同时service可以用一个Thread去处理这些连接。

现假设这样的一种场景:在同一时刻,同时有1w个client与service建立了连接,但是他们当中有9999个client在等待用户输入,然后才能才request发送至service。也就是说在这个时间节点,真正能传输数据的连接,只有一个。

而对于服务端来讲,因为这1w个client已经与service建立了三次握手,kernel已经为他们分别分配了资源创建了socket,在我们代码中已经accept了的client列表就可以获取到1w条数据。在代码中,后续我们需要对已经传输了数据的连接进行读取、业务处理。那我们的app如何知道这1w个连接里面哪些已经传输数据过来了呢?只能是对这1w条了连接进行遍历,对每一条连接根据FD去查找内核检测数据是否到达,也就是说需要进行1w次系统调用,不断进行用户态和内核态的切换。

我们将每个连接的IO读取操作抽象成一个对象:

在这里插入图片描述

如图所示,有3个client与service建立了连接,我们的程序需要对已经传输过来的数据进行处理,为了找到哪一个连接有数据传输过来了,需要遍历3条连接,每次遍历需要调用kernel的read方法,再由kernel去连接中真正读取数据。

IO模型

  • 同步:APP通过调用kernel去读取IO,自己完成所有IO操作
  • 异步:APP不直接调用kernel去读取IO,而是kernel自己读取到IO数据后,将数据放入APP的buffer,APP直接读取自己的buffer,IO操作就像不是APP发起,也不是由APP执行,目前只有windows支持
  • 阻塞:APP线程读取IO时,不能做其他事情,blocking
  • 非阻塞:APP线程在读取IO时,还能做其他事情,nonblocking

多路复用

我们总结上面NIO的问题,原因点在于会对每一个连接进行多次系统调用才能感知是否有数据达到,从而导致进行频繁的系统调用。

多路复用的基本原则,就是尽量减少因IO读取而造成的频繁系统调用。所谓多路复用,是指通过一次系统调用,获得IO状态,获取到IO状态之后,由APP自己对符合状态的IO进行读写操作。

无论是BIO,NIO还是多路复用,都是同步IO模型,其中BIO是同步阻塞模型,NIO和多路复用是同步非阻塞模型。

select

select是符合POSIX标准的最基本的多路复用器,每个os系统都会支持,那么什么是Select?我们来看下Unix和Linux下的文档:

Linux:
在这里插入图片描述
Unix:
在这里插入图片描述

下面以其中一个来看下Select的描述:
在这里插入图片描述
根据Select的描述文档,“some of their descriptors are ready for reading, are ready for writing”,Select用于管理那边已经准备读或者写的IO描述符(即我们前面介绍过的FD)
“ select() returns the number of ready descriptors that are contained in the descriptor sets”,表明select返回值是所有IO描述符中,已经准备好读写的那些描述符的个数。

到此处,我们可以得知Select是由kernel管理的,用于管理I/O FD的复用器,可以帮我们从内核中一次找到当前有哪些IO已经可以进行读写操作:

在这里插入图片描述
根据Select的入参,会有fd set,这既是所有的IO。以上图为例,APP线程已经持有了3个连接,其3个IO绑定的FD分别是fd(8),fd(9),fd
(10),现在我们的Service需要知道哪一个连接发送数据过来了,需要读取到相应的数据然后做业务处理。当有Select之后,我们APP只需要把这三个fd一次性传给select,由select在内核中进行遍历,找到fd(9)有数据传输过来,最后将fd(9)给到我们的APP,然后我们APP线程根据拿到的fd(9)找到IO2,再从IO2中读取数据。

至此,我们来比较下使用Select和不使用Select的差异点:
相同点:都需要遍历所有IO
不同点:非select:遍历操作发生在APP线程,处于用户态,调用的read方法处于内核态,也就是说每一次遍历,都会进行一次用户态和内核态的切换
Select:遍历操作发生在内核,由OS支持,不需要频繁的切换

我们看下用Select总共需要几次系统调用:
第一次:将所有fd传给select
第二次:根据select返回的fd,去调用read/write

poll

select是符合POSIX标准的最基本的多路复用器,每个OS会基于select更高级别的封装。
poll与select没有本质区别,工作模式与select也是一样,需要传入所有的fd,kernel监听所有端口返回已经ready的fd。

poll与select的区别主要在于可传入的fd数量上:
select一次可传入的fd是由数量限制的,32bit机器一般限制在1024个,64bit机器数量限制在2048
poll则对fd数量没有限制,它基于链表存储。

基于以上的介绍,我们可以发现,Select / Poll 相较于NIO,优势在于减少了NIO中频繁的系统调用,改由kernel在内核中进行遍历

Select / Poll 依然存在比较大的问题:

  1. 每次调用都需要传入全量的fd,kernel每一次调用都需要遍历全量的fd,传入1000个fd,可能只有一个fd有数据到达
  2. select/poll方法都是单次调用,第二次调用时,还需要将第一次已经传入了的fd再次传入方法

事件中断

在介绍epoll之前,我们需要了解一个OS的概念:中断
举个例子,为什么我们移动鼠标,我们就可以在屏幕上看到光标在移动呢。这就是因为我们鼠标在不断的向CPU发出中断指令,不停的向CPU告诉鼠标当前往哪个方向移动了多少DPI,CPU在接收到鼠标发出的中断指令之后,根据指令查询自己的中断向量表,结合鼠标发出的数据,最终计算出光标应该向哪个方向移动多少像素。

回到我们IO这个话题,我们需要知道有哪些IO有数据到达,我们必须先提前知道数据是如何到达的。很明显的是,这里面会涉及到我们的IO设备:网卡
在这里插入图片描述
数据传输的过程大致如下:

  1. client将数据包通过网络发送到达service的网卡
  2. 网卡接收到数据之后,将数据存入buffer,同时向CPU发送IO中断指令
  3. CPU接收到指令之后,CPU暂停处理当前任务,开始响应中断指令,调用网卡驱动的回调函数(callBack),读取出网卡收到的数据,并将读出来的数据放入内存。

网卡可以在收到一个数据包之后就产生中断,也可以在收集到多个数据包写入buffer之后,再产生中断,将buffer中的所有数据包一次性写入内存。

基于中断以及后续的callback,可以将一次中断理解成一个event(事件),后续的callback便是对这次的event的处理。

不论是BIO还是NIO,Select,Poll,内核对于callBack的处理,只是将网卡发送来的数据,关联到每一个连接的fd的buffer,当后续我们的APP在根据所持有的fd列表去问询哪些数据有到达时,kernel根据fd去对应的fd buffer中检测是否有数据。

epoll

还是老规则,我们先来了解什么是epoll:
在这里插入图片描述

我们可以注意到几个关键词:
I/O event:epoll基于io event
file descriptors 、 any of them: epoll关注的也还是文件描述符fd
then registered via epoll_ctl(2) : 注册epoll ctl
epoll_create 、 epoll wait:epoll的两个关键性动作

epoll与前面介绍的几种IO,本质区别在于对callBack事件的处理上。

下面我们一同来看下epoll是如何处理IO的:

epoll create

我们先来看下epoll create,还是老办法,我们先来看下什么是epoll create:
在这里插入图片描述
文档中有介绍epoll_create用于创建一个新的epoll实例 ,返回值是一个新的文件描述符fd

epoll ctl

在这里插入图片描述
根据epoll_ctl的文档,我们可以发现ctl只用来管理epoll里面的文件描述符的,其入参有一个epfd,这个就是epoll_create方法返回的那个fd,提供add,mod,del三个方法

epoll wait

在这里插入图片描述
可以看到epoll wait返回的就是已经有数据到达,可以进行读写的fd个数

epoll过程

至此,我们来重新梳理下一个client与service的交互过程,为了方便理解,我们仅以一个client为例。

  1. service启动,client也启动,并且与service建立了连接。在service端,kernel对该连接分配描述符FD(4)进行bind操作,(假设系统分配的是FD(4))
  2. service调用epoll_create系统方法,创建出一个epoll实例,返回的epfd是FD(6),此时kernel中已经存在一颗红黑树与FD(6)建立联系
  3. service调用epoll_ctl系统方法,传入FD(6),FD(4),Add,结果便是找到FD(6)对应的那颗红黑树,将FD(4)对应的accept维护在这颗树中
  4. client向service的网卡发送数据包,service的网卡在接收到service的数据包之后,产生中断指令,service的CPU读取网卡接收到的数据包信息,同时根据当前连接的FD(4)找到那颗红黑树,将红黑树中对应的这条信息删除,维护一个链表,将FD(4)组合信息放入该链表
  5. service调用epoll_wait,直接访问步骤4中的链表,如果链表中有数据则直接返回,如果没有数据,则代表当前还没有连接数据到达。
  6. epoll_wait有数据返回之后,可以拿到FD(4),即代表FD(4)有数据达到了,service可以调用系统方法read()从FD(4)的buffer中将client发送过来的数据包读取出来。

大体过程如下:
在这里插入图片描述

为了对比,我们也展示下select/poll的过程:
在这里插入图片描述

我们可以看到,对于client发送数据包到网卡,网卡产生中断,cpu将数据写入fd的buffer,这些步骤select/poll 和epoll都会发生
不同的是,select/poll在bind事件发生之后,没有动作,在app调用select之后,才开始在内核中进行所有fd的遍历,去fd的buffer中检测是否有数据到达
epoll在bind事件之后,就已经开始了epoll的处理,create,ctl已经在内存中维护了需要关注的那些fd,等到这些fd的数据到达之后,将这些有数据的fd放入链表,链表里面有数据即代表可以读取到数据,从而避免了每次都需要遍历全量的fd

假设现在同时又1000个client建立了连接,那create之后,红黑树中维护了1000个fd,当其中某一个fd有数据过来之后,红黑树中删除这个fd,同时将该fd放进链表。当调用wait之后,直接从链表中拿到了这个fd,不需要对1000个fd进行循环遍历。

我们app得到有数据到达的fd之后,还是需要调用系统read方法才能在代码中获取到传过来的数据包,所以epoll依然是同步非阻塞。

kqueue

在Unix下,和epoll类似的是kqueue

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值