1. 程式人生 > >Linux內核哈希表分析與應用

Linux內核哈希表分析與應用

構造方法 init lis 個數 無需 表示 字節 div 擴展

目錄(?)[+]

Linux內核哈希表分析與應用

Author:tiger-john
Time:2012-12-20
mail:[email protected]
Blog:http://blog.csdn.NET/tigerjb/article/details/8450995

轉載請註明出處。

前言:

1.基本概念:

散列表(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。

2. 常用的構造散列函數的方法

散列函數能使對一個數據序列的訪問過程更加迅速有效,通過散列函數,數據元素將被更快地定位。散列表的常用構造方法有:
 (1)直接定址法
 (2)數字分析法
 (3)平方取中法
 (4)折疊法
 (5)隨機數法
 (6)除留余數法

3、處理沖突的方法

  散列表函數設計好的情況下,可以減少沖突,但是無法完全避免沖突。常見有沖突處理方法有:
(1)開放定址法
(2)再散列法
(3)鏈地址法(拉鏈法)
(4)建立一個公共溢出區

4. 散列表查找性能分析

 散列表的查找過程基本上和造表過程相同。一些關鍵碼可通過散列函數轉換的地址直接找到,另一些關鍵碼在散列函數得到的地址上產生了沖突,需要按處理沖突的方法進行查找。在介紹的三種處理沖突的方法中,產生沖突後的查找仍然是給定值與關鍵碼進行比較的過程。所以,對散列表查找效率的量度,依然用平均查找長度來衡量。
  查找過程中,關鍵碼的比較次數,取決於產生沖突的多少,產生的沖突少,查找效率就高,產生的沖突多,查找效率就低。因此,影響產生沖突多少的因素,也就是影響查找效率的因素。影響產生沖突多少有以下三個因素:
1. 散列函數是否均勻;
2. 處理沖突的方法;
3. 散列表的裝填因子。


  散列表的裝填因子定義為:α= 填入表中的元素個數 / 散列表的長度。
  α是散列表裝滿程度的標誌因子。由於表長是定值,α與“填入表中的元素個數”成正比,所以,α越大,填入表中的元素較多,產生沖突的可能性就越大;α越小,填入表中的元素較少,產生沖突的可能性就越小。實際上,散列表的平均查找長度是裝填因子α的函數,只是不同處理沖突的方法有不同的函數。

一.Linux內核哈希表數據結構

hash最重要的是選擇適當的hash函數,從而平均的分配關鍵字在桶中的位置,從而優化查找 插入和刪除所用的時間。然而任何hash函數都會出現沖突問題。內核采用的解決哈希沖突的方法是:拉鏈法拉鏈法解決沖突的做法是:將所有關鍵字為同義詞的結點鏈接在同一個鏈表中。若選定的散列表長度為m,則可將散列表定義為一個由m個頭指針(struct hlist_head name)組成的指針數組T[0..m-1]。凡是散列地址為i的結點,均插入到以T[i]為頭指針的鏈表中。T中各分量的初值均應為空指針。在拉鏈法中,裝填因子α(裝填的元素個數/數組長度)可以大於 1,但一般均取α≤1。當然,用拉鏈法解決hash沖突也是有缺點的,指針需要額外的空間。

1. 其代碼位於include/linux/list.h中,3.0內核中將其數據結構定義放在了include/linux/types.h中

哈希表的數據結構定義:
如圖:
技術分享
struct hlist_head{
struct hlist_node *first;
}
struct hlist_node {
struct hlist_node *next,**pprev;
}
1>hlist_head表示哈希表的頭結點。哈希表中每一個entry(list_entry)所對應的都是一個鏈表(hlist).hlist_head結構體只有一個域,即first。First指針指向該hlist鏈表的第一個結點。
2>hlist_node結構體有兩個域,next和pprev。
(1)next指向下個hlist_node結點,倘若改結點是鏈表的最後一個節點,next則指向NULL
(2)pprev是一個二級指針,它指向前一個節點的next指針。

2.Linux 中的hlist(哈希表)和list是不相同的,在list中每個結點都是一樣的,不管頭結點還是其它結點,使用同一個結構體表示,但是在hlist中,頭結點使用的是struct hlist_head來表示的,而對於其它結點使用的是strcuct hlist_node這個數據結果來表示的。還有list是雙向循環鏈表,而hlist不是雙向循環鏈表。因為hlist頭結點中沒有prev變量。為什麽要這樣設計呢?

散列表的目的是為了方便快速的查找,所以散列表通常是一個比較大的數組,否則“沖突”的概率會非常大,這樣就失去了散列表的意義。如何來做到既能維護一張大表,又能不占用過多的內存呢?此時只能對於哈希表的每個entry(表頭結點)它的結構體中只能存放一個指針。這樣做的話可以節省一半的指針空間,尤其是在hash bucket很大的情況下。(如果有兩個指針域將占用8個字節空間)

3.hlist的結點有兩個指針,但是pprev是指針的指針,它指向的是前一個結點的next指針,為什麽要采用pprev,二不采用一級指針?

由於hlist不是一個完整的循環鏈表,在list中,表頭和結點是同一個數據結構,直接用prev是ok的。在hlist中,表頭中沒有prev,只有一個first。
1>為了能統一地修改表頭的first指針,即表頭的first指針必須修改指向新插入的結點,hlist就設計了pprev。list結點的pprev不再是指向前一個結點的指針,而是指向前一個節點(可能是表頭)中的next(對於表頭則是first)指針,從而在表頭插入的操作中可以通過一致的node->pprev訪問和修改前結點的next(或first)指針。
2>還解決了數據結構不一致,hlist_node巧妙的將pprev指向上一個節點的next指針的地址,由於hlist_head和hlist_node指向的下一個節點的指針類型相同,就解決了通用性。

二.哈希表的聲明和初始化宏

1.對哈希表頭結點進行初始化

實際上,struct hlist_head只定義了鏈表結點,並沒有專門定義鏈表頭,可以使用如下三個宏
#define HLIST_HEAD_INIT { .first = NULL}
#define HLIST_HEAD(name) struct hlist_head name = {.first = NULL}
#define INIT_HLIST_HEAD(ptr) ((ptr->first)=NULL))
1>name 為結構體 struct hlist_head{}的一個結構體變量。
2>HLIST_HEAD_INIT 宏只進行初始化
Eg: struct hlist_head my_hlist = HLIST_HEAD_INIT
調用HLIST_HEAD_INIT對my_hlist哈希表頭結點只進行初始化,將表頭結點的fist指向空。
3>HLIST_HEAD(name)函數宏既進行聲明並且進行初始化。
Eg: HLIST_HEAD(my_hlist);
調用HLIST_HEAD函數宏對my_hlist哈希表頭結點進行聲明並進行初始化。將表頭結點的fist指向空。
4>HLIST_HEAD宏在編譯時靜態初始化,還可以使用INIT_HLIST_HEAD在運行時進行初始化
Eg:
INIT_HLIST_HEAD(&my_hlist);
調用INIT_HLIST_HEAD倆將my_hlist進行初始化,將其first域指向空即可。

2.對哈希表結點進行初始化

1>Linux 對哈希表結點初始化提供了一個接口:
static iniline void INIT_HLIST_NODE(struct hlist_node *h)
(1) h:為哈希表結點
2>實現:
static inline void INIT_HLIST_NODE(struct hlist_node *h)
{
h->next = NULL;
h->pprev = NULL;
}
改內嵌函數實現了對struct hlist_node 結點進行初始化操作,將其next域和pprev都指向空,實現其初始化操作。

三.哈希鏈表的基本操作(插入,刪除,判空)

1.判斷哈希鏈表是否為空

1>function:函數判斷哈希鏈表是否為空,如果為空則返回1.否則返回0
2>函數接口:
static inline int hlist_empty(const struct hlist_head *h)
h:指向哈希鏈表的頭結點。
3>函數實現:
static inline int hlist_empty(const struct hlist_head *h)
{
return !h->first;
}
通過判斷頭結點中的first域來判斷其是否為空。如果first為空則表示該哈希鏈表為空。

2.判斷節點是否在hash表中

1>function:判斷結點是否已經存在hash表中。
2>函數接口:
static inline int hlist_unhashed(const struct hlist_node *h)
h:指向哈希鏈表的結點
3>函數實現:
static inline int hlist_unhashed(const struct hlist_node *h)
{
return !h->pprev
}
通過判斷該結點的pprev是否為空來判斷該結點是否在哈希鏈表中。 h->pprev等價於h節點的前一個節點的next域。如果前一個節點的next域為空,說明 改節點不在哈希鏈表中。

3.哈希鏈表的刪除操作

1>function:將一個結點從哈希鏈表中刪除。
2>函數接口:
static inline void hlist_del(struct hlist_node *n)
? n: 指向hlist的鏈表結點
static inline void hlist_del_init(struct hlist_node *n)
? n: 指向hlist的鏈表結點
3>函數實現
static inline void __hlist_del(struct hlist_node *n)
{
struct hlist_node *next = n->next;
struct hlist_node **pprev = n->pprev;
*pprev = next;
if (next)
next->pprev = pprev;
}
Step1:首先獲取n的下一個結點next
Step2: n->pprev指向n的前一個結點的next指針的地址,這樣*pprev就代表n前一個節點的下一個結點的地址(目前指向n本身)
Step3:*pprev=next,即將n的前一個節點和n的下一個結點關聯起來。
Step4:如果n是鏈表的最後一個結點,那麽n->next即為空,則無需任何操作;否則,next->pprev=pprev,將n的下一個結點的pprev指向n的pprev(既修改後結點的pprev數值)
此時,我們可以假設 在hlist_node 中采用單級指針,那麽該如何進行操作呢?
此時在進行Step3操作時,就需要判斷結點是否為頭結點。可以用n->prev是否為NULLL來區分頭結點和普通結點。
Eg:
struct my_hlist_node *next = n->next ;
struct my_hlist_node *prev = n->prev ;
if(n->prev)
n->prev->next = next ;
else
n->prev = NULL ;
if(next)    
next->prev = prev ;
那為什麽不進行以上的操作?
(1)代碼不夠簡潔。使用hlist_node結點的話,頭結點和普通結點是一致的;
static inline void hlist_del(struct hlist_node *n)
{
__hlist_del(n);
n->next = LIST_POISON1;
n->pprev = LIST_POISON2;
}
Step1:調用__hlist_del(n),刪除哈希鏈表結點n(即修改n的前一個結點和後一個結點的之間的關系)
Step2和Step3:將n結點的next和pprev域分別指向LIST_POISON1和LIST_POISON2。這樣設置是為了保證不在鏈表中的結點項不能被訪問。
static inline void hlist_del_init(struct hlist_node *n)
{
if (!hlist_unhashed(n)) {
__hlist_del(n);
INIT_HLIST_NODE(n);
}
}
Step1:先判斷該結點是否在哈希鏈表中,如果不在則不進行刪除。如果是則進行第二步
Step2:調用__hlist_del刪除結點n
Step3:調用INIT_HLIST_NODE,將結點n進行初始化。
說明:
hlist_del和hlist_del_init都是調用__hlist_dle來刪除結點n。唯一不同的是對結點n的處理,前者是將n設置為不可用,後者是將其設置為一個空的結點。

4.添加哈希結點

1>function:將一個結點添加到哈希鏈表中。
hlist_add_head:將結點n插在頭結點h之後。
hlist_add_before:將結點n插在next結點的前面(next在哈希鏈表中)
hlist_add_after:將結點next插在n之後(n在哈希鏈表中)
3.0內核新添加了hlist_add_fake函數。
2>Linux 內核提供了三個接口:
static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h)
struct hlist_node *n: n為將要插入的哈希結點
struct hlist head *h: h為哈希鏈表的頭結點。
static inline void hlist_add_before(struct hlist node *n,struct hlist_node *next)
struct hlist node *n: n為將要插入的哈希結點.
struct hlist node *next :next為原哈希鏈表中的哈希結點。
static inline void hlist_add_after(struct hlist node *n,struct hlist_node *next)
struct hlist node *n: n與原哈希鏈表中的哈希結點
struct hlist node *next: next為將要插入的哈希結點
註:在3.0內核中新添加了hlist_add_fake
static inline void hlist_add_fake(struct hlist_node *n)
struct hlist_node *n :n鏈表哈希結點
3>函數實現:
static inline void hlist_add_head(struct hlist_node *n,struct hlist_head *h)
{
struct hlist_node *first = h->first;
n->next = first;
if (first)
first->pprev = &n->next;
h->first = n;
n->pprev = &h->first;
}
Step1: first = h->first。獲得當前鏈表的首個結點.
Step2: 將first賦值給n結點的next域。讓n的next與first關聯起來。
Step3: 如果first不為空,則將first的pprev指向n的next域。此時完成了first結點的關聯。
如果fist為空,則不進行操作。
Step4: h->first = n; 將頭結點的fist域指向n,使n成為鏈表的首結點。
Step5: n->pprev = &h->first; 將n結點的pprev指向鏈表的fist域,此時完成了對n結點的關聯。


