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

來源:互聯網
上載者:User

    當你必須一個複雜的系統,協調系統的方方面面,靈活地支援各種機制和策略,即使很簡單的問題也會變得很複雜。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_EXCLUSIVE0x01void *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的使用——實現completioncompletion是一種建立在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)#endifstatic 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 __schedwait_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 __scheddo_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 __schedwait_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 __schedwait_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;elsex->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時也可能沒有線程阻塞,也許只用在特殊情況下或者較為寬鬆的場合。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.