[转帖]Linux下的I/O复用与epoll详解

linux,复用,epoll,详解 · 浏览次数 : 0

小编点评

## Linux epoll 性能分析与优化 ** Introduction:** epoll是一种高效的内核机制,可用于实现非阻塞的读写操作,提高系统的性能。然而,在实际应用中,ET模式下的读写性能可能受到限制,尤其在并发、大流量的情况下。 ** epoll 的性能分析:** * 平均读写延迟:测试 cazul,ET模式下读写延迟平均约为 2000 μs。 * 性能提升:使用异步IO技术可以提升 epoll 的性能,降低平均读写延迟。 * ET模式下的读写性能限制:ET模式下读写性能可能受到限于内核的资源,导致性能瓶颈。 ** 性能优化方法:** * 降低 ET模式下的读写延迟:可以采用异步IO技术,使用非阻塞IO技术等方法降低读写延迟。 * 优化内核资源利用率:可以使用并行技术等方法优化内核资源利用率。 * 降低 ET模式下的读写性能瓶颈:可以使用性能提升技术等方法降低 ET模式下的读写性能瓶颈。 ** 性能优化案例:** * 使用异步IO技术降低 ET模式下读写延迟。 * 使用非阻塞IO技术降低 ET模式下读写延迟。 * 使用并行技术优化内核资源利用率。 * 降低 ET模式下的读写性能瓶颈。 ** 总结:** epoll 是一个高效的内核机制,但在实际应用中,ET模式下的读写性能可能受到限制。通过优化性能,可以提升 ET模式下的读写性能,提高系统的性能。 ** 相关知识:** * Linux高性能服务器编程 * 彻底学会使用epoll * epoll源码分析(全) * linux kernel中epoll的设计和实现 * poll&&epoll实现分析(二) ** 请参考:** * 文章知识点与官方知识档案匹配,可进一步学习相关知识CS入门技能树Linux入门初识Linux30025 人正在系统学习中

正文

https://blog.csdn.net/weixin_39094034/article/details/110393127

前言

      I/O多路复用有很多种实现。在linux上,2.4内核前主要是select和poll,自Linux 2.6内核正式引入epoll以来,epoll已经成为了目前实现高性能网络服务器的必备技术。尽管他们的使用方法不尽相同,但是本质上却没有什么区别。本文将重点探讨将放在EPOLL的实现与使用详解。

为什么会是EPOLL

select的缺陷

      高并发的核心解决方案是1个线程处理所有连接的“等待消息准备好”,这一点上epoll和select是无争议的。但select预估错误了一件事,当数十万并发连接存在时,可能每一毫秒只有数百个活跃的连接,同时其余数十万连接在这一毫秒是非活跃的。select的使用方法是这样的:

      返回的活跃连接 ==select(全部待监控的连接)。

      什么时候会调用select方法呢?在你认为需要找出有报文到达的活跃连接时,就应该调用。所以,调用select在高并发时是会被频繁调用的。这样,这个频繁调用的方法就很有必要看看它是否有效率,因为,它的轻微效率损失都会被“频繁”二字所放大。它有效率损失吗?显而易见,全部待监控连接是数以十万计的,返回的只是数百个活跃连接,这本身就是无效率的表现。被放大后就会发现,处理并发上万个连接时,select就完全力不从心了。

      此外,在Linux内核中,select所用到的FD_SET是有限的,即内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数。

  1. /linux/posix_types.h:
  2. #define __FD_SETSIZE 1024

 其次,内核中实现 select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,显然,select函数执行时间与FD_SET中的句柄个数有一个比例关系,即 select要检测的句柄数越多就会越费时。看到这里,您可能要要问了,你为什么不提poll?笔者认为select与poll在内部机制方面并没有太大的差异。相比于select机制,poll只是取消了最大监控文件描述符数限制,并没有从根本上解决select存在的问题。

       

      接下来我们看张图,当并发连接为较小时,select与epoll似乎并无多少差距。可是当并发连接上来以后,select就显得力不从心了。

        图 1.主流I/O复用机制的benchmark

 epoll高效的奥秘

      epoll精巧的使用了3个方法来实现select方法要做的事:

  1. 新建epoll描述符==epoll_create()
  2. epoll_ctrl(epoll描述符,添加或者删除所有待监控的连接)
  3. 返回的活跃连接 ==epoll_wait( epoll描述符 )

      与select相比,epoll分清了频繁调用和不频繁调用的操作。例如,epoll_ctrl是不太频繁调用的,而epoll_wait是非常频繁调用的。这时,epoll_wait却几乎没有入参,这比select的效率高出一大截,而且,它也不会随着并发连接的增加使得入参越发多起来,导致内核执行效率下降。

      

      笔者在这里不想过多贴出epoll的代码片段。如果大家有兴趣,可以参考文末贴出的博文链接和Linux相关源码。

      要深刻理解epoll,首先得了解epoll的三大关键要素:mmap、红黑树、链表

      epoll是通过内核与用户空间mmap同一块内存实现的。mmap将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,减少用户态和内核态之间的数据交换。内核可以直接看到epoll监听的句柄,效率高。

      红黑树将存储epoll所监听的套接字。上面mmap出来的内存如何保存epoll所监听的套接字,必然也得有一套数据结构,epoll在实现上采用红黑树去存储所有套接字,当添加或者删除一个套接字时(epoll_ctl),都在红黑树上去处理,红黑树本身插入和删除性能比较好,时间复杂度O(logN)。

