linux內核部件分析(十一)——waitqueue與線程的阻塞

    當你必須一個複雜的系統,協調系統的方方面面,靈活地支持各種機制和策略,即使很簡單的問題也會變得很複雜。linux絕對就是這樣一個複雜的系統。所以我們要理解它,儘量從原理的角度去理解事務的處理流程,儘量避免各種細枝末節的干擾,儘量規避那些足以壓垮自己的龐然大物。(儘管細緻末節和龐然大物很可能就是linux閃光的地方,但我們還是小心爲上。)

原理

    現在我們來考慮linux中線程的阻塞。它的原理很簡單。我們有一個要阻塞的線程A和要喚醒它的線程B(當然也可以是中斷處理例程ISR),有一個兩者共知的等待隊列Q(也許這個等待隊列屬於一個信號量什麼的)。首先是線程A阻塞,要加入等待隊列Q,需要先申請一個隊列節點N,節點N中包含指向線程A的線程控制塊(TCB)的指針,然後A就可以將自己的線程狀態設爲阻塞,並調用schedule()將自己踢出CPU的就緒隊列。過了一定時間,線程B想要喚醒等待隊列Q中的線程,它只需要獲得線程A的TCB指針,將線程A狀態設爲就緒即可。等線程A恢復運行,將節點N退出等待隊列Q,完成整個從阻塞到恢復的流程。

    原理講起來總是沒意思的,下面我們還是看代碼吧。我們規避複雜的任務狀態轉換和調度的內容,即使對等待隊列的分析,也是按照從基礎到擴展的順序。代碼出現在三個地方:include/linux/wait.h , kernel/wait.c, kernel/sched.c。不用說wait.h是頭文件,wait.c是實現的地方,而sched.c則體現了waitqueue的一種應用(實現completion)。爲了更好地分析completion,我們還需要include/linux/completion.h。


waitqueue實現

我們仍然先看數據結構。

struct __wait_queue_head {
	spinlock_t lock;
	struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int flags, void *key);

int default_wake_function(wait_queue_t *wait, unsigned mode, int flags, void *key);

struct __wait_queue {
	unsigned int flags;
#define WQ_FLAG_EXCLUSIVE	0x01
	void *private;
	wait_queue_func_t func;
	struct list_head task_list;
};

typedef struct __wait_queue wait_queue_t;


其中,wait_queue_head_t 就是等待隊列頭,wait_queue_t 就是隊列節點。

wait_queue_head_t 包括一個自旋鎖lock,還有一個雙向循環隊列task_list,這在我們的預料之內。

wait_queue_t 則包括較多,我們先來劇透一下。

    flags變量只可能是0或者WQ_FLAG_EXCLUSIVE。flags標誌隻影響等待隊列喚醒線程時的操作,置爲WQ_FLAG_EXCLUSIVE則每次只允許喚醒一個線程,爲0則無限制。

    private指針,其實就是指向TCB的指針。

    func是一個函數指針,指向用於喚醒隊列中線程的函數。雖然提供了默認的喚醒函數default_wake_function,但也允許靈活的設置隊列的喚醒函數。

    task_list是一個雙向循環鏈表節點,用於鏈入等待隊列的鏈表。


依照舊例,waitqueue在數據結構之後爲我們提供了豐富的初始化函數。因爲太多了,我們只好分段列出。

#define __WAITQUEUE_INITIALIZER(name, tsk) {				\
	.private	= tsk,						\
	.func		= default_wake_function,			\
	.task_list	= { NULL, NULL } }

#define DECLARE_WAITQUEUE(name, tsk)					\
	wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

#define __WAIT_QUEUE_HEAD_INITIALIZER(name) {				\
	.lock		= __SPIN_LOCK_UNLOCKED(name.lock),		\
	.task_list	= { &(name).task_list, &(name).task_list } }

#define DECLARE_WAIT_QUEUE_HEAD(name) \
	wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

這是用宏定義,在聲明變量時進行的初始化。

void __init_waitqueue_head(wait_queue_head_t *q, struct lock_class_key *key)
{
	spin_lock_init(&q->lock);
	lockdep_set_class(&q->lock, key);
	INIT_LIST_HEAD(&q->task_list);
}

