前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >select和epoll模型

select和epoll模型

原创
作者头像
大学里的混子
修改2019-03-22 17:01:55
9990
修改2019-03-22 17:01:55
举报
文章被收录于专栏:LeetCodeLeetCode

转自https://www.cnblogs.com/lojunren/p/3856290.html

利用select或者epoll实现I/O多路复用。

select的缺陷:

高并发的核心解决方案是一个线程处理所有连接的“等待消息准备好”,当有数十万并发连接存在时,可能每一毫秒只有数百个连接是活跃的。其余的在这一毫秒都是非活跃的。select使用的方法是:

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

什么时候调用select方法?当需要找出有报文到达的活跃连接时,就应该调用。

首先监控数十万的连接但是返回的只有数百个活跃连接,这本身就是无效率的表现。

其次在Linux内核中,select所用到的FD_SET是有限的(即监控的连接数目是有限的)

__FD_SETSIZE定义了FD_SET的句柄个数。

并且内核中实现select是用轮询的方法,既每次检测都会遍历所有FD_SET中的句柄

显然当select函数监控的连接数越多那么每次检测都要遍历的句柄数就会越多时间就越浪费

相比于select机制,poll只是取消了最大监控文件描述符的限制,其它的并和select并没有区别

epoll高效的奥秘:

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

1.新建epoll描述符(epoll_create())

2.epoll_ctl(添加、删除或者修改所有待监控的连接)

3.返回活跃连接(epoll_wait())

与select相比,epoll分清了频繁调用和不频繁滴啊用的操作。如:epoll_ctl是不频繁调用的

而epoll_wait是非常频繁调用的,而epoll_wait却几乎没有入参,所以相比select效率高,

并且也不会随着并发连接的增加使得入参越来越多,导致内核执行效率下降。

epoll的三大关键要素:mmap、红黑树、链表。

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

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

管理红黑树和就绪链表的结构

代码语言:javascript
复制
struct eventpoll
{
    spin_lock_t lock;            //对本数据结构的访问
    struct mutex mtx;            //防止使用时被删除
    wait_queue_head_t wq;        //sys_epoll_wait() 使用的等待队列
    wait_queue_head_t poll_wait; //file->poll()使用的等待队列
    struct list_head rdllist;    //事件满足条件的链表
    struct rb_root rbr;          //用于管理所有fd的红黑树
    struct epitem *ovflist;      //将事件到达的fd进行链接起来发送至用户空间
}

红黑树节点的结构

代码语言:javascript
复制
struct epitem
{
    struct rb_node rbn;            //用于主结构管理的红黑树
    struct list_head rdllink;       //事件就绪队列
    struct epitem *next;           //用于主结构体中的链表
    struct epoll_filefd ffd;         //每个fd生成的一个结构
    int nwait;                 
    struct list_head pwqlist;     //poll等待队列
    struct eventpoll *ep;          //该项属于哪个主结构体
    struct list_head fllink;         //链接fd对应的file链表
    struct epoll_event event;  //注册的感兴趣的事件,也就是用户空间的epoll_event
 }

添加以及返回事件

通过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和相应的fd发送到用户空间(封装在strcut epoll_event,从epoll_wait返回)

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

EPOLL的使用

文件描述符的创建

代码语言:javascript
复制
#include <sys/epoll.h> int epoll_create(int size);

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

注册监控事件

代码语言:javascript
复制
#include <sys/epoll.h> int epoll_ctl( int epfd, int op, int fd, struct epoll_event *event );

函数说明:

     efd:要操作的文件描述符

     op:指定操作类型

操作类型:

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

     EPOLL_CTL_MOD:修改fd上的注册事件

     EPOLL_CTL_DEL:删除fd上的注册事件

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

     epoll_event定义:

