Epoll模型

看了Epoll模型的一篇博客,对其进行总结,原文请见http://blog.163.com/huchengsz@126/blog/static/73483745201181824629285/

1. select函数的缺点

       select用到的FD_SET是有限的,即内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数,在Linux2.6中是1024,搜索内核源代码的include/linux/posix_types.h:
#define __FD_SETSIZE 1024
如果想要同时检测1025个句柄的可读状态是不可能用select实现的,或者同时检测1025个句柄的可写状态也是不可能的。其次,内核中实现 select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,select要检测的句柄数越多就会越费时。

2. Epoll的优点
<1>支持一个进程打开大数目的socket描述符(FD)
    select一个进程所打开的FD是有一定限制的,一是可以选择修改这个宏然后重新编译内核,会带来网络效率的下降,二是可以选择多进程的解决方案(传统的Apache方案)。Epoll支持的FD上限是最大可以打开文件的数目,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

<2>IO效率不随FD数目增加而线性下降
       select/poll另一个致命弱点就是当有一个很大的socket集合,由于网络延时,任一时间只有部分的socket是"活跃"的, 但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但Epoll只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的,只有"活跃"的socket才会主动的去调用callback函数。在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。

<3>使用mmap加速内核与用户空间的消息传递。
       无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就 很重要,Epoll是通过内核于用户空间mmap同一块内存实现的。

3. Epoll的工作模式
       Epoll有2种工作方式:LT和ET。
       LT(level triggered)是缺省的工作方式,同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。所以,这种模式编程出错误可能性要小一点,传统的select/poll都是这种模型的代表。
       ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。

4. Epoll的使用方法

       Epoll的操作共4个 API:epoll_create, epoll_ctl, epoll_wait和close。

       首先通过epoll_create(int maxfds)来创建一个epoll的句柄,其中maxfds为你epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后将通过这个句柄来进行操作。在用完之后,用close()来关闭epoll句柄。之后在网络主循环里面,每一帧的调用 epoll_wait(int epfd, epoll_event events, int max events, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:nfds = epoll_wait(kdpfd, events, max_events, -1);

       其中kdpfd为用epoll_create创建之后的句柄,events是一个 epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。 max_events是当前需要监听的所有socket句柄数。最后一个timeout是epoll_wait的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件发生,为任意正整数的时候表示等这么长的时间。一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。


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

EPOLLONESHOT : 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里

 

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

等待事件的产生,类似于 select() 调用。参数 events 用来从内核得到事件的集合, maxevents 告之内核这个events 有多大,这个 maxevents 的值不能大于创建 epoll_create() 时的 size ,参数 timeout 是超时时间(毫秒,0 会立即返回, -1 将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回 0 表示已超时。


 

EPOLL 事件有两种模型:

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

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


Epoll使用示例:
01  //epoll_wait之后应该是一个循环,遍历所有的事件:
02  for (n  =  0; n  < nfds;  ++n)
03 {
04          if (events[n].da ta.fd  == listener)
05         { //如果是主socket的事件的话,则表示有新连接进入了,进行新连接的处理。
06                 client  = accept (listener, ( struct sockaddr  *&local,  &addrlen);
07                  if (client  <  0)
08                 {
09                         perror ( "accept");
10                          continue;
11                 }
12                 setnonblocking (client);         // 将新连接置于非阻塞模式
13                 ev.events  = EPOLLIN | EPOLLET;  // 并且将新连接也加入EPOLL的监听队列。
14                  //注意,这里的参数EPOLLIN | EPOLLET并没有设置对写socket的监听,
15                  //如果有写操作的话,这个时候epoll是不会返回事件的,
16                  //如果要对写操作也监听的话,应该是EPOLLIN | EPOLLOUT | EPOLLET
17                 ev.da ta.fd  = client;
18                  if (epoll_ctl (kdpfd, EPOLL_CTL_ADD, client,  &ev)  <  0)
19                 {
20                          /*
21                                  设置好event之后,将这个新的event通过epoll_ctl加入到epoll的监听队列里面,
22                                  这里用EPOLL_CTL_ADD来加一个新的epoll事件,通过EPOLL_CTL_DEL来减少一个epoll事件,通
23                                  过EPOLL_CTL_MOD来改变一个事件的监听方式.
24                          */
25                         fprintf (stderr,  "epoll set insertion error: fd=%d", client);
26                          return  - 1;
27                 }
28         }
29          else                                                             // 如果不是主socket的事件的话,则代表是一个用户socket的事件,
30                 do_use_fd (events[n].da ta.fd);  //则来处理这个用户socket的事情,比如说read(fd,xxx)之类的,或者一些其他的处理。
31 }


Epoll使用示例:

01  while (TRUE)
02 {
03          int nfds  = epoll_wait (m_epoll_fd, m_events, MAX_EVENTS,EPOLL_TIME_OUT);        //等待EPOLL时间的发生,相当于监听,
04          //至于相关的端口,需要在初始化EPOLL的时候绑定。
05          if (nfds  <=  0)
06                  continue;
07         m_bOnTimeChecking  = FALSE;
08         G_CurTime  = time ( NULL);
09          for ( int i  =  0; i  < nfds; i ++)
10         {
11                  try
12                 {
13                          if (m_events[i].da ta.fd  == m_listen_http_fd)     //如果新监测到一个HTTP用户连接到绑定的HTTP端口,
14                                  //建立新的连接。由于我们新采用了SOCKET连接,所以基本没用。
15                         {
16                                 On AcceptHttpEpoll ();
17                         }
18                          else  if (m_events[i].da ta.fd  == m_listen_sock_fd)        //如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,
19                                  //建立新的连接。
20                         {
21                                 On AcceptSockEpoll ();
22                         }
23                          else  if (m_events[i].events  & EPOLLIN)  //如果是已经连接的用户,并且收到数据,那么进行读入。
24                         {
25                                 On ReadEpoll (i);
26                         }
27 
28                         On WriteEpoll (i);        //查看当前的活动连接是否有需要写出的数据。
29                 }
30                  catch ( int)
31                 {
32                         PRINTF ( "CATCH捕获错误 \n ");
33                          continue;
34                 }
35         }
36         m_bOnTimeChecking  = TRUE;
37         On Timer ();                                      //进行一些定时的操作,主要就是删除一些短线用户等。
38 }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值