前面我們說到過list_head,這是linux中通用的鏈表形式,雙向循環鏈表,功能強大,實現簡單優雅。可如果您認爲list_head就是鏈表的極致,應該在linux鏈表界一統天下,那可就錯了。據我所知,linux內核代碼中至少還有兩種鏈表能佔有一席之地。一種就是hlist,一種就是本節要介紹的klist。雖然三者不同,但hlist和klist都可以看成是從list_head中發展出來的,用於特殊的鏈表使用情景。hlist是用於哈希表中。衆所周知,哈希表主要就是一個哈希數組,爲了解決映射衝突的問題,常常把哈希數組的每一項做成一個鏈表,這樣有多少重複的都可以鏈進去。但哈希數組的項很多,list_head的話每個鏈表頭都需要兩個指針的空間,在稀疏的哈希表中實在是一種浪費,於是就發明了hlist。hlist有兩大特點,一是它的鏈表頭只需要一個指針,二是它的每一項都可以找到自己的前一節點,也就是說它不再循環,但仍是雙向。令人不解的是,hlist的實現太繞了,比如它明明可以直接指向前一節點,卻偏偏指向指針地址,還是前一節點中指向後一節點的指針地址。即使這種設計在實現時佔便宜,但它理解上帶來的不便已經遠遠超過實現上帶來的小小便利。
同hlist一樣,klist也是爲了適應某類特殊情形的要求。考慮一個被簡化的情形,假設一些設備被鏈接在設備鏈表中,一個線程命令卸載某設備,即將其從設備鏈表中刪除,但這時該設備正在使用中,這時就出現了衝突。當前可以設置臨界區並加鎖,但因爲使用一個設備而鎖住整個設備鏈表顯然是不對的;又或者可以從設備本身做文章,讓線程阻塞,這當然也可以。但我們上節瞭解了kref,就該知道linux對待這種情況的風格,給它一個引用計數kref,等計數爲零就刪除。klist就是這麼幹的,它把kref直接保存在了鏈表節點上。之前說到有線程要求刪除設備,之前的使用仍存在,所以不能實際刪除,但不應該有新的應用訪問到該設備。klist就提供了一種讓節點在鏈表上隱身的方法。下面還是來看實際代碼吧。
klist的頭文件是include/linux/klist.h,實現在lib/klist.c。
struct klist_node;
struct klist {
spinlock_t k_lock;
struct list_head k_list;
void (*get)(struct klist_node *);
void (*put)(struct klist_node *);
} __attribute__ ((aligned (4)));
#define KLIST_INIT(_name, _get, _put) \
{ .k_lock = __SPIN_LOCK_UNLOCKED(_name.k_lock), \
.k_list = LIST_HEAD_INIT(_name.k_list), \
.get = _get, \
.put = _put, }
#define DEFINE_KLIST(_name, _get, _put) \
struct klist _name = KLIST_INIT(_name, _get, _put)
extern void klist_init(struct klist *k, void (*get)(struct klist_node *),
void (*put)(struct klist_node *));
struct klist_node {
void *n_klist; /* never access directly */
struct list_head n_node;
struct kref n_ref;
};
可以看到,klist的鏈表頭是struct klist結構,鏈表節點是struct klist_node結構。先看struct klist,除了包含鏈表需要的k_list,還有用於加鎖的k_lock。剩餘的get()和put()函數是用於struct klist_node嵌入在更大的結構中,這樣在節點初始時調用get(),在節點刪除時調用put(),以表示鏈表中存在對結構的引用。再看struct klist_node,除了鏈表需要的n_node,還有一個引用計數n_ref。還有一個比較特殊的指針n_klist,n_klist是指向鏈表頭struct klist的,但它的第0位用來表示是否該節點已被請求刪除,如果已被請求刪除則在鏈表循環時是看不到這一節點的,循環函數將其略過。現在你明白爲什麼非要在struct klist的定義後加上__attribute__((aligned(4)))。不過說實話這樣在x86下仍然不太保險,但linux選擇了相信gcc,畢竟是多年的戰友和兄弟了,相互知根知底。
看過這兩個結構,想必大家已經較爲清楚了,下面就來看看它們的實現。
/*
* Use the lowest bit of n_klist to mark deleted nodes and exclude
* dead ones from iteration.
*/
#define KNODE_DEAD 1LU
#define KNODE_KLIST_MASK ~KNODE_DEAD
static struct klist *knode_klist(struct klist_node *knode)
{
return (struct klist *)
((unsigned long)knode->n_klist & KNODE_KLIST_MASK);
}
static bool knode_dead(struct klist_node *knode)
{
return (unsigned long)knode->n_klist & KNODE_DEAD;
}
static void knode_set_klist(struct klist_node *knode, struct klist *klist)
{
knode->n_klist = klist;
/* no knode deserves to start its life dead */
WARN_ON(knode_dead(knode));
}
static void knode_kill(struct klist_node *knode)
{
/* and no knode should die twice ever either, see we're very humane */
WARN_ON(knode_dead(knode));
*(unsigned long *)&knode->n_klist |= KNODE_DEAD;
}
前面的四個函數都是內部靜態函數,幫助API實現的。knode_klist()是從節點找到鏈表頭。knode_dead()是檢查該節點是否已被請求刪除。
knode_set_klist設置節點的鏈表頭。knode_kill將該節點請求刪除。細心的話大家會發現這四個函數是對稱的,而且都是操作節點的內部函數。
void klist_init(struct klist *k, void (*get)(struct klist_node *),
void (*put)(struct klist_node *))
{
INIT_LIST_HEAD(&k->k_list);
spin_lock_init(&k->k_lock);
k->get = get;
k->put = put;
}
klist_init,初始化klist。
static void add_head(struct klist *k, struct klist_node *n)
{
spin_lock(&k->k_lock);
list_add(&n->n_node, &k->k_list);
spin_unlock(&k->k_lock);
}
static void add_tail(struct klist *k, struct klist_node *n)
{
spin_lock(&k->k_lock);
list_add_tail(&n->n_node, &k->k_list);
spin_unlock(&k->k_lock);
}
static void klist_node_init(struct klist *k, struct klist_node *n)
{
INIT_LIST_HEAD(&n->n_node);
kref_init(&n->n_ref);
knode_set_klist(n, k);
if (k->get)
k->get(n);
}
又是三個內部函數,add_head()將節點加入鏈表頭,add_tail()將節點加入鏈表尾,klist_node_init()是初始化節點。注意在節點的引用計數初始化時,因爲引用計數變爲1,所以也要調用相應的get()函數。
void klist_add_head(struct klist_node *n, struct klist *k)
{
klist_node_init(k, n);
add_head(k, n);
}
void klist_add_tail(struct klist_node *n, struct klist *k)
{
klist_node_init(k, n);
add_tail(k, n);
}
klist_add_head()將節點初始化,並加入鏈表頭。
klist_add_tail()將節點初始化,並加入鏈表尾。
它們正是用上面的三個內部函數實現的,可見linux內核中對函數複用有很強的執念,其實這裏add_tail和add_head是不用的,縱觀整個文件,也只有klist_add_head()和klist_add_tail()對它們進行了調用。
void klist_add_after(struct klist_node *n, struct klist_node *pos)
{
struct klist *k = knode_klist(pos);
klist_node_init(k, n);
spin_lock(&k->k_lock);
list_add(&n->n_node, &pos->n_node);
spin_unlock(&k->k_lock);
}
void klist_add_before(struct klist_node *n, struct klist_node *pos)
{
struct klist *k = knode_klist(pos);
klist_node_init(k, n);
spin_lock(&k->k_lock);
list_add_tail(&n->n_node, &pos->n_node);
spin_unlock(&k->k_lock);
}
klist_add_after()將節點加到指定節點後面。
klist_add_before()將節點加到指定節點前面。
這兩個函數都是對外提供的API。在list_head中都沒有看到有這種API,所以說需求決定了接口。雖說只有一步之遙,klist也不願讓外界介入它的內部實現。
之前出現的API都太常見了,既沒有使用引用計數,又沒有跳過請求刪除的節點。所以klist的亮點在下面,klist鏈表的遍歷。
struct klist_iter {
struct klist *i_klist;
struct klist_node *i_cur;
};
extern void klist_iter_init(struct klist *k, struct klist_iter *i);
extern void klist_iter_init_node(struct klist *k, struct klist_iter *i,
struct klist_node *n);
extern void klist_iter_exit(struct klist_iter *i);
extern struct klist_node *klist_next(struct klist_iter *i);
以上就是鏈表遍歷需要的輔助結構struct klist_iter,和遍歷用到的四個函數。
struct klist_waiter {
struct list_head list;
struct klist_node *node;
struct task_struct *process;
int woken;
};
static DEFINE_SPINLOCK(klist_remove_lock);
static LIST_HEAD(klist_remove_waiters);
static void klist_release(struct kref *kref)
{
struct klist_waiter *waiter, *tmp;
struct klist_node *n = container_of(kref, struct klist_node, n_ref);
WARN_ON(!knode_dead(n));
list_del(&n->n_node);
spin_lock(&klist_remove_lock);
list_for_each_entry_safe(waiter, tmp, &klist_remove_waiters, list) {
if (waiter->node != n)
continue;
waiter->woken = 1;
mb();
wake_up_process(waiter->process);
list_del(&waiter->list);
}
spin_unlock(&klist_remove_lock);
knode_set_klist(n, NULL);
}
static int klist_dec_and_del(struct klist_node *n)
{
return kref_put(&n->n_ref, klist_release);
}
static void klist_put(struct klist_node *n, bool kill)
{
struct klist *k = knode_klist(n);
void (*put)(struct klist_node *) = k->put;
spin_lock(&k->k_lock);
if (kill)
knode_kill(n);
if (!klist_dec_and_del(n))
put = NULL;
spin_unlock(&k->k_lock);
if (put)
put(n);
}
/**
* klist_del - Decrement the reference count of node and try to remove.
* @n: node we're deleting.
*/
void klist_del(struct klist_node *n)
{
klist_put(n, true);
}
以上的內容乍一看很難理解,其實都是klist實現必須的。因爲使用kref動態刪除,自然需要一個計數降爲零時調用的函數klist_release。
klist_dec_and_del()就是對kref_put()的包裝,起到減少節點引用計數的功能。
至於爲什麼會出現一個新的結構struct klist_waiter,也很簡單。之前說有線程申請刪除某節點,但節點的引用計數仍在,所以只能把請求刪除的線程阻塞,就是用struct klist_waiter阻塞在klist_remove_waiters上。所以在klist_release()調用時還要將阻塞的線程喚醒。knode_kill()將節點設爲已請求刪除。而且還會調用put()函數。
釋放引用計數是調用klist_del(),它通過內部函數klist_put()完成所需操作:用knode_kill()設置節點爲已請求刪除,用klist_dec_and_del()釋放引用,調用可能的put()函數。
/**
* klist_remove - Decrement the refcount of node and wait for it to go away.
* @n: node we're removing.
*/
void klist_remove(struct klist_node *n)
{
struct klist_waiter waiter;
waiter.node = n;
waiter.process = current;
waiter.woken = 0;
spin_lock(&klist_remove_lock);
list_add(&waiter.list, &klist_remove_waiters);
spin_unlock(&klist_remove_lock);
klist_del(n);
for (;;) {
set_current_state(TASK_UNINTERRUPTIBLE);
if (waiter.woken)
break;
schedule();
}
__set_current_state(TASK_RUNNING);
}
klist_remove()不但會調用klist_del()減少引用計數,還會一直阻塞到節點被刪除。這個函數纔是請求刪除節點的線程應該調用的。
int klist_node_attached(struct klist_node *n)
{
return (n->n_klist != NULL);
}
klist_node_attached()檢查節點是否被包含在某鏈表中。
以上是klist的鏈表初始化,節點加入,節點刪除函數。下面是klist鏈表遍歷函數。
struct klist_iter {
struct klist *i_klist;
struct klist_node *i_cur;
};
extern void klist_iter_init(struct klist *k, struct klist_iter *i);
extern void klist_iter_init_node(struct klist *k, struct klist_iter *i,
struct klist_node *n);
extern void klist_iter_exit(struct klist_iter *i);
extern struct klist_node *klist_next(struct klist_iter *i);
klist的遍歷有些複雜,因爲它考慮到了在遍歷過程中節點刪除的情況,而且還要忽略那些已被刪除的節點。宏實現已經無法滿足要求,迫不得已,只能用函數實現,並用struct klist_iter記錄中間狀態。
void klist_iter_init_node(struct klist *k, struct klist_iter *i,
struct klist_node *n)
{
i->i_klist = k;
i->i_cur = n;
if (n)
kref_get(&n->n_ref);
}
void klist_iter_init(struct klist *k, struct klist_iter *i)
{
klist_iter_init_node(k, i, NULL);
}
klist_iter_init_node()是從klist中的某個節點開始遍歷,而klist_iter_init()是從鏈表頭開始遍歷的。
但你又要注意,klist_iter_init()和klist_iter_init_node()的用法又不同。klist_iter_init_node()可以在其後直接對當前節點進行訪問,也可以調用klist_next()訪問下一節點。而klist_iter_init()只能調用klist_next()訪問下一節點。或許klist_iter_init_node()的本意不是從當前節點開始,而是從當前節點的下一節點開始。
static struct klist_node *to_klist_node(struct list_head *n)
{
return container_of(n, struct klist_node, n_node);
}
struct klist_node *klist_next(struct klist_iter *i)
{
void (*put)(struct klist_node *) = i->i_klist->put;
struct klist_node *last = i->i_cur;
struct klist_node *next;
spin_lock(&i->i_klist->k_lock);
if (last) {
next = to_klist_node(last->n_node.next);
if (!klist_dec_and_del(last))
put = NULL;
} else
next = to_klist_node(i->i_klist->k_list.next);
i->i_cur = NULL;
while (next != to_klist_node(&i->i_klist->k_list)) {
if (likely(!knode_dead(next))) {
kref_get(&next->n_ref);
i->i_cur = next;
break;
}
next = to_klist_node(next->n_node.next);
}
spin_unlock(&i->i_klist->k_lock);
if (put && last)
put(last);
return i->i_cur;
}
klist_next()是將循環進行到下一節點。實現中需要注意兩點問題:1、加鎖,根據經驗,單純對某個節點操作不需要加鎖,但對影響整個鏈表的操作需要加自旋鎖。比如之前klist_iter_init_node()中對節點增加引用計數,就不需要加鎖,因爲只有已經擁有節點引用計數的線程纔會特別地從那個節點開始。而之後klist_next()中則需要加鎖,因爲當前線程很可能沒有引用計數,所以需要加鎖,讓情況固定下來。這既是保護鏈表,也是保護節點有效。符合kref引用計數的使用原則。2、要注意,雖然在節點切換的過程中是加鎖的,但切換完訪問當前節點時是解鎖的,中間可能有節點被刪除(這個通過spin_lock就可以搞定),也可能有節點被請求刪除,這就需要注意。首先要忽略鏈表中已被請求刪除的節點,然後在減少前一個節點引用計數時,可能就把前一個節點刪除了。這裏之所以不調用klist_put(),是因爲本身已處於加鎖狀態,但仍要有它的實現。這裏的實現和klist_put()中類似,代碼不介意在加鎖狀態下喚醒另一個線程,但卻不希望在加鎖狀態下調用put()函數,那可能會涉及釋放另一個更大的結構。
void klist_iter_exit(struct klist_iter *i)
{
if (i->i_cur) {
klist_put(i->i_cur, false);
i->i_cur = NULL;
}
}
klist_iter_exit(),遍歷結束函數。在遍歷完成時調不調無所謂,但如果想中途結束,就一定要調用klist_iter_exit()。
klist主要用於設備驅動模型中,爲了適應那些動態變化的設備和驅動,而專門設計的鏈表。klist並不通用,但它真的很新奇。 我看到它時,震驚於鏈表竟然可以專門異化成這種樣子。如果你是松耦合的結構,如果你手下淨是些桀驁不馴的傢伙,那麼不要只考慮kref,你可能還需要klist。