Netty(四)select,poll,epoll区别

       经过上一章我们可以发现jdk1.8中nio对selector的操作实际上调用了epoll的三个函数,分别是,epoll_create,epoll_ctl,epoll_wait.这就说明了我们我们nio底层使用了epoll的多路复用机制.而在jdk1.4版本之前IO多路复用的实现还有select和poll.

Linux内核事件机制

     在Linux内核中存在着等待队列的数据结构,该数据结构是基于双端链表实现,Linux内核通过将阻塞的进程任务添加到等待队列中,而进程任务被唤醒则是在队列轮询遍历检测是否处于就绪状态,如果是那么会在等待队列中删除等待节点并通过节点上的回调函数进行通知然后加入到cpu就绪队列中等待cpu调度执行.其具体流程主要包含以下两个处理逻辑,即休眠逻辑以及唤醒逻辑.linux内核的休眠与唤醒机制有了上述认知之后,接下来揭开IO复用模型设计的本质就相对会比较容易理解

1.IO多路复用

    在讲述IO复用模型的实现前,我们先简单回顾下IO复用模型的思路,从上述的IO复用模型图看出,一个进程可以处理N个socket描述符的操作,等待对应的socket为可读的时候就会执行对应的read_process处理逻辑,也就是说这个时候我们站在read_process的角度去考虑,我只需要关注socket是不是可读状态,如果不可读那么我就休眠,如果可读你要通知我,这个时候我再调用recvfrom去读取数据就不会因内核没有准备数据处于等待,这个时候只需要等待内核将数据复制到用户空间的缓冲区中就可以了.

2.IO多路复用的实现select poll epoll

一. select(下面fd即文件描述符,也可以理解为socket)

int select(
    int nfds,
    fd_set *readfds,
    fd_set *writefds,
    fd_set *exceptfds,
    struct timeval *timeout);

       1.用户进程向内核发起select函数的调用,并携带socket描述符集合从用户空间复制到内核空间,由内核对socket集合进行可读状态的监控.

      2.select遍历自己监控的socket,挨个调用socket 的poll逻辑以便检查该socket 是否有可读事件,遍历完所有的socket 后,如果没有任何一个socket 可读,那么select会调用schedule_timeout进入schedule循环,使得process进入睡眠。如果在timeout时间内某个socket 上有数据可读了,或者等待timeout了,则调用select的process会被唤醒,接下来select就是遍历监控的socket 集合,挨个收集可读事件并返回给用户.(当fd集合中有某一个fd有事件触发时,仅仅是改变了fd的状态,内核是不知道哪一个fd有事件触发的,需要轮询才知道的)

通过上面的select逻辑过程分析,相信大家都意识到,select存在三个问题:

      1.每次调用select,都需要把被监控的fds集合从用户态空间拷贝到内核态空间,期间用户空间与内核空间来回切换开销非常大,再加上调用select的频率本身非常频繁,这样导致高频率调用且大内存数据的拷贝,严重影响性能.
      2.能监听端口的数量有限,单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上为3264),当然我们可以对宏FD_SETSIZE(类似JAVA的成员变量)进行修改,然后重新编译内核,但是性能可能会受到影响,一般该数和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认1024个,64位默认2048。

    3.被监控的fds集合中,只要有一个有数据可读,整个socket集合就会被遍历一次调用socket的poll函数收集可读事件:由于当初的需求是朴素,仅仅关心是否有数据可读这样一个事件,当事件通知来的时候,由于数据的到来是异步的,我们不知道事件来的时候,有多少个被监控的socket有数据可读了,于是,只能挨个遍历每个socket来收集可读事件,如果每次只有一个socket事件可读,那么每次轮询遍历的事件复杂度是O(n),影响到性能.

    4.服务程序也要遍历全量的fds,查看每个文件描述符的revents字段是否需要读写操作

二.poll

int poll(
struct pollfd *fds, 
nfds_tnfds, 
int timeout
);

     poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。

    但是他用了pollfd结构而不是select的fd_set结构,使得poll支持的fds集合限制远大于select的1024。poll虽然解决了fds集合大小1024的限制问题,但是仍然采用轮训方式每次轮询遍历的事件复杂度是O(n).

