event_io_map

哈希表操作函数

hashcode与equals函数

哈希表初始化

哈希表元素查找

哈希表扩容

哈希表元素插入

哈希表元素替换

哈希表元素删除

自定义条件删除元素

哈希表第一个非空元素

哈希表下一个元素

释放哈希表

向event_io_map中添加event

激活event_io_map中的event

删除event_io_map中的event


以下源码均基于libevent-2.0.21-stable。

在libevent中,自定义了一个哈希表结构用于实现event_io_map,该哈希表相关代码在ht-internal.h中,从该头文件的名称也可以看出来,libevent中的哈希表是一个并不对用户开放的内部使用结构。哈希表用于实现event_io_map,因此先来看看event_io_map的定义。

event_io_map

在event_internal.h文件中,有如下定义:

  1. #ifdef WIN32
  2. /* If we're on win32, then file descriptors are not nice low densely packed
  3. integers. Instead, they are pointer-like windows handles, and we want to
  4. use a hashtable instead of an array to map fds to events.
  5. */
  6. #define EVMAP_USE_HT
  7. #endif
  8.  
  9. /* #define HT_CACHE_HASH_VALS */
  10.  
  11. #ifdef EVMAP_USE_HT //在windows下就是用哈希结构,否则直接使用event_signal_map
  12. #include "ht-internal.h"
  13. struct event_map_entry;
  14. HT_HEAD(event_io_map, event_map_entry); //定义一个结构体名为event_io_map,其中的哈希表元素类型为event_map_entry
  15. #else
  16. #define event_io_map event_signal_map
  17. #endif

根据上述定义可知,仅在Windows环境下,event_io_map会使用哈希结构,否则event_io_map直接按event_signal_map使用。

       这里涉及到了一个宏定义HT_HEAD以及一个结构体event_map_entry,先来看看event_map_entry结构体的定义。

  1. #ifdef EVMAP_USE_HT
  2. struct event_map_entry {
  3. HT_ENTRY(event_map_entry) map_node; //构成哈希表中同一个桶下的链表
  4. evutil_socket_t fd; //文件描述符
  5. union { /* This is a union in case we need to make more things that can
  6. be in the hashtable. */
  7. struct evmap_io evmap_io; //存放同一个文件描述符/信号值下的所有event
  8. } ent;
  9. };

可以看到,在event_map_entry这一结构体中,还涉及到了一个HT_ENTRY宏定义,除此之外,其中包含了一个文件描述符/信号值成员fd,以及一个联合体中定义了一个evmap_io类型的结构体。对于这里的HT_ENTRY以及前面的HT_HEAD,各自的宏定义如下所示:

  1. #define HT_HEAD(name, type) \
  2. struct name { \
  3. /* The hash table itself. */ \
  4. struct type **hth_table; /* 哈希表 可以当做数组 hth_table[i]就是该数组中第i个struct type * 类型元素*/ \
  5. /* How long is the hash table? */ \
  6. unsigned hth_table_length; /*哈希表的容量*/ \
  7. /* How many elements does the table contain? */ \
  8. unsigned hth_n_entries; /*哈希表中的元素数量*/ \
  9. /* How many elements will we allow in the table before resizing it? */ \
  10. unsigned hth_load_limit; /*哈希表扩容阈值*/ \
  11. /* Position of hth_table_length in the primes table. */ \
  12. int hth_prime_idx; /*哈希表长度在素数数组中的索引*/ \
  13. }
  14.  
  15. #ifdef HT_CACHE_HASH_VALUES
  16. #define HT_ENTRY(type) \
  17. struct { \
  18. struct type *hte_next; \
  19. unsigned hte_hash; \
  20. }
  21. #else
  22. #define HT_ENTRY(type) \
  23. struct { \
  24. struct type *hte_next; \
  25. }
  26. #endif

对于HT_HEAD宏定义,其中包含5个成员,各自的含义可参照注释。需要注意的是第一个成员struct type **hth_table,从成员名来看就是哈希表,这是一个二级指针,在C语言中规定,p[i]等价于*(p+i),换到这里来看,hth_table是一个指向type *类型的指针,因此(p+i)就是从首地址偏移了i个type *类型大小的地址,而*(p+i)自然也就是从首地址算起,第i个type *类型的元素了,因此,hth_table作为一个二级指针,那么hth_table[i]也就是第i个type*类型的元素,hth_table也就相当于一个type *类型的数组了。

再来看 HT_ENTRY这一宏定义,它定义了一个匿名结构体,该结构体的接个头由宏定义HT_CACHE_HASH_VALUES控制,在libevent中,我目前尚未找到HT_CACHE_HASH_VALUES的定义,也就是说,该宏定义默认为未定义的,可由用户自行选择是否定义。

       接下来,替换以上两个宏定义,再回到event_map_entry和event_io_map中,得到各自定义如下:

  1. struct event_map_entry
  2. {
  3. struct
  4. {
  5. struct event_map_entry *hte_next; //构成链表
  6. #ifdef HT_CACHE_HASH_VALUES
  7. unsigned hte_hash;
  8. #endif
  9. }map_node;
  10.  
  11. evutil_socket_t fd;
  12. union
  13. {
  14. struct evmap_io evmap_io;
  15. }ent;
  16. };
  17.  
  18.  
  19. struct event_io_map
  20. {
  21. //哈希表,连续地址分配
  22. struct event_map_entry **hth_table;
  23. //哈希表的长度
  24. unsigned hth_table_length;
  25. //哈希的元素个数
  26. unsigned hth_n_entries;
  27. //哈希表扩容阈值,当哈希表中元素数目达到这个值就需要进行扩容
  28. unsigned hth_load_limit;
  29. //哈希表的长度所对应素数数组中的索引
  30. int hth_prime_idx;
  31. };

在event_io_map中,二级指针hth_table刚刚说了,可以看做是一个数组,这个数组的各个元素类型为event_map_entry *类型,而在event_map_entry结构体的定义中,一个hte_next很明显表明了这里会存在一个event_map_entry的链表。在来看后面的evmap_io结构体定义,如下所示:

  1. struct evmap_io
  2. {
  3. struct event_list events; //event双向链表
  4. ev_uint16_t nread; //链表中读事件的个数
  5. ev_uint16_t nwrite; //链表中写事件的个数
  6. };

