zoukankan      html  css  js  c++  java
  • select poll epoll三者之间的比较

    一、概述


      说到Linux下的IO复用,系统提供了三个系统调用,分别是select poll epoll。那么这三者之间有什么不同呢,什么时候使用三个之间的其中一个呢?

      下面,我将从系统调用原型来分析其中的不同。

    二、系统接口原型


      1. select

            #include <sys/select.h>
    
           int select(int nfds, fd_set *readfds, fd_set *writefds,
                      fd_set *exceptfds, struct timeval *timeout);
    
           int pselect(int nfds, fd_set *readfds, fd_set *writefds,
                       fd_set *exceptfds, const struct timespec *timeout,
                       const sigset_t *sigmask);
            

      2. poll

            #include <poll.h>
    
           int poll(struct pollfd *fds, nfds_t nfds, int timeout);
         int ppoll(struct pollfd *fds, nfds_t nfds,
                   const struct timespec *timeout_ts, const sigset_t *sigmask);
    struct pollfd {
                   int   fd;         /* file descriptor */
                   short events;     /* requested events */
                   short revents;    /* returned events */
               };

      3. epoll

           #include <sys/epoll.h>
    
           int epoll_wait(int epfd, struct epoll_event *events,
                          int maxevents, int timeout);
           int epoll_pwait(int epfd, struct epoll_event *events,
                          int maxevents, int timeout,
                          const sigset_t *sigmask);

    三、参数对比


    1. select

    • select的第一个参数nfdsfdset集合中最大描述符值加1fdset是一个位数组,其大小限制为__FD_SETSIZE1024),位数组的每一位代表其对应的描述符是否需要被检查;
    • select的第二三四个参数表示需要关注读、写、错误事件的文件描述符位数组,这些参数既是输入参数也是输出参数,可能会被内核修改用于标示哪些描述符上发生了关注的事件。所以每次调用select前都需要重新初始化fdset
    • timeout参数为超时时间,该结构会被内核修改,其值为超时剩余的时间。
    • select对应于内核中的sys_select调用,sys_select首先将第二三四个参数指向的fd_set拷贝到内核,然后对每个被SET的描述符调用进行poll,并记录在临时结果中(fdset),如果有事件发生,select会将临时结果写到用户空间并返回;当轮询一遍后没有任何事件发生时,如果指定了超时时间,则select会睡眠到超时,睡眠结束后再进行一次轮询,并将临时结果写到用户空间,然后返回。
    • select返回后,需要逐一检查关注的描述符是否被SET(事件是否发生)。

    2. poll

    •   pollselect不同,通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只需要被初始化一次。
    • poll的实现机制与select类似,其对应内核中的sys_poll,只不过poll向内核传递pollfd数组,然后对pollfd中的每个描述符进行poll,相比处理fdset来说,poll效率更高。
    • poll返回后,需要对pollfd中的每个元素检查其revents值,来得指事件是否发生。

      poll事件类型

    事件 描述 是否可作为输入 是否可作为输出
    POLLIN 数据(包括普通数据和优先数据)
    POLLRDNORM 普通数据可读
    POLLRDBAND 优先级带数据可读(Linux不支持)
    POLLPRI 高优先级数据可读,比如TCP带外数据
    POLLOUT 数据(包括普通数据和优先数据)可写
    POLLWRNORM 普通数据可写
    POLLWRBAND 优先级带数据可写
    POLLRDHUP TCP连接被对方关闭,或者对方关闭了写操作。它由GNU引入
    POLLERR 错误
    POLLHUP 挂起。比如管道的写端被关闭后,读端描述符上将收到POLLHUP事件
    POLLNVAL 文件描述符没有打开

    3. epoll

    • epoll是Linux特有的I/O复用函数。它在实现上与select、poll有很大的差异。首先,epoll使用一组函数来完成任务,而不是单个函数
    • 其次,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无需像select和poll那样每次调用都要重复传入文件的事件放在内核里的一个事件表中。但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表;
    • epoll通过epoll_create创建一个用于epoll轮询的描述符,通过epoll_ctl添加/修改/删除事件,通过epoll_wait检查事件,epoll_wait第二个参数用于存放结果
    • epollselectpoll不同,首先,其不用每次调用都向内核拷贝事件描述信息,在第一次调用后,事件信息就会与对应的epoll描述符关联起来。另外epoll不是通过轮询,而是通过在等待的描述符上注册回调函数,当事件发生时,回调函数负责把发生的事件存储在就绪事件链表中,最后写到用户空间。
    • epoll返回后,该参数指向的缓冲区中即为发生的事件,对缓冲区中每个元素进行处理即可,而不需要像pollselect那样进行轮询检查。

     

    四、性能对比


      selectpoll的内部实现机制相似,性能差别主要在于向内核传递参数以及对fdset的位操作上,另外,select存在描述符数的硬限制,不能处理很大的描述符集合

      这里主要考察pollepoll在不同大小描述符集合的情况下性能的差异。

     

      测试程序会统计在不同的文件描述符集合的情况下,1s pollepoll调用的次数。

      统计结果如下,从结果可以看出,poll而言,每秒钟内的系统调用数目虽集合增大而很快降低,而epoll基本保持不变,具有很好的扩展性

     

    描述符集合大小

    poll

    epoll

    1

    331598

    258604

    10

    330648

    297033

    100

    91199

    288784

    1000

    27411

    296357

    5000

    5943

    288671

    10000

    2893

    292397

    25000

    1041

    285905

    50000

    536

    293033

    100000

    224

    285825

     

    五、连接数

     


     

      我本人也曾经在项目中用过select和epoll,对于select,感触最深的是linux下select最大数目限制(windows 下似乎没有限制),每个进程的select最多能处理FD_SETSIZE个FD(文件句柄),如果要处理超过1024个句柄,只能采用多进程了。
      常见的使用select的多进程模型是这样的:

      一个进程专门accept,成功后将fd通过UNIX socket传递给子进程处理,父进程可以根据子进程负载分派。

      曾经用过1个父进程 + 4个子进程 承载了超过4000个的负载。
      这种模型在我们当时的业务运行的非常好。

     

      epoll在连接数方面没有限制,当然可能需要用户调用API重现设置进程的资源限制。

     

    六、相同点


    • 都能同时监听多个文件描述符;
    • 它们将等待由timeout参数指定的超时时间,直到一个或者多个文件描述符上有事件发生时返回,返回值是就绪的文件描述符的数量;

     

    七、不同点


     

      对于select:

      1. 只能通过三个结构体参数处理三种事件,分别是:可读、可写和异常事件,而不能处理更多的事件;

      2. 这三个参数既是输入参数,也是输出参数,因此,在每次调用select之前,都得对fd_set进行重置;

      对于poll:

      1. 将文件描述符和事件关联在一起,任何事件都被统一处理,从而使得编程接口简洁不少;

      2. 内核改变的变量是revents,而不是events,因此,调用之前不需要再重置;

     

      由于每次select和poll调用都返回整个用户注册的事件集合(其中包括就绪的和未就绪的),所以应用程序索引就绪文件描述符的时间复杂度为O(n)。

      而epoll采用与select和poll完全不同的方式来管理用户注册的事件。

     

    八、poll和epoll在使用上的差别


     

    /*poll example*/
    /*如何索引poll返回的就绪文件描述符*/
    int ret = poll(fds, MAX_EVENT_NUMBER, -1);
    /*必须遍历所有已注册文件描述符并找到其中的就绪者(当然,可以利用ret来稍作优化)*/
    for(int i = 0; i < MAX_EVENT_NUMBER; ++i)
    {
        if(fds[i].revents & POLLIN)
        {
        int sockfd = fds[i].fd;
            //deal with sockfd.
        }
    }
    
    
    /*epoll example*/
    int epfd = epoll_create(MAXSIZE);
    
    struct epoll_event ev,events[5000];
    //设置与要处理的事件相关的文件描述符
    ev.data.fd=listenfd;
    //设置要处理的事件类型
    ev.events=EPOLLIN|EPOLLET;
    //注册epoll事件
    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
    
    int nfds = epoll_wait(epfd,events,6000,-1);
    //处理所发生的所有事件     
    for(int i = 0; i< nfds; ++i)
    {
        //new accept.
        if(events[i].data.fd == listenfd)
        {
            printf("listen=%d
    ",events[i].data.fd);
            connfd = accept(listenfd,(sockaddr *)(&clientaddr), &clilen);
            if(connfd<0)
            {
                perror("connfd<0");
                exit(1);
            }            
            setnonblocking(connfd);                 
            char *str = inet_ntoa(clientaddr.sin_addr);
            std::cout<<"connec_ from >>"<<str<<"  "<<connfd<<std::endl;
    
            //设置用于读操作的文件描述符
            ev.data.fd = connfd;
    
            //设置用于注测的读操作事件
            //ev.data.ptr = NULL;
            ev.events = EPOLLIN|EPOLLET;
    
            //注册ev
            epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
            ev.data.fd = listenfd;
    
            //设置要处理的事件类型
            ev.events=EPOLLIN|EPOLLET;
           
            //注册epoll事件
            epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev);
            continue;
        }
        else if(events[i].events & EPOLLIN)
        {
            num1++;
            //fprintf(stderr,"reading! %d
    ",num1);               
            if( (sockfd = events[i].data.fd) <= 0)
            {
                num1--;
                continue;
            }
            
            new_task = NULL;
            while(new_task == NULL)
                new_task = new task();
                                           
            new_task->fd = sockfd;
            new_task->next=NULL;
                                                    
            //fprintf(stderr,"sockfd %d",sockfd);
            //添加新的读任务
            pthread_mutex_lock(&mutex);
            if(readhead == NULL)
            {
                readhead = new_task;
                readtail = new_task;
            }   
            else
            {   
                readtail->next=new_task;
                readtail=new_task;
            }   
            //唤醒所有等待cond1条件的线程
            pthread_cond_broadcast(&cond1);
            pthread_mutex_unlock(&mutex);  
            continue;
        }
        else if(events.events & EPOLLOUT)
        {   
            //fprintf(stderr,"EPOLLOUT");
            num++;
            rdata=(struct user_data *)events[i].data.ptr;
            sockfd =rdata->fd;
            if(old == sockfd)
            {
                fprintf(stderr,"repreted sockfd=%d
    ",sockfd);
                //exit(1);
            }
            old=sockfd;       
            //fprintf(stderr,"write  %d
    ",num);
            int size=write(sockfd, rdata->line, rdata->n_size);
            //fprintf(stderr,"write=%d delete rdata
    ",size);
            fprintf(stderr,"addr=%x fdwrite=%d size=%d
    ",rdata,rdata->fd,size);
            
            if(rdata!=NULL)//主要问题导致delete重复相同对象 events返回对象相同
            {
                delete rdata;
                rdata=NULL;
            }
            
            //设置用于读操作的文件描述符
            //fprintf(stderr,"after delete rdata
    ");
            ev.data.fd=sockfd;
                                 
            //设置用于注测的读操作事件
            ev.events=EPOLLIN|EPOLLET;
    
            //修改sockfd上要处理的事件为EPOLIN
            res = epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
            
            while(res==-1)
            {
                //fprintf(stderr,"out error");
                exit(1);
            }
            //fprintf(stderr,"out EPOLLOUT
    ");
            continue;
        }
        else if(events.events&(EPOLLHUP|EPOLLERR))
        {
            //fprintf(stderr,"EPPOLLERR
    ");
            int fd=events.data.fd;
            if(fd>6000)
            {
                fd=((struct user_data*)(events.data.ptr))->fd;
            }
            //设置用于注测的读操作事件
            ev.data.fd=fd;
            ev.events=EPOLLIN|EPOLLET|EPOLLOUT;
    
            //修改sockfd上要处理的事件为EPOLIN
            epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&ev);
        }
    }

     

     

     

     

     

  • 相关阅读:
    mysql 导入报错(ERROR 1840 (HY000) at line 24: @@GLOBAL.GTID_PURGED can only be set when @@GLOBAL.GTID_E)
    Docker容器开机自动启动
    linux 查看内存条详情命令
    Linux 网络性能测试工具 iperf 的安装和使用
    redis基准性能测试
    pmm的安装,这里推荐下载官方提供的脚本,直接执行这个脚本就可以完成安装
    mysqlslap压力测试时出现"Can't connect to MySQL server"
    Linux监控工具介绍系列——iostat
    提高RabbitMQ的File descriptors
    python 打包
  • 原文地址:https://www.cnblogs.com/wiessharling/p/4106295.html
Copyright © 2011-2022 走看看