linux內核部件分析(四)——更強的鏈表klist

     前面我們說到過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。





 


 

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