关于event_list类型,可以在event_struct.h中找到其相关语句:(关于TAILQ

TAILQ_HEAD (event_list, event);  //名为event_list的头结点,链表结点为event类型

也就是说,在evmap_io中,定义了一个event的双向链表,以及一个nread变量和nwrite变量,而一个event_map_entry对应一个evmap_io,也就对应一个event的双向链表。

因此总结得出event_io_map的结构,如下所示:

这样,我们就大概知道了libevent中哈希表的结构:

hth_table就是哈希表中的“桶数组”,它的每一个元素都是一个“桶”,这里将哈希表分为主链和冲突链,哈希表主链上的各个结点放置的是地址连续的event_map_entry指针,通过下标如hth_table[i]就可以直接访问第i个结点,它的底层就是*(hth_table+i),这是一个寻址的过程,时间复杂度为O(1)。主链上每一个这样的event_map_entry指针都指向一个event_map_entry实体。如果有多个event_map_entry实体对应同一个桶,这就引起了哈希冲突,从图中也可得知,libevent是采用拉链法来解决哈希冲突的,如图中褐色框中的内容所示。

每个event_map_entry都对应了一个fd,对于event这是一个文件描述符,对于signal这是一个信号值,也就对应了一个监听事件。如果多个事件对应同一个fd,那么这些事件就会被放到evmap_io所对应的event事件双向链表中,evmap_io中的nread和nwrite则分别记录了这个双向链表中读类型和写类型的event数量

总之,event_io_map中会保存所有通过event_add添加的io event。

哈希表操作函数

在ht-internal.h中,哈希表相关的函数主要都是以宏函数来实现的,找到这些函数的宏定义如下所示:

  1. #define HT_EMPTY(head) \
  2. ((head)->hth_n_entries == 0)
  3.  
  4. /* How many elements in 'head'? */
  5. #define HT_SIZE(head) \
  6. ((head)->hth_n_entries)
  7.  
  8. #define HT_FIND(name, head, elm) name##_HT_FIND((head), (elm))
  9. #define HT_INSERT(name, head, elm) name##_HT_INSERT((head), (elm))
  10. #define HT_REPLACE(name, head, elm) name##_HT_REPLACE((head), (elm))
  11. #define HT_REMOVE(name, head, elm) name##_HT_REMOVE((head), (elm))
  12. #define HT_START(name, head) name##_HT_START(head)
  13. #define HT_NEXT(name, head, elm) name##_HT_NEXT((head), (elm))
  14. #define HT_NEXT_RMV(name, head, elm) name##_HT_NEXT_RMV((head), (elm))
  15. #define HT_CLEAR(name, head) name##_HT_CLEAR(head)
  16. #define HT_INIT(name, head) name##_HT_INIT(head)

这里只有两个函数直接给出了实现,而其他函数都是宏定义为name##_HT_XXX的宏函数上面,可以发现,这些宏函数对应了三个参数:name、head以及elm。宏定义中的“##”起的是连接作用。从函数名上大概能推断出函数的功能,不过如果直接去看这些宏函数的定义会非常懵的,在event_io_map中,这些宏定义中的name参数直接替换为"event_io_map"即可。

将这些宏定义全部替换(用gcc -E编译),然后就可以看到event_io_map中的这些函数的实现。

在分析函数实现之前,有必要先回过头再看一下event_io_map结构体:

  1. struct event_io_map
  2. {
  3. //哈希表,连续地址分配
  4. struct event_map_entry **hth_table;
  5. //哈希表的长度
  6. unsigned hth_table_length;
  7. //哈希的元素个数
  8. unsigned hth_n_entries;
  9. //哈希表扩容阈值,当哈希表中元素数目达到这个值就需要进行扩容
  10. unsigned hth_load_limit;
  11. //哈希表的长度所对应素数数组中的索引
  12. int hth_prime_idx;
  13. };

hth_table:哈希表,hth_table[i]就是哈希表中的第i个桶;

hth_table_length:哈希表的长度。为了让元素散列更加均匀,哈希表的长度最好选为素数,libevent中提供了一个素数数组,哈希表的长度都是以该数组中的元素为准,即使是哈希表扩容,也是一样的,该素数数组定义如下:

  1. static unsigned name##_PRIMES[] = { \
  2. 53, 97, 193, 389, \
  3. 769, 1543, 3079, 6151, \
  4. 12289, 24593, 49157, 98317, \
  5. 196613, 393241, 786433, 1572869, \
  6. 3145739, 6291469, 12582917, 25165843, \
  7. 50331653, 100663319, 201326611, 402653189, \
  8. 805306457, 1610612741 \
  9. };

可以发现,素数数组中的每个元素都是素数,并且后一个元素近似为前一个元素两倍。而hth_table_length也只能取该数组中的某个值;

hth_n_entries:哈希表中的元素个数;

hth_load_limit:哈希表的扩容阈值。扩容阈值的含义是:当哈希表中的元素个数达到这个值后,哈希表就需要进行扩容;

hth_prime_idx:哈希表的长度对应素数数组中该元素的索引。比如说哈希表的长度为193,那么hth_prime_idx就为2。

hashcode与equals函数

在java的HashMap中,提供了一个hashcode方法以及equals方法,前者可以将不同类型参数转换为一个int型的散列值,而后再通过哈希算法找到对应的桶位置,而后者则是用于判断哈希表中两个元素是否相等。

在libevent中,也定义了这两个函数。

event_io_map中为event_map_entry类型的参数提供了一个取相应hashcode的函数hashsocket,根据英文注释可知,由于windows中socket的低2~3位是没有多大影响的,也就是可能会存在很多socket的低2~3位是相同的情况。如果两个socket只有低2~3位不同,那么说明这两个socket的值是非常接近的,而在哈希表中,对于非常接近的元素也希望把他们尽量的散列开,因此这里并没有直接使用event_map_entry的fd作为hashcode,而是先将fd的二进制位进行旋转,将其低2位放到高2位去,然后再与原来的fd进行求和,这样,即使值相近的两个socket的hashcode也会有很大的差别,更有利于散列。如下所示:(evmap.c)

  1. static inline unsigned
  2. hashsocket(struct event_map_entry *e)
  3. {
  4. /* On win32, in practice, the low 2-3 bits of a SOCKET seem not to
  5. * matter. Our hashtable implementation really likes low-order bits,
  6. * though, so let's do the rotate-and-add trick. */
  7. unsigned h = (unsigned) e->fd;
  8. h += (h >> 2) | (h << 30); // h>>2后只剩前面30位,h<<30后只有最后2位,相当于把最后2位放到了最前面,然后再与原数相加
  9. return h;
  10. }