/*next must be !=NULL*/
static inline void hlist_add_before(struct hlist_node *n, struct hlist_node *next)
{
n->pprev = next->pprev;
n->next = next;
next->pprev = &n->next;
*(n->pprev) =n ;
}
Step1: n->pprev = next->prev;將next的pprev賦值給n->pprev。使n的pprev 指向next的前一個結點的next。
Step2: n->next = next;將n結點的next指向next,完成對n結點的關聯。
Step3: next->pprev = &n->next;此時修改next結點的pprev,使其指向n的next的地址。此時完成next結點的關聯。
Step4: *(n->pprev) =n;此時*(n->pprev)即n結點前面的next,使其指向n。完成對n結點的關聯。
註:
(1)next不能為空(next即哈希鏈表中原有的結點)
(2)n為新插入的結點。
static inline void hlist_add_after(struct hlist_node *n, struct hlist_node *next )
{
next->next = n->next;
n->next = next;
next->pprev = &n->next;
if (next->next)
next->next->pprev = &next->next;
}
n為原哈希鏈表中的結點, next新插入的結點。 將結點next插入到n之後(next是新插入的節點)
Step1: next->next = n->next; 將next->next指向結點n的下一個結點。
Step2: n->next = next; 修改n結點的next,使n指向next。
Step3: next->pprev = &n->next; 將next的pprev指向n的next
Step4: 判斷next後的結點是否為空如果,為空則不進行操作,否則將next後結點的pprev指向自己的next 處。
static inline void hlist_add_fake(struct hlist_node *n)
{
n->pprev =&n->next;
}
對這個函數的含義不太明白,望高人指點。

