http://www.javashuo.com/article/p-onckbwhc-bm.htmlhtml
https://www.jianshu.com/p/aa486512e989linux
https://cloud.tencent.com/developer/article/1005481算法
最后看看epoll独有的两种模式LT和ET。不管是LT和ET模式,都适用于以上所说的流程。区别是,LT模式下,只要一个句柄上的事件一次没有处理完,会在之后调用epoll_wait时次次返回这个句柄,而ET模式仅在第一次返回。 这件事怎么作到的呢? 当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时咱们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,而后清空准备就绪list链表, 最后,epoll_wait干了件事,就是检查这些socket,若是不是ET模式(就是LT模式的句柄了),而且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了。 因此,非ET的句柄,只要它上面还有事件,epoll_wait每次都会返回。而ET模式的句柄,除非有新中断到,即便socket上的事件没有处理完,也是不会次次从epoll_wait返回的。
epoll为何使用红黑树 由于epoll要求快速找到某个句柄,所以首先是一个Map接口,候选实现: 哈希表 O(1) 红黑树 O(lgn) 跳表 近似O(lgn) 听说老版本的内核和FreeBSD的Kqueue使用的是哈希表. 我的理解如今内核使用红黑树的缘由: 哈希表. 空间因素,可伸缩性. (1)频繁增删. 哈希表须要预估空间大小, 这个场景下没法作到. 间接影响响应时间,假如要resize,原来的数据还得移动.即便用了一致性哈希算法, 也难以知足非阻塞的timeout时间限制.(时间不稳定) (2) 百万级链接,哈希表有镂空的空间,太浪费内存. 跳表. 慢于红黑树. 空间也高. 红黑树. 经验判断,内核的其余地方如防火墙也使用红黑树,实践上看性能最优.
fd数量受限于内核内存大小,理论上无限
epoll 跟mmap不要紧 没用到mmap
http://www.javashuo.com/article/p-wsiwtuxy-m.html编程
后面的ETLT区别写得好数组
在这里,笔者强烈推荐《完全学会使用epoll》系列博文,这是笔者看过的,对epoll的ET和LT模式讲解最为详尽和易懂的博文。下面的实例均来自该系列博文。限于篇幅缘由,不少关键的细节,不能彻底摘录。服务器
话很少说,直接上代码。并发
程序一:app
#include <stdio.h> #include <unistd.h> #include <sys/epoll.h> int main(void) { int epfd,nfds; struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件 epfd = epoll_create(1); //只须要监听一个描述符——标准输入 ev.data.fd = STDIN_FILENO; ev.events = EPOLLIN|EPOLLET; //监听读状态同时设置ET模式 epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); //注册epoll事件 for(;;) { nfds = epoll_wait(epfd, events, 5, -1); for(int i = 0; i < nfds; i++) { if(events[i].data.fd==STDIN_FILENO) printf("welcome to epoll's word!\n"); } } }
接下来咱们将上面程序的第11行作以下修改:socket
1 ev.events=EPOLLIN; //默认使用LT模式
编译并运行,结果以下:ide
程序陷入死循环,由于用户输入任意数据后,数据被送入buffer且没有被读出,因此LT模式下每次epoll_wait都认为buffer可读返回读就绪。致使每次都会输出”welcome to epoll's world!”。
程序二:
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/epoll.h> 4 5 int main(void) 6 { 7 int epfd,nfds; 8 struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件 9 epfd = epoll_create(1); //只须要监听一个描述符——标准输入 10 ev.data.fd = STDIN_FILENO; 11 ev.events = EPOLLIN; //监听读状态同时设置LT模式 12 epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); //注册epoll事件 13 for(;;) 14 { 15 nfds = epoll_wait(epfd, events, 5, -1); 16 for(int i = 0; i < nfds; i++) 17 { 18 if(events[i].data.fd==STDIN_FILENO) 19 { 20 char buf[1024] = {0}; 21 read(STDIN_FILENO, buf, sizeof(buf)); 22 printf("welcome to epoll's word!\n"); 23 } 24 } 25 } 26 }
编译并运行,结果以下:
本程序依然使用LT模式,可是每次epoll_wait返回读就绪的时候咱们都将buffer(缓冲)中的内容read出来,因此致使buffer再次清空,下次调用epoll_wait就会阻塞。因此可以实现咱们所想要的功能——当用户从控制台有任何输入操做时,输出”welcome to epoll's world!”
程序三:
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/epoll.h> 4 5 int main(void) 6 { 7 int epfd,nfds; 8 struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件 9 epfd = epoll_create(1); //只须要监听一个描述符——标准输入 10 ev.data.fd = STDIN_FILENO; 11 ev.events = EPOLLIN|EPOLLET; //监听读状态同时设置ET模式 12 epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); //注册epoll事件 13 for(;;) 14 { 15 nfds = epoll_wait(epfd, events, 5, -1); 16 for(int i = 0; i < nfds; i++) 17 { 18 if(events[i].data.fd==STDIN_FILENO) 19 { 20 printf("welcome to epoll's word!\n"); 21 ev.data.fd = STDIN_FILENO; 22 ev.events = EPOLLIN|EPOLLET; //设置ET模式 23 epoll_ctl(epfd, EPOLL_CTL_MOD, STDIN_FILENO, &ev); //重置epoll事件(ADD无效) 24 } 25 } 26 } 27 }
编译并运行,结果以下:
程序依然使用ET,可是每次读就绪后都主动的再次MOD IN事件,咱们发现程序再次出现死循环,也就是每次返回读就绪。可是注意,若是咱们将MOD改成ADD,将不会产生任何影响。别忘了每次ADD一个描述符都会在epitem组成的红黑树中添加一个项,咱们以前已经ADD过一次,再次ADD将阻止添加,因此在次调用ADD IN事件不会有任何影响。
程序四:
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/epoll.h> 4 5 int main(void) 6 { 7 int epfd,nfds; 8 struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件 9 epfd = epoll_create(1); //只须要监听一个描述符——标准输入 10 ev.data.fd = STDOUT_FILENO; 11 ev.events = EPOLLOUT|EPOLLET; //监听读状态同时设置ET模式 12 epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev); //注册epoll事件 13 for(;;) 14 { 15 nfds = epoll_wait(epfd, events, 5, -1); 16 for(int i = 0; i < nfds; i++) 17 { 18 if(events[i].data.fd==STDOUT_FILENO) 19 { 20 printf("welcome to epoll's word!\n"); 21 } 22 } 23 } 24 }
编译并运行,结果以下:
这个程序的功能是只要标准输出写就绪,就输出“welcome to epoll's world”。咱们发现这将是一个死循环。下面具体分析一下这个程序的执行过程:
程序五:
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/epoll.h> 4 5 int main(void) 6 { 7 int epfd,nfds; 8 struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件 9 epfd = epoll_create(1); //只须要监听一个描述符——标准输入 10 ev.data.fd = STDOUT_FILENO; 11 ev.events = EPOLLOUT|EPOLLET; //监听读状态同时设置ET模式 12 epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev); //注册epoll事件 13 for(;;) 14 { 15 nfds = epoll_wait(epfd, events, 5, -1); 16 for(int i = 0; i < nfds; i++) 17 { 18 if(events[i].data.fd==STDOUT_FILENO) 19 { 20 printf("welcome to epoll's word!"); 21 } 22 } 23 } 24 }
编译并运行,结果以下:
与程序四相比,程序五只是将输出语句的printf的换行符移除。咱们看到程序成挂起状态。由于第一次epoll_wait返回写就绪后,程序向标准输出的buffer中写入“welcome to epoll's world!”,可是由于没有输出换行,因此buffer中的内容一直存在,下次epoll_wait的时候,虽然有写空间可是ET模式下再也不返回写就绪。回忆第一节关于ET的实现,这种状况缘由就是第一次buffer为空,致使epitem加入rdlist,返回一次就绪后移除此epitem,以后虽然buffer仍然可写,可是因为对应epitem已经再也不rdlist中,就不会对其就绪fd的events的在检测了。
程序六:
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/epoll.h> 4 5 int main(void) 6 { 7 int epfd,nfds; 8 struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件 9 epfd = epoll_create(1); //只须要监听一个描述符——标准输入 10 ev.data.fd = STDOUT_FILENO; 11 ev.events = EPOLLOUT; //监听读状态同时设置LT模式 12 epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev); //注册epoll事件 13 for(;;) 14 { 15 nfds = epoll_wait(epfd, events, 5, -1); 16 for(int i = 0; i < nfds; i++) 17 { 18 if(events[i].data.fd==STDOUT_FILENO) 19 { 20 printf("welcome to epoll's word!"); 21 } 22 } 23 } 24 }
编译并运行,结果以下:
程序六相对程序五仅仅是修改ET模式为默认的LT模式,咱们发现程序再次死循环。这时候缘由已经很清楚了,由于当向buffer写入”welcome to epoll's world!”后,虽然buffer没有输出清空,可是LT模式下只有buffer有写空间就返回写就绪,因此会一直输出”welcome to epoll's world!”,当buffer满的时候,buffer会自动刷清输出,一样会形成epoll_wait返回写就绪。
程序七:
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/epoll.h> 4 5 int main(void) 6 { 7 int epfd,nfds; 8 struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件 9 epfd = epoll_create(1); //只须要监听一个描述符——标准输入 10 ev.data.fd = STDOUT_FILENO; 11 ev.events = EPOLLOUT|EPOLLET; //监听读状态同时设置LT模式 12 epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev); //注册epoll事件 13 for(;;) 14 { 15 nfds = epoll_wait(epfd, events, 5, -1); 16 for(int i = 0; i < nfds; i++) 17 { 18 if(events[i].data.fd==STDOUT_FILENO) 19 { 20 printf("welcome to epoll's word!"); 21 ev.data.fd = STDOUT_FILENO; 22 ev.events = EPOLLOUT|EPOLLET; //设置ET模式 23 epoll_ctl(epfd, EPOLL_CTL_MOD, STDOUT_FILENO, &ev); //重置epoll事件(ADD无效) 24 } 25 } 26 } 27 }
编译并运行,结果以下:
程序七相对于程序五在每次向标准输出的buffer输出”welcome to epoll's world!”后,从新MOD OUT事件。因此至关于每次都会返回就绪,致使程序循环输出。
通过前面的案例分析,咱们已经了解到,当epoll工做在ET模式下时,对于读操做,若是read一次没有读尽buffer中的数据,那么下次将得不到读就绪的通知,形成buffer中已有的数据无机会读出,除非有新的数据再次到达。对于写操做,主要是由于ET模式下fd一般为非阻塞形成的一个问题——如何保证将用户要求写的数据写完。
要解决上述两个ET模式下的读写问题,咱们必须实现:
请思考如下一种场景:在某一时刻,有多个链接同时到达,服务器的 TCP 就绪队列瞬间积累多个就绪链接,因为是边缘触发模式,epoll 只会通知一次,accept 只处理一个链接,致使 TCP 就绪队列中剩下的链接都得不处处理。在这种情形下,咱们应该如何有效的处理呢?
解决的方法是:解决办法是用 while 循环抱住 accept 调用,处理完 TCP 就绪队列中的全部链接后再退出循环。如何知道是否处理完就绪队列中的全部链接呢? accept 返回 -1 而且 errno 设置为 EAGAIN 就表示全部链接都处理完。
关于ET的accept问题,这篇博文的参考价值很高,若是有兴趣,能够连接过去围观一下。
由于ET模式下的读写须要一直读或写直到出错(对于读,当读到的实际字节数小于请求字节数时就能够中止),而若是你的文件描述符若是不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞。这样就不能在阻塞在epoll_wait上了,形成其余文件描述符的任务饥饿。
这样的实例,网上已经有不少了(包括参考连接),笔者这里就略过了。
LT:水平触发,效率会低于ET触发,尤为在大并发,大流量的状况下。可是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,所以不用担忧事件丢失的状况。
ET:边缘触发,效率很是高,在并发,大流量的状况下,会比LT少不少epoll的系统调用,所以效率高。可是对编程要求高,须要细致的处理每一个请求,不然容易发生丢失事件的状况。
从本质上讲:与LT相比,ET模型是经过减小系统调用来达到提升并行效率的。