I/O模型以及多路复用三种实现方式select、poll和epoll比较及区别

Unix与Java的IO模型

1,同步和异步的概念:针对比如接口调用,服务调用,API类库调用等

同步:用者必须要等待这个接口的磁盘读写或者网络通信的操作执行完毕了,调用者才能返回,这就是“同步”,如下图所示:
在这里插入图片描述
异步:所谓的“异步”,就是说这个调用者调用接口之后,直接就返回了,他去干别的事儿了,也不管那个接口的磁盘读写或者是网络通信是否成功,然后这个接口后续如果干完了自己的任务,比如写完了文件或者是什么的,会反过来通知调用者,之前你的那个调用成功了。可以通过一些内部通信机制来通知,也可以通过回调函数来通知,如下图:
在这里插入图片描述

2,阻塞和非阻塞:针对的是底层IO操作

阻塞:比如现在我们的程序想要通过网络读取数据,如果是阻塞IO模式,一旦发起请求到操作系统内核去从网络中读取数据,就会阻塞在那里,必须要等待网络中的数据到达了之后,才能从网络读取数据到内核,再从内核返回给程序,如下图。
在这里插入图片描述
非阻塞:程序发送请求给内核要从网络读取数据,但是此时网络中的数据还没到,此时不会阻塞住,内核会返回一个异常消息给程序,程序可以干点别的,然后不断去轮询去访问内核,看请求的数据是否读取到了 ,如下图:
在这里插入图片描述

3,BIO,NIO和AIO

BIO
主要就是同步阻塞IO模型,在Java里叫做BIO,在JDK1.4之前。在Java代码里调用IO相关接口,发起IO操作之后,Java程序就会同步等待,这个同步指的是Java程序调用IO API接口的层面而言。

而IO API在底层的IO操作是基于阻塞IO来的,向操作系统内核发起IO请求,系统内核会等待数据就位之后,才会执行IO操作,执行完毕了才会返回

NIO
在JDK 1.4之后提供了NIO,他的概念是同步非阻塞,也就是说如果你调用NIO接口去执行IO操作,其实还是同步等待的,但是在底层的IO操作上 ,会对系统内核发起非阻塞IO请求,以非阻塞的形式来执行IO。

也就是说,如果底层数据没到位,那么内核返回异常信息,不会阻塞住,但是NIO接口内部会采用非阻塞方式过一会儿再次调用内核发起IO请求,直到成功为止。

但是之所以说是同步非阻塞,这里的“同步”指的就是因为在你的Java代码调用NIO接口层面是同步的,你还是要同步等待底层IO操作真正完成了才可以返回,只不过在执行底层IO的时候采用了非阻塞的方式来执行罢了

NIO网络通信与IO多路复用模型
实际上,如果基于NIO进行网络通信,采取的就是多路复用的IO模型,这个多路复用IO模型针对的是网络通信中的IO场景来说的。

简单来说,就是在基于Socket进行网络通信的时候,如果有多个客户端跟你的服务端建立了Socket连接,那你就需要维护多个Socket连接。

而所谓的多路复用IO模型,就是说你的Java代码直接通过一个select函数调用,直接会进入一个同步等待的状态。

这也是为什么说NIO一定是“同步”的,因为你必须在这里同步等待某个Socket连接有请求到来。

接着你就要同步等着select函数去对底层的多个 Socket 连接进行轮询,不断的查看各个 Socket 连接谁有请求到达,就可以让select函数返回,交给我们的Java程序来处理。

select函数在底层会通过非阻塞的方式轮询各个Socket,任何一个Socket如果没有数据到达,那么非阻塞的特性会立即返回一个信息。

然后select函数可以轮询下一个Socket,不会阻塞在某个Socket上,所以底层是基于这种非阻塞的模式来“监视”各个Socket谁有数据到达的。

这就是所谓的“同步非阻塞”,但是因为操作系统把上述工作都封装在一个select函数调用里了,可以对多路Socket连接同时进行监视,所以就把这种模型称之为“IO多路复用”模型。

通过这种IO多路复用的模型,就可以用一个线程,调用一个select函数,然后监视大量的客户端连接了,如下图
在这里插入图片描述
AIO
最后就是JDK 1.7之后,又支持了AIO,也叫做NIO 2.0,他就支持异步IO模型了。

我们先说一下异步IO模型是什么意思。

