异步网络IO_select_poll_epoll的区别

select和poll只有水平触发_select,FD_ZERO,FD_SET,FD_ISSET,FD_CLR_epoll的水平触发(ET)有数据未处理一直触发,边缘触发(LT)只触发一次_epoll_create、epoll_ctl、epoll_wait、close  

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万左右的连接。

 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;
}

你可能感兴趣的:(异步网络IO_select_poll_epoll的区别)