天天看點

Linux 核心 hlist 詳解

在linux核心中,hlist(哈希連結清單)使用非常廣泛。本文将對其資料結構和核心函數進行分析。

和hlist相關的資料結構有兩個:hlist_head 和 hlist_node

結構如下圖所示:

Linux 核心 hlist 詳解

hlist_head結構體隻有一個域,即first。 first指針指向該hlist連結清單的第一個節點。

hlist_node結構體有兩個域,next 和pprev。 next指針很容易了解,它指向下個hlist_node結點,倘若該節點是連結清單的最後一個節點,next指向null。

pprev是一個二級指針, 它指向前一個節點的next指針的位址。為什麼我們需要這樣一個指針呢?它的好處是什麼?

在回答這個問題之前,我們先研究另一個問題:為什麼哈希表的實作需要兩個不同的資料結構?

哈希表的目的是為了友善快速的查找,是以哈希表中hash桶的數量通常比較大,否則“沖突”的機率會非常大,這樣也就失去了哈希表的意義。如何做到既能維護一張大表,又能不使用過多的記憶體呢?就隻能從資料結構上下功夫了。是以對于哈希表的每個hash桶,它的結構體中隻存放一個指針,解決了占用空間的問題。現在又出現了另一個問題:資料結構不一緻。顯然,如果hlist_node采用傳統的next,prev指針,對于第一個節點和後面其他節點的處理會不一緻。這樣并不優雅,而且效率上也有損失。

hlist_node巧妙地将pprev指向上一個節點的next指針的位址,由于hlist_head的first域指向的結點類型和hlist_node指向的下一個結點的結點類型相同,這樣就解決了通用性!

如果要删除hash桶對應連結清單中的第一個普通結點

Linux 核心 hlist 詳解

對應的程式代碼如下:

如果要删除hash桶對應連結清單中的非第一個結點

Linux 核心 hlist 詳解

可以看到删除第一個普通結點和删除非第一個普通結點的代碼是一樣的。

下面再來看看如果hlist_node中包含兩個分别指向前驅結點和後繼結點的指針

Linux 核心 hlist 詳解

很明顯删除hash桶對應連結清單中的非第一個普通結點,隻需要如下兩行代碼:

可是,如果是删除的hash桶對應連結清單中的第一個普通結點:

此時 n->prev->next = n->next 就會出問題,因為hash桶的表頭結點沒有next域

是以,明顯在這種情況下删除hash桶對應連結清單的第一個普通結點和非第一個普通結點的代碼是不一樣的。

同樣的情況也存在于插入操作。

附一張在hash桶頭結點之後,插入第一個普通結點的圖:

Linux 核心 hlist 詳解

在周遊上,如果使用hlist_hode, list_node指針進行周遊,兩者過程大緻相似。

如果使用其寄生結構的指針進行周遊,則hlist與list也略有不同,hlist在周遊時需要一個指向hlist_node的臨時指針,該指針的引入,一是為了周遊,而list的周遊在list_entry的參數中實作了,更主要的目的在于判斷結束,因為hlist最後一個節點的next為null,隻有hlist_node指向null時才算結束,而這個null不包含在任何寄生結構内,不能通過tpos->member的方式通路到,故臨時變量pos的引入時必須的。

另外,list和hlist的周遊都實作了safe版本,因在周遊時,沒有任何特别的東西來阻止對連結清單執行删除操作(通常在使用連結清單時使用鎖來保護并發通路)。安全版本的周遊函數使用臨時存放的方法使得檢索連結清單時能不被删除操作所影響。

附上linux核心中與hlist相關的完整代碼: