漫談Linux內核哈希表(1)

關於哈希表,在內核裏設計兩個很重要的數據結構:
   
哈希鏈表節點 css

點擊(此處)摺疊或打開 node

  1. /*Kernel Version : 3.4.x [include/linux/types.h]*/
  2. struct hlist_node {
  3.     struct hlist_node *next, **pprev;
  4. };
    能夠看到哈希節點和內核普通雙向鏈表的節點惟一的區別就在於,前向節點 pprev 是個兩級指針,至於爲何這樣設計而不採用 struct list_head{} 來做爲哈希鏈表的節點,咱們後面會詳細介紹。另一個重要的數據結構是,哈希鏈表的表頭。

   
哈希鏈表 表頭

點擊(此處)摺疊或打開 linux

  1. /*Kernel Version : 3.4.x [include/linux/types.h]*/
  2. struct hlist_head {
  3.     struct hlist_node *first;
  4. };
    由於哈希鏈表並不須要 雙向循環 的技能,它通常適用於單向散列的場景。因此,爲了減小開銷,並無用 struct hlist_node{} 來表明哈希表頭,而是從新設計 struct hlist_head{} 這個數據結構。此時,一個哈希表頭就只須要 4Byte 了,相比於 struct hlist_node{} 來講,存儲空間已經減小了一半。這樣一來,在須要大量用到哈希鏈表的場景,其存儲空間的節約是很是明顯的,特別是在嵌入式設備領域。


   接下來
,咱們來重點回答一下哈希節點裏那個兩級指針的問題。先講個小插曲,記得本人當年剛參加工做時,導師給安排了一個活兒,那時候年輕氣盛、血氣方剛,沒一下子功夫,三下五除二就搞定了。而後拿着本身的「傑做」去師傅看,師傅瞄了一眼說,你這函數簡直是一坨shi(和喬老爺當年罵另一個程序員的用詞、語氣差很少),誰讓你函數入參傳個三級指針進去的?這段代碼TM能維護麼?誰看得懂?完了以後感受本身還受了莫大的委屈同樣,不過誰的人生沒有那麼點波瀾壯闊的過往呢,就像有句名言說的:程序寫出來是給人看的,順帶能在機器上運行。OK,那這個故事跟咱們要介紹的哈希節點的關係在哪兒呢?沒錯,就是struct hlist_node{}裏那個前向的兩級指針的存在乎義。

    關於兩級指針的目的與意義,讓
咱們採用反證法來看看,若是struct hlist_node{}被設計成以下一級指針的樣子,會發生什麼:
程序員

點擊(此處)摺疊或打開 數據結構

  1. struct hlist_node {
  2.     struct hlist_node *next, *pprev;
  3. };
    假如咱們如今已經有一個哈希鏈表了myhlist(先別管這個鏈表是怎麼來的),鏈表裏有4個節點node1~node4:

    
   而後就有如下兩個問題跟着冒出來:
   
1)、在往哈希鏈myhlist裏插入node1時必須這麼寫: ide

點擊(此處)摺疊或打開 函數

  1. mylist.first = node1;
  2. node1->pprev=( struct hlist_node*)&mylist;
   除此以外,在插入 node2~node4 以及後續其餘節點時 ( 假如按順序插入的話 ) ,寫法以下(X>=2

點擊(此處)摺疊或打開 spa

  1. node[X]->next = node[X+1];
  2. node[X]->pprev = node[X-1];

簡而言之啥意思呢?往哈希鏈表裏插入元素時,若是在表頭的第一個位置上插入元素,和插入在哈希鏈表的其餘位置上的代碼處理邏輯是不同的。由於哈希表頭是list_head類型,而其餘節點都是list_node類型。

   2
)、一樣,若是刪除節點時,對於非首節點,以node2爲例: 設計

點擊(此處)摺疊或打開 指針

  1. node2->pprev->next = node2->next;
  2. node2->next->pprev = node2->pprev;
    若是要刪除首節點 node1 呢,則寫法以下:

