1. 程式人生 > >操作系統 之 哈希表 Linux 內核 應用淺析

操作系統 之 哈希表 Linux 內核 應用淺析

after try 定位 意義 csdn 而是 ini move ons

1.基本概念

散列表(Hash table。也叫哈希表)。是依據關鍵碼值(Key value)而直接進行訪問的數據結構。

也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄。以加快查找的速度。

這個映射函數叫做散列函數。存放記錄的數組叫做散列表。

2. 經常使用的構造散列函數的方法
散列函數能使對一個數據序列的訪問過程更加迅速有效。通過散列函數。數據元素將被更快地定位。散列表的經常使用構造方法有:
(1)直接定址法
(2)數字分析法
(3)平方取中法
(4)折疊法
(5)隨機數法
(6)除留余數法
3、處理沖突的方法
散列表函數設計好的情況下,能夠降低沖突,可是無法全然避免沖突。常見有沖突處理方法有:
(1)開放定址法
(2)再散列法
(3)鏈地址法(拉鏈法)
(4)建立一個公共溢出區
4. 散列表查找性能分析

散列表的查找過程基本上和造表過程同樣。

一些關鍵碼可通過散列函數轉換的地址直接找到,還有一些關鍵碼在散列函數得到的地址上產生了沖突,須要按處理沖突的方法進行查找。

在介紹的三種處理沖突的方法中,產生沖突後的查找仍然是給定值與關鍵碼進行比較的過程。所以,對散列表查找效率的量度。依舊用平均查找長度來衡量。


查找過程中,關鍵碼的比較次數。取決於產生沖突的多少,產生的沖突少,查找效率就高。產生的沖突多,查找效率就低。因此,影響產生沖突多少的因素,也就是影響查找效率的因素。

影響產生沖突多少有下面三個因素:
1. 散列函數是否均勻;
2. 處理沖突的方法。
3. 散列表的裝填因子。


散列表的裝填因子定義為:α= 填入表中的元素個數 / 散列表的長度。
α是散列表裝滿程度的標誌因子。因為表長是定值。α與“填入表中的元素個數”成正比,所以,α越大。填入表中的元素較多,產生沖突的可能性就越大。α越小,填入表中的元素較少,產生沖突的可能性就越小。實際上,散列表的平均查找長度是裝填因子α的函數,僅僅是不同處理沖突的方法有不同的函數。
一.Linux內核哈希表數據結構
hash最重要的是選擇適當的hash函數,從而平均的分配keyword在桶中的位置,從而優化查找 插入和刪除所用的時間。然而不論什麽hash函數都會出現沖突問題。

內核採用的解決哈希沖突的方法是:拉鏈法拉鏈法解決沖突的做法是:將全部keyword為同義詞的結點鏈接在同一個鏈表中。若選定的散列表長度為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來區分頭結點和普通結點。
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 內核 應用淺析