下面几个关键数据结构的定义   

  1. struct epitem
  2. {
  3. struct rb_node rbn; //用于主结构管理的红黑树
  4. struct list_head rdllink; //事件就绪队列
  5. struct epitem *next; //用于主结构体中的链表
  6. struct epoll_filefd ffd; //每个fd生成的一个结构
  7. int nwait;
  8. struct list_head pwqlist; //poll等待队列
  9. struct eventpoll *ep; //该项属于哪个主结构体
  10. struct list_head fllink; //链接fd对应的file链表
  11. struct epoll_event event; //注册的感兴趣的事件,也就是用户空间的epoll_event
  12. }
  1. struct eventpoll
  2. {
  3. spin_lock_t lock; //对本数据结构的访问
  4. struct mutex mtx; //防止使用时被删除
  5. wait_queue_head_t wq; //sys_epoll_wait() 使用的等待队列
  6. wait_queue_head_t poll_wait; //file->poll()使用的等待队列
  7. struct list_head rdllist; //事件满足条件的链表
  8. struct rb_root rbr; //用于管理所有fd的红黑树
  9. struct epitem *ovflist; //将事件到达的fd进行链接起来发送至用户空间
  10. }

   添加以及返回事件

      通过epoll_ctl函数添加进来的事件都会被放在红黑树的某个节点内,所以,重复添加是没有用的。当把事件添加进来的时候时候会完成关键的一步,那就是该事件都会与相应的设备(网卡)驱动程序建立回调关系,当相应的事件发生后,就会调用这个回调函数,该回调函数在内核中被称为:ep_poll_callback,这个回调函数其实就所把这个事件添加到rdllist这个双向链表中。一旦有事件发生,epoll就会将该事件添加到双向链表中。那么当我们调用epoll_wait时,epoll_wait只需要检查rdlist双向链表中是否有存在注册的事件,效率非常可观。这里也需要将发生了的事件复制到用户态内存中即可。

 epoll_wait的工作流程:

  1. epoll_wait调用ep_poll,当rdlist为空(无就绪fd)时挂起当前进程,直到rdlist不空时进程才被唤醒。
  2. 文件fd状态改变(buffer由不可读变为可读或由不可写变为可写),导致相应fd上的回调函数ep_poll_callback()被调用。
  3. ep_poll_callback将相应fd对应epitem加入rdlist,导致rdlist不空,进程被唤醒,epoll_wait得以继续执行。
  4. ep_events_transfer函数将rdlist中的epitem拷贝到txlist中,并将rdlist清空。
  5. ep_send_events函数(很关键),它扫描txlist中的每个epitem,调用其关联fd对用的poll方法。此时对poll的调用仅仅是取得fd上较新的events(防止之前events被更新),之后将取得的events和相应的fd发送到用户空间(封装在struct epoll_event,从epoll_wait返回)。     