当取得元素相应的hashcode值之后,就需要通过这个hashcode来找到它在哈希表中相应的桶的位置。这里采用的方法是直接让hashcode对哈希表长度取模%,得到的结果就是该元素在哈希表中桶的位置。 如果找到了桶的位置,并且桶是空的,那么直接将该元素放到桶中,如果桶中已经有元素了,说明此时产生了哈希冲突,如前所述,这里解决哈希冲突的方法是将冲突的元素组合成一个链表。举个例子,java中的HashMap或者是C++中的Map,存储的都是一个key-value键值对,当你通过key去寻找对应的value时,就会对value相应的hashcode先通过哈希算法(比如这里是hashcode取哈希表长度的模)求得key对应的桶的位置,然后再在桶中去看是否有元素与传入的元素"equals",如果没有那么就是查找失败,否则就返回equals成功的那个元素。

libevent中通过eqsocket函数来判断哈希表中的两个元素是否相同,判断的方法很简单,就是看二者的fd是否相等即可。

  1. static inline int
  2. eqsocket(struct event_map_entry *e1, struct event_map_entry *e2)
  3. {
  4. return e1->fd == e2->fd;
  5. }

哈希表初始化

event_io_map哈希表初始化函数为event_io_map_HT_INIT,需要注意的是,这里只是让hth_table初始化为NULL,并没有为其分配实际的内存,哈希表此时也并不存在。如下所示:

  1. static inline void event_io_map_HT_INIT(struct event_io_map *head) //哈希表初始化
  2. {
  3. head->hth_table_length = 0;
  4. head->hth_table = NULL;
  5. head->hth_n_entries = 0;
  6. head->hth_load_limit = 0;
  7. head->hth_prime_idx = -1;
  8. }

哈希表元素查找

这里定义了两个哈希表元素查找函数,一个是_event_io_map_HT_FIND_P,一个则是_event_io_map_HT_FIND。二者都是在哈希表中去寻找与传入参数elm的fd相等的元素,不同的是前者返回的是哈希表中fd与elm相同的元素地址的指针,而后者则是返回该元素的地址。

  1. static inline struct event_map_entry **
  2. _event_io_map_HT_FIND_P(struct event_io_map *head,
  3. struct event_map_entry *elm) //返回哈希表中,fd与elm相等的元素地址,如果哈希表不存在就返回NULL,如果哈希表中不存在该元素*p=NULL
  4. {
  5. struct event_map_entry **p;
  6. if (!head->hth_table) //哈希表未分配
  7. return NULL;
  8.  
  9. #ifdef HT_CACHE_HASH_VALUES
  10. p = &((head)->hth_table[((elm)->map_node.hte_hash) //先将传入的elm进行哈希后取模,找到该元素在哈希表中的位置,然后找到哈希表中该位置上的元素,p为该元素地址
  11. % head->hth_table_length]);
  12. #else
  13. p = &((head)->hth_table[(hashsocket(*elm))%head->hth_table_length]);
  14. #endif
  15.  
  16. while (*p) //从该位置开始遍历整个冲突链,查找冲突链上是否有与elm的fd相等的元素,如果有就返回该元素地址
  17. {
  18. //判断是否相等。在实现上,只是简单地根据fd来判断是否相等
  19. if (eqsocket(*p, elm))
  20. return p;
  21.  
  22. //p存放的是hte_next成员变量的地址
  23. p = &(*p)->map_node.hte_next;
  24. }
  25.  
  26. return p; //如果没找到,那么*p = NULL
  27. }
  1. static inline struct event_map_entry *
  2. event_io_map_HT_FIND(const struct event_io_map *head,
  3. struct event_map_entry *elm) //返回的是哈希表中该结点元素
  4. {
  5. struct event_map_entry **p;
  6. struct event_io_map *h = (struct event_io_map *) head;
  7.  
  8. #ifdef HT_CACHE_HASH_VALUES
  9. do
  10. { //计算哈希值
  11. (elm)->map_node.hte_hash = hashsocket(elm);
  12. } while(0);
  13. #endif
  14.  
  15. p = _event_io_map_HT_FIND_P(h, elm); //p为NULL说明哈希表未分配,如果已分配,但是没找到,那么*p为NULL
  16. return p ? *p : NULL; //返回NULL可能是哈希表本身未分配,也可能是哈希表中没有该元素
  17. }

哈希表扩容

哈希表中每次扩容都是接近加倍,加倍扩容可以一次取得较大的容量,也就不用经常扩容了。不过对于哈希表扩容来说,它的作用不单单是为了增大空间,它也能降低哈希冲突发生的概率。

哈希表中通过HT_GENERATE宏设置了扩容因子,默认为0.5,而哈希表的扩容阈值,就是哈希表的长度乘以这个扩容因子得到的值,当哈希表中元素的个数超过扩容阈值,那么就说明需要进行扩容了。实际上扩容因子的设置并不固定,这里之所以将其设置为0.5,是为了降低哈希冲突概率的同时也避免频繁的扩容。