點擊(此處)摺疊或打開

  1. ((struct hlist_head*)(node1->pprev))->first = node1->next;
  2. node1->next->pprev = ( struct hlist_node*)&mylist或者 node1->next->pprev = node1->pprev;
    很明顯,內核開發者們怎麼會允許這樣的代碼存在,並且還要充分考慮效率的問題。那麼,當 hlist_node.pprev 被設計成兩級指針後有啥好處?
    仍是以刪除節點爲例,若是要刪除首節點,由於node1->pprev裏保存的是myhlist的地址,而myhlist.first永遠都指向哈希鏈表的第一個節點,咱們要間接改變表頭裏的hlist_node類型的first指針的值,能想到的最直接的辦法固然是二級指針,這是兩級指針的宿命所決定的,爲了間接改變一級指針所指的內存地址的場景。這樣一來,node節點裏的pprev其實指向的是其前一個節點裏的第一個指針元素的地址。對於hlist_head來講,它裏面只有一個指針元素,就是first指針;而對於hlist_node來講,第一個指針元素就是next。具體以下所示:

因此,記住,當咱們在代碼中看到相似與*(hlist_node->pprev)這樣的代碼時,咱們內心應該清楚,此時正在哈希表裏操做當前節點前一個節點裏的第一個指針元素所指向的內存地址,只是以間接的方式實現罷了。那麼回到刪除哈希鏈表節點的場景,當刪除首節點時,此時狀況就變成了:

點擊(此處)摺疊或打開

  1. *(node1->pprev) = node1->next;
  2. node1->next->pprev = node1->pprev;
    刪除非首節點的狀況也同樣:

點擊(此處)摺疊或打開

  1. *(node2->pprev) = node2->next;
  2. node2->next->pprev = node2->pprev;
    這樣一來,咱們對hlist_node裏的諒解指針pprev的存在價值與意義應該很明白了,之後不至於再被眼花繚亂的取地址操做符給弄暈了。OK,扯了這麼多,讓咱們看看內核是如何實現刪除哈希鏈表裏的節點的__hlist_del():
   
   
你們自行將上述函數裏的入參n換成node2,最終和咱們上面推斷的結果是一致的:
   
    在標準的哈希鏈表裏,由於最後一個節點的 next=NULL ,因此在執行第二句有效代碼前首先要對當前節點的 next 值進行判斷才行。
    內核提供了 hlist_add_head() ,用於實現向哈希鏈表裏插入節點:

點擊(此處)摺疊或打開

  1. hlist_add_head(struct hlist_node *n, struct hlist_head *h)
    其中n表示待插入的節點,h表示哈希鏈表表頭。在剛初始化完哈希表myhlist的狀況下,依次調用四次hlist_add_head(),每次調用後myhlist哈希表的狀況以下:
   
( 備註:雙箭頭表示兩級指針,單箭頭表示一級指針 )
   
理論上說,內核應該再提供一個對稱的方法hlist_add_tail()纔算完美,用於將哈希鏈表操做成以下的樣子:


   還有
hlist_add_behind()hlist_add_before(),在3.17版本以前hlist_add_behind()的名字仍是hlist_add_after(),不過做用都同樣。兩個函數原型分別以下:

點擊(此處)摺疊或打開

  1. hlist_add_before(struct hlist_node *n,struct hlist_node *next);
  2. hlist_add_behind(struct hlist_node *n,struct hlist_node *prev);
    其中n是待插入的節點,next或者prev都是n的相對位置參考節點,其做用分別是:
   
hlist_add_before():在next節點的前面插入n節點;
 
hlist_add_behind():在prev節點的後面插入n節點;

    接下來,讓咱們…..

   
1) 、在 node4 節點的 前面 插入 node3
   
注意 hlist_add_before() 有個約束條件,那就是 next!=NULL。

   
2) 、在 node1 的節點 後面 插入 node5
   一樣的約束條件也適用於hlist_add_behind(),即prev!=NULL
   未完,待續...
相關文章
相關標籤/搜索