三.哈希鏈表的其他操作

1.哈希鏈表的移動

1>function:將以個哈希聊表的頭結點用new結點代替,將以前的頭結點刪除。
2>接口:
static inline void hlist_move_list(struct hlist_head *old, struct hlist_head *new)
struct hlist_head *old:原先哈希鏈表的頭結點
struct hlist_head *new:新替換的哈希鏈表的頭結點
3>實現:
static inline void hlist_move_list(struct hlist_head *old, struct hlist_head *new)
{
new->first = old->first;
if (new->first)
new->fist->pprev = &new->first;
old->first = NULL;
}
Step1: 將new結點的first指向old的第一個結點
Step2: 判斷鏈表頭結點後是否有哈希結點。如果為空,則不操作。否則,將表頭後的第一個結點的pprev指向新表頭結點的first.
Step3:將原先哈希鏈表頭結點的first指向空。

四.哈希鏈表的遍歷

為了方便核心應用遍歷鏈表,linux鏈表將遍歷操作抽象成幾個宏。在分析遍歷宏之前,先分析下如何從鏈表中訪問到我們所需要的數據項

1.hlist_entry(ptr,type,member)

1>function:通過成員指針獲得整個結構體的指針
Linux鏈表中僅保存了數據項結構中hlist_head成員變量的地址,可以通過hlist_entry宏通過hlist_head成員訪問到作為它的所有者的結點數據
2>接口:
hlist_entry(ptr,type,member)
? ptr:ptr是指向該數據結構中hlist_head成員的指針,即存儲該數據結構中鏈表的地址值。
? type:是該數據結構的類型。
? member:改數據項類型定義中hlist_head成員的變量名。
3>hlist_entry宏的實現
#define hlist_entry(ptr, type, member) \
container_of(ptr, type, member)
hlist_entry宏調用了container_of宏,關於container_of宏的用法見:

