C++ Epoll的封装

 

开发高性能网络程序时,windows开发者们言必称iocp,linux开发者们则言必称epoll。大家都明白epoll是一种IO多路复用技术,可以非常高效的处理数以百万计的socket句柄,比起以前的select和poll效率高大发了。我们用起epoll来都感觉挺爽,确实快,那么,它到底为什么可以高速处理这么多并发连接呢?

 

先简单回顾下如何使用C库封装的3个epoll系统调用吧。

1 int epoll_create(int size);  
2 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
3 int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

 

使用起来很清晰,首先要调用epoll_create建立一个epoll对象。参数size是内核保证能够正确处理的最大句柄数,多于这个最大数时内核可不保证效果。

epoll_ctl可以操作上面建立的epoll,例如,将刚建立的socket加入到epoll中让其监控,或者把 epoll正在监控的某个socket句柄移出epoll,不再监控它等等。

epoll_wait在调用时,在给定的timeout时间内,当在监控的所有句柄中有事件发生时,就返回用户态的进程。

从上面的调用方式就可以看到epoll比select/poll的优越之处:因为后者每次调用时都要传递你所要监控的所有socket给select/poll系统调用,这意味着需要将用户态的socket列表copy到内核态,如果以万计的句柄会导致每次都要copy几十几百KB的内存到内核态,非常低效。而我们调用epoll_wait时就相当于以往调用select/poll,但是这时却不用传递socket句柄给内核,因为内核已经在epoll_ctl中拿到了要监控的句柄列表。

所以,实际上在你调用epoll_create后,内核就已经在内核态开始准备帮你存储要监控的句柄了,每次调用epoll_ctl只是在往内核的数据结构里塞入新的socket句柄。

在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控socket。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。

epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket,这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。

 1 static int __init eventpoll_init(void)  
