VPP-BIHASH实现分析

/vppinfa/bihash_template.h

本文件为本数据结构的核心头文件

概念一

key-value-pair   键-值-对。 组成HASH表的各元素,

在C语言中,一般定义成一个struct,其中一部分是key, 另一部分是value。

每1个key在hash表中只能出现1次

通过key用来计算hash值。不同的key可能计算出相同的hash值,如果表中存在2个这样的表项,则称这2个表项hash冲突。

 

概念二

hash bucket. HASH槽。主要用来组织具有相同hash值的hash表项,通过这个hash槽,可以找到此hash值的所有表项。

hash槽,也称为hash冲突链。逻辑上,可以看成具有相同hash值的表项链接在一起。

概念三

hash buckets. HASH桶,由多个HASH槽组成。一般定义成一个数组,数组下标即为对应hash槽的HASH值。

BIHASH的主要特点

1. HASH槽数目固定。HASH槽数目初始化时固定

2. HASH链由数组实现,可动态扩展,实际内存按2的幂指数扩展

3. HASH链内再分组,再HASH(找到某个分组放新hash表项),如果无法解决冲突问题,则回退成线性方式(不分组,找一个空闲的位置放表项)

4. 多线程操作时,读操作无锁

5. 结果,兼顾了容量和性能的较优的HASH结构。

6. 对key 和 value没有特别要求,通用性强。

PAGE

typedef struct BV (clib_bihash_value)
{
  union
  {
    BVT (clib_bihash_kv) kvp[BIHASH_KVP_PER_PAGE];
    u64 next_free_as_u64;
  };
} BVT (clib_bihash_value);

page本意是页的意思,在这里与操作系统的内存页没有关系,只是借用了这个概念,代表一小块内存,在这块内存中,存在BIHASH_KVP_PER_PAGE个键值对。这个结构体就是用来保存键值对数据的结构体。

在后面代码中我们会看到,hash槽中,就记录了这个结构体的地址,实际上,在本算法中,每个hash槽记录了以这个结构体定义的动态数组的首地址。

这里使用了C语言联合。在本块内存(动态数组)使用后,回收的时候,不是返回给系统,而是放入空闲链,由next_free_as_u64来组织空闲链。下一次需要时,先从空闲链中获取。

hash槽实现

typedef struct
{
  union
  {
    struct
    {
      u64 offset:BIHASH_BUCKET_OFFSET_BITS;
      u64 lock:1;
      u64 linear_search:1;
      u64 log2_pages:8;
      u64 refcnt:16;
    };
    u64 as_u64;
  };
} BVT (clib_bihash_bucket);

offseet共有36bit, 能寻址64G bytes的内存空间。这里使用offset而不是一个明确的指针,是因为BIHASH初始时直接从系统获取一块大内存,然后在此基础上再分配。这里的offset指的是相对于大内存首地址的偏移。

lock主要用于多线程对bucket的互斥访问。

log2_pages主要代表当前hash槽有几页( 1 << log2_pages) 数据。 即offset指向的内存的动态数组的大小( 如log2_pages为4时,此hash槽有16页)。

refcnt 当前hash槽中,有多少有效的键值对,假定当前有16页,每页有4个键值对,则最多可以有64个键值对,其中有效的键值对为refcnt个,其实说的就是此hash槽中有效的hash表项数目。

as_u64用于整体读写上述字段时用,在多线程编程中,方便进行原子操作。

HASH表头多CPU间共享的信息

/* *INDENT-OFF* */
typedef CLIB_PACKED (struct {
  /*
   * Backing store allocation. Since bihash manages its own
   * freelists, we simple dole out memory starting from alloc_arena[alloc_arena_next].
   */
  u64 alloc_arena_next;	/* Next offset from alloc_arena to allocate, definitely NOT a constant */
  u64 alloc_arena_size;	/* Size of the arena */
  /* Two SVM pointers stored as 8-byte integers */
  u64 alloc_lock_as_u64;
  u64 buckets_as_u64;
  /* freelist list-head arrays/vectors */
  u64 freelists_as_u64;
  u32 nbuckets;	/* Number of buckets */
  /* Set when header valid */
  volatile u32 ready;
  u64 pad[2];
}) BVT (clib_bihash_shared_header);

alloc_arena_next.  BIHASH内部分配内存时,依次从前往后按需切一块内存。此字段表示已分配内存和未分配内存的分界线,下一次分配内存从这里开始。这个字段是相对于整个大内存的偏移量。

alloc_arena_size.  整个大内存的大小,单位字节。

alloc_lock_as_u64. 多cpu环境下申请bihash内存需要先获取这个lock,此字段只是表面锁变量的偏移位置

buckets_as_u64   hash槽数组在大内存中的偏移位置

freelists_as_u64  空闲链数组在大内存中的偏移位置

nbuckets   当前hash槽的数目

ready表明这个结构体数据是否初始化好了。这个结构体存在的原因是,一个HASH表,在每个CPU上都有1个主结构,但这个share结构体中的字段所对应的信息只能有1份,即在所有CPU间共享。ready字段用来同步多cpu之间的初始化过程。主CPU把主结构初始化,从CPU才初始化自己的结构,共享的结构只能由主CPU初始化。

HASH表主结构

typedef struct
{
  BVT (clib_bihash_bucket) * buckets;
  volatile u32 *alloc_lock;

    BVT (clib_bihash_value) ** working_copies;
  int *working_copy_lengths;
    BVT (clib_bihash_bucket) saved_bucket;

  u32 nbuckets;
  u32 log2_nbuckets;
  u8 *name;

  u64 *freelists;

#if BIHASH_32_64_SVM
    BVT (clib_bihash_shared_header) * sh;
  int memfd;
#else
    BVT (clib_bihash_shared_header) sh;
#endif

  u64 alloc_arena;		/* Base of the allocation arena */

  /**
    * A custom format function to print the Key and Value of bihash_key instead of default hexdump
    */
  format_function_t *fmt_fn;

} BVT (clib_bihash);

buckets。 hash桶的地址,即大内存块首地址+hash桶在大内存中的偏移。这个地址也是第0号hash槽所在的内存位置

alloc_lock, BIHASH的内存管理互斥锁变量所占的地址。注意这个变量就存放在大内存块内,刚从系统申请大内存块时,就需要在大内存块中取一块出来,代表这个变量。这个变量需要占用CACHE LINE大小。关于自旋锁等锁变量为何要占用cacheline 大小,请查看其他文档。

working_copies, working_copy_lengths 这2个变量分别定义了2个数组。每个元素对应一个CPU。具体作用是:当我们需要扩大hash槽时,如hash槽原来有16页,现在要扩大到32页,则把原来16页的数据先复制到一个副本中。然后将hash桶切换到副本,所有的其它读线程暂时使用副本中的数据。然后当前线程继续做扩充动作,这么做使得读线程不需要等待hash桶锁。当切换完成后,后续的读操作就操作的是新的32页数据了。

saved_bucket 用于保留hash槽切换前,旧的hash槽的值。hash槽扩充过程中使用

nbuckets 总共的hash槽数,在创建hash表时,调整成2的幂指数形式。如创建hash表时,需要创建含500个hash槽的hash表,则软件调整成含512( 1 << 9) 个hash槽的hash表。这样做的目的是提高寻找hash槽的效率(用位运算&而不是%运算就可以定位hash槽)。

log2_nbuckets, 针对上述例子 , log2_nbuckets == 9。 即等式 (1 << log2_nbuckets) == nbuckets恒成立。

name  本hash表的名称

freelists 空闲页链表首地址。此变量维护了一个空闲页链表的数组。第0号元素维护了含1页的空闲页链表。第1号元素维护了含2页的空闲页链表;第2号元素维护了含4页的空闲页链表。... 重申一下,这里的页指的是包含几个key-value-pair的实际内存空间,与操作系统的内存页大小并不一致。

#if BIHASH_32_64_SVM
    BVT (clib_bihash_shared_header) * sh;
  int memfd;
#else
    BVT (clib_bihash_shared_header) sh;
#endif

这里的sh主要用于BIHASH内部的内存管理,之前已经消息描述了这个结构。memfd表示这个大内存块使用命名的mmap获取。

alloc_arena 指的是大内存块的首地址

fmt_fn用于hash表项的格式化输出函数,由使用者定义,在打印hash表时使用。

BUG

在读代码的过程中,发现了疑似bug, 当前使用的版本是,未来的版本可能会解决这些问题

commit 88076749e663e35925c2212eb79e2ec4ce023772 (HEAD -> master, origin/master)
Author: Mike Bly 
Date:   Mon Sep 24 10:13:06 2018 -0700

疑似bug1

static inline void *BV (alloc_aligned) (BVT (clib_bihash) * h, uword nbytes)
{
  uword rv;

  /* Round to an even number of cache lines */
  nbytes += CLIB_CACHE_LINE_BYTES - 1;
  nbytes &= ~(CLIB_CACHE_LINE_BYTES - 1);

  rv = alloc_arena_next (h);
  alloc_arena_next (h) += nbytes;

  if (rv >= alloc_arena_size (h))
    os_out_of_memory ();

  return (void *) (uword) (rv + alloc_arena (h));
}

在if判断中,应该用rv + nbytes来判断,而不是rv来判断,这样才知道当前这个表项是否能使用。后果,当hash表刚好耗尽时,对最后一个表项的写操作可能会内存越界。

疑似BUG2

void BV (clib_bihash_free) (BVT (clib_bihash) * h)
{
  vec_free (h->working_copies);
#if BIHASH_32_64_SVM == 0
  vec_free (h->freelists);
#else
  if (h->memfd > 0)
    (void) close (h->memfd);
#endif
  clib_mem_vm_free ((void *) (uword) (alloc_arena (h)), alloc_arena_size (h));
  memset (h, 0, sizeof (*h));
}

这里有释放workong_copies,但没有释放working_copy_lengths。 虽然这一小块内存不大,但时间久了,hash模块多了,还是会慢慢泄露掉内存吧。

你可能感兴趣的:(VPP)