函数分析:
文件 hash.c
__hash_open :哈希表打开函数,根据数据库文件名和哈希函数信息打开哈希表,函数有 6 个参数:
file :字符指针,传入数据库文件名;
flags :整型,文件打开标志;
mode :整型,文件打开模式;
info : HASHINFO 类型数据,导入哈希表信息;
dflags :整型,没见到使用;
返回值:数据库类型( DB )指针;
伪代码:
如果参数 flags 标记数据库文件以只写方式打开,则返回失败;不从数据库文件中读取数据不能建立打开哈希表(除非新建);
为哈希表分配表结构 HTAB 类型变量的内存,使用 hashp 指针操作这一对象;
将 hashp 的 fp 成员变量初始化为- 1 ;
如果 file 参数为空,则为 file 参数分配一个临时文件名,并将其赋值给 hashp 的 fname 成员;
将 flags 参数赋值给 hashp 的 flags 成员;
如果 file 参数传入时不为空且数据库文件可读写,则将 hashp 的 save_file 成员赋值为真,表示对数据库的变更需要写回磁盘;
按照 flags 和 mode 方式打开 file 文件,并将文件描述符赋给 hashp 的 fp 成员;
如果是新建表,则使用 init_hash 函数初始化 hashp 指向的哈希表基本信息;否则, 1 、判断 info 是否有效,且 hash 成员(哈希函数)不为空,将 hashp 的 hash 函数初始化为 info 的 hash 成员,否则使用默认的哈希函数, 2 、使用 hget_header 函数获取哈希表的头部信息,即 hashp 的 hdr 成员; 3 、验证取得的头部信息,包括魔数、版本和哈希函数是否匹配;最后根据哈希表头部信息的 spares 数组判断哈希表位图页的个数 bpages ,判断的方法就是哈希表最后一个分裂点记录了哈希表溢出页面的总数 N ,由于位图页从 0 计数,且为了将最后为使用完的一页计算在内,需要将 N 加上一个一页包含的位数最大值(即 (hashp->hdr.bsize << BYTE_SHIFT) – 1 ),从而补齐最后一页。然后根据溢出页序号高 17 位为位图页编号,低 15 位为位图页内位偏移(从 1 计数),所以将上述计算的和右移 15 位,即可得到当前哈希表位图页的个数 bpages ;这一过程好比使用去尾法找大于数 x 的最小整数值,如果 x 等于 5.2 ,则需要将 5.2+0.9 = 6.2 在执行去尾操作,得到 6 ,即大于 5.2 的最小整数;最后,将 bpages 赋值给 hashp 的 nmaps 成员,同时将 mapp 数组的前 bpages 项置为 0 ,因为此时没有将位图页读入内存,所以不能为 mapp 数组项赋值,随着哈希表的使用, mapp 将得到位图页的首地址;
初始化缓冲池:计算缓冲池的大小,缓冲的文件对象,最后使用 mpool_open 函数打开并初始化缓冲池;
对于新建的哈希表,需要初始化哈希表的位图页及相关成员,使用 init_htab 函数完成;
初始化游标队列;
为 hashp 的 split_buf 分配内存,为哈希表扩展时桶分裂作准备;
根据哈希表是否是新表,设置 hashp 的 new_file 成员;
为数据库结构 dbp 分配内存并初始化;
其内部数据存储使用 hashp ,关闭函数为 hash_close ,删除函数为 hash_delete ,获取数据库文件描述服函数为 hash_fd ,获取函数为 hash_get ,写入函数为 hash_put ,顺序访问函数为 hash_seq ,同步函数为 hash_sync ,数据库实现类型为哈希表;
返回 dbp ;
hash_close 函数:哈希表关闭函数;文件内部函数;关闭数据库;
函数参数:
dbp :数据库指针;
返回值:成功或失败;
伪代码:
获取数据库的哈希表 hashp ;
使用 hdestroy 函数关闭哈希表 hashp ;
释放 dbp 变量所占内存;
返回 hdestroy 函数关闭哈希表的结果;
hash_fd 函数:获取数据库文件描述符;文件内部函数;
函数参数:
dbp :数据库指针;
返回值:数据库文件描述符或失败- 1 ;
伪代码:
获取哈希表 hashp ;
如果 hashp 的 fp 成员不为- 1 ,则返回 fp ;
init_hash 函数:哈希表初始化函数,文件内部函数;
函数参数:
hashp :哈希表指针;
file :文件名;
info : HASHINFO 类型指针,传入创建信息;
返回值:哈希表指针;
伪代码:
首先将 hashp 中的各成员使用程序缺省的值;
如果 file 不为 NULL ,则读取 file 的基本信息,根据实际文件系统来选择合适的页面大小,从而获得最佳的性能;
根据 info 传入的信息调整 hashp 中的成员变量;
返回 hashp ;
init_htab 函数:初始化哈希表结构,与 init_hash 函数的区别在于,前者只是初始化一些与硬件相关的信息,而本函数主要侧重于哈希表头部信息的初始化及头部信息关联的 HTAB 成员的初始化,完成哈希表结构的布局,为页面管理作准备;
函数参数:
hashp : HTAB 类型指针,传入待初始化的哈希表;
nelem :整型,预计哈希表将要存放元素的数量;
返回值:成功或失败;
伪代码:
本函数首先通过 nelem 计算出初始化时哈希表的桶数 nbuckets ,然后根据 nbuckets 调整 spares 等头部信息的值,并初始化位图页;
首先,计算 nbuckets 的值:
前文已述填充因子 ffactor 表示一个桶中最大存放记录的数量,超过这一数量则哈希表应该扩展,即使扩展时分裂的同不是这个桶,但是这个桶仍然是不正常的,会导致下次对该桶的插入或修改引发哈希表的分裂,即桶中记录的数量可以超过填充因子,但是超过填充因子仍然可以向桶中添加记录;
所以桶哈希表中桶的数量就是元素的数量除以填充因子,这样在理想情况下,如果哈希表真的刚好存储 nelem 个元素的话,哈希表刚好达到使用的极限而不用扩展(实际元素并不是均匀分布在各个桶中而引发扩展);
为了达到较好的哈希效果和充分利用页面资源,从前文可知,当最大桶号恰好是 2 的幂- 1 (即共有 2 的幂个桶时,桶号从 0 计数)时,记录在哈希函数上对各个桶号是等概率的,而且从下文可知,如果哈希表不扩展的话,溢出页与桶页之间没有预留页,从而充分利用空间。
所以 nbuckets 初始化为大于上述计算值的最小 2 的幂,另外,桶的数量不能低于 2 ,所以 nbuckets 最小为 2 ,即 2 的 1 次幂;
计算完 nbuckets ,则哈希表最大桶号为 nbuckets - 1 ,即头部信息的 max_bucket 等于 nbucktes - 1 ,同理可得到高位和低位掩码;根据 nbuckets 初始化 spares 数组,假设 nbuckets 为 2 的 l2 次幂,那么根据最大桶号为 nbuckets-1 可知,大于最大桶号的最小 2 的幂为 nbuckets ,即哈希表的桶页正处于 l2-1 预留桶页之中,虽然已经将分裂点 l2-1 预留的空间消耗完,但是并没有计入 l2 分裂点,所以 spares 的 l2 项即 spares[l2] 保存哈希表从初始化直至第一次扩展之间所有的溢出页面数量,初始化为程序分配了 l2+1 个空闲的溢出页面,即 spares[l2]=l2+1 ,将 ovfl_point 初始化为 l2 ,需要注意的是: 1 、现在处于新建哈希表初始化阶段,桶号已经到达 l2-1 分裂点,在此之前没有任何溢出页分配,所以 spares 数组的前 l2-1 想全部为 0 ; 2 、由于哈希表新建,并没有位图页,而位图页实际上也计入到溢出页内,所以 spares[l2] 中分配的溢出页面中第 1 页将被用作位图页,实际预留的溢出空闲页只有 l2 页,这将在后续位图页初始化时体现出来,这是如此,从而导致 last_freed 成员赋值为 2 ,即序号 2 之前的溢出页都不空闲(注意,溢出页序号从 1 计数,否则将于对应分裂点预留的桶页最后一页的页面编号冲突);
计算头部信息占用页面的数量,即计算头部信息 hdrpages 的值,计算方式是将头部信息尺寸除以页面的大小,注意需要补齐最后一页; hdrpages 的作用就是标识哈希表非原数据页的起始页的页面编号,后续桶号、溢出页号转页面编号都是基于这一编号的;
使用 __ibitmap 函数初始化位图页,从上可知,这里初始化的位图页位于分裂点 l2 的第 1 页,同时在位图页中预留 l2+1 个溢出页,其中第 0 位置位,表示第 1 页已经使用(用作本位图页);
返回;
hget_header 函数:从数据库文件中获取哈希表头部信息;
函数参数:
hashp :哈希表指针;
page_size :页面大小;
返回值:从文件中获取的字节数;
伪代码:
获取 hashp 的 hdr 成员首地址 hdt_dest ;
将 hashp 的 fd 读指针移动到文件开始处;
从 fp 中读 HASHHDR 尺寸个字节到 hdr_dest ;
如果读取的字节数不等于 HASHHDR 的尺寸,则返回失败;
返回读取的字节数;
hput_header 函数:写回头部信息;
函数参数:
hashp :哈希表指针;
返回值:无
伪代码:
获取 hashp 的 hdr 成员首地址 whdrp ;
将 hashp 的 fp 成员读指针移动到文件的开始处;
向文件中写入 whdrp 地址开始的 HASHHDR 尺寸个字节;
返回;
hdestroy 函数:销毁哈希表;文件内部参数;
函数参数:
hashp : HTAB 指针;
返回值:成功或失败;
伪代码:
使用 flush_meta 函数将头部信息和位图页信息写回文件;
释放 split_buf 、 bigdata_buf 和 bigkey_buf 等成员指向的缓冲区;
同步缓冲池到文件;
关闭缓冲池;
关闭 fp ;
判断是否是临时数据库,从而决定是否删除临时文件;
释放 hashp 结构;
返回;
hash_sync 函数:将修改的内容写回磁盘;文件内部函数;
函数参数:
dbp :数据库指针;
flags :未使用;
返回值:成功或失败;
伪代码:
获取数据库中哈希表指针;
使用 flash_meta 写回元数据,使用 mpool_sync 写回缓冲区更新;
返回操作结果;
flush_meta 函数:写回元数据,文件内部函数;
函数参数:
hashp : HTAB 指针;
返回值:成功或失败;
伪代码:
如果哈希表是临时表,则返回;
使用 hput_header 函数写回头部信息;
遍历 mapp 数组,将各位图页写回磁盘;
返回;
hash_get 函数:根据键值获取记录的函数,文件内部函数;
函数参数:
dbp : DB 类型指针;
key : DBT 类型指针,传入键值;
data : DBT 类型指针,返回数据值;
flag :整型,未使用;
返回值:成功或失败;
伪代码:
使用 hash_access 函数获取记录,传入 HASH_GET 访问类型;
hash_put 函数:更新或插入记录操作,文件内部函数;
函数参数:
dbp : DB 类型指针;
key : DBT 类型指针,传入记录的键值;
data : DBT 类型指针;传入记录的数据值;
flag :操作标记;
返回值:成功或失败;
伪代码:
如果 flag 不等于 0 而且不等于 R_NOOVERWRITE 则返回失败;
如果哈希表是只读的,则返回失败;
如果 flag 等于 R_NOOVERWRITE 则传入 HASH_PUTNEW 标记使用 hash_access 函数插入记录,否则传入 HASH_PUT 标记使用 hash_access 函数更新记录;
返回操作结果;
hash_delete 函数:删除记录函数,文件内部函数;
dbp : DB 类型指针;
key : DBT 类型,传入待删除记录键值;
flag :未使用;
返回值:成功或失败;
伪代码:
如果哈希表为只读,则返回失败;
通过传入 HASH_DELETE 标记使用 hash_access 函数删除记录;
hash_access 函数:记录访问函数,根据前文所述,关于记录的更新、插入、获取、删除均通过 hash_access 函数完成,所以本函数需要上述函数完成调用前的判断和预处理,本函数将不作类似处理;
函数参数:
hashp : HTAB 指针;
action : ACTION 共用体,用于表示操作的类型-- HASH_GET, HASH_PUT, HASH_PUTNEW, HASH_DELETE, HASH_FIRST, HASH_NEXT ;
key : DBT 指针,传入记录键值;
val : DBT 指针,根据操作类型传入或传出数据值;
返回值:成功或失败;
伪代码:
将桶中本记录前已有记录数 num_items 初始化为 0 ;
如果是更新或插入记录,则计算存储记录需要的空间并保存到 item_info 的 seek_size 成员中,由于大数据只需保存索引部分( 4 字节),普通数据则需要保存整条记录和索引,但是程序中只将记录尺寸赋给 seek_size 不知何解?如果是其它操作,则无须计算 seek_size ,赋 0 即可;
将 item_info 的 seek_found_page 初始化为 0 ,尚未找到;
调用哈希函数获取桶号 bucket ;
将查找游标 cursor 初始化,其 pagep 赋为 NULL ;使用 __get_item_reset 函数将游标复位;
将 cursor 的当前桶号置为 bucket ,即将在 bucket 桶中查找;
1 、使用 __get_item_next 获取游标当前记录并保存结构到 item_info ,同时游标下移;
2 、如果 item_info 的 status 指示访问失败在返回错误;指示达到桶的最后一条记录则转 5 ;成功获取则转 3 ;
3 、 num_items 递增;
4 、判断获取的记录是否是要找的记录,如果是则转 7 ;否则转 1 ;对于记录的比较,对于大数据,则使用 __find_bigpair 函数判断;对于普通数据则只用判断二者键值的尺寸和 memcpy 的比较值;
5 、此时完成了对桶 bucket 中所有记录的访问和比较,没有找到指定的记录;使用 __get_item_done 函数将 cursor 作查找完成后的修正;转 6 ;
6 、由于指定记录没有找到,所以对于更新或插入操作,使用 __addel 函数将指定记录插入到 bucket 桶的第 num_items 条记录处,其实 num_items 主要作用是为判断本桶的插入操作是否需要引起表扩展,因为 num_items 此时就是桶中记录的数量;对于获取、删除或其它操作则返回异常;
7 、到达此处则表明指定记录已经找到,而且就在 bucket 桶的第 num_items 条记录处,同样使用 __get_item_done 将 cursor 修正;转 8 ;
8 、如果是插入新的记录操作,则由于表中已经存在此条记录,则返回异常;
如果是获取操作,对于大数据记录,使用 __big_return 函数获取,对于普通数据直接将查找是获得的值赋给 val ,返回 val ;
如果是更新操作,则先使用 __delpair 函数删除原记录,然后使用 __addel 函数添加新的记录,由于此时 num_items 不能代表桶中记录的数量,所以使用 UNKNOWN 参数让 __addel 自行判断是否需要引起表扩展;
如果是删除操作,则使用 __delpair 函数删除指定记录;
其它操作则中断程序;
9 、返回成功;
游标函数:下面将主要介绍几个关于游标的函数,主要用于数据库(哈希表)的顺序遍历操作,没有仔细阅读;
__cursor_creat 函数:创建并初始化一个游标;
函数参数:
dbp : DB 类型指针;
返回值:新生成的游标;
伪代码:
为游标 new_curs 分配内存;
为 new_curs 的 internal 成员分配查找记录项的内存;
成员变量和函数初始化;
将 new_curs 推进 dbp 中哈希表的游标队列;
返回 new_curs ;
cursor_get 函数:??
cursor_delete 函数:
hash_seq 函数:
__expand_table 函数:扩展哈希表;
函数参数:
hashp : HTAB 指针;
返回值:成功或失败;
伪代码:
本函数的主要功能是取得新旧桶桶号;同时更新哈希表的头部信息;
首先,将哈希表的最大桶号递增 1 ,同时得到新桶桶号 new_bucket ;
然后获取旧桶桶号,通过前文的分析,知道新桶和旧桶据有最大的低若干位,或者说,在低位掩码下相等;得旧桶桶号 old_bucket ;
使用 __new_page 函数根据 new-buckets 为新桶分配页面;
由于最大桶号增加,可能会导致对应分裂点预留的空间消耗完,则判断最大桶号当前所在的分裂点的下一个分裂点 spare_ndx ,如果 spare_ndx 大于 ovfl_point 即当前溢出页面分配所在的分裂点,则需要将溢出页面分裂点移动到下一个分裂点,同时将下一个分裂点对应的 spares 项即 spares[spare_ndx] 初始化为当前分裂点的 spares 项 spares[ovfl_point] ;然后将 spare_ndx 赋值给 ovfl_point ,完成溢出页面分配分裂点的转移;
上述操作中需要注意的是:由于分裂点从 0 计数,而且桶号从 0 计数,所以 spare_ndx 在程序中的计算表达式实际是最大桶号分裂点的下一个分裂点,由于桶号的页面使用的是溢出页面预留的值,则二者不会处于同一个分裂点中,溢出页面的分裂点至少比最大桶号的分裂点大 1 , ovfl_point 即溢出页面的分裂点,所以,正常情况下, ovfl_point 大于等于 spare_ndx ,一旦 spare_ndx 大于 ovfl_point ,则表示最大桶号所在分裂点已经进入到溢出页面分裂点,此时,这一分裂点中不能在继续分配溢出页面,从而转移到下一个分裂点, spare_ndx 最多只能大 ovfl_point 一个分裂点; spares 数组记录对应分裂点时已经分配的溢出页总数,所以有上述赋值操作;
判断是否需要升级掩码;若是则升级掩码;
判断 new_bucket 的页面编号是否超过系统允许值;
使用 __split_page 分裂旧桶中的记录;
返回分裂结果;
__call_hash 函数:哈希函数的接口函数;从前文可知,哈希函数返回 32 位数,而实际只需要其中的低若干位,本函数实现对哈西函数返回值的过滤;
函数参数:
hashp : HTAB 指针;
k : 8 字节数指针,传入待计算的键值字符型的指针;
len :整型,键值长度;
返回值:桶号;
伪代码:
调用 hashp 的 hash 成员函数得到哈希值 n ;
使用高位掩码得到高位掩码下的桶号 bucket ;
如果 bucket 大于最大桶号,则表示需要在低半个表中继续查找;使用地位掩码得到桶号 bucket ;
返回 bucket ;
其余剩下一些字节大小端转换函数,大体同 B 树中类似。