1. 程式人生 > >linux核心分析--核心中使用的資料結構之雜湊表hlist(三)

linux核心分析--核心中使用的資料結構之雜湊表hlist(三)

前言:

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)


其實關於hlist的分析在上篇文章linux核心分析--核心中的資料結構之雙鏈表(一)中有所介紹,hlist和介紹TAILQ_QUEUE非常相似!