與我昨天寫的《linux通用鏈表》類似,主要分析linux提供的頭文件代碼(linux/list.h)。
一、結構定義及初始化
/* hash頭節點定義 */ struct hlist_head { struct hlist_node *first; }; /* hash節點定義 */ struct hlist_node { struct hlist_node *next, **pprev; }; /* 頭節點初始化 */ #define HLIST_HEAD_INIT { .first = NULL } #define HLIST_HEAD(name) struct hlist_head name = { .first = NULL } #define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL) /* hash節點初始化 */ #define INIT_HLIST_NODE(ptr) ((ptr)->next = NULL, (ptr)->pprev = NULL)
這裏需要說明兩個問題:
1. 頭節點爲什麼只有一個指針成員,難道不能和hash節點同一定義?
2. hash節點中爲什麼用**pprev,而不和通用鏈表那樣使用*prev?
hash鏈表的目的就是解決在大量數據下的查找問題。也就是說一般情況下hash桶會很大,頭節點也就會很多。爲了節省內存,設計人員就只使用一個指針,指向hash鏈的第一個節點。這樣就導致頭節點和hash鏈節點的數據結構不一致,有爲了實現通用性就出現了二級指針**pprev,指向前一個節點的*next域。如何看出通用性的,請看下面是如何刪除節點的。
二、刪除節點
這裏與通用鏈表的刪除節點接口類似,LIST_POISON1和LIST_POISON2可以參考《linux通用鏈表》中介紹。
/* 從hash鏈中刪除節點,不需要考慮是否爲鏈中首節點。 * 如果將**pprev設計爲*prev,由於頭節點後hash鏈中節點數據結構不一致, * 那就要特殊處理鏈中首節點的prev,因爲它指向的數據結構與其他節點 * prev指向的不一樣。 */ 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; } static inline void hlist_del(struct hlist_node *n) { __hlist_del(n); n->next = LIST_POISON1; n->pprev = LIST_POISON2; } static inline void hlist_del_init(struct hlist_node *n) { if (n->pprev) { __hlist_del(n); //從hash鏈中刪除節點 INIT_HLIST_NODE(n);//節點初始化 } }
三、添加節點
/* 添加到hash鏈的頭部 */ static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h) { struct hlist_node *first = h->first; /* 將原來的first節點連接到n後面 */ n->next = first; if (first) first->pprev = &n->next; /* 再將n連接到頭結點h */ h->first = n; n->pprev = &h->first; } /* 將n添加到next節點前面,但要保證next節點非空 */ 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; } /* 將next添加到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; /* next不是添加到hash鏈的尾部,則需要設置下一節點的pprev */ if(next->next) next->next->pprev = &next->next; }
四、hash鏈表遍歷
與標準鏈表中遍歷接口類似,有entry/safe之分。
先介紹一條語句{ n = pos->next; 1; }。它是複合語句,返回值爲最後的一條語句1;。即該語句永遠爲真。在hlist_for_each函數中,如果pos爲空則循環結束,如果pos不爲空,那麼pos->next可能爲空,但該循環還不能結束,所以就使用{ n = pos->next; 1; }確保&&後面爲真。
/* 返回hash鏈中的用戶數據結構地址 */ #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 && ({ n = pos->next; 1; }); \ pos = n) /* 在循環內,可訪問用戶定義的數據結構(帶有entry關鍵字) */ #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) /* 從pos的下一個節點開始遍歷 */ #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) /* 從pos節點開始遍歷 */ #define hlist_for_each_entry_from(tpos, pos, member) \ for (; pos&& \ ({ tpos = hlist_entry(pos, typeof(*tpos), member); 1;}); \ pos = pos->next) /* 安全模式下遍歷,n爲循環時使用的臨時變量,只能對pos操作 */ #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)
五、其他
/* 判斷節點h是否在hash鏈表中。 很巧妙使用了pprev,該指針指向前一節點的next域 */ static inline int hlist_unhashed(const struct hlist_node *h) { return !h->pprev; } /* 判斷hash鏈表是否爲空 */ static inline int hlist_empty(const struct hlist_head *h) { return !h->first; }