您的位置:首页 > 其它

libevent源码分析之event_io_map与event_signal_map

2015-06-16 18:25 591 查看
首先,文章主要借鉴 http://blog.csdn.net/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)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: