Epoll模型【转】

转自:http://www.javashuo.com/article/p-rsdsolix-dq.htmlhtml

相比于select,epoll最大的好处在于它不会随着监听fd数目的增加而下降效率。由于在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,天然耗时越多。而且,在linux/posix_types.h头文件有这样的声明:linux

#define __FD_SETSIZE    1024编程

表示select最多同时监听1024个fd,固然,能够经过修改头文件再重编译内核来扩大这个数目,但这彷佛并不治本。数组

 

经常使用模型的特色缓存

Linux 下设计并发网络程序,有典型的 Apache 模型( Process Per Connection ,简称 PPC ), TPC ( Thread Per Connection )模型,以及 select 模型和 poll 模型。网络

1 、PPC/TPC 模型数据结构

这两种模型思想相似,就是让每个到来的链接一边本身作事去,别再来烦我 。只是 PPC 是为它开了一个进程,而 TPC 开了一个线程。但是别烦我是有代价的,它要时间和空间啊,链接多了以后,那么多的进程 / 线程切换,这开销就上来了;所以这类模型能接受的最大链接数都不会高,通常在几百个左右。并发

2 、select 模型socket

  1. 最大并发数限制,由于一个进程所打开的 FD (文件描述符)是有限制的,由 FD_SETSIZE 设置,默认值是 1024/2048 ,所以 Select 模型的最大并发数就被相应限制了。本身改改这个 FD_SETSIZE ?想法虽好,但是先看看下面吧 …函数

  2. 效率问题, select 每次调用都会线性扫描所有的 FD 集合,这样效率就会呈现线性降低,把 FD_SETSIZE 改大的后果就是,你们都慢慢来,什么?都超时了??!!

  3. 内核 / 用户空间 内存拷贝问题,如何让内核把 FD 消息通知给用户空间呢?在这个问题上 select 采起了内存拷贝方法。

三、 poll 模型

  基本上效率和 select 是相同的, select 缺点的 2 和 3 它都没有改掉。

 

Epoll 的提高

Epoll 的改进之处。

  1. Epoll 没有最大并发链接的限制,上限是最大能够打开文件的数目,这个数字通常远大于 2048, 通常来讲这个数目和系统内存关系很大 ,具体数目能够 cat /proc/sys/fs/file-max 察看。

  2. 效率提高, Epoll 最大的优势就在于它只管你“活跃”的链接 ,而跟链接总数无关,所以在实际的网络环境中, Epoll 的效率就会远远高于 select 和 poll 。

  3. 内存拷贝, Epoll 在这点上使用了“共享内存 ”,这个内存拷贝也省略了。

 

Epoll 为何高效

Epoll 的高效和其数据结构的设计是密不可分的。

首先回忆一下 select 模型,当有 I/O 事件到来时, select 通知应用程序有事件到了快去处理,而应用程序必须轮询全部的 FD 集合,测试每一个 FD 是否有事件发生,并处理事件。

 

int res = select(maxfd+1, &readfds, NULL, NULL, 120);

if (res > 0)

{

    for (int i = 0; i < MAX_CONNECTION; i++)

    {

        if (FD_ISSET(allConnection[i], &readfds))

        {

            handleEvent(allConnection[i]);

        }

    }

}

// if(res == 0) handle timeout, res < 0 handle error

 

Epoll 不只会告诉应用程序有I/0 事件到来,还会告诉应用程序相关的信息,这些信息是应用程序填充的,所以根据这些信息应用程序就能直接定位到事件,而没必要遍历整个FD 集合。

int res = epoll_wait(epfd, events, 20, 120);

for (int i = 0; i < res;i++)

{

    handleEvent(events[n]);

}

 

epoll的接口很是简单,一共就三个函数:

1. int epoll_create(int size);

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

 

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

epoll的事件注册函数,它不一样与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。控制某个 Epoll 文件描述符上的事件:注册、修改、删除。

 

第一个参数是epoll_create()的返回值,建立 Epoll 专用的文件描述符。相对于 select 模型中的 FD_SET 和 FD_CLR 宏。

第二个参数表示动做,用三个宏来表示:

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)来讲的。

  EPOLLONESHOT: 只监听一次事件,当监听完此次事件以后,若是还须要继续监听这个socket的话,须要再次把这个socket加入到EPOLL队列里

 