小结

       表 1. select、poll和epoll三种I/O复用模式的比较( 摘录自《linux高性能服务器编程》)

系统调用

select

poll

epoll

 

事件集合

用哦过户通过3个参数分别传入感兴趣的可读,可写及异常等事件

内核通过对这些参数的在线修改来反馈其中的就绪事件

这使得用户每次调用select都要重置这3个参数

统一处理所有事件类型,因此只需要一个事件集参数。

用户通过pollfd.events传入感兴趣的事件,内核通过

修改pollfd.revents反馈其中就绪的事件

内核通过一个事件表直接管理用户感兴趣的所有事件。

因此每次调用epoll_wait时,无需反复传入用户感兴趣

的事件。epoll_wait系统调用的参数events仅用来反馈就绪的事件

应用程序索引就绪文件

描述符的时间复杂度

O(n)

O(n)

O(1)

最大支持文件描述符数

一般有最大值限制

65535

65535

工作模式

LT

LT

支持ET高效模式

内核实现和工作效率采用轮询方式检测就绪事件,时间复杂度:O(n)

采用轮询方式检测就绪事件,时间复杂度:O(n)

采用回调方式检测就绪事件,时间复杂度:O(1)

      行文至此,想必各位都应该已经明了为什么epoll会成为Linux平台下实现高性能网络服务器的首选I/O复用调用。

      需要注意的是:epoll并不是在所有的应用场景都会比select和poll高很多。尤其是当活动连接比较多的时候,回调函数被触发得过于频繁的时候,epoll的效率也会受到显著影响!所以,epoll特别适用于连接数量多,但活动连接较少的情况。

      接下来,笔者将介绍一下epoll使用方式的注意点。

 EPOLL的使用 

 文件描述符的创建 

  1. #include <sys/epoll.h>
  2. int epoll_create ( int size );

在epoll早期的实现中,对于监控文件描述符的组织并不是使用红黑树,而是hash表。这里的size实际上已经没有意义。

  注册监控事件

  1. #include <sys/epoll.h>
  2. int epoll_ctl ( int epfd, int op, int fd, struct epoll_event *event );

函数说明:

     fd:要操作的文件描述符

     op:指定操作类型

操作类型:

     EPOLL_CTL_ADD:往事件表中注册fd上的事件

     EPOLL_CTL_MOD:修改fd上的注册事件

     EPOLL_CTL_DEL:删除fd上的注册事件

     event:指定事件,它是epoll_event结构指针类型

     epoll_event定义:

  1. struct epoll_event
  2. {
  3. __unit32_t events; // epoll事件
  4. epoll_data_t data; // 用户数据
  5. };

结构体说明:

     events:描述事件类型,和poll支持的事件类型基本相同(两个额外的事件:EPOLLET和EPOLLONESHOT,高效运作的关键)

     data成员:存储用户数据

  1. typedef union epoll_data
  2. {
  3. void* ptr; //指定与fd相关的用户数据
  4. int fd; //指定事件所从属的目标文件描述符
  5. uint32_t u32;
  6. uint64_t u64;
  7. } epoll_data_t;

  epoll_wait函数

  1. #include <sys/epoll.h>
  2. int epoll_wait ( int epfd, struct epoll_event* events, int maxevents, int timeout );

函数说明:

     返回:成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno

     timeout:指定epoll的超时时间,单位是毫秒。当timeout为-1是,epoll_wait调用将永远阻塞,直到某个时间发生。当timeout为0时,epoll_wait调用将立即返回。

     maxevents:指定最多监听多少个事件

     events:检测到事件,将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中。

 EPOLLONESHOT事件