event_io_map的扩容函数为event_io_map_HT_GROW,其定义如下:

  1. int event_io_map_HT_GROW(struct event_io_map *head, unsigned size) //哈希表扩容
  2. {
  3. unsigned new_len, new_load_limit;
  4. int prime_idx;
  5.  
  6. struct event_map_entry **new_table;
  7. //哈希表当前长度对应素数表中最后一个元素,此时不能再扩容了
  8. if (head->hth_prime_idx == (int)event_io_map_N_PRIMES - 1)
  9. return 0;
  10.  
  11. //还未达到哈希表扩充阈值,无需扩容
  12. if (head->hth_load_limit > size)
  13. return 0;
  14. //执行到这里说明哈希表需要进行扩容了,下面开始扩容
  15. prime_idx = head->hth_prime_idx; //保存当前哈希表长度对应的素数数组索引
  16.  
  17. do {
  18. new_len = event_io_map_PRIMES[++prime_idx]; //从素数数组中得到扩容后的长度
  19.  
  20. new_load_limit = (unsigned)(0.5*new_len); //更新扩容阈值
  21. } while (new_load_limit <= size
  22. && prime_idx < (int)event_io_map_N_PRIMES); //如果当前扩容后size依然超过了阈值,那么继续选取素数数组中下一个元素作为扩容大小
  23.  
  24. //执行到这里说明哈希表的扩容大小已经确定,现在分配实际空间
  25. if ((new_table = mm_malloc(new_len*sizeof(struct event_map_entry*)))) //malloc分配扩容大小的空间 注意此时原来的哈希表依然存在
  26. {
  27. unsigned b;
  28. memset(new_table, 0, new_len*sizeof(struct event_map_entry*)); //清空哈希表
  29.  
  30. //这里不能直接用realloc来扩容,因此哈希表长度变化后各个元素在哈希表中的位置也会改变,因此需要hash,将原哈希表上的每个元素重新放置到新的哈希表上,包括未冲突的
  31. //实际上分配的只是一个新的哈希表的主链,新的哈希表和原来的哈希表对应同样的冲突链表
  32. for (b = 0; b < head->hth_table_length; ++b) //遍历原来的哈希表
  33. {
  34. struct event_map_entry *elm, *next;
  35. unsigned b2;
  36.  
  37. elm = head->hth_table[b]; //遍历哈希表上的主链的结点,然后再遍历主链结点所在的冲突链
  38. while (elm) //从该元素开始,检查它的map_node链表,看是否有冲突
  39. {
  40. next = elm->map_node.hte_next; //取得所属链表的下一个元素
  41.  
  42. #ifdef HT_CACHE_HASH_VALUES //如果使用缓存的哈希值
  43. b2 = (elm)->map_node.hte_hash % new_len; //直接用缓存的哈希值取模得到元素在哈希表上的索引
  44. #else //如果不使用缓存值就调用hashsocket获取哈希值 再取模
  45. b2 = (hashsocket(*elm)) % new_len;
  46. #endif
  47. //用头插法插入数据
  48. //将该元素放到重新hash后的位置,这里相当于是头插法
  49. elm->map_node.hte_next = new_table[b2]; //更新该元素所在的冲突链表位置
  50. new_table[b2] = elm; //将该元素放到重新hash后的位置
  51.  
  52. elm = next; //冲突链下一个元素
  53. }
  54. }
  55.  
  56. if (head->hth_table) //释放原来的哈希表
  57. mm_free(head->hth_table);
  58.  
  59. head->hth_table = new_table; //更新event_io_map中的哈希表
  60. }
  61. else //新哈希表malloc扩容失败,前面可以看到,malloc分配后新老哈希表都同时存在,因此分配失败的原因可能是空间不够装下两个哈希表
  62. { //尝试用realloc分配尝试是否能节约空间。realloc内部也会调用malloc分配一个新空间,然后将原空间中的数据复制过去后再释放原空间,
  63. //因此在一般情况下即使使用realloc分配,在某一时刻内存中也会同时存在两个哈希表,内存不足的问题并不会得到解决
  64. //这里之所以使用realloc,是为了尝试realloc的特殊情况,如果原空间的后面刚好有一段空闲内存,并且与原空间加起来刚好可以放下新空间,那么就会直接在原空间上进行“增长”,这样就节省了内存
  65. //因此这里使用realloc并不一定就能改善空间状况,只是在malloc分配失败的情况下的一种尝试解决的办法,并不一定成功
  66. unsigned b, b2;
  67.  
  68.  
  69. new_table = mm_realloc(head->hth_table,
  70. new_len*sizeof(struct event_map_entry*)); //用realloc进行重新分配,分配后直接将原哈希表上的主节点复制到新哈希表上,冲突链表还是原来的不变
  71.  
  72. if (!new_table) //如果realloc还是分配失败,就退出
  73. return -1;
  74. //执行到这里说明realloc分配成功,新分配的哈希表与原来的哈希表在原有位置上的延伸,延伸部分还需要初始化
  75. memset(new_table + head->hth_table_length, 0,
  76. (new_len - head->hth_table_length)*sizeof(struct event_map_entry*) //初始化延伸部分的空间
  77. );
  78.  
  79. for (b=0; b < head->hth_table_length; ++b) //遍历哈希表上的主节点
  80. {
  81. struct event_map_entry *e, **pE;
  82.  
  83. for (pE = &new_table[b], e = *pE; e != NULL; e = *pE) //从主节点开始遍历冲突链表
  84. {
  85. //计算重新哈希后在新哈希表中的位置
  86. #ifdef HT_CACHE_HASH_VALUES
  87. b2 = (e)->map_node.hte_hash % new_len;
  88. #else
  89. b2 = (hashsocket(*elm)) % new_len;
  90. #endif
  91. //对于冲突链A->B->C.
  92. //pE是二级指针,存放的是A元素的hte_next指针的地址值
  93. //e指向B元素。
  94.  
  95. //如果重新哈希取模后还是原来的位置
  96. if (b2 == b)
  97. {
  98. //此时,无需修改。接着处理冲突链中的下一个元素即可
  99. //pE向前移动,存放B元素的hte_next指针的地址值
  100. pE = &e->map_node.hte_next; //就不用更改当前结点,直接到冲突链下一个结点
  101. }
  102. else//重新哈希取模后需要改变位置
  103. {
  104. *pE = e->map_node.hte_next; //直接把当前结点的下一个结点放到当前结点的位置上,相当于从冲突链表中删除当前结点
  105.  
  106. e->map_node.hte_next = new_table[b2]; //再将当前结点以头插形式放到新的位置上去
  107. new_table[b2] = e;
  108. }
  109.  
  110. //这种再次哈希的方式,很有可能会对某些元素操作两次。
  111. //当某个元素第一次在else中处理,那么它就会被哈希到正确的节点
  112. //的冲突链上。随着外循环的进行,处理到正确的节点时。在遍历该节点
  113. //的冲突链时,又会再次处理该元素。此时,就会在if中处理。而不会
  114. //进入到else中。
  115. }
  116. }
  117.  
  118. head->hth_table = new_table; //更新哈希表
  119. }
  120. //更新event_io_map中的相关信息
  121. head->hth_table_length = new_len;
  122. head->hth_prime_idx = prime_idx;
  123. head->hth_load_limit = new_load_limit;
  124.  
  125. return 0;
  126. }

