libevent源码分析一--io事件响应

  这篇文章将分析libevent如何组织io事件,如何捕捉事件的发生并进行相应的响应。这里不会详细分析event与event_base的细节,仅描述io事件如何存储与如何响应。

1.  select

  libevent实现io事件的backend实际上使用的是io复用接口,如select, poll, epoll等,这里以最简单的select为例进行说明。首先简单介绍一下select接口:

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);

readfds, writefds, exceptfds均是文件描述符集合指针,调用该函数后,若readfds集合中的fd可读,或者writefds集合中的fd可写,或者exceptfds集合中的fd发生错误,或者阻塞的时间达到了timeout,函数返回。

  函数返回0,返回结果readfds集合中包含了入参readfds中现在读不会被阻塞的fd,返回结果writefds包含了对应的写不会被阻塞的fd,exceptfds包含了所有发生异常的fd。如果超时,函数返回-1,这些集合为空。

  可以看到,select调用需要传入感兴趣的io的文件描述符fd,而libevent中大家熟悉的是event_base,event结构体,event添加到event_base后就可以等待事件触发了,libevent是如何关联event, event_base与select的呢?

事实上,在libevent的源码select.c的,定义了一个数据结构struct selectop,

struct selectop {
    int event_fds;        /* Highest fd in fd set */
    int event_fdsz;
    int resize_out_sets;
    fd_set *event_readset_in;
    fd_set *event_writeset_in;
    fd_set *event_readset_out;
    fd_set *event_writeset_out;
};

它保存了libevent每次调用select需要使用到的入参与出参,这里保存的文件描述符是与加入到event_base中的event对应的。每当一个新的io的event加入到event_base, 内部都会将event对应的文件描述符fd加入到event_readset_in与event_writeset_in中,这取决于event感兴趣的是读事件还是写事件。相应的函数调用栈是:

  event_add->event_add_nolock_->evmap_io_add_->select_add

当libevent使用select作为io的backend时,event_base中的成员evbase即指向动态分配的结构selectop,那么所有感兴趣的io事件的文件描述符都已经保存在了属于event_base的selectop结构中,调用select时即可使用。

2.  event_io_map

  前面分析了select使用的入参如何保存,但除了事件对应的文件描述符,libevent同样需要保存结构event,因为event中还记录了许多其它的信息,如事件发生时调用的回调函数,事件的超时时间等。因此,加入到event_base中的event结构也需要保存,event_base中的成员struct event_io_map io的作用正是用来保存添加的io事件的event结构。

   struct event_io_map是一个hash表,如果是在非windows环境下,这个hash表可以简单地以一个动态数组实现。为描述简单,这里假设是在非windows环境下。其定义如下:

struct event_signal_map {
    /* An array of evmap_io * or of evmap_signal *; empty entries are
     * set to NULL. */
    void **entries;
    /* The number of entries available in entries */
    int nentries;
}; 

  它包含一个动态数组entries,数组长度以nentries表示。动态数组的内容是指向动态分配的结构evmap_io的指针。event对应的文件描述符fd作为它在动态数组中的索引,而同一个fd可能有多个感兴趣的事件加入到同一个event_base中,因此将它们连接起来构成双向链表以解决冲突,这个双向链表就是struct evmap_io,即以event的fd作为索引即可以找到这些事件组成的双向链表。event结构中的成员ev_io包含两个指针即双向链表的前驱指针与后继指针。event_base中的成员io的存储结构可以用图2-1表示,

图2-1 event_io_map

3.  event_base_loop

  包含回调信息的event结构已经存入了event_base的io成员,对于select,event相应的文件描述符也已经保存在了event_base的evbase成员指向的struct selectop结构中。那么libevent最终是如何等待读写事件的发生并最终调用相应的回调函数的呢?答案是event_base_loop函数。

  首先,针对io事件,event_base_loop每一次循环,都会调用后端的dispatch函数,针对select后端,这个dispatch函数是select_dispatch,而select_dispatch又会调用select函数。然后,当select返回结果后,select_dispatch根据文件描述符从event_base的hash表io中将触发的event的回调函数加入到event_base的待执行回调函数链表,这个待执行回调函数链表由event_base的成员struct evcallback_list *activequeues保存。最后,event_base_loop在dispatch之后即会执行链表上的回调函数,完成事件响应。

4.  activequeues

  activequeues是一个evcallback_list类型的动态数组,用来实现事件的优先级,数组每一个成员都是一个待执行回调函数的链表。event_base_loop中执行这些链表上的函数时,以索引0开始按递增的顺序扫描数组,若数组成员指向的链表不为空,则依次执行上面的回调函数,索引越小,对应链表上的回调函数越先被执行,构成了事件的优先级。activequeues的结构可使用图4-1表示,

图4-1 event_base的activequeues成员结构

  libevent处理io事件的流程简化总结为:首先将io事件的event结构加入到event_base的io成员中,并将对应的fd保存到evbase指向的结构中;然后event_base_loop调用dispatch,将触发的事件的回调函数添加到activequeues多优先级链表上;最后event_base_loop中执行activequeues上的回调函数,完成事件响应。

5.  部分概念说明表

event_base

libevent中的基本结构,所有的event添加到该结构的实例中,再调用event_base_loop处理其中的事件

event

libevent中表达一个事件的结构,包含了文件描述符,回调函数等信息

struct selectop

select后端的结构,存储select函数需要的参数信息的结构,event_base中使用evbase指向这些信息

select.c

libevent中实现select后端的源码文件

struct event_io_map

event_base中io成员的类型,用来存储io类型的event结构的hash表

struct evmap_io

双向链表描述结构,用于io类型的event

activequeues

event_base结构中的成员,evcallback_list类型的动态数组,保存待执行的回调函数的链表。

 

 

 

 

 

 

 

 

 

 

 

 

select后端的定义在select.c源码文件中

  event/event_callback结构定义在源码文件event_struct.h中

  event_base结构定义在源码文件event-internal.h中

  event_add/event_base_loop/event_add_nolock函数定义在event.c函数中

  evmap_io_add_函数与evmap_io/event_io_map结构定义在evmap.c文件中

posted @ 2019-08-15 18:13  yangzd~  阅读(512)  评论(0编辑  收藏  举报