zoukankan      html  css  js  c++  java
  • select、poll、epoll之间的区别总结

    select,poll,epoll简介:

     

    select

     select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

    1、 单个进程可监视的fd数量被限制,数组有大小限制;

    2 、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大;

    3 、对socket进行扫描时是线性扫描;

    4、select也是“水平触发”,如果报告了fd后,没有被处理,那么下次selectl时会再次报告该fd;

     

    poll

    poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

    1、它没有最大连接数的限制,原因是它是基于链表来存储的;

    2、和select一样大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

    3、poll也是线性扫描;

    4、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

    epoll

     epoll支持水平触发和边缘触发;

    1、最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。

    2、在前面说到的复制问题上,epoll使用mmap减少复制开销(使用mmap,内核和用户态共享数据)。

    3、还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

    4、虽然也有限制,可以认为无限大。

    select,poll,epoll的比较:

    1、支持一个进程所能打开的最大连接数

     select  单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上 FD_SETSIZE为32*64),当然我们可以对它进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。
     poll  poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。
      epoll  虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。

               epoll管理链接数上限:/proc/sys/fs/epoll/max_user_watchs

               根据epoll的实现,在64位环境下,epoll在内核中需要为每个fd消耗160Bytes。这部分内存可以通过slabtop查看。

    其次,根据linux内核2.6.32.43中对tcp协议栈的实现,分析内核中socket相关数据结构的内存开销。内核为每个应用层中打开的socket维护structsocket_alloc数据结构,它包含struct socket和struct inode结构,分别对应socket在tcp中的表示和vfs中的inode数据结构。

                在网络层中,还需要struct sock数据结构来表示socket。

     2、FD剧增后带来的IO效率问题

     select

     因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。
     poll  同上

     epoll

     因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

    3、消息传递方式

     select  内核需要将消息传递到用户空间,都需要内核拷贝动作。
     poll  同上
     epoll  epoll通过内核和用户空间通过mmap共享一块内存来实现的。

    综上比较:

           在选择select,poll,epoll时,要根据具体的使用场合以及select,poll,epoll这三种方式的自身特点。

           从表象看epoll的性能最好,但是在连接数少,并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多回调函数来完成。

           epoll在事件管理上,使用的是红黑树,可以快速的增删事件和快速查找事件。

    以上参考博客:

           http://xingyunbaijunwei.blog.163.com/blog/static/76538067201241685556302/

    select的接口,及解释:

          select()

    select()用来监控多个文件描述符,当fd变得可读/可写时,select()将标记可读/可写fd。select()现在被认为是低效的fd监控接口,在实际项目中通常用epoll()来代替select()。

    #include <unistd.h>
    #include <sys/select.h>
    #include <sys/types.h>
    #include <sys/time.h>
    
    int select(int maxfd, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
    
    void FD_CLR(int fd, fd_set* set);
    int  FD_ISSET(int fd, fd_set* set);
    void FD_SET(int fd, fd_set* set);
    void FD_ZERO(fd_set* set);

    select()的参数解析:

    1、一个参数maxfd是加入select()的最大文件描述符值+1,最大值为1024。可以修改FD_SETSIZE的值以使select()支持更多文件描述符监控,但必须重新编译内核,否则结果未知。

    2、中间3个参数是3个fd集合,分别是你想监听的可读fd、可写fd、异常fd。

    3、最后一个参数timeout是指定select()的超时时间。

          timeout的取值可以是:

                 NULL - 永久等待,直到有读/写/异常事件发生。
                 0 - 立即返回,此时select()为非阻塞状态。
                 其他值 - 指定select()等待时间。注意,timeout指定最长等待时间,但一旦有1个或多个fd可读/写/异常时select()就会返回。

    select()的返回值:
           0 - 超时,且没有任何读/写fd。
           > 0 - 有读/写fd,用FD_ISSET()进一步判断。
           -1 - select()出错。常见的错误包括: 
                 EINTR - 捕获到信号。通常可忽略。
                 EBADF - 有无效的文件描述符。

    Socket可读/写的常见情况分析:

    select()返回sockfd可读:

    1、Receive缓冲区的数据大于或等于low-water mark的值。low-water mark的值可通过SO_RCVLOWAT选项控制,默认是1。 (即读缓冲区中有数据)
           2、TCP连接接收到FIN,即Read half of the connections is closed。此时对sockfd的读操作将返回0,即EOF。

    3、如果sockfd是一个监听套接字,则表明有新连接,可调用accept()函数建立新连接。 
           4、Socket出错,此时对sockfd的读操作将返回-1。

    select()返回sockfd可写:
           1、Send缓冲区的数据大于或等于low-water mark的值。low-water mark的值可通过SO_SNDLOWAT选项控制,默认是2048。 (即缓冲区有数据)
           2、Write half of the connection is closed,对sockfd的写操作将产生SIGPIPE信号。 
           3、对非阻塞的sockfd调用connect(),connect()完成或失败。 
           4、Socket出错,此时对sockfd的写操作将返回-1。

    分析。若读缓冲有数据,则socket可读;若写缓冲有空间,则socket可写。如果socket出错,则它本身处于可读写状态,且调用read()/write()返回-1。若是Listen Socket,则有新连接来时它可读;若是Non-block Connect Socket,则连接成功时它可写。这些情况都不难理解,只有以上列出情况3,需要进一步说明。

    参考:http://www.berlinix.com/dev/network.php

    epoll的接口非常简单,一共就三个函数:
    1.创建epoll句柄
    int epfd = epoll_create(int size);

    创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

    2.将被监听的描述符添加到epoll句柄或从epool句柄中删除或者对监听事件进行修改。

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

    epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

    第一个参数是epoll_create()的返回值,
    第二个参数表示动作,用三个宏来表示:
    EPOLL_CTL_ADD: 注册新的fd到epfd中;
    EPOLL_CTL_MOD: 修改已经注册的fd的监听事件;
    EPOLL_CTL_DEL: 从epfd中删除一个fd;
    第三个参数是需要监听的fd,
    第四个参数是告诉内核需要监听什么事件,struct epoll_event结构如下:
    struct epoll_event {
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
    };
    events可以是以下几个宏的集合:
    EPOLLIN :      触发该事件,表示对应的文件描述符上有可读数据。(包括对端SOCKET正常关闭);
    EPOLLOUT: 触发该事件,表示对应的文件描述符上可以写数据;
    EPOLLPRI:   表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
    EPOLLERR: 表示对应的文件描述符发生错误;
    EPOLLHUP: 表示对应的文件描述符被挂断;
    EPOLLET:     将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
                               (
                                        水平触发(LT):这个是默认的工作方式,水平触发只要满足条件,就触发一个事件,只要有数据没有被获取,内核就会不断通知(例如:epoll_wait获取有数据可读,读取时却发生错误,那么就可以等待下一次通知,再去读数)。
                                        边缘触发(ET):它通知那些fd刚刚变为就绪态,并且只会通知一次(每当状态变化时,才会触发一次事件)。
                                        select因为要维护一个大的事件数组,同时要在内核和用户态进行大量数据拷贝,所以效率底下。
                                       epoll高性能:
                                               1、使用mmap减少复制开销(内核和用户空间共享同一块内存,减少内核态到用户态的拷贝);
                                               2、epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。
                               )
    EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。

    3. 等待事件触发,当超过timeout还没有事件触发时,就超时。
    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    等待事件的产生,类似于select()调用。
    参数:
    events用来从内核得到事件的集合;
    maxevents告之内核events数组的成员个数,这个maxevents的值不能大于创建epoll_create()时的size;
    timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。
    该函数返回需要处理的事件数目,返回的事件集合在events数组中。
    如返回0表示已超时。

    当产生了一个EPOLLIN事件后:
    读数据的时候需要考虑的是当recv()返回的大小如果等于要求的大小,即sizeof(buf),那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取(如果在ET水平模式下,可以等到下次事件触发时,再读数据):
    while(rs)                   //ET模型
    {
           buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
           if(buflen < 0)
           {
                 // 由于是非阻塞的模式,所以当errno为EAGAIN或EINT时,表示当前缓冲区已无数据可读(被其它线程可能读了)
                 // 在这里就当作是该次事件已处理处。因为是ET模式,如果有数据,内核还会继续触发读事件。

                 //即当buflen<0且errno=EAGAIN||errno=EINT时,表示没有数据了。(读/写都是这样)
                 if(errno == EAGAIN || errno == EINT)

                          break;
                 else
                          return;          //真的失败了。
           }
           else if(buflen == 0)
           {
                        // 这里表示对端的socket已正常关闭,收到了FIN包。soket在read到0个数据时,表示收到对端请求的FIN包。
           }
           if(buflen == sizeof(buf)
                        rs = 1;               // 需要再次读取(有可能是因为数据缓冲区buf太小,所以数据没有读完,可以等到下次再读,没有buf了)
           else
                        rs = 0;                //不需要再次读取(当buflen<sizeof(buf)时,非阻塞文件描述符的特性),
    }

    当产生了一个EPOLLOUT事件后:

    还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send()函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据。所以,需要封装socket_send()的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send()内部,当写缓冲已满(send()返回-1,且errno为EAGAIN),那么会等待后再重试。这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send()内部,但暂没有更好的办法。这种方法类似于readn和writen的封装(在《UNIX环境高级编程》中也有介绍)
    ssize_t socket_send(int sockfd, const char* buffer, size_t buflen)
    {
              ssize_t tmp;
              size_t total = buflen;
              const char *p = buffer;
              while(1)
              {
                       tmp = send(sockfd, p, total, 0);
                       if(tmp < 0)
                       {
                                  // 当send收到信号时,可以继续写,但这里返回-1.
                                  if(errno == EINTR)
                                         return -1;
                                  // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满,
                                  // 在这里做延时后再重试.
                                  if(errno == EAGAIN)
                                  {
                                         usleep(1000);
                                         continue;
                                  }
                                  return -1;
                       }
                       if((size_t)tmp == total)
                             return buflen;
                       total -= tmp;
                       p += tmp;
              }
              return tmp;

  • 相关阅读:
    【HEVC帧间预测论文】P1.8 Complexity Control of High Efficiency Video Encoders for Power-Constrained Devices
    【HEVC帧间预测论文】P1.7 Content Based Hierarchical Fast Coding Unit Decision Algorithm
    【HEVC帧间预测论文】P1.6 A Fast HEVC Inter CU Selection Method Based on Pyramid Motion Divergence
    【HEVC帧间预测论文】P1.5 Fast Coding Unit Size Selection for HEVC based on Bayesian Decision Rule
    【HEVC帧间预测论文】P1.4 Motion Vectors Merging: Low Complexity Prediction Unit Decision
    【HEVC帧间预测论文】P1.3 Fast Inter-Frame Prediction Algorithm of HEVC Based on Graphic Information
    【HEVC帧间预测论文】P1.2 An Efficient Inter Mode Decision Approach for H.264 Video Codin
    【HEVC帧间预测论文】P1.1 基于运动特征的HEVC快速帧间预测算法
    【HEVC简介】DB-DeBlock Filter
    【HEVC简介】ALF-Adative Loop Filter
  • 原文地址:https://www.cnblogs.com/jokezl/p/10140802.html
Copyright © 2011-2022 走看看