哈希表元素插入

向哈希表中插入一个元素,就是先求得插入元素elm在哈希表中的位置,不管这个位置是否有元素,都直接以头插法的形式将新元素插在最前面。之所以采用头插法,是按照“最近新插入的元素可能在未来会被经常使用”的原则,插在链表头部这样即使发生了冲突,访问起来效率也更高。

  1. static inline void
  2. event_io_map_HT_INSERT(struct event_io_map *head,
  3. struct event_map_entry *elm) //哈希表插入一个元素
  4. {
  5. struct event_map_entry **p;
  6. if (!head->hth_table || head->hth_n_entries >= head->hth_load_limit)
  7. event_io_map_HT_GROW(head, head->hth_n_entries+1); //如果哈希表未分配,或者哈希表超到或者达到扩容阈值(表明不能再插入新元素),就需要扩容
  8.  
  9. ++head->hth_n_entries; //哈希表元素加1
  10.  
  11. #ifdef HT_CACHE_HASH_VALUES
  12. do
  13. { //计算哈希值并进行缓存,后面获取哈希值时就不需要再调用哈希函数了
  14. (elm)->map_node.hte_hash = hashsocket(elm);
  15. } while (0);
  16.  
  17. p = &((head)->hth_table[((elm)->map_node.hte_hash)
  18. % head->hth_table_length]); //找到哈希表上该哈希值取模后的位置
  19. #else
  20. p = &((head)->hth_table[(hashsocket(*elm))%head->hth_table_length]);
  21. #endif
  22.  
  23.  
  24. //使用头插法,即后面才插入的链表,反而会在冲突链表头。
  25. elm->map_node.hte_next = *p;
  26. *p = elm;
  27. }

哈希表元素替换

哈希表元素替换,实际上是替换元素的地址,先传入一个元素elm,然后寻找哈希表中与该元素fd相同的元素,然后将该元素的地址替换为新元素的地址,并将该元素的冲突链接在新元素的后面,从而完成替换。

  1. static inline struct event_map_entry *
  2. event_io_map_HT_REPLACE(struct event_io_map *head,
  3. struct event_map_entry *elm) //替换哈希表中的结点元素
  4. {
  5. struct event_map_entry **p, *r;
  6.  
  7. if (!head->hth_table || head->hth_n_entries >= head->hth_load_limit) //如果哈希表未分配或者元素个数达到扩容阈值,那么就进行扩容
  8. event_io_map_HT_GROW(head, head->hth_n_entries+1);
  9.  
  10. #ifdef HT_CACHE_HASH_VALUES
  11. do
  12. {
  13. (elm)->map_node.hte_hash = hashsocket(elm); //缓存哈希值
  14. } while(0);
  15. #endif
  16.  
  17. p = _event_io_map_HT_FIND_P(head, elm); //p为哈希表中fd与elm相同的结点元素的地址
  18.  
  19. r = *p; //保存原结点元素地址
  20. *p = elm; //给结点元素赋新值,这里是让哈希表上原来元素的地址换成了新元素的地址,完成了替换
  21.  
  22. if (r && (r!=elm)) //如果原结点元素与新元素不同
  23. {
  24. elm->map_node.hte_next = r->map_node.hte_next; //把原结点元素后面的冲突链接上去
  25.  
  26. r->map_node.hte_next = NULL;
  27. return r; //返回被替换掉的元素
  28. }
  29. else //到这里说明哈希表在新元素对应的地方没有元素,或者原结点元素与新结点元素就是同一个
  30. {
  31. ++head->hth_n_entries; //为什么元素个数加1? 不明白
  32. return NULL; //返回NULL,表示哈希表中对应位置没有元素,无法完成替换,或者新老元素本身就相同,无需替换
  33. }
  34. }

哈希表元素删除

哈希表元素删除就是找到哈希表中某一个元素fd与传入参数elm的fd相同的元素并删除该元素。不过需要注意的是,这里并没有对被删除的元素进行释放,因此调用该函数后应当对返回值指针进行释放。

  1. static inline struct event_map_entry *
  2. event_io_map_HT_REMOVE(struct event_io_map *head,
  3. struct event_map_entry *elm) //哈希表元素删除
  4. {
  5. struct event_map_entry **p, *r;
  6.  
  7. #ifdef HT_CACHE_HASH_VALUES
  8. do
  9. {
  10. (elm)->map_node.hte_hash = hashsocket(elm);
  11. } while (0);
  12. #endif
  13.  
  14. p = _event_io_map_HT_FIND_P(head,elm); //p是找到的哈希表上元素的地址
  15.  
  16. if (!p || !*p)//没有找到 //p为NULL说明哈希表未分配,*p为NULL说明没有找到elm
  17. return NULL;
  18.  
  19. r = *p; //r指向要被删除的元素
  20. *p = r->map_node.hte_next; //把该元素的地址换为它在冲突链上下一个元素的地址 实现删除
  21. r->map_node.hte_next = NULL;
  22.  
  23. --head->hth_n_entries; //元素数目减1
  24.  
  25. return r;
  26. }

自定义条件删除元素

如前面的删除元素函数,是删除fd相同的那个元素,删除的依据就是fd相同。这里还提供了一种自己定义删除依据的删除方式:先自定义一个函数fn,在遍历哈希表元素的时候,将各个元素与传入的elm作为参数调用fn,如果fn返回true,那么就将哈希表中该元素删除,如下所示:(可以看到,在该函数中对于删除的元素并没有进行释放,并且也没有返回被删除元素的地址,这样也无法从外部对删除的元素进行释放)

  1. static inline void
  2. event_io_map_HT_FOREACH_FN(struct event_io_map *head,
  3. int (*fn)(struct event_map_entry *, void *),
  4. void *data) //遍历哈希表上所有元素,以各个元素去调用fn,如果fn返回true则删除该元素
  5. {
  6. unsigned idx;
  7. struct event_map_entry **p, **nextp, *next;
  8.  
  9. if (!head->hth_table)
  10. return;
  11.  
  12. for (idx=0; idx < head->hth_table_length; ++idx) //遍历哈希表主链
  13. {
  14. p = &head->hth_table[idx]; //取哈希表主链上元素的地址
  15.  
  16. while (*p) //如果在该处有元素
  17. {
  18. nextp = &(*p)->map_node.hte_next; //nextp保存哈希表上该元素在冲突链上下一个元素的地址
  19. next = *nextp; //next保存该元素在冲突链上的下一个元素
  20.  
  21. //对B元素进行检查
  22. if (fn(*p, data))
  23. {
  24. --head->hth_n_entries;
  25.  
  26. *p = next; //直接将该元素的地址替换为下一个元素的地址,相当于删除该元素
  27. }
  28. else
  29. {
  30. p = nextp; //冲突链上下一个元素地址
  31. }
  32. }
  33. }
  34. }

哈希表第一个非空元素

哈希表第一个非空元素一定是在主链上,因此直接遍历哈希表的主链,不用去管冲突链,找到主链上第一个非空的元素返回其地址即可。由于主链上的元素都是event_map_entry *类型,因此这里返回的地址是event_map_entry **类型。

  1. static inline struct event_map_entry **
  2. event_io_map_HT_START(struct event_io_map *head) //返回哈希表主链上第一个非空元素的地址
  3. {
  4. unsigned b = 0;
  5.  
  6. while (b < head->hth_table_length)
  7. {
  8. if (head->hth_table[b])
  9. return &head->hth_table[b];
  10.  
  11. ++b;
  12. }
  13.  
  14. return NULL;
  15. }

哈希表下一个元素

这里提供了两个取下一个元素地址的函数event_io_map_HT_NEXT和event_io_map_HT_NEXT_RMV。前者是直接查找传入的elm所在冲突链上的下一个元素并返回其地址的指针,如果该元素是冲突链上最后一个元素,那么就从主链上找到下一个非空的元素,返回其地址的指针;后者则是返回下一个元素地址的指针,并且将elm从哈希表中删除。

  1. static inline struct event_map_entry **
  2. event_io_map_HT_NEXT(struct event_io_map *head,
  3. struct event_map_entry **elm) //返回elm在哈希表中下一个元素的地址的指针,先从elm所在冲突链中找到下一个元素,如果没有就从主链上下一个位置开始找
  4. {
  5. if ((*elm)->map_node.hte_next) //返回elm的下一个结点的地址
  6. {
  7. return &(*elm)->map_node.hte_next;
  8. }
  9. else //如果elm是其所在冲突链的最后一个结点,那么就找到下一条冲突链的第一个结点
  10. {
  11. #ifdef HT_CACHE_HASH_VALUES
  12. unsigned b = (((*elm)->map_node.hte_hash)
  13. % head->hth_table_length) + 1; //从elm所在冲突链在主链上位置的后一个开始
  14. #else
  15. unsigned b = ( (hashsocket(*elm)) % head->hth_table_length) + 1;
  16. #endif
  17.  
  18. while (b < head->hth_table_length)
  19. {
  20. //找到了第一个非空的结点,就返回该结点地址
  21. if (head->hth_table[b])
  22. return &head->hth_table[b];
  23. ++b;
  24. }
  25.  
  26. return NULL; //返回NULL说明elm所在冲突链包括主链后面都没有非空结点了
  27. }
  28. }
  1. static inline struct event_map_entry **
  2. event_io_map_HT_NEXT_RMV(struct event_io_map *head,
  3. struct event_map_entry **elm) //返回哈希表上elm的下一个元素的地址,如果是在elm的冲突链上,那么就把elm删除
  4. {
  5. #ifdef HT_CACHE_HASH_VALUES
  6. unsigned h = ((*elm)->map_node.hte_hash);
  7. #else
  8. unsigned h = (hashsocket(*elm));
  9. #endif
  10. //h中保存elm对应的哈希值
  11.  
  12. *elm = (*elm)->map_node.hte_next; //elm冲突链上下一个元素的地址,这里也相当于直接用elm下一个元素的地址替换elm的地址,删除了哈希表上elm的地址
  13.  
  14. --head->hth_n_entries; //哈希表元素个数减1
  15.  
  16. if (*elm) //返回elm所在冲突链上下一个元素的地址
  17. {
  18. return elm;
  19. }
  20. else //如果elm在其冲突链上是最后一个元素,那么就从后面的主链上找第一个非空的元素
  21. {
  22. unsigned b = (h % head->hth_table_length)+1;
  23.  
  24. while (b < head->hth_table_length)
  25. {
  26. if (head->hth_table[b])
  27. return &head->hth_table[b];
  28.  
  29. ++b;
  30. }
  31.  
  32. return NULL;
  33. }
  34. }

释放哈希表

  1. void event_io_map_HT_CLEAR(struct event_io_map *head)
  2. {
  3. if (head->hth_table) //释放哈希表
  4. mm_free(head->hth_table);
  5.  
  6. head->hth_table_length = 0;
  7.  
  8. event_io_map_HT_INIT(head); //重新初始化event_io_map
  9. }

向event_io_map中添加event

如前所述,当对一个io event调用event_add时,Libevent就会把这个io event添加到event_io_map中,添加到event_io_map所调用的函数是evmap_io_add,其定义如下所示:

  1. int
  2. evmap_io_add(struct event_base *base, evutil_socket_t fd, struct event *ev)
  3. {
  4. const struct eventop *evsel = base->evsel; //得到event所绑定的base对应的后端方法
  5. struct event_io_map *io = &base->io; //得到base对应的iomap
  6. struct evmap_io *ctx = NULL; //指向fd所对应的那个evmap_io,evmap_io中又含有一个event双向链表
  7. int nread, nwrite, retval = 0;
  8. short res = 0, old = 0; //old用于记录下fd所对应的事件链表中在ev未插入前的所有事件对应了哪几种类型(write/read)
  9. struct event *old_ev;
  10.  
  11. EVUTIL_ASSERT(fd == ev->ev_fd); //确保传入的fd等于event的fd
  12.  
  13. if (fd < 0)
  14. return 0;
  15.  
  16. #ifndef EVMAP_USE_HT //如果不使用哈希,那么用的就是signal_map
  17. if (fd >= io->nentries) { //如果fd大于当前哈希表中的元素数目就重新分配
  18. if (evmap_make_space(io, fd, sizeof(struct evmap_io *)) == -1)
  19. return (-1);
  20. }
  21. #endif
  22. GET_IO_SLOT_AND_CTOR(ctx, io, fd, evmap_io, evmap_io_init,
  23. evsel->fdinfo_len);
  24. //ctx就成为了fd对应的那个evmap_io的指针
  25. nread = ctx->nread; //记录下fd所有event中读事件实木
  26. nwrite = ctx->nwrite;//记录下fd所有event中写事件数目
  27.  
  28. if (nread) //如果fd对应的事件链表中有读类型事件
  29. old |= EV_READ; //表明在ev插入之前fd对应的event链表中就已经存在读类型事件了
  30. if (nwrite) //如果fd对应的事件链表中有写类型事件
  31. old |= EV_WRITE; //表明在ev插入之前fd对应的event链表中就已经存在写类型事件了
  32.  
  33. if (ev->ev_events & EV_READ) { //如果添加的ev为读类型
  34. if (++nread == 1) //读事件数目加1
  35. res |= EV_READ;
  36. }
  37. if (ev->ev_events & EV_WRITE) { //如果添加的ev为写类型
  38. if (++nwrite == 1) //写事件数目加1
  39. res |= EV_WRITE;
  40. }
  41. if (EVUTIL_UNLIKELY(nread > 0xffff || nwrite > 0xffff)) {
  42. event_warnx("Too many events reading or writing on fd %d",
  43. (int)fd);
  44. return -1;
  45. }
  46. //此时还没有将ev插入到fd下的那个链表中,遍历整个链表,需要保证fd下的链表中所有event要么都是ET要么都不是ET,不能混合使用
  47. if (EVENT_DEBUG_MODE_IS_ON() &&
  48. (old_ev = TAILQ_FIRST(&ctx->events)) &&
  49. (old_ev->ev_events&EV_ET) != (ev->ev_events&EV_ET)) {
  50. event_warnx("Tried to mix edge-triggered and non-edge-triggered"
  51. " events on fd %d", (int)fd);
  52. return -1;
  53. }
  54.  
  55. if (res) {//如果有读或写事件,就将event添加到base中
  56. void *extra = ((char*)ctx) + sizeof(struct evmap_io);
  57. /* XXX(niels): we cannot mix edge-triggered and
  58. * level-triggered, we should probably assert on
  59. * this. */
  60. if (evsel->add(base, ev->ev_fd,
  61. old, (ev->ev_events & EV_ET) | res, extra) == -1) //调用对应后端的add函数进行添加
  62. return (-1);
  63. retval = 1;
  64. }
  65. //记录fd下的读写event数量
  66. ctx->nread = (ev_uint16_t) nread;
  67. ctx->nwrite = (ev_uint16_t) nwrite;
  68. TAILQ_INSERT_TAIL(&ctx->events, ev, ev_io_next); //将event插入到fd对应的事件链表中
  69.  
  70. return (retval);
  71. }

这里如果不是使用哈希结构的话,event_io_map实际上就相当于一个数组,数组的元素索引就对应了一个fd值,因此当fd大于或等于该数组的元素个数,说明此时该数组就需要扩容了,就调用evmap_make_space函数进行扩容,可以参考非windows下的event_io_map实现

需要注意的是,这里的evmap_add函数只是将event添加到Libevent自定义的数据结构evmap_io中,而真正添加到内核事件监听集合中的操作实际上是通过event_base所绑定的后端方法的add函数来实现的。可以参考libevent对epoll的封装。

激活event_io_map中的event

激活event_io_map中的event使用的是evmap_io_active函数,不过这并不是一个对用户开放的接口。实现原理就是将fd对应的双向链表中发生了感兴趣事件的event通过event_active_nolock函数添加到激活队列中。其定义如下:

  1. void
  2. evmap_io_active(struct event_base *base, evutil_socket_t fd, short events)//将event_io_map中fd对应的event双向链表中关注事件类型与events有共同部分的事件添加到激活队列中
  3. {
  4. struct event_io_map *io = &base->io;
  5. struct evmap_io *ctx;
  6. struct event *ev;
  7.  
  8. #ifndef EVMAP_USE_HT
  9. EVUTIL_ASSERT(fd < io->nentries);
  10. #endif
  11. GET_IO_SLOT(ctx, io, fd, evmap_io); //找到fd对应的event_io_map中的event双向链表
  12.  
  13. EVUTIL_ASSERT(ctx);
  14. TAILQ_FOREACH(ev, &ctx->events, ev_io_next) { //遍历该链表
  15. if (ev->ev_events & events) //如果event的关注事件类型与发生的事件类型有共同部分
  16. event_active_nolock(ev, ev->ev_events & events, 1); //插入到激活队列中
  17. }
  18. }

event_active_nolock函数可以参考事件的激活

删除event_io_map中的event

evmap_io_del函数实际上就是evmap_io_add的逆操作,即是将event从event_io_map中删除,定义如下所示:

  1. int
  2. evmap_io_del(struct event_base *base, evutil_socket_t fd, struct event *ev)
  3. {
  4. const struct eventop *evsel = base->evsel;
  5. struct event_io_map *io = &base->io;
  6. struct evmap_io *ctx;
  7. int nread, nwrite, retval = 0;
  8. short res = 0, old = 0;
  9.  
  10. if (fd < 0)
  11. return 0;
  12.  
  13. EVUTIL_ASSERT(fd == ev->ev_fd);
  14.  
  15. #ifndef EVMAP_USE_HT
  16. if (fd >= io->nentries)
  17. return (-1);
  18. #endif
  19.  
  20. GET_IO_SLOT(ctx, io, fd, evmap_io); //ctx指向fd对应的那个evmap_io
  21.  
  22. nread = ctx->nread; //evmap_io中读事件数目
  23. nwrite = ctx->nwrite;//evmap_io中写事件数目
  24. //old保存删除前evmap_io中所有event的事件类型有哪些种类(读或写)
  25. if (nread)
  26. old |= EV_READ;
  27. if (nwrite)
  28. old |= EV_WRITE;
  29.  
  30. if (ev->ev_events & EV_READ) {//如果需要被删除的事件对读事件感兴趣
  31. if (--nread == 0) //读事件数目减1
  32. res |= EV_READ;
  33. EVUTIL_ASSERT(nread >= 0);
  34. }
  35. if (ev->ev_events & EV_WRITE) {//如果需要被删除的时间对写事件感兴趣
  36. if (--nwrite == 0) //写事件数目减1
  37. res |= EV_WRITE;
  38. EVUTIL_ASSERT(nwrite >= 0);
  39. }
  40.  
  41. if (res) {
  42. void *extra = ((char*)ctx) + sizeof(struct evmap_io);
  43. if (evsel->del(base, ev->ev_fd, old, res, extra) == -1) //调用后端的del函数
  44. return (-1);
  45. retval = 1;
  46. }
  47.  
  48. ctx->nread = nread;
  49. ctx->nwrite = nwrite;
  50. TAILQ_REMOVE(&ctx->events, ev, ev_io_next); //从evmap_io的双向链表中删除event
  51.  
  52. return (retval);
  53. }