2.遍歷操作

1>function:實際上就是一個for循環,從頭到尾進行遍歷。由於hlist不是循環鏈表,因此,循環終止條件是pos不為空。使用hlist_for_each進行遍歷時不能刪除pos(必須保證pos->next有效),否則會造成SIGSEGV錯誤。而使用hlist_for_each_safe則可以在遍歷時進行刪除操作。
2>接口:
Linux內核為哈希鏈表遍歷提供了兩個接口:
hlist_for_each(pos,head)
? pos: pos是一個輔助指針(即鏈表類型struct hlist_node),用於鏈表遍歷
? head:鏈表的頭指針(即結構體中成員struct hlist_head).
hlist_for_each_safe(pos,n,head)
? pos: pos是一個輔助指針(即鏈表類型struct hlist_node),用於鏈表遍歷
? n :n是一個臨時哈希結點指針(struct hlist_node),用於臨時存儲pos的下一個鏈表結點。
? head:鏈表的頭指針(即結構體中成員struct hlist_head).
3>函數實現:
(1)#define hlist_for_each(pos, head) \
for(pos = (head)->first; pos ; pos = pos->next)
pos是輔助指針,pos是從第一個哈希結點開始的,並沒有訪問哈希頭結點,直到pos為空時結束循環。
(2)#define hlist_for_each_safe(pos,n,head) \
for(pos = (head)->first,pos &&({n=pos->next;1;}) ; pos=n)
hlist_for_each是通過移動pos指針來達到遍歷的目的。但如果遍歷的操作中包含刪除pos指針所指向的節點,pos指針的移動就會被中斷,因為hlist_del(pos)將把pos的next、prev置成LIST_POSITION2和LIST_POSITION1的特殊值。當然,調用者完全可以自己緩存next指針使遍歷操作能夠連貫起來,但為了編程的一致性,Linxu內核哈希鏈表要求調用者另外提供一個與pos同類型的指針n,在for循環中暫存pos下一個節點的地址,避免因pos節點被釋放而造成的斷鏈。
此循環判斷條件為pos && ({n = pos->next;1;});
這條語句先判斷pos是否為空,如果為空則不繼續進行判斷。如果pos為真則進行判斷({n=pos->next;1;})—》該條語句為復合語句表達式,其數值為最後一條語句,即該條語句永遠為真,並且將post下一條結點的數值賦值給n。即該循環判斷條件只判斷pos是否為真,如果為真,則繼續朝下進行判斷。
({n-pos->next;1;})此為GCC 特有的C擴展,如果你不懂的話,可以參考GCC擴展