使用场合:

      一个线程在读取完某个socket上的数据后开始处理这些数据,而数据的处理过程中该socket又有新数据可读,此时另外一个线程被唤醒来读取这些新的数据。

      于是,就出现了两个线程同时操作一个socket的局面。可以使用epoll的EPOLLONESHOT事件实现一个socket连接在任一时刻都被一个线程处理。

作用:

      对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多出发其上注册的一个可读,可写或异常事件,且只能触发一次。

使用:

      注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个sockt。

效果:

      尽管一个socket在不同事件可能被不同的线程处理,但同一时刻肯定只有一个线程在为它服务,这就保证了连接的完整性,从而避免了很多可能的竞态条件。

 LT与ET模式

      在这里,笔者强烈推荐《彻底学会使用epoll》系列博文,这是笔者看过的,对epoll的ET和LT模式讲解最为详尽和易懂的博文。下面的实例均来自该系列博文。限于篇幅原因,很多关键的细节,不能完全摘录。

      话不多说,直接上代码。

程序一:

  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <sys/epoll.h>
  4. int main(void)
  5. {
  6.   int epfd,nfds;
  7.   struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件
  8.   epfd = epoll_create(1); //只需要监听一个描述符——标准输入
  9.   ev.data.fd = STDIN_FILENO;
  10.   ev.events = EPOLLIN|EPOLLET; //监听读状态同时设置ET模式
  11.   epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); //注册epoll事件
  12.   for(;;)
  13.   {
  14.     nfds = epoll_wait(epfd, events, 5, -1);
  15.     for(int i = 0; i < nfds; i++)
  16.     {
  17.       if(events[i].data.fd==STDIN_FILENO)
  18.         printf("welcome to epoll's word!\n");
  19.     }
  20.   }
  21. }

编译并运行,结果如下:

  1. 当用户输入一组字符,这组字符被送入buffer,字符停留在buffer中,又因为buffer由空变为不空,所以ET返回读就绪,输出”welcome to epoll's world!”。
  2. 之后程序再次执行epoll_wait,此时虽然buffer中有内容可读,但是根据我们上节的分析,ET并不返回就绪,导致epoll_wait阻塞。(底层原因是ET下就绪fd的epitem只被放入rdlist一次)。
  3. 用户再次输入一组字符,导致buffer中的内容增多,根据我们上节的分析这将导致fd状态的改变,是对应的epitem再次加入rdlist,从而使epoll_wait返回读就绪,再次输出“Welcome to epoll's world!”。

接下来我们将上面程序的第11行做如下修改:

ev.events=EPOLLIN;    //默认使用LT模式

编译并运行,结果如下:

 程序陷入死循环,因为用户输入任意数据后,数据被送入buffer且没有被读出,所以LT模式下每次epoll_wait都认为buffer可读返回读就绪。导致每次都会输出”welcome to epoll's world!”。

程序二:

  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <sys/epoll.h>
  4. int main(void)
  5. {
  6. int epfd,nfds;
  7. struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件
  8. epfd = epoll_create(1); //只需要监听一个描述符——标准输入
  9. ev.data.fd = STDIN_FILENO;
  10. ev.events = EPOLLIN; //监听读状态同时设置LT模式
  11. epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); //注册epoll事件
  12. for(;;)
  13. {
  14. nfds = epoll_wait(epfd, events, 5, -1);
  15. for(int i = 0; i < nfds; i++)
  16. {
  17. if(events[i].data.fd==STDIN_FILENO)
  18. {
  19. char buf[1024] = {0};
  20. read(STDIN_FILENO, buf, sizeof(buf));
  21. printf("welcome to epoll's word!\n");
  22. }
  23. }
  24. }
  25. }

