您的位置:首页 > 运维架构

OpenSSL学习笔记——哈希表

2010-04-29 15:41 190 查看
环境:OpenSSL 0.9.8l,Fedora 12

  今天学的是《OpenSSL编程》第四章 哈希表。这一章主要讲了OpenSSL中哈希表的用法,和堆栈一样,OpenSSL实现了一个适用于任何数据类型的哈希表。

  下面是哈希表中两个重要的数据结构,这些数据结构的定义在/crypto/lhash/lhash.h中:

  这个结构体是一个链表,链表的每一个节点都是一个用户存入的数据:

  typedef struct lhash_node_st

  {

   void *data;


//用于保存指向数据的指针


   struct lhash_node_st *next;


//形成一个链表


#ifndef OPENSSL_NO_HASH_COMP

   unsigned long hash;


//数据的哈希值


#endif

  } LHASH_NODE;


  这个结构体表示一个哈希表:

  typedef struct lhash_st

  {

   LHASH_NODE **b;  


//这是一个LHASH_NODE的指针数组,用于保存数据


   LHASH_COMP_FN_TYPE comp;  


//哈希表的比较函数,OpenSSL提供默认的比较函数


   LHASH_HASH_FN_TYPE hash;


//哈希表的哈希函数,OpenSSL提供默认的哈希函数


   unsigned int num_nodes;



   unsigned int num_alloc_nodes;


//b的长度


   unsigned int p;

   unsigned int pmax;

   unsigned long up_load;


/* load times 256 */   //装填因子的上限


   unsigned long down_load;


/* load times 256 */  //装填因子的下限


   unsigned long num_items;   


 //表中所存的数据的个数

/*下面的成员都是给各个操作计数用的,没什么好说的*/


   unsigned long num_expands;

   unsigned long num_expand_reallocs;

   unsigned long num_contracts;

   unsigned long num_contract_reallocs;

   unsigned long num_hash_calls;

   unsigned long num_comp_calls;

   unsigned long num_insert;

   unsigned long num_replace;

   unsigned long num_delete;

   unsigned long num_no_delete;

   unsigned long num_retrieve;

   unsigned long num_retrieve_miss;

   unsigned long num_hash_comps;

   int error;

  } LHASH;


 
 上面的结构体就是哈希表的结构体,要想理解OpenSSL中哈希表的实现,这个结构体中第一部分的成员每个的意义都必须理解才行。而这一部分的成员有一
些可以通过字面意思就知道它们做什么用的,但是还有3个是一两句注释说不清楚的,它们分别是:num_nodes,p,pmax。至于这3个成员分别什么
意思,一会儿下面再说,我们现在先看一下OpenSSL的哈希表是如何解决冲突的。

  从上面两个结构体,我们可以大概看
出,OpenSSL的哈希表是把数据data的地址保存在结构LHASH_NODE中,然后用哈希函数计算出数据data对应的哈希值,再通过得到的哈希
值计算出保存数据地址的结构体在数组b中的下标,最后把结构体的地址保存到数组b相应的下标处。

  不过,在哈希表中经常存在冲突:

    1. 因为哈希函数的问题导致两个不同数据的哈希值相同

    2. 因为两个不同数据的不同的哈希值,在计算在数组b中的下标时,得到的是相同的下标

  以上这两个原因都导致两个不同的数据在数组b中发生冲突,那么如何解决冲突就成为一个不可避免的问题。

  通常,哈希表中解决冲突的方法有下面几种:

    1. 开放定址法

    2. 再散列法

    3. 链地址法

    4. 建立一个公共溢出区

  在OpenSSL中,解决冲突的方法是链地址法。也就是说,当有多个数据因为上面所说的两个任一个原因,要存在数组b的同一个下标下面,这时,OpenSSL会建立一个链表,把这些数据存入同一个链表中,并把这个链表的头指针存入数组b的那个下标处。

 
 举个例子,比如现在有一个数据data1,哈希值为hash1,要把这个数据存入哈希表中,指向哈希表的指针为lh,假设,此时data1不会发生冲
突,并且通过计算得到data1所对应的下标是5,也就是说OpenSSL会通过下面的语句把data1存入哈希表中(为了代码清爽,下面代码只说明了意
思,OpenSSL源代码不是这个样子的):

  LHASH_NODE *new_node = (LHASH_NODE *)malloc(sizeof(LHASH_NODE));

new_node->data = &data;

  new_node->next = NULL;

  new_node->hash = hash1;

  lh->b[5] = new_node;

这样就可以把一个数据插入表lh中了。这时,数组b下标为5的元素处,应该是这个样子的:

  b[5]---------->LHASH_NODE(data1, hash1)---------->NULL


