Linux核心中的資料結構與演算法(三)雜湊鍊錶

2021-10-21 19:22:24 字數 4769 閱讀 3292

四,雜湊鍊錶

談到鍊錶就不得不談linux核心中另外乙個重要的結構,雜湊鍊錶。討論這個結構前,你需要對雜湊的最基本的概念要清楚哦,由於我們已經講過linux核心中的普通鍊錶的結構,這裡我們對比他們的區別來了解雜湊鍊錶會直觀一些。

linux鍊錶認為雙指標表頭雙迴圈鍊錶對於hash表來說過於浪費,因而設計了一套用於hash表的hlist的資料結構,單指標表頭雙迴圈鍊錶。hlish表頭僅有乙個指向首節點的指標,而沒有指向尾節點的指標,這樣在海量的hash表中儲存的表頭就能減少一半的空間消耗。

定義和初始化

我們先來看看雜湊煉表頭的定義以及雜湊鍊錶節點的定義。

此定義在include/linux/types.h中可以看到:

struct hlist_head ;

struct hlist_node ;

所以從定義上我們就可以知道雜湊鍊錶和核心普通雙向鍊錶的節點唯一的區別就在於,pprev是個兩級指標,為什麼是二級指標我們稍後會仔細討論,了解雜湊演算法的同學就知道雜湊鍊錶並不需要雙向迴圈的技能,它一般適用於單向雜湊的場景。再加上前面提到的節省儲存空間的考慮,在嵌入式裝置領域是非常明智的選擇。

此外在include/linux/list.h中包含鍊錶的相關初始化操作,本章節後續除非特意說明**皆出自本檔案。

#define hlist_head_init 

#define hlist_head(name) struct hlist_head name =

#define init_hlist_head(ptr) ((ptr)->first = null)

初始化雜湊桶,init_hlist_head,雜湊煉表頭節點初始化為null

初始化哈系桶中的每乙個節點

static inline void init_hlist_node(struct hlist_node *h)

2.鍊錶節點插入

雜湊鍊錶的插入操作提供了幾種不同方式的插入,有的小夥伴對於writeonce有疑問的我這裡多說一句,其實看write_once與read_once()的實現就能看出來,起到關鍵作用的關鍵字volatile,它告訴編譯器宣告這個變數很重要,不要把它當成乙個普通的變數,做出錯誤的優化。保證 cpu 每次都從記憶體重新讀取變數的值,而不是用暫存器中暫存的值。因為在 多執行緒/多核 環境中,不會被當前執行緒修改的變數,可能會被其他的執行緒修改,從記憶體讀才可靠。還有一部分原因是,這兩個巨集可以作為標記,提醒程式設計人員這裡面是乙個多核/多執行緒共享的變數,必要的時候應該加互斥鎖來保護。

2.1 煉表頭插入

那現在我們再開始討論雜湊鍊錶的插入操作,首先來看新增節點到煉表頭的介面:

static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h)

在這裡我們插入第乙個節點,first鍊錶原來為空。

hlist_add_head鍊錶為空時

當first鍊錶不為空時,我們插入乙個節點

1.n->next = first; first原來指向node old.

2.first指向node old, 不為空。first->pprev = &n->next; 相當於node old.pprev = &n->next

;即node old 的pprev儲存的是new節點的next元素的位址。

3.h->first = n;

4.n->pprev = &h->hirst;pprev指向的是first自身的位址;

2.2把節點插入到指定節點前面

1.n->pprev = next->pprev,即插入節點之前前乙個節點的next的位址;

2.n->next=next;

3.next->pprev = &n->next;

4.(n->pprev) = n; n節點到pprev指向的是node old 的next的位址。*(n->pprev)儲存的是n節點的位址,也就是node 

old.next = n;

2.2把節點插入到指定節點後邊

1.n->next = next->next; node_next是最後乙個節點,其next指向null;

2.prev->next = n; node old next 的next元素指向n節點;

3.n->pprev=&next->next;n節點的pprev指向next節點的next元素的所在位址;

此外如果n->next不為空帶代表原prev->next不是最後乙個節點,我們需要把原prev的下乙個節點的pprev寫成n->next的位址,即n->next->pprev=&n->next;

3.鍊錶節點刪除

static inline void __hlist_del(struct hlist_node *n)

static inline void hlist_del(struct hlist_node *n)

linux 核心還提供了刪除節點並且初始化該節點的結構,值得注意的是這裡面有乙個hlist_unhashed(n)判斷n節點是否在雜湊桶的乙個函式。

static inline void hlist_del_init(struct hlist_node *n)

}

4.遍歷

#define hlist_entry(ptr, type, member) container_of(ptr,type,member)

#define hlist_for_each(pos, head) \

for (pos = (head)->first; pos ; pos = pos->next)

#define hlist_for_each_safe(pos, n, head) \

for (pos = (head)->first; pos && (); \

pos = n)

#define hlist_entry_safe(ptr, type, member) \

()

與普通鍊錶類似,雜湊鍊錶的遍歷也提供了乙個根據struct hlist_node的ptr得到ptr所指位址的結構體的首位址的介面hlist_entry(ptr, type, member)。

hlist_entry_safe(ptr, type, member)多了一步判斷ptr是否為空的操作,如果為空則返回null。

所以hlist_for_each(pos, head)同理也就是乙個for迴圈,從頭遍歷到鍊錶尾。

而hlist_for_each_safe(pos, n, head)的作用也是乙個for迴圈,與上乙個不同的地方在於多了乙個n,防止在便利過程中鍛鍊的發生。刪除時用這個介面。

那麼hlist_entry與hlist_for_each的結合就是hlist_for_each_entry(pos, head, member) 

#define hlist_for_each_entry(pos, head, member)             \

for (pos = hlist_entry_safe((head)->first, typeof(*(pos)), member);\

pos; \

pos = hlist_entry_safe((pos)->member.next, typeof(*(pos)), member))

同樣linux也提供了安全遍歷介面hlist_for_each_entry_safe(pos, n, head, member)

#define hlist_for_each_entry_safe(pos, n, head, member)         \

for (pos = hlist_entry_safe((head)->first, typeof(*pos), member);\

pos && (); \

pos = hlist_entry_safe(n, typeof(*pos), member))

Linux核心中的資料結構與演算法(一)

一,序言 其實想寫這個系列很久了,因為本人工作的關係,平時接觸linux核心很多,從業後很多時候在網上查詢的東西是片面或者不系統的,打算給自己的知識庫進行個整理吧,打算開筆寫這個系列,再加上自己想考研了,算是重新學習,也算鞏固自己的知識吧。如果有什麼錯誤和疏漏,也請看客們給出批評意見,比心比心比心 ...

核心中重要的資料結構

任務鍊錶 task list 流程排程程式為每個活動的流程維護乙個資料塊。這些資料塊儲存在稱為任務列表的鏈結列表中。程序排程程式始終維護乙個指示當前活動程序的當前指標。記憶體對映 memry map 記憶體管理器基於每個程序儲存虛擬位址到實體地址的對映,還儲存有關如何獲取和替換特定頁面的其他資訊。此...

半原創 核心中的資料結構 SplayTree

核心中的資料結構 splay tree splay tree 伸展樹 是binary search tree 二叉搜尋樹 daniel sleator和robert e.tarjan發明。但此操作可能會花費o n 時間,但m次操作的最壞情況為o m log2 n splay tree是在節點訪問後,...