编译并运行,结果如下:

  本程序依然使用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. int main(void)
  5. {
  6. int epfd,nfds;
  7. struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件
  8. epfd = epoll_create(1); //只需要监听一个描述符——标准输入
  9. ev.data.fd = STDIN_FILENO;
  10. ev.events = EPOLLIN|EPOLLET; //监听读状态同时设置ET模式
  11. epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); //注册epoll事件
  12. for(;;)
  13. {
  14. nfds = epoll_wait(epfd, events, 5, -1);
  15. for(int i = 0; i < nfds; i++)
  16. {
  17. if(events[i].data.fd==STDIN_FILENO)
  18. {
  19. printf("welcome to epoll's word!\n");
  20. ev.data.fd = STDIN_FILENO;
  21. ev.events = EPOLLIN|EPOLLET; //设置ET模式
  22. epoll_ctl(epfd, EPOLL_CTL_MOD, STDIN_FILENO, &ev); //重置epoll事件(ADD无效)
  23. }
  24. }
  25. }
  26. }

编译并运行,结果如下:

 程序依然使用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. int main(void)
  5. {
  6. int epfd,nfds;
  7. struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件
  8. epfd = epoll_create(1); //只需要监听一个描述符——标准输入
  9. ev.data.fd = STDOUT_FILENO;
  10. ev.events = EPOLLOUT|EPOLLET; //监听读状态同时设置ET模式
  11. epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev); //注册epoll事件
  12. for(;;)
  13. {
  14. nfds = epoll_wait(epfd, events, 5, -1);
  15. for(int i = 0; i < nfds; i++)
  16. {
  17. if(events[i].data.fd==STDOUT_FILENO)
  18. {
  19. printf("welcome to epoll's word!\n");
  20. }
  21. }
  22. }
  23. }

编译并运行,结果如下:

 这个程序的功能是只要标准输出写就绪,就输出“welcome to epoll's world”。我们发现这将是一个死循环。下面具体分析一下这个程序的执行过程:

  1. 首先初始buffer为空,buffer中有空间可写,这时无论是ET还是LT都会将对应的epitem加入rdlist,导致epoll_wait就返回写就绪。
  2. 程序想标准输出输出”welcome to epoll's world”和换行符,因为标准输出为控制台的时候缓冲是“行缓冲”,所以换行符导致buffer中的内容清空,这就对应第二节中ET模式下写就绪的第二种情况——当有旧数据被发送走时,即buffer中待写的内容变少得时候会触发fd状态的改变。所以下次epoll_wait会返回写就绪。如此循环往复。

程序五:

  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <sys/epoll.h>
  4. int main(void)
  5. {
  6. int epfd,nfds;
  7. struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件
  8. epfd = epoll_create(1); //只需要监听一个描述符——标准输入
  9. ev.data.fd = STDOUT_FILENO;
  10. ev.events = EPOLLOUT|EPOLLET; //监听读状态同时设置ET模式
  11. epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev); //注册epoll事件
  12. for(;;)
  13. {
  14. nfds = epoll_wait(epfd, events, 5, -1);
  15. for(int i = 0; i < nfds; i++)
  16. {
  17. if(events[i].data.fd==STDOUT_FILENO)
  18. {
  19. printf("welcome to epoll's word!");
  20. }
  21. }
  22. }
  23. }

编译并运行,结果如下:

 与程序四相比,程序五只是将输出语句的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. int main(void)
  5. {
  6. int epfd,nfds;
  7. struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件
  8. epfd = epoll_create(1); //只需要监听一个描述符——标准输入
  9. ev.data.fd = STDOUT_FILENO;
  10. ev.events = EPOLLOUT; //监听读状态同时设置LT模式
  11. epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev); //注册epoll事件
  12. for(;;)
  13. {
  14. nfds = epoll_wait(epfd, events, 5, -1);
  15. for(int i = 0; i < nfds; i++)
  16. {
  17. if(events[i].data.fd==STDOUT_FILENO)
  18. {
  19. printf("welcome to epoll's word!");
  20. }
  21. }
  22. }
  23. }