简单来说,就是你的Java程序可以基于AIO API发起一个请求,比如说接收网络数据,AIO API底层会基于异步IO模型来调用操作系统内核。

此时不需要去管这个IO是否成功了,AIO接口会直接返回,你的Java程序也会直接返回。

然后,你的Java程序就可以去干别的事儿了。大家联想一下上面说的那个异步的例子,就可以理解这里为什么叫做异步了。

因为BIO、NIO都是同步的,你发起IO请求,都必须同步等待IO操作完成。

但是这里你发起一个IO请求,直接AIO接口就返回了,你就可以干别的事儿了,纯异步的方式。

不过你需要提供一个回调函数给AIO接口,一旦底层系统内核完成了具体的IO请求,比如网络读写之类的,就会回调你提供的回调函数。

比如说你要是通过网络读取数据,那么此时AIO接口就会把操作系统异步读取到的数据交给你的回调函数,如下图:
在这里插入图片描述

I/O过程:

图中明显忽略了很多细节,仅显示了涉及到的基本步骤。

    注意图中用户空间和内核空间的概念。用户空间是常规进程所在区域。JVM 就是常规进程, 驻守于用户空间。用户空间是非特权区域:比如,在该区域执行的代码就不能直接访问硬件设备。 内核空间是操作系统所在区域。内核代码有特别的权力:它能与设备控制器通讯,控制着用户区域 进程的运行状态,等等。最重要的是,所有 I/O 都直接(如这里所述)通 过内核空间。 当进程请求 I/O 操作的时候,它执行一个系统调用(有时称为陷阱)将控制权移交给内核。 C/C++程序员所熟知的底层函数 open( )、read( )、write( )和 close( )要做的无非就是建立和执行适当 的系统调用。当内核以这种方式被调用,它随即采取任何必要步骤,找到进程所需数据,并把数据 传送到用户空间内的指定缓冲区。内核试图对数据进行高速缓存或预读取,因此进程所需数据可能 已经在内核空间里了。如果是这样,该数据只需简单地拷贝出来即可。如果数据不在内核空间,则 进程被挂起,内核着手把数据读进内存。 看了图 ,您可能会觉得,把数据从内核空间拷贝到用户空间似乎有些多余。为什么不直接 让磁盘控制器把数据送到用户空间的缓冲区呢?这样做有几个问题。首先,硬件通常不能直接访问 用户空间 。其次,像磁盘这样基于块存储的硬件设备操作的是固定大小的数据块,而用户进程请 求的可能是任意大小的或非对齐的数据块。在数据往来于用户空间与存储设备的过程中,内核负责 数据的分解、再组合工作,因此充当着中间人的角色。

I/O多路复用发生在图read()操作之前,也就是传输fd。

我们通过比较select、poll和epoll处理I/O的过程来剖析其中的原因:
1. 用户态将文件描述符传入内核的方式:
select:创建3个文件描述符集并拷贝到内核中,分别监听读、写、异常动作。这里受到单个进程可以打开的fd数量限制,默认是1024。 
poll:将传入的struct pollfd结构体数组拷贝到内核中进行监听。 无连接限制
epoll:执行epoll_create会在内核的高速cache区中建立一颗红黑树以及就绪链表(该链表存储已经就绪的文件描述符)。接着用户执行的epoll_ctl函数添加文件描述符会在红黑树上增加相应的结点。

2. 内核态检测文件描述符是否可读可写的方式:
select:采用轮询方式,遍历所有fd,最后返回一个描述符读写操作是否就绪的mask掩码,根据这个掩码给fd_set赋值。 
poll:同样采用轮询方式,查询每个fd的状态,如果就绪则在等待队列中加入一项并继续遍历。 
epoll:采用回调机制。在执行epoll_ctl的add操作时,不仅将文件描述符放到红黑树上,而且也注册了回调函数,内核在检测到某文件描述符可读/可写时会调用回调函数,该回调函数将文件描述符放在就绪链表中。

3. 如何找到就绪的文件描述符并传递给用户态:
select:将之前传入的fd_set拷贝传出到用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。 
poll:将之前传入的fd数组拷贝传出用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。 
epoll:epoll_wait只用观察就绪链表中有无数据即可,最后将链表的数据返回给数组并返回就绪的数量。内核将就绪的文件描述符放在传入的数组中,所以只用遍历依次处理即可。这里返回的文件描述符是通过mmap让内核和用户空间共享同一块内存实现传递的,减少了不必要的拷贝。

