彻底搞懂epoll高效运行的原理( 二 )


另外,struct epoll event结构中的events域在这里的解释是:在被监测的文件描述符上实际发生的事件 。
参数timeout描述在函数调用中阻塞时间上限,单位是ms:

  • timeout = -1表示调用将一直阻塞,直到有文件描述符进入ready状态或者捕获到信号才返回;
  • timeout = 0用于非阻塞检测是否有描述符处于ready状态,不管结果怎么样,调用都立即返回;
  • timeout > 0表示调用将最多持续timeout时间,如果期间有检测对象变为ready状态或者捕获到信号则返回,否则直到超时 。
epoll的两种触发方式
epoll监控多个文件描述符的I/O事件 。epoll支持边缘触发(edge trigger,ET)或水平触发(level trigger,LT),通过epoll_wait等待I/O事件,如果当前没有可用的事件则阻塞调用线程 。
select和poll只支持LT工作模式,epoll的默认的工作模式是LT模式 。
1.水平触发的时机
  1. 对于读操作,只要缓冲内容不为空,LT模式返回读就绪 。
  2. 对于写操作,只要缓冲区还不满,LT模式会返回写就绪 。
当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写 。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你 。如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率 。
2.边缘触发的时机
  • 对于读操作
  1. 当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候 。
  2. 当有新数据到达时,即缓冲区中的待读数据变多的时候 。
  3. 当缓冲区有数据可读,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLIN事件时 。
  • 对于写操作
  1. 当缓冲区由不可写变为可写时 。
  2. 当有旧数据被发送走,即缓冲区中的内容变少的时候 。
  3. 当缓冲区有空间可写,且应用进程对相应的描述符进行EPOLL_CTL_MOD 修改EPOLLOUT事件时 。
当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写 。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你 。这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符 。
在ET模式下,缓冲区从不可读变成可读,会唤醒应用进程,缓冲区数据变少的情况,则不会再唤醒应用进程 。
举例1:
  1. 读缓冲区刚开始是空的
  2. 读缓冲区写入2KB数据
  3. 水平触发和边缘触发模式此时都会发出可读信号
  4. 收到信号通知后,读取了1KB的数据,读缓冲区还剩余1KB数据
  5. 水平触发会再次进行通知,而边缘触发不会再进行通知
举例2:(以脉冲的高低电平为例)
  • 水平触发:0为无数据,1为有数据 。缓冲区有数据则一直为1,则一直触发 。
  • 边缘触发发:0为无数据,1为有数据,只要在0变到1的上升沿才触发 。
JDK并没有实现边缘触发,Netty重新实现了epoll机制,采用边缘触发方式;另外像Nginx也采用边缘触发 。
JDK在Linux已经默认使用epoll方式,但是JDK的epoll采用的是水平触发,而Netty重新实现了epoll机制,采用边缘触发方式,netty epoll transport 暴露了更多的nio没有的配置参数,如 TCP_CORK, SO_REUSEADDR等等;另外像Nginx也采用边缘触发 。
epoll与select、poll的对比1. 用户态将文件描述符传入内核的方式
  • select:创建3个文件描述符集并拷贝到内核中,分别监听读、写、异常动作 。这里受到单个进程可以打开的fd数量限制,默认是1024 。
  • poll:将传入的struct pollfd结构体数组拷贝到内核中进行监听 。
  • epoll:执行epoll_create会在内核的高速cache区中建立一颗红黑树以及就绪链表(该链表存储已经就绪的文件描述符) 。接着用户执行的epoll_ctl函数添加文件描述符会在红黑树上增加相应的结点 。
2. 内核态检测文件描述符读写状态的方式