一、先来看看官方的说辞
epoll 对文件的描述符的操作有两种模式 : LT(Level Trigger, 电平触发)模式 和 ET(Edge Trigger ,边沿触发)模式。LT模式是默认的工作模式,这个模式下epoll相当于一个效率较高的poll。当往epoll中内核事件表中注册EPOLLET事件时,epoll将以ET模式来操作该文件描述符。ET是epoll的高效模式。
对于采用LT工作的文件描述符,当epolll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样当应用程序下次调用epoll_wait时,epolll_wait还会再次向应用程序通知此事件,直到有该事件被处理。而对于采用ET模式的文件描述符,当epoll_wait检测当其上有事件发生时并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epolll_wait调用不再降此事件通知应用程序,可见,ET模式在很大程度上降底了同一个epoll事件被重复触发的次数,因此效率要比LT模式高。
二、再来举例,写代码验证
完整的源码工程下载:https://download.csdn.net/download/libaineu2004/10888888
LT,我设置了静态变量num,来统计奇数偶数。偶数时,故意不处理IO事件,奇数时才处理,下断点可以观察LT的工作模式是怎么样的?
实践可以证明:对于采用LT工作的文件描述符,当epolll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样当应用程序下次调用epoll_wait时,epolll_wait还会再次向应用程序通知此事件,直到有该事件被处理。
void lt( epoll_event* events, int number, int epollfd, int listenfd ) { char buf[ BUFFER_SIZE ]; for ( int i = 0; i < number; i++ ) { int sockfd = events[i].data.fd; if ( sockfd == listenfd ) { struct sockaddr_in client_address; socklen_t client_addrlength = sizeof( client_address ); int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength ); addfd( epollfd, connfd, false ); } else if ( events[i].events & EPOLLIN ) { printf( "event trigger once\n" ); memset( buf, '\0', BUFFER_SIZE ); static int num = 0; int ret = 0; if ((num % 2) == 0) { printf( "event null\n" );; } else { ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 ); if( ret <= 0 ) { close( sockfd ); continue; } } num++; printf( "get %d bytes of content: %s\n", ret, buf ); } else { printf( "something else happened \n" ); } } }
再来验证ET,我同样地设置了静态变量count,来统计奇数偶数。偶数时,故意不处理IO事件,奇数时才处理,下断点可以观察ET的工作模式是怎么样的?
实践可以证明:对于采用ET模式的文件描述符,当epoll_wait检测当其上有事件发生时并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epolll_wait调用不再降此事件通知应用程序。另外,源码例子中,当count偶数时,程序员故意不处理IO事件,数据并不会丢失,而是会缓存起来,等到下次接收到新数据,IO事件触发时,即count奇数时,epolll_wait再次通知,会和本次数据一起读出来。
所以,为了一次性把数据读完,ET模式必须加上while(1)循环。
void et( epoll_event* events, int number, int epollfd, int listenfd ) { char buf[ BUFFER_SIZE ]; for ( int i = 0; i < number; i++ ) { int sockfd = events[i].data.fd; if ( sockfd == listenfd ) { struct sockaddr_in client_address; socklen_t client_addrlength = sizeof( client_address ); int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength ); addfd( epollfd, connfd, true ); } else if ( events[i].events & EPOLLIN ) { printf( "event trigger once\n" ); //while( 1 ) { memset( buf, '\0', BUFFER_SIZE ); static int count = 0; int ret = 0; if ((count % 2) == 0) { printf( "event null\n" ); } else { ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 ); if( ret < 0 ) { if( ( errno == EAGAIN ) || ( errno == EWOULDBLOCK ) ) { printf( "read later\n" ); break; } close( sockfd ); break; } else if( ret == 0 ) { close( sockfd ); } else { printf( "get %d bytes of content: %s\n", ret, buf ); } } count++; } } else { printf( "something else happened \n" ); } } }
三、最后看看ET模式,因为有个while循环,epoll是在边缘触发的时候要把套接字中的数据读干净,那么当有多个套接字时,在读的套接字一直不停的有数据到达,如何保证其他套接字不被饿死?说白了就是:
ET源源不断来数据,造成 while(1)死循环怎么办?
问 2018/12/30 11:17:24
请问一下,epoll是在边缘触发的时候要把套接字中的数据读干净,那么当有多个套接字时,在读的套接字一直不停的有数据到达,如何保证其他套接字不被饿死?
答 2018/12/30 11:23:16
et模式是有数据只通知一次,如果不处理不会继续通知。
答 2018/12/30 11:24:36
就像饭店你知道有人来了 你就得服务 但是你不应该只为他服务 你可以先上一部分菜给他吃着 然后给其他顾客上菜
答 2018/12/30 11:25:00
这样子避免了其他顾客的等待
答 2018/12/30 11:26:02
当顾客吃完了再请他走就ok啦
答 2018/12/30 11:31:10
具体实现应该是给每个连接添加一个时间记录 记录上次处理的时间 当再次需要处理时判断时间差 是否符合间隔(当然如果没有其他连接在等待处理就不需要间隔啦),如果符合时间间隔才进行处理。
问 2018/12/30 11:32:59
et模式是有数据只通知一次,如果不处理不会继续通知。时间间隔到了,不处理就丢失了
答 2018/12/30 11:50:40
谁跟你说丢失了
答 2018/12/30 11:53:51
epollwait会监听 触发事件加入队列处理 如果考虑均匀服务 那么需要计时器io不一次性处理完 如果不考虑就是直接处理完所有io事件就移除了
答 2018/12/30 11:56:01
et模式,如果通知事件触发不处理就需要下次收到数据才会再次通知 数据保留在缓冲期
答 2018/12/30 11:56:30
这样子的特性可以让你延迟数据处理的时间点
答 2018/12/30 11:57:48
比如你收到A发来的数据时不处理 当收到完A B C的数据再处理然后将数据发回去给它们
补充说明:
无论是et还是lt,都建议把数据读干净,都建议开一个缓冲区buffer(读内存比网卡快),异步处理数据,这样不至于阻塞后面的套接字;
ET源源不断来数据,造成 while(1)死循环怎么办?使用超时时间吧,while循环超过了指定的时间间隔,就打断循环体。另外,也需要检查源源不断来的数据的合法性,不合法就及时断开tcp连接。