首先,文章主要借鉴 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)