如,现在又来了一个数据data2,哈希值是hash2,但根据计算,data2也要存入数组b中下标为5的地方,这就会发生冲突,前面说
了,OpenSSL是通过链地址法来解决冲突的。这时,OpenSSL会通过下面的语句把data2链到data1的后面去(为了代码清爽,下面代码只说
明了意思,OpenSSL源代码不是这个样子的):

  LHASH_NODE *new_node = (LHASH_NODE *)malloc(sizeof(LHASH_NODE));

  new_node->data = &data2;

  new_node->next = NULL;

  new_node->hash = hash2;

  node1->next = new_node; //假设node1是指向数据为data1的结构的指针

这时,数组b下标为5的元素处,应该是这个样子的:

  b[5]---------->LHASH_NODE(data1, hash1)---------->LHASH_NODE(data2, hash2)---------->NULL

假如,现在又来了一个数据data3,哈希值为hash1(和data1的哈希值一样),显然,这个数据也应该存入数组b中下标为5的地方。此时,OpenSSL会通过上面的代码把它插入到哈希表中,插入完成后,数组b下标为5的元素处应该是这个样子的:

 
 b[5]---------->LHASH_NODE(data1,
hash1)---------->LHASH_NODE(data2,
hash2)---------->LHASH_NODE(data3, hash1)---------->NULL

从上面可以看出,OpenSSL是怎样通过链地址法来解决冲突的,也可以看出,在同一个链表中的数据,一部分是因为它们的哈希值一样,另一部分是因为它们的哈希值不一样,而在数组中所处的位置一样造成的。

  上面说明了OpenSSL的哈希表的解决冲突方面是如何做的。接下来,我们看一下,OpenSSL的哈希表是如何从一个数据的哈希值得到该数据在数组b中的下标的。

 
 可以想像,当哈希表已经很满的时候(目前不考虑哈希函数比较烂的情况),发生冲突是很经常的,这时,哈希表中会存在很多个链表,而要访问这些链表中的某
个数据时,需要把整个链表遍历一遍,时间复杂度会上升,这使得哈希表的优势不再明显。此时,要不然禁止再往哈希表中再插入数据,或者将哈希表的数组b增长
一些,并把那些链表的元素往数组b新增的部分均一些,这样就会使情况好一点。OpenSSL使用的就是后一种方法,当哈希表比较满时,就把数组b增长一
倍,并作一些相应的操作。

  至于,OpenSSL怎么知道什么时候表比较满,什么时候表比较不满,我们先来看一个概念,哈希表的装填因子(load factor),这个值用来表示哈希表的填满程度,这个值在OpenSSL中的计算方法如下:

  装填因子(a) = num_items / num_nodes

在OpenSSL中,给哈希表插入数据和删除数据时,会用装填因子判断是否应该扩大或者缩小表:

  插入函数中:

  if(lh->up_load <= lh->num_items * 256 / lh->num_nodes)

   expand(lh); 
 //如果地方不够,就把地方弄大一点

  删除函数中:

  if(lh->down_load >= lh->num_items * 256 / lh->num_nodes)

   contract(lh); 
 //如果地方剩得太多,就把地方弄小一点

其中,up_load = 2 * 256,down_load = 256。

  在OpenSSL中,数组b的初始化大小为MIN_NODES,这是一个宏定义,值为16,在初始化函数中,把几个有用的成员都设为下面的值:

  num_nodes = MIN_NODES / 2;

num_alloc_nodes = MIN_NODES;

p = 0;

pmax = MIN_NODES / 2;

从上面可以看出,数组b的总长度用num_alloc_nodes表示,其实从代码中也可以看出pmax的值始终等于num_alloc_nodes / 2。下面,要说明的是成员p,pmax,num_nodes是什么意思。

  在OpenSSL实现的哈希表中,虽然数组b的长度是num_alloc_nodes,但数组b中,数据并不是保存在0 ~ num_alloc_nodes - 1之间的,而是保存在0 ~ num_nodes - 1之间的。

  现在,我们先看一下上面提到的函数expand是怎么实现的:

  static void expand(LHASH *lh)