#define init_waitqueue_head(q)				\
	do {						\
		static struct lock_class_key __key;	\
							\
		__init_waitqueue_head((q), &__key);	\
	} while (0)

#ifdef CONFIG_LOCKDEP
# define __WAIT_QUEUE_HEAD_INIT_ONSTACK(name) \
	({ init_waitqueue_head(&name); name; })
# define DECLARE_WAIT_QUEUE_HEAD_ONSTACK(name) \
	wait_queue_head_t name = __WAIT_QUEUE_HEAD_INIT_ONSTACK(name)
#else
# define DECLARE_WAIT_QUEUE_HEAD_ONSTACK(name) DECLARE_WAIT_QUEUE_HEAD(name)
#endif
這一段代碼其實不要也可以,但因爲是簡單的細節,所以我們也覆蓋到了。

init_wait_queue_head()對等待隊列頭進行初始化。

另外定義了宏DECLARE_WAIT_QUEUE_HEAD_ONSTACK。根據配置是否使用CONFIG_LOCKDEP,決定其實現。

spinlock很複雜,配置了CONFIG_LOCKDEP就會定義一個局部靜態變量__key對spinlock使用的正確性進行檢查。檢查的過程很複雜,但既然是檢查,就是可以

砍掉的。因爲使用了局部靜態變量,所以只能檢查定義在棧上的變量,所以是DECLARE_WAIT_QUEUE_HEAD_ONSTACK。很多使用spinlock的地方都可以看到

這種檢查。


static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p)
{
	q->flags = 0;
	q->private = p;
	q->func = default_wake_function;
}

static inline void init_waitqueue_func_entry(wait_queue_t *q,
					wait_queue_func_t func)
{
	q->flags = 0;
	q->private = NULL;
	q->func = func;
}

static inline int waitqueue_active(wait_queue_head_t *q)
{
	return !list_empty(&q->task_list);
}
init_waitqueue_entry()和init_waitqueue_func_entry()是用於初始化waitqueue的函數。

waitqueue_active()查看隊列中是否有等待線程。


static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new)
{
	list_add(&new->task_list, &head->task_list);
}

/*
 * Used for wake-one threads:
 */
static inline void __add_wait_queue_tail(wait_queue_head_t *head,
						wait_queue_t *new)
{
	list_add_tail(&new->task_list, &head->task_list);
}

static inline void __remove_wait_queue(wait_queue_head_t *head,
							wait_queue_t *old)
{
	list_del(&old->task_list);
}
__add_wait_queue()將節點加入等待隊列頭部。

__add_wait_queue_tail()將節點加入等待隊列尾部。

__remove_wait_queue()將節點從等待隊列中刪除。

這三個都是簡單地用鏈表操作實現。


void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
	unsigned long flags;

	wait->flags &= ~WQ_FLAG_EXCLUSIVE;
	spin_lock_irqsave(&q->lock, flags);
	__add_wait_queue(q, wait);
	spin_unlock_irqrestore(&q->lock, flags);
}

void add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait)
{
	unsigned long flags;

	wait->flags |= WQ_FLAG_EXCLUSIVE;
	spin_lock_irqsave(&q->lock, flags);
	__add_wait_queue_tail(q, wait);
	spin_unlock_irqrestore(&q->lock, flags);
}

void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
	unsigned long flags;

	spin_lock_irqsave(&q->lock, flags);
	__remove_wait_queue(q, wait);
	spin_unlock_irqrestore(&q->lock, flags);
}

add_wait_queue()將節點加入等待隊列頭部。

add_wait_queue_exclusive()將節點加入等待隊列尾部。

remove_wait_queue()將節點從等待隊列中刪除。

這裏三個函數和前面三個函數最大的區別就是這裏加了禁止中斷的自旋鎖。從此也可以看出linux代碼的一個特色。以雙下劃線前綴的函數往往是供內部調用的,即使外界使用也要清楚此函數的功能,比如前面的__add_wait_queue()等三個函數,就只能在以加帶關中斷的自旋鎖時才能調用,目的是省去重複的加鎖。而add_wait_queue()等函數則更爲穩重一些。


或許你覺得不可思議,但waitqueue就是這麼簡單。下面我們來看看是怎樣用它來實現completion的。