对于evmap_io_event函数本身来说,只是将这个event从Libevent自定义数据结构event_io_map中删除,而实际上将该事件从内核监听事件集合中删除的函数还是通过event_base所绑定的后端方法中的del函数实现的。可以参考libevent对epoll的封装

转载自:https://blog.csdn.net/qq_28114615/article/details/95887231

libevent源码学习(7):event_io_map的更多相关文章

  1. libevent源码学习

    怎么快速学习开源库比如libevent? libevent分析 - sparkliang的专栏 - 博客频道 - CSDN.NET Libevent源码分析 - luotuo44的专栏 - 博客频道 ...

  2. libevent源码学习(8):event_signal_map解析

    目录event_signal_map结构体向event_signal_map中添加event激活event_signal_map中的event删除event_signal_map中的event以下源码 ...

  3. libevent源码学习(9):事件event

    目录在event之前需要知道的event_baseevent结构体创建/注册一个event向event_base中添加一个event设置event的优先级激活一个event删除一个event获取指定e ...

  4. libevent源码学习(11):超时管理之min_heap

    目录min_heap的定义向min_heap中添加eventmin_heap中event的激活以下源码均基于libevent-2.0.21-stable.       在前文中,分析了小顶堆min_h ...

  5. libevent源码学习(10):min_heap数据结构解析

    min_heap类型定义min_heap函数构造/析构函数及初始化判断event是否在堆顶判断两个event之间超时结构体的大小关系判断堆是否为空及堆大小返回堆顶event分配堆空间堆元素的上浮堆元素 ...

  6. libevent源码学习(6):事件处理基础——event_base的创建

    目录前言创建默认的event_baseevent_base的配置event_config结构体创建自定义event_base--event_base_new_with_config禁用(避免使用)某一 ...

  7. libevent源码学习(2):内存管理

    目录 内存管理函数 函数声明 event-config.h 函数定义 event_mm_malloc_ event_mm_calloc_ event_mm_strdup_ event_mm_reall ...

  8. libevent源码学习(1):日志及错误处理

    目录 错误处理函数 函数声明 __attribute__指令 函数定义 可变参数宏 _warn_helper函数 日志处理 event_log日志处理入口 日志处理回调函数指针log_fn 设置日志处 ...

  9. libevent源码学习之event

    timer event libevent添加一个间隔1s持续触发的定时器如下: struct event_base *base = event_base_new(); struct event *ti ...

随机推荐

  1. THUSC2021 游记

    Day -6 - 2459343 请了一天假在家卷 whk,u1s1 星期六为啥要去上学呢(bushi 中午 12:00 左右得知自己有去参加 THUSC 的资格 然后就是一堆待填写的资料和报名表 发 ...

  2. LOJ #2185 / 洛谷 P3329 - [SDOI2015]约数个数和(莫比乌斯函数)

    LOJ 题面传送门 / 洛谷题面传送门 题意: 求 \(\sum\limits_{i=1}^n\sum\limits_{j=1}^md(ij)\),\(d(x)\) 为 \(x\) 的约数个数. \( ...

  3. FVCOM泥沙模块河流边界处理

    简介 入流河流携带泥沙可以按照节点和边界两种形式给定,这两种方法都是在相关的节点上进行直接赋值,并不能保证进入计算域内泥沙总体积. 相关设置 XX_run.nml 河流参数设置 &NML_RI ...

  4. R数据科学-1

    R数据科学(R for Data Science) Part 1:探索 by: PJX for 查漏补缺 exercise: https://jrnold.github.io/r4ds-exercis ...

  5. Synteny和collinear的区别

    在比较基因组学的时候,经常会听到"共线性"这个词,但是与其对应的有两个不同的概念,即 (1) synteny (2) collinear 二者的区别如下图所示: 可以看到,synt ...

  6. dlang 泛型

    1 import std.stdio, std.string; 2 3 void main() 4 { 5 bool find(T)(T[] all, T sub) 6 { 7 foreach(eac ...

  7. 准确率,召回率,F值,ROC,AUC

    度量表 1.准确率 (presion) p=TPTP+FP 理解为你预测对的正例数占你预测正例总量的比率,假设实际有90个正例,10个负例,你预测80(75+,5-)个正例,20(15+,5-)个负例 ...

  8. Leetcode中的SQL题目练习(二)

    175. Combine Two Tables https://leetcode.com/problems/combine-two-tables/description/ Description Pe ...

  9. 大数据学习day39----数据仓库02------1. log4j 2. 父子maven工程(子spring项目的创建)3.项目开发(埋点日志预处理-json数据解析、清洗过滤、数据集成实现、uid回补)

    1. log4j(具体见log4j文档) log4j是一个java系统中用于输出日志信息的工具.log4j可以将日志定义成多种级别:ERROR  /  WARN  /  INFO  /  DEBUG ...

  10. ALitum技巧

    创建异型焊盘的方法 SCH与PCB同步修改后元器件乱跑的解决方法 Altium 在PCB重新编号更新到SCH原理图的方法 同步问题 其他技巧: 当前层亮色,其他层灰色切换:SHIFT+S