三.epoll

    1.在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率,他是通过中断感知到有事件发生,然后触发回调,把有事件的fd放进我们的rdlist。如前面我们所说,在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。

   而且通过上面select和poll的函数发现每次调用都需要应用程序把fd的数组传进去,这个fd数组每次都要再用户态和内核态之间传递,影响效率.为此epoll设计了"逻辑上的epfd",epfd是一个数字,把fd数组关联到上面,然后每次向内核传递的是epfd这个数字,创建epoll空间的同时将其从用户空间拷贝到内核中,此后epoll对socket描述的注册监听通过epoll空间来进行操作,仅一次拷贝

   2.epoll里面有两个数据结构

  • rbr: 红黑树。为了支持对海量连接的高效查找、插入和删除,eventpoll 内部使用的就是红黑树。通过红黑树来管理用户主进程accept添加进来的所有 socket 连接。
  • ready_list 就绪的描述符链表。当有连接就绪的时候,内核会把就绪的连接放到 rdllist 链表里。这样应用进程只需要判断链表就能找出就绪进程,而不用去遍历红黑树的所有节点了。

3.epoll的接口非常简单,一共就三个函数:

  • epoll_create:创建一个epoll句柄
  • epoll_ctl:向 epoll 对象中添加/修改/删除要管理的连接
  • epoll_wait:等待其管理的连接上的 IO 事件

4.整个epoll总结分成三个步骤

  1.事件注册.通过函数epoll_ctl实现,对于服务器而言,是accept,read,write三种事件;对于客户端而言 是connect,read,write三种事件.

  2.轮询这三个事件是否就绪.通过函数epoll_wait实现.有事件发生就返回,这里的轮询不是轮询整个fd,是轮询我们的就绪事件列表有没有事件发生.

  3.事件就绪,执行实际的I/O操作.通过函数accept/read/write实现

   这里解释一下什么叫事件就绪?

     1.read事件就绪:远程有数据来了,socket读缓冲区里有数据,需要调用read函数处理.

     2.write事件就绪:指本地的socket写缓冲区是否可写.如果缓冲区没满,则一直是可写的,write事件是一直就绪的,可以调用write函数,只有发送大文件时,socket写缓冲区被占满,write事件才不是就绪状态.

    3.accpet事件就绪:有新的连接进入,需要调用accept函数处理.

5.epoll水平触发(LT)和边缘触发(RT)

1) socket接收数据的缓冲区不为空的时候,则一直触发读事件,相当于"不断地询问是否有数据可读"

2) socket发送数据的缓冲区不全满的时候,则一直触发写事件,相当于"不断地询问是否有空闲区域可以让数据写入" 本质上就是一个不断进行交流的过程, 水平触发如下图所示:

  • 边缘触发

1) socket接收数据的缓冲区发生变化,则触发读取事件,也就是当空的接收数据的socket缓冲区这个时候有数据传送过来的时候触发

2) socket发送数据的缓冲区发生变化,读缓冲区从空转为非空的时候触发一次;写缓冲区状态,从满转为非满的时候触发一次.比如用户发送了一个大文件,把写缓存区塞满了,之后缓存区可写就会发生一次从满到不满的切换.

  

3.总结

select,poll,epoll都是IO多路复用机制,即可以监视多个描述符,一旦某个描述符就绪(读或写就绪),能够通知程序进行相应读写操作。 但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

  • select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
  • select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。
selectpollepoll
性能随着连接数的增加,性能急剧下降,处理成千上万的并发连接数时,性能很差随着连接数的增加,性能急剧下降,处理成千上万的并发连接数时,性能很差随着连接数的增加,性能基本没有变化
连接数一般1024无限制无限制
内存拷贝每次调用select拷贝每次调用poll拷贝fd首次调用epoll_ctl拷贝,每次调用epoll_wait不拷贝
数据结构bitmap数组红黑树
内在处理机制线性轮询线性轮询FD挂在红黑树,通过事件回调callback
时间复杂度O(n)O(n)O(1)

参考:深入浅出理解select、poll、epoll的实现 - 知乎

        深入分析select&poll&epoll原理 - 云+社区 - 腾讯云
        select、poll、epoll - IO模型超详解_XueyinGuo的博客-CSDN博客

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值