3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

 等待 I/O 事件的发生;参数说明:

  epfd: 由 epoll_create() 生成的 Epoll 专用的文件描述符;

  epoll_event: 用于回传代处理事件的数组;

  maxevents: 每次能处理的事件数;

  timeout: 等待 I/O 事件发生的超时值;

  返回发生事件数。

  相对于 select 模型中的 select 函数。

  参数events用来从内核获得事件的集合,maxevents告以内核这个events有多大,这个maxevents的值不能大于建立epoll_create()时的size,参数timeout是超时时间(毫秒,0会当即返回,-1将不肯定,也有说法说是永久阻塞)。该函数返回须要处理的事件数目,如返回0表示已超时。

  生成一个 Epoll 专用的文件描述符,实际上是申请一个内核空间,用来存放你想关注的 socket fd 上是否发生以及发生了什么事件。 size 就是你在这个 Epoll fd 上能关注的最大 socket fd 数,大小自定,只要内存足够。

 

EPOLL事件有两种模型:

Edge Triggered (ET)  边缘触发 只有数据到来,才触发,无论缓存区中是否还有数据。

Level Triggered (LT)  水平触发 只要有数据都会触发。

 

例如:

1. 咱们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符

2. 这个时候从管道的另外一端被写入了2KB的数据

3. 调用epoll_wait(2),而且它会返回RFD,说明它已经准备好读取操做

4. 而后咱们读取了1KB的数据

5. 调用epoll_wait(2)......

 

Edge Triggered 工做模式:

若是咱们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)以后将有可能会挂起,由于剩余的数据还存在于文件的输入缓冲区内,并且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工做模式才会汇报事件。所以在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,由于在第2步执行了一个写操做,而后,事件将会在第3步被销毁。由于第4步的读取操做没有读空文件输入缓冲区内的数据,所以咱们在第5步调用 epoll_wait(2)完成后,是否挂起是不肯定的。epoll工做在ET模式的时候,必须使用非阻塞套接口,以免因为一个文件句柄的阻塞读/阻塞写操做把处理多个文件描述符的任务饿死。最好如下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。

   i    基于非阻塞文件句柄

   ii   只有当read(2)或者write(2)返回EAGAIN时才须要挂起,等待。但这并非说每次read()时都须要循环读,直到读到产生一个EAGAIN才认为这次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时,就能够肯定此时缓冲中已没有数据了,也就能够认为此事读事件已处理完成。

 

Level Triggered 工做模式

相反的,以LT方式调用epoll接口的时候,它就至关于一个速度比较快的poll(2),而且不管后面的数据是否被使用,所以他们具备一样的职能。由于即便使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者能够设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。所以当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须做的事情。

详细解释ET, LT:

LT(level triggered)是缺省的工做方式,而且同时支持block和no-block socket.在这种作法中,内核告诉你一个文件描述符是否就绪了,而后你能够对这个就绪的fd进行IO操做。若是你不做任何操做,内核仍是会继续通知你的,因此,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的表明.

ET(edge-triggered)是高速工做方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核经过epoll告诉你。而后它会假设你知道文件描述符已经就绪,而且不会再为那个文件描述符发送更多的就绪通知,直到你作了某些操做致使那个文件描述符再也不为就绪状态了(好比,你在发送,接收或者接收请求,或者发送接收的数据少于必定量时致使了一个EWOULDBLOCK 错误)。可是请注意,若是一直不对这个fd做IO操做(从而致使它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍须要更多的benchmark确认(这句话不理解)。

 

在许多测试中咱们会看到若是没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高不少,可是当咱们遇到大量的idle- connection(例如WAN环境中存在大量的慢速链接),就会发现epoll的效率大大高于select/poll。(未测试)

 

另外,当使用epoll的ET模型来工做时,当产生了一个EPOLLIN事件后,

读数据的时候须要考虑的是当recv()返回的大小若是等于请求的大小,那么颇有多是缓冲区还有数据未读完,也意味着该次事件尚未处理完,因此还须要再次读取:

while(rs)

{

  buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);

  if(buflen < 0)

  {

    // 因为是非阻塞的模式,因此当errno为EAGAIN时,表示当前缓冲区已无数据可读

    // 在这里就看成是该次事件已处理处.

    if(errno == EAGAIN)

     break;

    else

     return;

   }

   else if(buflen == 0)

   {

     // 这里表示对端的socket已正常关闭.

   }

   if(buflen == sizeof(buf)

     rs = 1;   // 须要再次读取

   else

     rs = 0;

}

还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),因为是非阻塞的socket,那么send()函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会此次请求发送的数据.因此,须要封装socket_send()的函数用来处理这种状况,该函数会尽可能将数据写完再返回,返回-1表示出错。在socket_send()内部,当写缓冲已满(send()返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send()内部,但暂没有更好的办法.

 

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;

}

相关文章
相关标签/搜索