代码语言:javascript
复制
1 struct epoll_event2 {3     __unit32_t events;    // epoll事件4     epoll_data_t data;     // 用户数据 5 };

结构体说明:

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

     data成员:存储用户数据

代码语言:javascript
复制
typedef union epoll_data{    void* ptr;              //指定与fd相关的用户数据     int fd;                 //指定事件所从属的目标文件描述符     uint32_t u32;    uint64_t u64;} epoll_data_t;

epoll_wait函数 

代码语言:javascript
复制
#include <sys/epoll.h>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模式讲解最为详尽和易懂的博文。下面的实例均来自该系列博文。限于篇幅原因,很多关键的细节,不能完全摘录。

代码语言:javascript
复制
#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");     }  }}
  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!”。

将注册的事件改为ev.events=EPOLLIN; //默认使用LT模式

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

代码语言:javascript
复制
#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;                                //监听读状态同时设置LT模式    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)            {                char buf[1024] = {0};                read(STDIN_FILENO, buf, sizeof(buf));                printf("welcome to epoll's word!\n");            }                    }    }}

本程序依然使用LT模式,但是每次epoll_wait返回读就绪的时候我们都将buffer(缓冲)中的内容read出来,所以导致buffer再次清空,下次调用epoll_wait就会阻塞。所以能够实现我们所想要的功能——当用户从控制台有任何输入操作时,输出”welcome to epoll's world!”

代码语言:javascript
复制
#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 = STDOUT_FILENO;    ev.events = EPOLLOUT|EPOLLET;                        //监听读状态同时设置ET模式    epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev);    //注册epoll事件    for(;;)    {        nfds = epoll_wait(epfd, events, 5, -1);        for(int i = 0; i < nfds; i++)        {            if(events[i].data.fd==STDOUT_FILENO)            {                printf("welcome to epoll's word!\n");            }                    }    }}

这个程序的功能是只要标准输出写就绪,就输出“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会返回写就绪。如此循环往复。
代码语言:javascript
复制
#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 = STDOUT_FILENO;    ev.events = EPOLLOUT|EPOLLET;                        //监听读状态同时设置ET模式    epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev);    //注册epoll事件    for(;;)    {        nfds = epoll_wait(epfd, events, 5, -1);        for(int i = 0; i < nfds; i++)        {            if(events[i].data.fd==STDOUT_FILENO)            {                printf("welcome to epoll's word!");            }                    }    }}

与程序四相比,程序五只是将输出语句的printf的换行符移除。我们看到程序成挂起状态。因为第一次epoll_wait返回写就绪后,程序向标准输出的buffer中写入“welcome to epoll's world!”,但是因为没有输出换行,所以buffer中的内容一直存在,下次epoll_wait的时候,虽然有写空间但是ET模式下不再返回写就绪。回忆第一节关于ET的实现,这种情况原因就是第一次buffer为空,导致epitem加入rdlist,返回一次就绪后移除此epitem,之后虽然buffer仍然可写,但是由于对应epitem已经不再rdlist中,就不会对其就绪fd的events的在检测了。

代码语言:javascript
复制
#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 = STDOUT_FILENO;    ev.events = EPOLLOUT;                                //监听读状态同时设置LT模式    epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev);    //注册epoll事件    for(;;)    {        nfds = epoll_wait(epfd, events, 5, -1);        for(int i = 0; i < nfds; i++)        {            if(events[i].data.fd==STDOUT_FILENO)            {                printf("welcome to epoll's word!");            }                    }    }}

程序六相对程序五仅仅是修改ET模式为默认的LT模式,我们发现程序再次死循环。这时候原因已经很清楚了,因为当向buffer写入”welcome to epoll's world!”后,虽然buffer没有输出清空,但是LT模式下只有buffer有写空间就返回写就绪,所以会一直输出”welcome to epoll's world!”,当buffer满的时候,buffer会自动刷清输出,同样会造成epoll_wait返回写就绪。

经过前面的案例分析,我们已经了解到,当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模式为什么要设置在非阻塞模式下工作

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

小结

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

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

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

1.2 两种加入rdlist途径的不同

下面我们来分析一下图中两种将epitem加入rdlist方式(也就是红线和蓝线)的区别。

l 红线:fd状态改变是才会触发。那么什么情况会导致fd状态的改变呢?

对于读取操作:

(1) 当buffer由不可读状态变为可读的时候,即由空变为不空的时候。

(2) 当有新数据到达时,即buffer中的待读内容变多的时候。

对于写操作:

(1) 当buffer由不可写变为可写的时候,即由满状态变为不满状态的时候。

(2) 当有旧数据被发送走时,即buffer中待写的内容变少得时候。

l 蓝线:fd的events中有相应的时间(位置1)即会触发。那么什么情况下会改变events的相应位呢?

对于读操作:

(1) buffer中有数据可读的时候,即buffer不空的时候fd的events的可读为就置1。

对于写操作:

(1) buffer中有空间可写的时候,即buffer不满的时候fd的events的可写位就置1。

说明:红线是时间驱动被动触发,蓝线是函数查询主动触发

https://blog.csdn.net/daaikuaichuan/article/details/83862311

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • select的缺陷:
  • epoll高效的奥秘:
  • epoll的三大关键要素:mmap、红黑树、链表。
  • 添加以及返回事件
    • epoll_wait的工作流程:
      • EPOLL的使用
        • EPOLLONESHOT事件
          • LT与ET模式
            • 1.2 两种加入rdlist途径的不同
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档