waitqueue的使用——實現completion


completion是一種建立在waitqueue之上的信號量機制,它的接口簡單,功能更簡單,是waitqueue之上最好的封裝例子。

struct completion {
	unsigned int done;
	wait_queue_head_t wait;
};
completion的結構很簡單,用done來進行計數,用wait保存等待隊列。

#define COMPLETION_INITIALIZER(work) \
	{ 0, __WAIT_QUEUE_HEAD_INITIALIZER((work).wait) }

#define COMPLETION_INITIALIZER_ONSTACK(work) \
	({ init_completion(&work); work; })

#define DECLARE_COMPLETION(work) \
	struct completion work = COMPLETION_INITIALIZER(work)

#ifdef CONFIG_LOCKDEP
# define DECLARE_COMPLETION_ONSTACK(work) \
	struct completion work = COMPLETION_INITIALIZER_ONSTACK(work)
#else
# define DECLARE_COMPLETION_ONSTACK(work) DECLARE_COMPLETION(work)
#endif

static inline void init_completion(struct completion *x)
{
	x->done = 0;
	init_waitqueue_head(&x->wait);
}

/* reinitialize completion */
#define INIT_COMPLETION(x) ((x).done = 0)

以上是completion結構的初始宏定義和初始化函數。我們又在其中看到了CONFIG_LOCKDEP,已經熟悉了。

/**
 * wait_for_completion: - waits for completion of a task
 * @x:  holds the state of this particular completion
 *
 * This waits to be signaled for completion of a specific task. It is NOT
 * interruptible and there is no timeout.
 *
 * See also similar routines (i.e. wait_for_completion_timeout()) with timeout
 * and interrupt capability. Also see complete().
 */
void __sched wait_for_completion(struct completion *x)
{
	wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_UNINTERRUPTIBLE);
}

static long __sched
wait_for_common(struct completion *x, long timeout, int state)
{
	might_sleep();

	spin_lock_irq(&x->wait.lock);
	timeout = do_wait_for_common(x, timeout, state);
	spin_unlock_irq(&x->wait.lock);
	return timeout;
}


static inline long __sched
do_wait_for_common(struct completion *x, long timeout, int state)
{
	if (!x->done) {
		DECLARE_WAITQUEUE(wait, current);

		wait.flags |= WQ_FLAG_EXCLUSIVE;
		__add_wait_queue_tail(&x->wait, &wait);
		do {
			if (signal_pending_state(state, current)) {
				timeout = -ERESTARTSYS;
				break;
			}
			__set_current_state(state);
			spin_unlock_irq(&x->wait.lock);
			timeout = schedule_timeout(timeout);
			spin_lock_irq(&x->wait.lock);
		} while (!x->done && timeout);
		__remove_wait_queue(&x->wait, &wait);
		if (!x->done)
			return timeout;
	}
	x->done--;
	return timeout ?: 1;
}


wait_for_completion()將線程阻塞在completion上。關鍵過程在計數值爲0時調用do_wait_for_common阻塞。
do_wait_for_common()首先用DECLARE_WAITQUEUE()定義一個初始化好的wait_queue_t,並調用__add_wait_queuetail()將節點加入等待隊列尾部。然後調用signal_pending_state()檢查線程信號與等待狀態的情況,如果允許信號響應並且有信號阻塞在線程上,自然不必再阻塞了,直接返回-ERESTARTSYS。否則調用__set_current_state()設置線程狀態(線程阻塞的狀態分TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE,前者允許信號中斷,後者則不允許),並調用schedule_timeout()將當前線程從就緒隊列換出。注意completion會在被喚醒時檢查計數是否可被佔用,有時喚醒了卻無法佔用時只得再次阻塞。最後獲得計數後調用__remove_wait_queue()將局部變量節點從等待隊列中刪除。

do_wait_for_common()最後一行的c語句不符合標準,這也是gcc擴展的一部分。timeout爲0時返回1,否則返回timeout值。
schedule_timeout()的功能是使當前線程至少睡眠timeout個jiffies時間片,timeout值爲MAX_SCHEDULE_TIMEOUT時無限睡眠。返回值爲0,如因響應信號而提前恢復,則返回剩餘的timeout計數。

unsigned long __sched
wait_for_completion_timeout(struct completion *x, unsigned long timeout)
{
	return wait_for_common(x, timeout, TASK_UNINTERRUPTIBLE);
}

int __sched wait_for_completion_interruptible(struct completion *x)
{
	long t = wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_INTERRUPTIBLE);
	if (t == -ERESTARTSYS)
		return t;
	return 0;
}

unsigned long __sched
wait_for_completion_interruptible_timeout(struct completion *x,
					  unsigned long timeout)
{
	return wait_for_common(x, timeout, TASK_INTERRUPTIBLE);
}

int __sched wait_for_completion_killable(struct completion *x)
{
	long t = wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_KILLABLE);
	if (t == -ERESTARTSYS)
		return t;
	return 0;
}
wait_for_completion_timeout()使用帶超時時間的阻塞。
wait_for_completion_interruptible()使用允許信號打斷的阻塞。
wait_for_completion_interruptible_timeout()使用帶超時時間的允許信號打斷的阻塞。
wait_for_completion_killable()使用允許被殺死的阻塞。
四者都是wait_for_completion()的變種,通過wait_for_common()調用do_wait_for_common()實現。


void complete(struct completion *x)
{
	unsigned long flags;

	spin_lock_irqsave(&x->wait.lock, flags);
	x->done++;
	__wake_up_common(&x->wait, TASK_NORMAL, 1, 0, NULL);
	spin_unlock_irqrestore(&x->wait.lock, flags);
}

/*
 * The core wakeup function. Non-exclusive wakeups (nr_exclusive == 0) just
 * wake everything up. If it's an exclusive wakeup (nr_exclusive == small +ve
 * number) then we wake all the non-exclusive tasks and one exclusive task.
 *
 * There are circumstances in which we can try to wake a task which has already
 * started to run but is not in state TASK_RUNNING. try_to_wake_up() returns
 * zero in this (rare) case, and we handle it by continuing to scan the queue.
 */
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
			int nr_exclusive, int wake_flags, void *key)
{
	wait_queue_t *curr, *next;

	list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
		unsigned flags = curr->flags;

		if (curr->func(curr, mode, wake_flags, key) &&
				(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
			break;
	}
}
complete()喚醒阻塞的線程。通過調用__wake_up_common()實現。

這裏curr->func()調用的一般是default_wake_function()。

int default_wake_function(wait_queue_t *curr, unsigned mode, int wake_flags,
			  void *key)
{
	return try_to_wake_up(curr->private, mode, wake_flags);
}
default_wake_function()將正睡眠的線程喚醒,調用try_to_wake_up()實現。try_to_wake_up()內容涉及TCB狀態等問題,我們將其忽略。


void complete_all(struct completion *x)
{
	unsigned long flags;

	spin_lock_irqsave(&x->wait.lock, flags);
	x->done += UINT_MAX/2;
	__wake_up_common(&x->wait, TASK_NORMAL, 0, 0, NULL);
	spin_unlock_irqrestore(&x->wait.lock, flags);
}
complete_all()喚醒等待在completion上的所有線程。


bool try_wait_for_completion(struct completion *x)
{
	int ret = 1;

	spin_lock_irq(&x->wait.lock);
	if (!x->done)
		ret = 0;
	else
		x->done--;
	spin_unlock_irq(&x->wait.lock);
	return ret;
}
try_wait_for_completion()試圖在不阻塞情況下獲得信號量計數。


/**
 *	completion_done - Test to see if a completion has any waiters
 *	@x:	completion structure
 *
 *	Returns: 0 if there are waiters (wait_for_completion() in progress)
 *		 1 if there are no waiters.
 *
 */
bool completion_done(struct completion *x)
{
	int ret = 1;

	spin_lock_irq(&x->wait.lock);
	if (!x->done)
		ret = 0;
	spin_unlock_irq(&x->wait.lock);
	return ret;
}
completion_done()檢查是否有線程阻塞。但這裏實現過於簡單,因爲在返回0時也可能沒有線程阻塞,也許只用在特殊情況下或者較爲寬鬆的場合。














發佈了49 篇原創文章 · 獲贊 32 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章