2 {
3 ... ...
4
5 /* Allocates slab cache used to allocate "struct epitem" items */
6 epi_cache = kmem_cache_create("eventpoll_epi", sizeof(struct epitem),
7 0, SLAB_HWCACHE_ALIGN|EPI_SLAB_DEBUG|SLAB_PANIC,
8 NULL, NULL);
9
10 /* Allocates slab cache used to allocate "struct eppoll_entry" */
11 pwq_cache = kmem_cache_create("eventpoll_pwq",
12 sizeof(struct eppoll_entry), 0,
13 EPI_SLAB_DEBUG|SLAB_PANIC, NULL, NULL);
14
15 ... ...

 

epoll的高效就在于,当我们调用epoll_ctl往里塞入百万个句柄时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的句柄给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。

而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已,如何能不高效?!

那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。

如此,一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。

最后看看epoll独有的两种模式LT和ET。无论是LT和ET模式,都适用于以上所说的流程。区别是,LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时次次返回这个句柄,而ET模式仅在第一次返回。

这件事怎么做到的呢?当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时我们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,epoll_wait干了件事,就是检查这些socket,如果不是ET模式(就是LT模式的句柄了),并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了。所以,非ET的句柄,只要它上面还有事件,epoll_wait每次都会返回。而ET模式的句柄,除非有新中断到,即使socket上的事件没有处理完,也是不会次次从epoll_wait返回的。

 

  1 /*
2 * Each file descriptor added to the eventpoll interface will
3 * have an entry of this type linked to the hash.
4 */
5 struct epitem {
6 /* RB-Tree node used to link this structure to the eventpoll rb-tree */
7 struct rb_node rbn;
8 //红黑树,用来保存eventpoll
9
10 /* List header used to link this structure to the eventpoll ready list */
11 struct list_head rdllink;
12 //双向链表,用来保存已经完成的eventpoll
13
14 /* The file descriptor information this item refers to */
15 struct epoll_filefd ffd;
16 //这个结构体对应的被监听的文件描述符信息
17
18 /* Number of active wait queue attached to poll operations */
19 int nwait;
20 //poll操作中事件的个数
21
22 /* List containing poll wait queues */
23 struct list_head pwqlist;
24 //双向链表,保存着被监视文件的等待队列,功能类似于select/poll中的poll_table
25
26 /* The "container" of this item */
27 struct eventpoll *ep;
28 //指向eventpoll,多个epitem对应一个eventpoll
29
30 /* The structure that describe the interested events and the source fd */
31 struct epoll_event event;
32 //记录发生的事件和对应的fd
33
34 /*
35 * Used to keep track of the usage count of the structure. This avoids
36 * that the structure will desappear from underneath our processing.
37 */
38 atomic_t usecnt;
39 //引用计数
40
41 /* List header used to link this item to the "struct file" items list */
42 struct list_head fllink;
43 双向链表,用来链接被监视的文件描述符对应的struct file。因为file里有f_ep_link,用来保存所有监视这个文件的epoll节点
44
45 /* List header used to link the item to the transfer list */
46 struct list_head txlink;
47 双向链表,用来保存传输队列
48
49 /*
50 * This is used during the collection/transfer of events to userspace
51 * to pin items empty events set.
52 */
53 unsigned int revents;
54 //文件描述符的状态,在收集和传输时用来锁住空的事件集合
55 };
56
57 //该结构体用来保存与epoll节点关联的多个文件描述符,保存的方式是使用红黑树实现的hash表.
58 //至于为什么要保存,下文有详细解释。它与被监听的文件描述符一一对应.
59 struct eventpoll {
60 /* Protect the this structure access */
61 rwlock_t lock;
62 //读写锁
63
64 /*
65 * This semaphore is used to ensure that files are not removed
66 * while epoll is using them. This is read-held during the event
67 * collection loop and it is write-held during the file cleanup
68 * path, the epoll file exit code and the ctl operations.
69 */
70 struct rw_semaphore sem;
71 //读写信号量
72
73 /* Wait queue used by sys_epoll_wait() */
74 wait_queue_head_t wq;
75 /* Wait queue used by file->poll() */
76
77 wait_queue_head_t poll_wait;
78 /* List of ready file descriptors */
79
80 struct list_head rdllist;
81 //已经完成的操作事件的队列。
82
83 /* RB-Tree root used to store monitored fd structs */
84 struct rb_root rbr;
85 //保存epoll监视的文件描述符
86 };
87
88 //这个结构体保存了epoll文件描述符的扩展信息,它被保存在file结构体的private_data
89 //中。它与epoll文件节点一一对应。通常一个epoll文件节点对应多个被监视的文件描述符。
90 //所以一个eventpoll结构体会对应多个epitem结构体。那么,epoll中的等待事件放在哪里呢?见下面
91 /* Wait structure used by the poll hooks */
92 struct eppoll_entry {
93 /* List header used to link this structure to the "struct epitem" */
94 struct list_head llink;
95 /* The "base" pointer is set to the container "struct epitem" */
96 void *base;
97 /*
98 * Wait queue item that will be linked to the target file wait
99 * queue head.
100 */
101 wait_queue_t wait;
102 /* The wait queue head that linked the "wait" wait queue item */
103 wait_queue_head_t *whead;
104 };
105
106 //与select/poll的struct poll_table_entry相比,epoll的表示等待队列节点的结
107 //构体只是稍有不同,与struct poll_table_entry比较一下。
108 struct poll_table_entry {
109 struct file * filp;
110 wait_queue_t wait;
111 wait_queue_head_t * wait_address;
112 };

 
Epoller组件的实现主要是对于epoll_create,epoll_ctl,epoll_wait等的封装,该Epoll可以设置
  • 可选择采用边缘触发还是选择触发的模式bEt,默认为边缘触发 
  • 可指定该Epoll可以指定监听的最大套接字数目max_connections

/**
 * @brief epoller操作类,已经默认采用了EPOLLET方式做触发
 */
class  TC_Epoller
{
public :

      /**
      * @brief 构造函数.
      * 
     * @param bEt 默认是ET模式,当状态发生变化的时候才获得通知
      */
     TC_Epoller( bool  bEt =  true );

      /**
     * @brief 析够函数.
      */
     ~TC_Epoller();

      /**
      * @brief 生成 epoll句柄.
      * 
     * @param max_connections epoll服务需要支持的最大连接数
      */
      void  create( int  max_connections);

      /**
      * @brief 添加监听句柄.
      * 
     * @param fd    句柄
     * @param data  辅助的数据, 可以后续在epoll_event中获取到
     * @param event 需要监听的事件EPOLLIN|EPOLLOUT
     *             
      */
      void  add( int  fd,  long  long  data, __uint32_t event);

      /**
      * @brief 修改句柄事件.
      * 
     * @param fd    句柄
     * @param data  辅助的数据, 可以后续在epoll_event中获取到
     * @param event 需要监听的事件EPOLLIN|EPOLLOUT
      */
      void  mod( int  fd,  long  long  data, __uint32_t event);

      /**
      * @brief 删除句柄事件.
      * 
     * @param fd    句柄
     * @param data  辅助的数据, 可以后续在epoll_event中获取到
     * @param event 需要监听的事件EPOLLIN|EPOLLOUT
      */
      void  del( int  fd,  long  long  data, __uint32_t event);

      /**
      * @brief 等待时间.
      * 
      * @param millsecond 毫秒
     * @return int       有事件触发的句柄数
      */
      int  wait( int  millsecond);

      /**
     * @brief 获取被触发的事件.
      *
      * @return struct epoll_event&被触发的事件
      */
      struct  epoll_event & get( int  i) { assert(  _pevs  != 0);  return  _pevs  [i]; }

protected :

      /**
      * @brief 控制 epoll,将EPOLL设为边缘触发EPOLLET模式
     * @param fd    句柄,在create函数时被赋值
     * @param data  辅助的数据, 可以后续在epoll_event中获取到
     * @param event 需要监听的事件
      * @param op    EPOLL_CTL_ADD: 注册新的 fdepfd 中;
      *                  EPOLL_CTL_MOD:修改已经注册的 fd的监听事件;
      *                  EPOLL_CTL_DEL:从 epfd中删除一个fd
      * 
      */
      void  ctrl( int  fd,  long  long  data, __uint32_t events,  int  op);

protected :

     /**
     *      epoll
     */
     int  _iEpollfd ;

      /**
     * 最大连接数
      */
      int     _max_connections ;

      /**
     * 事件集
      */
      struct  epoll_event  * _pevs ;

     /**
     * 是否是ET模式
     */
     bool  _et ;
};


TC_Epoller::TC_Epoller( bool  bEt)
{
      _iEpollfd    = -1;
      _pevs        = NULL;
     _et          = bEt;
      _max_connections  = 1024;
}

TC_Epoller::~TC_Epoller()
{
      if ( _pevs  != NULL)
     {
             delete []  _pevs ;
             _pevs  = NULL;
     }

      if ( _iEpollfd  > 0)
     {
            close( _iEpollfd );
     }
}

void  TC_Epoller::ctrl( int  fd,  long  long  data, __uint32_t events,  int  op)
{
      struct  epoll_event  ev;
     ev.data.u64 = data;
     if ( _et )
    {
        ev.events   = events | EPOLLET ;
    }
     else
    {
        ev.events   = events;
    }

     epoll_ctl( _iEpollfd  , op, fd, &ev);
}

void  TC_Epoller::create( int  max_connections)
{
      _max_connections  = max_connections;

      _iEpollfd  = epoll_create( _max_connections  + 1);

      if ( _pevs  != NULL)
     {
             delete []  _pevs ;
     }

      _pevs  =  new  epoll_event[ _max_connections  + 1];
}

void  TC_Epoller::add( int  fd,  long  long  data, __uint32_t event)
{
     ctrl(fd, data, event, EPOLL_CTL_ADD);
}

void  TC_Epoller::mod( int  fd,  long  long  data, __uint32_t event)
{
     ctrl(fd, data, event, EPOLL_CTL_MOD);
}

void  TC_Epoller::del( int  fd,  long  long  data, __uint32_t event)
{
     ctrl(fd, data, event, EPOLL_CTL_DEL);
}

int  TC_Epoller::wait( int  millsecond)
{
      return  epoll_wait( _iEpollfd  ,  _pevs  ,  _max_connections  + 1, millsecond);

你可能感兴趣的:(C++ Epoll的封装)