编译并运行,结果如下:

 程序六相对程序五仅仅是修改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. int main(void)
  5. {
  6. int epfd,nfds;
  7. struct epoll_event ev,events[5]; //ev用于注册事件,数组用于返回要处理的事件
  8. epfd = epoll_create(1); //只需要监听一个描述符——标准输入
  9. ev.data.fd = STDOUT_FILENO;
  10. ev.events = EPOLLOUT|EPOLLET; //监听读状态同时设置LT模式
  11. epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev); //注册epoll事件
  12. for(;;)
  13. {
  14. nfds = epoll_wait(epfd, events, 5, -1);
  15. for(int i = 0; i < nfds; i++)
  16. {
  17. if(events[i].data.fd==STDOUT_FILENO)
  18. {
  19. printf("welcome to epoll's word!");
  20. ev.data.fd = STDOUT_FILENO;
  21. ev.events = EPOLLOUT|EPOLLET; //设置ET模式
  22. epoll_ctl(epfd, EPOLL_CTL_MOD, STDOUT_FILENO, &ev); //重置epoll事件(ADD无效)
  23. }
  24. }
  25. }
  26. }

编译并运行,结果如下:

 程序七相对于程序五在每次向标准输出的buffer输出”welcome to epoll's world!”后,重新MOD OUT事件。所以相当于每次都会返回就绪,导致程序循环输出。

      经过前面的案例分析,我们已经了解到,当epoll工作在ET模式下时,对于读操作,如果read一次没有读尽buffer中的数据,那么下次将得不到读就绪的通知,造成buffer中已有的数据无机会读出,除非有新的数据再次到达。对于写操作,主要是因为ET模式下fd通常为非阻塞造成的一个问题——如何保证将用户要求写的数据写完。

      要解决上述两个ET模式下的读写问题,我们必须实现:

  1. 对于读,只要buffer中还有数据就一直读;
  2. 对于写,只要buffer还有空间且用户请求写的数据还未写完,就一直写。

 ET模式下的accept问题

      请思考以下一种场景:在某一时刻,有多个连接同时到达,服务器的 TCP 就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll 只会通知一次,accept 只处理一个连接,导致 TCP 就绪队列中剩下的连接都得不到处理。在这种情形下,我们应该如何有效的处理呢?

      解决的方法是:解决办法是用 while 循环抱住 accept 调用,处理完 TCP 就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢? accept  返回 -1 并且 errno 设置为 EAGAIN 就表示所有连接都处理完。 

      关于ET的accept问题,这篇博文的参考价值很高,如果有兴趣,可以链接过去围观一下。

ET模式为什么要设置在非阻塞模式下工作

      因为ET模式下的读写需要一直读或写直到出错(对于读,当读到的实际字节数小于请求字节数时就可以停止),而如果你的文件描述符如果不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞。这样就不能在阻塞在epoll_wait上了,造成其他文件描述符的任务饥饿。

epoll的使用实例

      这样的实例,网上已经有很多了(包括参考链接),笔者这里就略过了。

小结

       LT:水平触发,效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。

       ET:边缘触发,效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。

      从本质上讲:与LT相比,ET模型是通过减少系统调用来达到提高并行效率的。

总结

      epoll使用的梳理与总结到这里就告一段落了。限于篇幅原因,很多细节都被略过了。后面参考给出的链接,强烈推荐阅读。疏谬之处,万望斧正!   

备注

     本文有相当份量的内容参考借鉴了网络上各位网友的热心分享,特别是一些带有完全参考的文章,其后附带的链接内容更直接、更丰富,笔者只是做了一下归纳&转述,在此一并表示感谢。

参考

      《Linux高性能服务器编程》

      《彻底学会使用epoll》(系列博文)

      《epoll源码分析(全) 》

      《linux kernel中epoll的设计和实现

      《poll&&epoll实现分析(二)——epoll实现

 

 

 

 

 

文章知识点与官方知识档案匹配,可进一步学习相关知识
CS入门技能树Linux入门初识Linux30025 人正在系统学习中

与[转帖]Linux下的I/O复用与epoll详解相似的内容:

