linux內核hlist分析

在Linux內核中,hlist(哈希鏈表)使用非常廣泛。本文將對其數據結構和核心函數進行分析。

和hlist相關的數據結構有兩個(1)hlist_head (2)hlist_node

 struct hlist_head {
         struct hlist_node *first;
 };
 
 struct hlist_node {
         struct hlist_node *next, **pprev;
 };
顧名思義, hlist_head表示哈希表的頭結點。 哈希表中每一個entry(hlist_head)所對應的都是一個鏈表(hlist),該鏈表的結點由hlist_node表示。

 hlist_head結構體只有一個域,即first。 first指針指向該hlist鏈表的第一個節點。

hlist_node結構體有兩個域,next 和pprev。 next指針很容易理解,它指向下個hlist_node結點,倘若該節點是鏈表的最後一個節點,next指向NULL。

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

在回答這個問題之前,我們先研究另一個問題:爲什麼散列表的實現需要兩個不同的數據結構?

散列表的目的是爲了方便快速的查找,所以散列表通常是一個比較大的數組,否則“衝突”的概率會非常大, 這樣也就失去了散列表的意義。如何做到既能維護一張大表,又能不使用過多的內存呢?就只能從數據結構上下功夫了。所以對於散列表的每個entry,它的結構體中只存放一個指針,解決了佔用空間的問題。現在又出現了另一個問題:數據結構不一致。顯然,如果hlist_node採用傳統的next,prev指針, 對於第一個節點和後面其他節點的處理會不一致。這樣並不優雅,而且效率上也有損失。

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

下面我們再來看一看hlist_node這樣設計之後,插入 刪除這些基本操作會有什麼不一樣。

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;
}
__hlist_del用於刪除節點n。

首先獲取n的下一個節點next, n->pprev指向n的前一個節點的next指針的地址, 這樣×pprev就代表n前一個節點的下一個節點(現在即n本身),第三行代碼*pprev=next;就將n的前一個節點和下一個節點關聯起來了。至此,n節點的前一個節點的關聯工作就完成了,現在再來完成下一個節點的關聯工作。如果n是鏈表的最後一個節點,那麼n->next即爲空, 則無需任何操作,否則,next->pprev = pprev。

給鏈表增加一個節點需要考慮兩個條件:(1)是否爲鏈表的首個節點(2)普通節點。

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;
 }
首先討論條件(1)。

first = h->first; 獲取當前鏈表的首個節點;

n->next = fist;  將n作爲鏈表的首個節點,讓first往後靠;

先來看最後一行 n->pprev - &h->first; 將n的pprev指向hlist_head的first指針,至此關於節點n的關聯工作就做完了。

再來看倒數第二行 h->first = n; 將節點h的關聯工作做完;

最後我們再來看原先的第一個節點的關聯工作,對於它來說,僅僅需要更新一下pprev的關聯信息: first->pprev = &n->next;

接下來討論條件(2)。 這裏也包括兩種情況:a)插在當前節點的前面b)插在當前節點的後面

/* 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;
 }

先討論情況a) 將節點n 插到next之前  (n是新插入的節點)

還是一個一個節點的搞定(一共三個節點), 先搞定節點n

n->pprev = next->prev;   將 next 的pprev 賦值給n->pprev  n取代next的位置

n->next = next;   將next作爲n的下一個節點, 至此節點n的關聯動作完成。

next->pprev = &n->next; next的關聯動作完成。

*(n->pprev) = n;   n->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;
 
         if(next->next)
                 next->next->pprev  = &next->next;
 }
再來看情況b) 將結點next插入到n之後 (next是新插入的節點)

具體步驟就不分析了。 應該也很容易。

下面我還要介紹一個函數:

static inline int hlist_unhashed(const struct hlist_node *h)
 {
         return !h->pprev;
 }
這個函數的目的是判斷該節點是否已經存在hash表中。這裏處理得很巧妙。 判斷前一個節點的next指向的地址是否爲空。


最後我們看一個具體的例子,Linux內核是如何管理pid的。(正好和上一篇介紹pid的文章相呼應:))   基於內核3.0.3

內核初始化時要調用pidhash_init()創建哈希表。 該函數會在 start_kernel()函數裏被調用(init/main.c Line 509)

void __init pidhash_init(void)
 {
         int i, pidhash_size;
 
         pid_hash = alloc_large_system_hash("PID", sizeof(*pid_hash), 0, 18,
                                            HASH_EARLY | HASH_SMALL,
                                            &pidhash_shift, NULL, 4096);
         pidhash_size = 1 << pidhash_shift;
 
         for (i = 0; i < pidhash_size; i++)
                 INIT_HLIST_HEAD(&pid_hash[i]);
 }
從這個函數可以看到內核會在slab上分配一個大小爲pidhash_size的數組,然後爲每一個entry進行初始化(INIT_HLIST_HEAD)

在alloc_pid函數裏

struct pid *alloc_pid(struct pid_namespace *ns)
 {
         struct pid *pid;
         enum pid_type type;
         int i, nr;
         struct pid_namespace *tmp;
         struct upid *upid;
 
         pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL);  /×在slab上分配pid結構體×/
         if (!pid)
                 goto out;
 
         tmp = ns;
         for (i = ns->level; i >= 0; i--) {       /×雖然這裏是for循環,實際只會運行一次,因爲現在只支持global namespace即ns->level=0×/
                 nr = alloc_pidmap(tmp);          /×在各級pid_namespace上尋找並分配pid的值×/
                 if (nr < 0)
                         goto out_free;
 
                 pid->numbers[i].nr = nr;
                 pid->numbers[i].ns = tmp;
                 tmp = tmp->parent;
         }
 
         get_pid_ns(ns);
         pid->level = ns->level;
         atomic_set(&pid->count, 1);
         for (type = 0; type < PIDTYPE_MAX; ++type)
                 INIT_HLIST_HEAD(&pid->tasks[type]);   
 
         upid = pid->numbers + ns->level;
         spin_lock_irq(&pidmap_lock);
         for ( ; upid >= pid->numbers; --upid)
                 hlist_add_head_rcu(&upid->pid_chain,
                                 &pid_hash[pid_hashfn(upid->nr, upid->ns)]);   /×將各級namespace中的upid插入pidhash的哈希表裏×/
         spin_unlock_irq(&pidmap_lock);
 
 out:
         return pid;
 
 out_free:
         while (++i <= ns->level)
                 free_pidmap(pid->numbers + i);
 
         kmem_cache_free(ns->pid_cachep, pid);
         pid = NULL;
         goto out;
 }


注:

(1)本文中如果發現任何錯誤請幫我指出。 非常感謝!

(2)歡迎和大家進行交流。

(3)本文系原創,如需轉載請標明出處。







發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章