//lh为要增长的哈希表

  {
//刚才说,当存储地方变大的时候,得把一些链表里的数据匀到那些新多出来的地方去,

//函数expand执行一次,就会把数组下标为p的那一行链表中一部分数据匀到下标为p + pmax的那一行去,

   //然后给p加1,因为p是从0开始的,所以p + pmax是从pmax开始的,

   //当第一次执行完此函数时,p值为1,num_nodes值为9,pmax值为8,也就是说,函数expand执行完后,

   //会把数组b中,下标为0的那一行中的一部分数据,移动到下标为8的那一行中去,

   //这些被移动的元素都是哈希值对pmax取余为p - 1,对num_alloc_nodes取余为pmax + p - 1的数据,

   //这是因为num_alloc_nodes = 2 * pmax,所以当装填因子还没有到上限时,expand函数不会执行,

   //而此时的p值为0,所有数据的下标都是通过nn = hash % pmax得到的,这就会导致当hash值为p, pmax +p,

//
2pmax + p,
3pmax + p, 4pmax + p, ....时,都会落到下标为0的那一行,而当装填因子达到上限时,

   //
需要多增大一些空间,
这时,会把第p行的数据匀一部分到第p + pmax行去,

   //
这样,就会把hash值为pmax + p, 3pmax + p, ....这些数据
匀到过去,

   //
这就是为什么下面在判断哪个数据该移动时,是用hash % num_alloc_nodes来计算的,

//而不是用hash % pmax来计算的。

   LHASH_NODE **n,**n1,**n2,*np;

   unsigned int p,i,j;

   unsigned long hash,nni;

   lh->num_nodes++;

   lh->num_expands++;

   p=(int)lh->p++;

   n1= &(lh->b[p]);

   n2= &(lh->b[p+(int)lh->pmax]);
//一会儿要把n1所指向的链表的一部分数据匀到n2所指的那一行去

   *n2=NULL;
/* 27/07/92 - eay - undefined pointer bug */

   nni=lh->num_alloc_nodes;

   for (np= *n1; np != NULL; )

   {

#ifndef OPENSSL_NO_HASH_COMP

    hash=np->hash;

#else

    hash=lh->hash(np->data);

    lh->num_hash_calls++;

#endif

    if ((hash%nni) != p)

    {
/* move it */ //这些数据就是应该被匀过去的数据

     *n1= (*n1)->next;

     np->next= *n2;

     *n2=np;

    }

    else

     n1= &((*n1)->next);

    np= *n1;

   }

   if ((lh->p) >= lh->pmax)

   {
//如果p和pmax相等,就说明从0 ~ pmax - 1行,每一行都被匀过了,这也就意味着表已经实在没有地方了,

    //这时就应该把数组b的长度增为原来的2倍,pmax也增为原来的2倍,p设为0

    j=(int)lh->num_alloc_nodes*2;

    n=(LHASH_NODE **)OPENSSL_realloc(lh->b, (int)(sizeof(LHASH_NODE *)*j));

    if (n == NULL)

    {

     lh->error++;

     lh->p=0;

     return;

    }

    /* else */

    for (i=(int)lh->num_alloc_nodes; i<j; i++)/* 26/02/92 eay */

     n[i]=NULL; /* 02/03/92 eay */

    lh->pmax=lh->num_alloc_nodes;

    lh->num_alloc_nodes=j;

    lh->num_expand_reallocs++;

    lh->p=0;

    lh->b=n;

   }

  }

  函数contract和函数expand干的是相反的事,在这里就不多说了。

  当有一个数据要插入表lh时,假设这个数据的哈希值为hash1,OpenSSL是通过下面的方法来确定该数据在数组中的下标的:

  int nn = hash1 % lh->pmax;

if(nn < lh->p)

nn = hash1 % lh->num_alloc_nodes;


样,nn就是该数据所对应的下标了,要注意的是,lh->num_alloc_nodes是lh->pmax的2倍,当第一次算出来的nn小
于lh->p时,就说明这个数据处于被匀过的那些行中,那么,就应该再看一下,这个数据是否可以被放到第p + pmax行去,才要让nn =
hash1 % lh->num_alloc_nodes。

  到此,OpenSSL哈希表结构的各个重要成员是什么意思就应该清楚了。

  这里还要多说明的一个函数是getrn,此函数是一个内部函数,但很多函数都调用了它:

  static LHASH_NODE **getrn(LHASH *lh, const void *data, unsigned long *rhash)

  {
//这个函数的作用确定在哈希表lh中,data保存在哪个LHASH_NODE结构中,返回值是指向那个LHASH_NODE

   //的指针的地址。假设返回值为rn,若*rn == NULL,则说明表中没有与data一样的数据,如果要插入,

   //则可直接插入,如果要删除,则无法删除(因为表中没有这一项);而若*rn != NULL,则说明表中有一项,

   //且这个项的内容与data相同,这一项的LHASH_NODE结构的地址为*rn,若要插入,则变为replace,

   //若要删除,则可直接删除。

   LHASH_NODE **ret,*n1;

   unsigned long hash,nn;

   LHASH_COMP_FN_TYPE cf;

   hash=(*(lh->hash))(data);

   lh->num_hash_calls++;

   *rhash=hash;

   nn=hash%lh->pmax;

   if (nn < lh->p)

    nn=hash%lh->num_alloc_nodes;

   cf=lh->comp;

   ret= &(lh->b[(int)nn]);

   for (n1= *ret; n1 != NULL; n1=n1->next)

   {

#ifndef OPENSSL_NO_HASH_COMP

    lh->num_hash_comps++;

    if (n1->hash != hash)

    {
//先比哈希值,如果哈希值不一样,则值肯定不一样

     ret= &(n1->next);

     continue;

    }

#endif

    lh->num_comp_calls++;

    if(cf(n1->data,data) == 0)

     break;

    ret= &(n1->next);

   }

   return(ret);

  }

  下面,我们来看关于哈希表的各个操作函数的用法:

  1. LHASH *lh_new(LHASH_HASH_FN_TYPE h, LHASH_COMP_FN_TYPE c)

   