[转帖]Linux下的I/O复用与epoll详解

https://blog.csdn.net/weixin_39094034/article/details/110393127 前言 I/O多路复用有很多种实现。在linux上,2.4内核前主要是select和poll,自Linux 2.6内核正式引入epoll以来,epoll已经成为了目前实现高性

[转帖]​Linux开源存储漫谈(2)IO性能测试利器fio

fio(Flexible I/O Tester)正是非常常用的文件系统和磁盘 I/O 性能基准测试工具。提供了大量的可定制化选项,可以用来测试,裸盘、一个单独的分区或者文件系统在各种场景下的 I/O 性能,包括了不同块大小、不同 I/O 引擎以及是否使用缓存等场景。 ubuntu安装fio非常简单

[转帖]网络编程 Epoll原理和底层机制分析

https://zhuanlan.zhihu.com/p/579839553 同 I/O 多路复用和信号驱动 I/O 一样,Linux 的 epoll(event poll)API 可以检查多个文件 描述符上的 I/O 就绪状态。epoll 是 Linux 下多路复用IO接口 select/poll

[转帖]Linux查看硬件信息超强命令sar,以及可视化工具ksar

https://juejin.cn/post/6947470401135968286 一、概述 sar(System Activity Reporter,系统活动情况报告)是Linux下系统运行状态统计工具,可从多方面对系统的活动进行报告,包括:文件的读写情况、系统调用的使用情况、磁盘I/O、CPU

[转帖]Linux磁盘I/O(二):使用vm.dirty_ratio和vm.dirty_background_ratio优化磁盘性能

文件缓存是一项重要的性能改进,在大多数情况下,读缓存在绝大多数情况下是有益无害的(程序可以直接从RAM中读取数据)。写缓存比较复杂,Linux内核将磁盘写入缓存,过段时间再异步将它们刷新到磁盘。这对加速磁盘I/O有很好的效果,但是当数据未写入磁盘时,丢失数据的可能性会增加。 当然,也存在缓存被写爆的

[转帖]从Linux零拷贝深入了解Linux-I/O

https://aijishu.com/a/1060000000375591 作者:kevineluo,腾讯 CSIG 后台开发工程师 本文将从文件传输场景以及零拷贝技术深究 Linux I/O 的发展过程、优化手段以及实际应用。 前言 存储器是计算机的核心部件之一,在完全理想的状态下,存储器应该要

[转帖]【学习笔记】Linux下CPU性能评估

Linux下CPU性能评估 1、 vmstat监控CPU使用情况 【说明】 procs: l r表示运行和等待CPU时间片的进程数,这个值如果长期大于系统CPU的个数,就说明CPU不足,需要增加CPU。 l b表示在等待资源的进程数,比如正在等待I/O或者内存交换等。 memory: l swpd:

[转帖]linux下 进程io队列,IO队列和IO调度

IO体系概览 先看看本文主题IO调度和IO队列处于整个IO体系的哪个位置,这个IO体系是非常重要的,了解IO体系我们可以对整个IO过程有个全面的认识。虽然一下两下并不清楚IO体系各个部分的细节,但是我们总是能从这儿找到脉络。知道什么问题在什么位置,这个是解决问题的时候最关键的。任何所谓的专家都不太可

[转帖]如何提高Linux下块设备IO的整体性能?

http://www.yunweipai.com/6989.html 运维派隶属马哥教育旗下专业运维社区,是国内成立最早的IT运维技术社区,欢迎关注公众号:yunweipai领取学习更多免费Linux云计算、Python、Docker、K8s教程关注公众号:马哥linux运维 作者介绍 邹立巍 Li

[转帖]Linux系统top命令中的io使用率,很多人都误解了它的具体含义

https://baijiahao.baidu.com/s?id=1641356547223820839&wfr=spider&for=pc 最近在做连续数据流的缓冲系统,C语言代码实现后,粗略测试了下,功能上应该没有问题。那么,接下来就该测试性能了。输入 top 命令,的确可以看到一系列 cpu