epoll的水平触发LT以及边沿触发ET的原理及使用及优缺点
网络编程相关文章:
在IO多路复用的几种方法中,select和poll只支持水平触发,而epoll支持水平触发和边缘触发两种形式,因此在并发网络编程中该如何选择哪种触发方式呢?(epoll的原理不清楚的可以看一下这篇文章link)
水平触发和边缘触发的不同会影响epoll的事件通过epoll_wait函数的响应。
水平触发(LT):只要缓冲区有数据,epoll_wait就会一直被触发,直到缓冲区为空;(有数据会连续触发)
边沿触发(ET):只有所监听的事件状态改变或者有事件发生时,epoll_wait才会被触发;(有数据只触发一次)
水平触发和边沿触发怎么使用呢?
LT和ET的代码案例:
首先介绍下epoll涉及的函数
int epoll_create(int size);//创建一个监听红黑树,并且返回红黑树的根节点 失败:-1,设置errno int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);//对该监听红黑树所做的操作 //总共三个操作可选 //EPOLL_CTL_ADD 添加fd到监听红黑树 //EPOLL_CTL_MOD 修改fd在监听红黑树上的监听事件 //EPOLL_CTL_DEL 将一个fd从监听红黑树上取下(取消监听) int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout)//监听文件描述符
水平触发LT
int epollfd = epoll_create( 5 );//创建epoll对象,epollfd是保存文件描述符的红黑树根节点 epoll_event event; event.data.fd = fd; event.events = EPOLLIN | EPOLLLT;//添加EPOLLLT事件 epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);//通过epoll_ctl函数添加监听的fd
边沿触发ET
int epollfd = epoll_create( 5 );//创建epoll对象,epollfd是保存文件描述符的红黑树根节点 epoll_event event; event.data.fd = fd; event.events = EPOLLIN | EPOLLET;//添加EPOLLET事件 epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);//通过epoll_ctl函数添加监听的fd
使用水平触发LT或边沿触发ET的结果及处理
开始说了,不同的触发方式会影响epoll_wait函数,那该怎么处理呢?
水平触发LT
//LT int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );//通过epoll_wait监听到epoll事件响应,events中会保存响应的事件队列 sockfd = events[i].data.fd;//从事件队列中取出对应的文件描述符fd n = recv(sockfd , buff, MAXLNE, 0);//通过recv将sockfd缓冲区的数据接收进buff中
水平触发写到上面就可以实现接收数据了,当buff中接收满了时,如果sockfd中假如还有数据没传完,不用担心。水平触发LT会继续触发EPOLLIN事件,epoll_wait函数会再次响应该事件,再来继续接收数据。这就是水平触发(有数据会连续触发)的意思。
边沿触发ET
//ET int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );//通过epoll_wait监听到epoll事件响应,events中会保存响应的事件队列 sockfd = events[i].data.fd;//从事件队列中取出对应的文件描述符fd m_read_idx = 0;//记录一次读数据后的位置索引 while(true) {// // 从buff + m_read_idx索引出开始保存数据 n = recv(sockfd , buff + m_read_idx, MAXLNE, 0 );//通过recv将sockfd缓冲区的数据接收进buff中 if (n == -1) { if( errno == EAGAIN || errno == EWOULDBLOCK ) { // 没有数据 break; } } else if (n == 0) { // 对方关闭连接 //close(sockfd); break; } m_read_idx += n; }
可以看出边沿触发ET和水平触发在读取数据时有点不同。
因为边沿触发ET(有数据只触发一次),所以假如一次recv系统调用无法将sockfd的数据全部读完的话,水平触发不会再触发EPOLLIN事件,epoll_wait函数也不会u对这一个sockfd做出响应。所以当使用ET做触发模式时,我们通常使用while()循环来将sockfd的数据全部读出来。上面程序通过m_read_idx索引记录每次读完后的位置也是常用的方法。
水平触发和边沿触发的优缺点
水平触发LT:
优点:程序简单,会完整地读取所有数据。
缺点:重复地事件触发会影响高并发服务器地性能,因为epoll监控事件涉及到系统调用,需要用户态-内核态的转换。LT消耗了大量的系统资源,影响服务器性能;
边沿触发ET:
优点:每次epoll_wait只用触发一次,通过程序逻辑实现读取缓冲区的所有数据,工作效率高,大大提升了服务器性能;
缺点(没归纳出来,随便写一个):不能保证数据的完整。(这个可以通过上面提到的程序逻辑实现完整地读取数据)
边沿触发没什么缺点?那是不是用epoll就用ET边沿触发就好了?
我的理解是, YES。在日常用epoll实现并发处理,可以优先使用“边沿触发(EPOLLET)+非阻塞IO”模式。
在高并发服务器中边沿触发(ET) 的效率更高
因为边沿触发只在数据到来的一刻才触发,很多时候服务器在接受大量数据时会先接受数据头部(水平触发在此触发第一次,边沿触发第一次)。
接着服务器通过解析头部决定要不要接这个数据。此时,如果不接受数据,水平触发需要手动清除(水平触发当有数据时,会一直触发,直到没有数据可读),而边沿触发可以将清除工作交给一个定时的清除程序去做(只触发一次,不需要的数据可以不读),自己立刻返回。
但是如果sockfd中发送的数据较小,我一次recv就能全部读完,这样LT也不会重复触发epoll事件,和边沿触发的性能差不多,那我们为什么不用更简单的水平触发呢,当然可以使用。那其实就可以理解水平触发和边沿触发是有一个分界点,就是看sockfd的数据是小数据还是大数据。recv的BUFFER_LENGTH如果一次能接收完recv buffer中的数据,就是小数据,一次接收不完就是大数据。小数据就用水平触发,大数据就用边沿触发
但但但但是LT还在一种场景有使用,就是Nginx服务器中listenfd是用的水平触发 。(网络服务器中一般涉及两类sockfd,一种是用于监听是否有连接请求的socket,一种是用于传输数据的socket(上面讲的sockfd都是用于传输数据的))
有一些解释是listenfd用水平触发,如果多个client同时连接进来,listenfd里面积攒多个连接的话,accept一次只处理一个连接,防止漏掉连接,选择水平触发。