这个函数的作用是为用户产生一个哈希表,成功则返回指向哈希表的指针,否则返回NULL。需要两个参数,这两个参数分别是两个函数指针。第一个参数代表计算哈希值的哈希函数,第二个参数代表比较函数。这两个函数类型的定义如下:

    typedef int (*LHASH_COMP_FN_TYPE)(const void *, const void *);

    typedef unsigned long (*LHASH_HASH_FN_TYPE)(const void *);

  2. void lh_free(LHASH *lh)

   

个函数的作用是销毁一个哈希表,哈希表通过参数传入。这个函数销毁的只是哈希表中的数组b,和各个数据节点所占的内存,并不销毁数据所占的内存。如果想销
毁哈希表的时候,同时也将数据所占内存销毁,那么就得自己写一个销毁数据的函数void
func_free(),在执行lh_free(lh)之前,执行一次lh_doall(lh, func_free)才行。

  3. void *lh_insert(LHASH *lh, void *data)

   
这个函数的作用是把data插入到哈希表lh中去,如果以前表中就有与data内容一样的数据,则把新的data存进去,把老的data通过返回值传出来。不过这个函数有一个Bug,在插入新值时,不论成功与否,返回值都是NULL。



  4. void *lh_delete(LHASH *lh, const void *data)

   
这个函数的作用是把哈希表中与data值一样的那一项从哈希表lh中删去,如果成功,则返回表中与data值一样的那个data,如果失败,则返回NULL,这里的失败是指,哈希表中没有data。

  5. void *lh_retrieve(LHASH *lh, const void *data)

   
这个函数的作用是在哈希表中查找是否存在与data值一样的项,若有,则返回该项在哈希表中保存的值,否则,返回NULL。

  6. void lh_doall(LHASH *lh, LHASH_DOALL_FN_TYPE func)

   这个函数的作用是用函数func遍历哈希表lh中每一项元素,函数类型LHASH_DOALL_FN_TYPE的定义如下:

    typedef void (*LHASH_DOALL_FN_TYPE)(void *);

   这里的参数将会是结构LHASH_NODE的成员data。

  7. void lh_doall_arg(LHASH *lh, LHASH_DOALL_ARG_FN_TYPE func, void *arg)

   
这个函数的作用是用函数func遍历哈希表lh中每一个元素,与上一个函数不同的是,这次的遍历函数,可以由用户传入一个参数,参数arg最后会做为参数传入函数func,函数类型LHASH_DOALL_ARG_FN_TYPE的定义如下:

    typedef void (*LHASH_DOALL_ARG_FN_TYPE)(void *, void *);

   这里的第一个参数将会是结构LHASH_NODE的成员data,第二个参数将会是arg。

  8. unsigned long lh_strhash(const char *c)

   这个函数是OpenSSL默认的哈希函数。

  9. void lh_stats(const LHASH *lh, FILE *out)

   这个函数的作用是把哈希表的状态输出到文件out中。实现在文件lh_stats.c中。

  10. void lh_node_stats(const LHASH *lh, FILE *out)

   这个函数的作用是把哈希表节点的状态输出到文件out中。实现在文件lh_stats.c中。

  11. void lh_node_usage_stats(const LHASH *lh, FILE *out)

   这个函数的作用是把哈希表使用的状态输出到文件out中。实现在文件lh_stats.c中。

  12. void lh_stats_bio(const LHASH *lh, BIO *out)

   这个函数的作用是把哈希表的状态输出到BIO out中。实现在文件lh_stats.c中。

  13. void lh_node_stats_bio(const LHASH *lh, BIO *out)

   
这个函数的作用是把哈希表节点的状态输出到BIO out中。实现在文件lh_stats.c中。

  14. void lh_node_usage_stats_bio(const LHASH *lh, BIO *out)

   这个函数的作用是把哈希表使用的状态输出到BIO out中。实现在文件lh_stats.c中。

 
 到此,OpenSSL中哈希表中如何实现的就说完了,至于哈希表是怎么用的,这里就不多说了。还有,就是使用时一些宏的用法,这里没有说,不过,在官方
文档里,这一点还是比较详细的:http://openssl.org/docs/crypto/lhash.html。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: