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

I/O过程:

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

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

    注意图中用户空间和内核空间的概念。用户空间是常规进程所在区域。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)用户空间和内核空间的复制非常消耗资源;

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

2、poll

      同步多路IO复用

      调用过程和select类似

      时间复杂度:O(n)

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

 

 

3、epoll

      同步多路IO复用      

      时间复杂度:O(1)
 

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

 

综上所述:

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

     

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

     

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

     

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

     

  • 消息传递方式

     

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

     

     

    综上,在选择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更高。