libevent源码分析之event_io_map与event_signal_map
2015-06-16 18:25
591 查看
首先,文章主要借鉴 http://blog.csdn.net/luotuo44/article/details/38403241,十分感谢!
现在,我们来看看libevent中使用的哈希表
在学习此数据结构之前,要知道一个事实,什么时候会使用到这个哈希表:
【由此也可见,在linux等平台上的evmap_io是十分简单的,只是单纯的数组来实现evmap_io】
如下是整个哈希表的结构体,可以通过分析其中的成员来认识这个哈希表具体的结构:
回顾之前对SGI STL中哈希表的认识,哈希表解决冲突问题的方式就是使用开链法,其余还可以使用线性探索,二次线性等
通过 struct type** hth_table, 以及HT_ENTRY中的next指针,我们就可以发现其使用的解决冲突的方法也是开链法。
整体的结构如下图:
整体的操作这里就不再详谈了,读了一遍后发现与STL中的哈希表类似,只是依旧全是用宏实现的操作(不过部分内容属于没看懂要干嘛,以后遇到再说)
所有的操作中,都是以event_map_entry为基本单位操作的,并没有深入到event,意味着将来还是要通过调用者来进一步处理的
接着来看看在libevent中的实际应用
分析的函数是evmap_io_add
其中最关键的函数就是函数内的一个宏:
理解这个宏,那么evmap_io_add函数就可以明白大部分了
将宏进行gcc -E的预编译后,得到如下内容:
接下来就认识一下较为简单的event_signal_map
上面讲到,在windows这种平台上我们会定义event_io_map为比较复杂的哈希表类型,而在linux等平台上,libevent则采用了简单的哈希表类型
差别在于,windows平台上我们会使用取模得到事件应该插入在哈希表上的位置,这样会导致不同fd的事件在哈希表的同一个bucket上,于是需要链表将这些不同fd事件连接起来,又要考虑到一个fd可以有多个event结构体,又要通过链表来连接,所以整个结构体显得较为复杂
而在linux等平台上,得到的fd不仅是从小到大的,而且可能是连续的,而且可能会重用之前关闭的,这就使fd的值不会太大或太随机,于是在此平台上寻找某fd事件对应的哈希表中bucket时,不令其取模,而是直接找对应的位置. 若还不理解,那么下面的内容可以帮助理解.
这里不讲此平台上的event_io_map了,因为:
因为linux等平台上的信号数量和windows平台上的信号数量都很少, 只有二位数个或个位数个。
接下来看看event_signal_map:
给出的操作函数本就不多,因为数据结构比较简单,就挑下面一个拿出来看看
接下来看看此数据结构的操作函数:
接下来看看这种数据结构在libevent中的应用:
核心依旧在这个宏上:
因为下面的内容并不难理解,就直接在上面注释了
现在,我们来看看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 #endiflinux等平台上的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)
相关文章推荐
- 虚拟地址和物理地址的概念
- 俄罗斯方块(详解)
- mysql多实例,my.cnf 4G conf配置安装配置
- 【转载】恼人的函数指针(二):指向类成员的指针
- JavaScript 的 parseInt 取整
- AngularJS学习小结
- HTML-CSS
- 154在屏幕中绘图时设置透明度(扩展知识:为图片视图添加点击手势识别器,来实现点击事件操作)
- Ternary Search Tree Java实现
- SRS配置采集(ingest)
- OpenCms创建网站的过程示意图——专用OpenCms人们刚开始学习
- jsp防盗链代码
- Android Binder机制理解
- 有趣的笔试题(1)
- intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
- Tiny210编译和烧写u-boot步骤
- poj2583---Series Determination
- Kafka实战-实时日志统计流程
- response.getWriter().write()与out.print()的区别
- cocos2d-x3.3获取时间