五.用鏈表外的結構體地址來進行遍歷,而不用哈希鏈表的地址進行遍歷

Linux提供了從三種方式進行遍歷,一種是從哈希鏈表第一個哈希結點開始遍歷;第二種是從哈希鏈表中的pos結點的下一個結點開始遍歷;第三種是從哈希鏈表中的當前結點開始進行遍歷。

1.從哈希鏈表第一個哈希結點開始進行遍歷

1>function: 從哈希鏈表的第一個哈希結點開始進行遍歷。hlist_for_each_entry在進行遍歷時不能刪除pos(必須保證pos->next有效),否則會造成SIGSEGV錯誤。而使用hlist_for_each_entry_safe則可以在遍歷時進行刪除操作。
2>Linux提供了兩個接口來實現從哈希表第一個結點開始進行遍歷
hlist_for_each_entry(tpos, pos, head, member)
? tpos: 用於遍歷的指針,只是它的數據類型是結構體類型而不是strut hlist_head 類型
? pos: 用於遍歷的指針,只是它的數據類型是strut hlist_head 類型
? head:哈希表的頭結點
? member: 該數據項類型定義中hlist_head成員的變量名
hlist_for_each_entry_safe(tpos, pos, n, head, member)
? tpos: 用於遍歷的指針,只是它的數據類型是結構體類型而不是strut hlist_head 類型
? pos: 用於遍歷的指針,只是它的數據類型是strut hlist_head 類型
? n: 臨時指針用於占時存儲pos的下一個指針,它的數據類型也是struct hlist_list類型
? head: 哈希表的頭結點
? member: 該數據項類型定義中hlist_head成員的變量名
3>實現
#define hlist_for_each_entry(tpos,pos,head,member) \
for (pos = (head)->first; \
pos && \
({tpos = hlist_entry(pos, typeof(*tpos),member);1;});\
pos = pos->next)

#define hlist_for_each_entry_safe(tpos, pos, n, head, member) \
for (pos = (head)->first; \
pos && ({ n = pos->next;1;}) && \
({tpos = hlist_entry(pos, typeof(*tpos),member);1;}); \
pos = n)

2. 從哈希鏈表中的pos結點的下一個結點開始遍歷

1>function:從pos結點的下一個結點進行遍歷。
2>函數接口:
hlist_for_each_entry_continue(tpos ,pos, member)
? tpos: 用於遍歷的指針,只是它的數據類型是結構體類型而不是strut hlist_head 類型
? pos: 用於遍歷的指針,只是它的數據類型是strut hlist_head 類型
? member: 該數據項類型定義中hlist_head成員的變量名
3>函數實現:
#define hlist_for_each_entry_continue(tpos, pos, member) \
for (pos = (pos)->next; \
pos && \
({tpos = hlist_entry(pos,typeof(*tpos),member);1;}); \
pos = pos->next)

3.從哈希鏈表中的pos結點的當前結點開始遍歷

1>function:從當前某個結點開始進行遍歷。hlist_for_entry_continue是從某個結點之後開始進行遍歷。
2>函數接口:
hlist_for_each_entry_from(tpos, pos, member)
? tpos: 用於遍歷的指針,只是它的數據類型是結構體類型而不是strut hlist_head 類型
? pos: 用於遍歷的指針,只是它的數據類型是strut hlist_head 類型
? member: 該數據項類型定義中hlist_head成員的變量名
3>實現
#define hlist_for_each_entry_from(tpos, pos, member) \
for (; pos && \
({tpos = hlist_entry(pos,typeof(*tpos),member);1;});\
pos = pos->next)

Linux內核哈希表分析與應用