在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)歡迎和大家進行交流。