深入理解Linux IO复用之epoll( 三 )

  • ovflist主要是暂态处理,比如调用ep_poll_callback()回调函数的时候发现eventpoll的ovflist成员不等于EP_UNACTIVE_PTR,说明正在扫描rdllist链表,这时将就绪事件对应的epitem加入到ovflist链表暂存起来,等rdllist链表扫描完再将ovflist链表中的元素移动到rdllist链表中;
  • 如图展示了红黑树、双链表、epitem之间的关系:
  •  
    深入理解Linux IO复用之epoll

    文章插图
     
    注:rbr表示rb_root,rbn表示rb_node 上文给出了其在内核中的定义
    • epoll_wait的数据拷贝
    常见错误观点:epoll_wait返回时,对于就绪的事件,epoll使用的是共享内存的方式,即用户态和内核态都指向了就绪链表,所以就避免了内存拷贝消耗网上抄来抄去的观点
    关于epoll_wait使用共享内存的方式来加速用户态和内核态的数据交互,避免内存拷贝的观点,并没有得到2.6内核版本代码的证实,并且关于这次拷贝的实现是这样的:
    深入理解Linux IO复用之epoll

    文章插图
     
    5.ET模式和LT模式
    • 简单理解
    默认采用LT模式,LT支持阻塞和非阻塞套,ET模式只支持非阻塞套接字,其效率要高于LT模式,并且LT模式更加安全 。
    LT和ET模式下都可以通过epoll_wait方法来获取事件,LT模式下将事件拷贝给用户程序之后,如果没有被处理或者未处理完,那么在下次调用时还会反馈给用户程序,可以认为数据不会丢失会反复提醒;
    ET模式下如果没有被处理或者未处理完,那么下次将不再通知到用户程序,因此避免了反复被提醒,却加强了对用户程序读写的要求;
     
    • 深入理解
    上面的简单理解在网上随便找一篇都会讲到,但是LT和ET真正使用起来,还是存在一定难度的 。
    • LT的读写操作
    LT对于read操作比较简单,有read事件就读,读多读少都没有问题,但是write就不那么容易了,一般来说socket在空闲状态时发送缓冲区一定是不满的,假如fd一直在监控中,那么会一直通知写事件,不胜其烦 。
    所以必须保证没有数据要发送的时候,要把fd的写事件监控从epoll列表中删除,需要的时候再加入回去,如此反复 。
    天下没有免费的午餐,总是无代价地提醒是不可能的,对应write的过度提醒,需要使用者随用随加,否则将一直被提醒可写事件 。
    • ET的读写操作
    fd可读则返回可读事件,若开发者没有把所有数据读取完毕,epoll不会再次通知read事件,也就是说如果没有全部读取所有数据,那么导致epoll不会再通知该socket的read事件,事实上一直读完很容易做到 。
    若发送缓冲区未满,epoll通知write事件,直到开发者填满发送缓冲区,epoll才会在下次发送缓冲区由满变成未满时通知write事件 。
    ET模式下只有socket的状态发生变化时才会通知,也就是读取缓冲区由无数据到有数据时通知read事件,发送缓冲区由满变成未满通知write事件 。
    • 一道面试题
     
    使用Linux epoll模型的LT水平触发模式,当socket可写时,会不停的触发socket可写的事件,如何处理?网络流传的腾讯面试题
    这道题目对LT和ET考察比较深入,验证了前文说的LT模式write问题 。
    普通做法:
    当需要向socket写数据时,将该socket加入到epoll等待可写事件 。接收到socket可写事件后,调用write()或send()发送数据,当数据全部写完后, 将socket描述符移出epoll列表,这种做法需要反复添加和删除 。
    改进做法:
    向socket写数据时直接调用send()发送,当send()返回错误码EAGAIN,才将socket加入到epoll,等待可写事件后再发送数据,全部数据发送完毕,再移出epoll模型,改进的做法相当于认为socket在大部分时候是可写的,不能写了再让epoll帮忙监控 。
    上面两种做法是对LT模式下write事件频繁通知的修复,本质上ET模式就可以直接搞定,并不需要用户层程序的补丁操作 。
     
    • ET模式的线程饥饿问题
    如果某个socket源源不断地收到非常多的数据,在试图读取完所有数据的过程中,有可能会造成其他的socket得不到处理,从而造成饥饿问题 。
    解决办法:为每个已经准备好的描述符维护一个队列,这样程序就可以知道哪些描述符已经准备好了但是并没有被读取完,然后程序定时或定量的读取,如果读完则移除,直到队列为空,这样就保证了每个fd都被读到并且不会丢失数据,流程如图:


    推荐阅读