4. 继续重新监听时如何重复以上步骤:
select:将新的监听文件描述符集合拷贝传入内核中,继续以上步骤。 
poll:将新的struct pollfd结构体数组拷贝传入内核中,继续以上步骤。 
epoll:无需重新构建红黑树,直接沿用已存在的即可。

通过以上步骤我们可以发现以下几点:
select和poll的动作基本一致,只是poll采用链表来进行文件描述符的存储,而select采用fd标注位来存放,所以select会受到最大连接数的限制,而poll不会。
select、poll、epoll虽然都会返回就绪的文件描述符数量。但是select和poll并不会明确指出是哪些文件描述符就绪,而epoll会。造成的区别就是,系统调用返回后,调用select和poll的程序需要遍历监听的整个文件描述符找到是谁处于就绪,而epoll则直接处理就行了。
select、poll都需要将有关文件描述符的数据结构拷贝进内核,最后再拷贝出来。而epoll创建的有关文件描述符的数据结构本身就存于内核态中,系统调用返回时也采用mmap共享存储区,需要拷贝的次数大大减少。
select、poll采用轮询的方式来检查文件描述符是否处于就绪态,而epoll采用回调机制。造成的结果就是,随着fd的增加,select和poll的效率会线性降低,而epoll不会受到太大影响,除非活跃的socket很多。
最后总结一下,epoll比select和poll高效的原因主要有两点: 
1. 减少了用户态和内核态之间的文件描述符拷贝 
2. 减少了对就绪文件描述符的遍历

 

优缺点对比:

1、select

      同步多路IO复用     

     时间复杂度:O(n)

     fd_set(监听的端口个数):32位机默认是1024个,64位机默认是2048。(对应单线程最大连接数)

缺点:

        (1)单进程可以打开fd有限制(连接数受限);

        (2)对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低;

        (2)用户空间和内核空间的复制非常消耗资源;

2、poll

      同步多路IO复用

      调用过程和select类似

      时间复杂度:O(n)

      其和select不同的地方:采用链表的方式替换原有fd_set数据结构,poll使用pollfd的指针,pollfd结构包含了要监视的event和发生的             evevt,不再使用select传值的方法。更方便,而使其没有连接数的限制。

 

 

3、epoll

      同步多路IO复用      

      时间复杂度:O(1)
 

 

综上所述:

  • 支持一个进程所能打开的最大连接数

     

     

  • FD剧增后带来的IO效率问题

     

     

  • 消息传递方式

     

     

     

    综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点:
    1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
    2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。

最后描述:

select

  • select能监控的描述符个数由内核中的FD_SETSIZE限制,仅为1024,这也是select最大的缺点,因为现在的服务器并发量远远不止1024。即使能重新编译内核改变FD_SETSIZE的值,但这并不能提高select的性能。
  • 每次调用select都会线性扫描所有描述符的状态,在select结束后,用户也要线性扫描fd_set数组才知道哪些描述符准备就绪,等于说每次调用复杂度都是O(n)的,在并发量大的情况下,每次扫描都是相当耗时的,很有可能有未处理的连接等待超时。
  • 每次调用select都要在用户空间和内核空间里进行内存复制fd描述符等信息。

poll

  • poll使用pollfd结构来存储fd,突破了select中描述符数目的限制。
  • 与select的后两点类似,poll仍然需要将pollfd数组拷贝到内核空间,之后依次扫描fd的状态,整体复杂度依然是O(n)的,在并发量大的情况下服务器性能会快速下降。

epoll

  • epoll维护的描述符数目不受到限制,而且性能不会随着描述符数目的增加而下降。
  • 服务器的特点是经常维护着大量连接,但其中某一时刻读写的操作符数量却不多。epoll先通过epoll_ctl注册一个描述符到内核中,并一直维护着而不像poll每次操作都将所有要监控的描述符传递给内核;在描述符读写就绪时,通过回掉函数将自己加入就绪队列中,之后epoll_wait返回该就绪队列。也就是说,epoll基本不做无用的操作,时间复杂度仅与活跃的客户端数有关,而不会随着描述符数目的增加而下降。
  • epoll在传递内核与用户空间的消息时使用了内存共享,而不是内存拷贝,这也使得epoll的效率比poll和select更高。
  • 5
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值