OpenSSL学习笔记——哈希表

来源:互联网 发布:radium mac 编辑:程序博客网 时间:2024/06/10 22:34

环境: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,和各个数据节点所占的内存,并不销毁数据所占的内存。如果想销毁哈希表的时候,同时也将数据所占内存销毁,那么就得自己写一个销毁数据的函数voidfunc_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。

原创粉丝点击