libevent源码分析之event_io_map与event_signal_map

首先,文章主要借鉴   http://blog.youkuaiyun.com/luotuo44/article/details/38403241,十分感谢!


现在,我们来看看libevent中使用的哈希表

在学习此数据结构之前,要知道一个事实,什么时候会使用到这个哈希表:
/* On some platforms, fds start at 0 and increment by 1 as they are
   allocated, and old numbers get used.  For these platforms, we
   implement io maps just like signal maps: as an array of pointers to
   struct evmap_io.  But on other platforms (windows), sockets are not
   0-indexed, not necessarily consecutive, and not necessarily reused.
   There, we use a hashtable to implement evmap_io.
*/
可见,比如windowns平台上,产生的socket,不像linux里一样从小到大安排不仅连续还可能会重用,而是给出一个飘忽不定的数,于是才使用哈希表实现的evmap_io来处理
【由此也可见,在linux等平台上的evmap_io是十分简单的,只是单纯的数组来实现evmap_io】

如下是整个哈希表的结构体,可以通过分析其中的成员来认识这个哈希表具体的结构:

struct event_io_map {                                                         
   /* The hash table itself. */                                        
   struct event_map_entry **hth_table;                                            
   /* How long is the hash table? */                                   
   unsigned hth_table_length;                                          
   /* How many elements does the table contain? */                     
   unsigned hth_n_entries;                                             
   /* How many elements will we allow in the table before resizing it? */ 
   unsigned hth_load_limit;                                            
   /* Position of hth_table_length in the primes table. */             
   //之前接触过STL的哈希表,其中数组长度的增长是按照素数增长的,我想这里也是这样,所以下面当前长度在素数数组中的位置
   int hth_prime_idx;                                                  
 }

struct event_map_entry {
         HT_ENTRY(event_map_entry) map_node;
         evutil_socket_t fd;
         union { /* This is a union in case we need to make more things that can
                            be in the hashtable. */
                 struct evmap_io evmap_io;
         } ent;
 };

//关于HT_ENTRY,指的是如果定义了某个宏,除了相同部分的指向下一个节点的指针,就另外多出一个变量
//该变量多出来干嘛的呢?
//根据对整个哈希表的理解,可以在其他操作源代码中看出,以后判断event结构体插在哈希表的哪一个位置,不是直接根据fd取模判断的,还要将fd经过某种处理后再通过取模放入哈希表中的某位置
//那样岂不是每次想知道某event在哪个bucket都要对fd进行某种处理,然后再判断? 
//如果定义了HT_CACHE_HASH_VALUES,那么多出来的hte_hash变量就是存这个fd经过某种处理后的值的! 
#ifdef HT_CACHE_HASH_VALUES
#define HT_ENTRY(type)                          \
  struct {                                      \
    struct type *hte_next;                      \
    unsigned hte_hash;                          \
  }
#else
#define HT_ENTRY(type)                          \
  struct {                                      \
    struct type *hte_next;                      \
  }
#endif

//可以看到的是,对于某一个fd,这里也使用了event_list
//然而这个event_list使用的就是上一章讲到的TAILQ,这么说那为什么对于一个fd会有好多event呢?
//这是因为libevent允许用同一个文件描述符或信号值,调用event_add多次。此时,同一个文件描述符或信号值就有多个event与其相关了
struct evmap_io {
         struct event_list events;
         ev_uint16_t nread;
         ev_uint16_t nwrite;
 };


回顾之前对SGI STL中哈希表的认识,哈希表解决冲突问题的方式就是使用开链法,其余还可以使用线性探索,二次线性等
通过 struct type** hth_table, 以及HT_ENTRY中的next指针,我们就可以发现其使用的解决冲突的方法也是开链法。
整体的结构如下图:






 
整体的操作这里就不再详谈了,读了一遍后发现与STL中的哈希表类似,只是依旧全是用宏实现的操作(不过部分内容属于没看懂要干嘛,以后遇到再说)
所有的操作中,都是以event_map_entry为基本单位操作的,并没有深入到event,意味着将来还是要通过调用者来进一步处理的

接着来看看在libevent中的实际应用
分析的函数是evmap_io_add
//要看此函数,最好在理解了此函数下面的宏讲解再回头来看
int
evmap_io_add(struct event_base *base, evutil_socket_t fd, struct event *ev)
{
    const struct eventop *evsel = base->evsel;
    struct event_io_map *io = &base->io;
    struct evmap_io *ctx = NULL;
    int nread, nwrite, retval = 0;
    short res = 0, old = 0;
    struct event *old_ev;

    EVUTIL_ASSERT(fd == ev->ev_fd);

    if (fd < 0)
        return 0;

#ifndef EVMAP_USE_HT
    if (fd >= io->nentries) {
        if (evmap_make_space(io, fd, sizeof(struct evmap_io *)) == -1)
            return (-1);
    }
#endif
    GET_IO_SLOT_AND_CTOR(ctx, io, fd, evmap_io, evmap_io_init,
                         evsel->fdinfo_len);
        //至此,我们知道了应该把事件插在哪里
        //于是着手处理一些细节问题
    nread = ctx->nread;
    nwrite = ctx->nwrite;

        //查看原来的哈希表中fd是注册的什么事件
        //以下细节暂时不去理解,这里涉及到对整个libevent的理解
    if (nread)
        old |= EV_READ;
    if (nwrite)
        old |= EV_WRITE;

    if (ev->ev_events & EV_READ) {
        if (++nread == 1)
            res |= EV_READ;
    }
    if (ev->ev_events & EV_WRITE) {
        if (++nwrite == 1)
            res |= EV_WRITE;
    }
    if (EVUTIL_UNLIKELY(nread > 0xffff || nwrite > 0xffff)) {
        event_warnx("Too many events reading or writing on fd %d",
            (int)fd);
        return -1;
    }
    if (EVENT_DEBUG_MODE_IS_ON() &&
        (old_ev = TAILQ_FIRST(&ctx->events)) &&
        (old_ev->ev_events&EV_ET) != (ev->ev_events&EV_ET)) {
        event_warnx("Tried to mix edge-triggered and non-edge-triggered"
            " events on fd %d", (int)fd);
        return -1;
    }

    if (res) {
        void *extra = ((char*)ctx) + sizeof(struct evmap_io);
        /* XXX(niels): we cannot mix edge-triggered and
         * level-triggered, we should probably assert on
         * this. */
        if (evsel->add(base, ev->ev_fd,
            old, (ev->ev_events & EV_ET) | res, extra) == -1)
            return (-1);
        retval = 1;
    }

    ctx->nread = (ev_uint16_t) nread;
    ctx->nwrite = (ev_uint16_t) nwrite;

        //最后插入在同一个fd的TAILQ队列里
    TAILQ_INSERT_TAIL(&ctx->events, ev, ev_io_next);

    return (retval);
}



其中最关键的函数就是函数内的一个宏:
GET_IO_SLOT_AND_CTOR(ctx, io, fd, evmap_io, evmap_io_init, evsel->fdinfo_len);
这个宏内调用了两个宏,值得注意的是宏_HT_FIND_OR_INSERT的最后两个参数略长...它的原型是:
#define _HT_FIND_OR_INSERT(name, field, hashfn, head, eltype, elm, var, y, n) ...

#define GET_IO_SLOT_AND_CTOR(x, map, slot, type, ctor, fdinfo_len)    \
    do {                                \
        struct event_map_entry _key, *_ent;            \
        _key.fd = slot;                        \
        _HT_FIND_OR_INSERT(event_io_map, map_node, hashsocket, map, \
            event_map_entry, &_key, ptr,            \
            {                            \
                _ent = *ptr;                \
            },                            \
            {                            \
                _ent = mm_calloc(1,sizeof(struct event_map_entry)+fdinfo_len); \
                if (EVUTIL_UNLIKELY(_ent == NULL))        \
                    return (-1);            \
                _ent->fd = slot;                \
                (ctor)(&_ent->ent.type);            \
                _HT_FOI_INSERT(map_node, map, &_key, _ent, ptr) \
                });                    \
        (x) = &_ent->ent.type;                    \
    } while (0)


理解这个宏,那么evmap_io_add函数就可以明白大部分了
将宏进行gcc -E的预编译后,得到如下内容:
do{
    //这个_ent就是将来我们可能要添加到哈希表中去的一个元素
    //那_key是干嘛用的 ? 居然还不是指针类型的!
    //这个key是用来给我在哈希表中查找,在哈希表中fd代表的事件(链)是不是已经存在了!
    //存在的话,那这个key因为是栈中申请的,不需要我们去处理(释放)
    //不存在的话,就创建一个
    struct event_map_entry _key, *_ent;
    _key.fd = fd;

    struct event_io_map *_ptr_head = io;
    //看到这个二级指针,我们就该想到哈希表中的FIND_P函数了
    struct event_map_entry **ptr;

    //事先要判断是否需要扩充哈希表
    if (!_ptr_head->hth_table
                || _ptr_head->hth_n_entries >= _ptr_head->hth_load_limit)
    {
        event_io_map_HT_GROW(_ptr_head,
            _ptr_head->hth_n_entries + 1);
    }

    #ifdef HT_CACHE_HASH_VALUES
        do{
            (&_key)->map_node.hte_hash = hashsocket((&_key));
        } while(0);
    #endif

    //这里在整个哈希表中查找fd是否已经存在在哈希表中了,之前谈到过,哈希表只能允许一个fd存在,然而一个fd可以联系到多个event结构体
    //于是我们要看看哈希表中是否已经有了这个fd,如果没有就创建,然后返回它的地址;如果存在这个fd了,我们就要返回这个entry地址。给我们用来把事件插进去!
    ptr = _event_io_map_HT_FIND_P(_ptr_head, (&_key));  

    if (*ptr)
    {
        _ent = *ptr;
    }
    else
    {
        //不存在的话,就创建一个entry,可以让我们把事件存进去
        _ent = mm_calloc(1, sizeof(struct event_map_entry) + evsel->fdinfo_len);
        if (EVUTIL_UNLIKELY(_ent == NULL))
            return (-1);

        _ent->fd = fd;
        (evmap_io_init)(&_ent->ent.evmap_io);

#ifdef HT_CACHE_HASH_VALUES
        do
        {
            ent->map_node.hte_hash = (&_key)->map_node.hte_hash;
        }while(0);
#endif
        _ent->map_node.hte_next = NULL;

        //既然我们没有在哈希表的某bucket里找到相同的fd,那么此时ptr指针肯定是存的是哈希表某bucket里最后一个元素的next指针的地址
        *ptr = _ent;
        ++(io->hth_n_entries);
        }

        //于是我们取得了该entry的地址,好让我们过会儿去把事件插进去
    (ctx) = &_ent->ent.evmap_io;
}while (0)


接下来就认识一下较为简单的event_signal_map

上面讲到,在windows这种平台上我们会定义event_io_map为比较复杂的哈希表类型,而在linux等平台上,libevent则采用了简单的哈希表类型
差别在于,windows平台上我们会使用取模得到事件应该插入在哈希表上的位置,这样会导致不同fd的事件在哈希表的同一个bucket上,于是需要链表将这些不同fd事件连接起来,又要考虑到一个fd可以有多个event结构体,又要通过链表来连接,所以整个结构体显得较为复杂

而在linux等平台上,得到的fd不仅是从小到大的,而且可能是连续的,而且可能会重用之前关闭的,这就使fd的值不会太大或太随机,于是在此平台上寻找某fd事件对应的哈希表中bucket时,不令其取模,而是直接找对应的位置. 若还不理解,那么下面的内容可以帮助理解.

这里不讲此平台上的event_io_map了,因为:

#ifdef EVMAP_USE_HT
#include "ht-internal.h"
struct event_map_entry;
HT_HEAD(event_io_map, event_map_entry);
#else
#define event_io_map event_signal_map
#endif
linux等平台上的event_io_map可以这样设计, 那windows和linux等平台的event_signal_map为什么可以同时使用这个结构体呢?
因为linux等平台上的信号数量和windows平台上的信号数量都很少, 只有二位数个或个位数个。

接下来看看event_signal_map:
//虽然使用的是void **, 但实际使用的时候自然是用的evmap_signal 
//在了解windows下的event_io_map后再来看这个,显得十分简单易懂
struct event_signal_map
{
    void **entries;
    int nentries;
};

struct evmap_signal {
    //event_list也就是TAILQ
    struct event_list events;
};


给出的操作函数本就不多,因为数据结构比较简单,就挑下面一个拿出来看看
接下来看看此数据结构的操作函数:
static int
evmap_make_space(struct event_signal_map *map, int slot, int msize)
{
    if(map->nentries <= slot)
    {
        int nentries = map->nentries?map->nentries:32;
        void **tmp;

        while(nentries <= slot)
            nentries <<= 1;

        //这里使用的是realloc, 也是讲究的. 因为这种简单的哈希表在使用realloc后是无需重新调整的,因为fd代表的事件插入时是直接插入的,不需要取模什么的
        //如果使用malloc,还要原封不动的搬过来明显麻烦
        tmp = (void **)mm_realloc(map->entries, nentries*msize);
        if(NULL == tmp)
            return -1;

        memset(&tmp[map->nentries], 0, 
            (nentries - map->nentries) * msize);

        map->nentries = nentries;
        map->entries = tmp;
    }
    return 0;
}




接下来看看这种数据结构在libevent中的应用:
int
evmap_signal_add(struct event_base *base, int sig, struct event *ev)
{
    const struct eventop *evsel = base->evsigsel;
    struct event_signal_map *map = &base->sigmap;
    struct evmap_signal *ctx = NULL;

    if (sig >= map->nentries) {
        if (evmap_make_space(
            map, sig, sizeof(struct evmap_signal *)) == -1)
            return (-1);
    }
    GET_SIGNAL_SLOT_AND_CTOR(ctx, map, sig, evmap_signal, evmap_signal_init,
        base->evsigsel->fdinfo_len);

    if (TAILQ_EMPTY(&ctx->events)) {
        if (evsel->add(base, ev->ev_fd, 0, EV_SIGNAL, NULL)
            == -1)
            return (-1);
    }

    TAILQ_INSERT_TAIL(&ctx->events, ev, ev_signal_next);

    return (1);
}


核心依旧在这个宏上:
GET_SIGNAL_SLOT_AND_CTOR(ctx, map, sig, evmap_signal, evmap_signal_init,
        base->evsigsel->fdinfo_len);
理解了这个宏那么evmap_signal_add函数就容易理解了
因为下面的内容并不难理解,就直接在上面注释了
#define GET_SIGNAL_SLOT_AND_CTOR(x, map, slot, type, ctor, fdinfo_len)    \
    do {                                \
                //因为数据结构的简单,所以操作就显得简单多了
                //如果不存在一个可以插入event的地方,那就需要创建了
        if ((map)->entries[slot] == NULL) {            \
            (map)->entries[slot] =                \
                mm_calloc(1,sizeof(struct type)+fdinfo_len); \
            if (EVUTIL_UNLIKELY((map)->entries[slot] == NULL)) \
                return (-1);                \
                        //记得初始化一下
            (ctor)((struct type *)(map)->entries[slot]);    \
        }                            \
        (x) = (struct type *)((map)->entries[slot]);        \
    } while (0)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值