源出處: http://www.startos.com/linux/tips/2011011921499_6.html
4.2 wake_up 的實現細節
\kernel \sched.c
/*
* 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 sync, void *key)
{
struct list_head *tmp, *next;
list_for_each_safe(tmp, next, &q->task_list) {
wait_queue_t *curr = list_entry(tmp, wait_queue_t, task_list);
unsigned flags = curr->flags;
if (curr->func(curr, mode, sync, key) &&
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
// 迴圈結束的條件包括list_for_each_safe本身、排他性(flags & WQ_FLAG_EXCLUSIVE)以及喚醒非0個進程,!--nr_exclusive非常巧妙,當傳入0時,!--nr_exclusive的結果總是0,if條件不可能成立,就無法break,即表示喚醒所有的進程。對於非WQ_FLAG_EXCLUSIVE進程,由於(flags & WQ_FLAG_EXCLUSIVE)為0後,就不計算!--nr_exclusive,因此這個過程可以喚醒所有的非 WQ_FLAG_EXCLUSIVE進程。但遇到WQ_FLAG_EXCLUSEVE之後的任意進程無法喚醒。
最終哪個進程運行是由 schedule決定的。
/**
* __wake_up - wake up threads blocked on a waitqueue.
* @q: the waitqueue
* @mode: which threads
* @nr_exclusive: how many wake-one or wake-many threads to wake up
* @key: is directly passed to the wakeup function
*/
void fastcall __wake_up(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, void *key)
{
unsigned long flags;
spin_lock_irqsave(&q->lock, flags);
__wake_up_common(q, mode, nr_exclusive, 0, key);
//通用的wakeup,不可重新進入的,需要__wake_up對之進行封裝
spin_unlock_irqrestore(&q->lock, flags);
}
EXPORT_SYMBOL(__wake_up);
根據int nr_exclusive值喚醒對應的進程,只是更改了進程的狀態,具體何時運行由schedule決定。
並沒有將喚醒的進程從等待隊列中刪除,只有當schedule獲得cpu時才從等待隊列中刪除。
\include\linux\wait.h
void FASTCALL(__wake_up(wait_queue_head_t *q, unsigned int mode, int nr, void *key));
extern void FASTCALL(__wake_up_locked(wait_queue_head_t *q, unsigned int mode));
extern void FASTCALL(__wake_up_sync(wait_queue_head_t *q, unsigned int mode, int nr));
#define wake_up(x) __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_nr(x, nr) __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, nr, NULL)
#define wake_up_all(x) __wake_up(x, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE, 0, NULL)
#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
#define wake_up_interruptible_all(x) __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)
各種wakeup通過宏定義的形式本質上就是一個函數__wake_up,但對外的介面不一樣,這樣對外的意義明確,相當於採用了預設參數,而不是在各個wakeup內部調用函數,省掉了函數調用的開銷。在實現代碼複用的同時保證了對外的明確介面,值得借鑒。
#define wake_up_locked(x) __wake_up_locked((x), TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE)
#define wake_up_interruptible_sync(x) __wake_up_sync((x),TASK_INTERRUPTIBLE, 1)
5 獨佔等待和進階休眠
5.1 獨佔等待
當一個進程調用 wake_up 在等待隊列上,所有的在這個隊列上等待的進程被置為可啟動並執行。 這在許多情況下是正確的做法。但有時,可能只有一個被喚醒的進程將成功獲得需要的資源,而其餘的將再次休眠。這時如果等待隊列中的進程數目大,這可能嚴重降低系統效能。為此,核心開發人員增加了一個“獨佔等待”選項。它與一個正常的睡眠有 2 個重要的不同:
2 當等待隊列入口設定了 WQ_FLAG_EXCLUSEVE 標誌,它被添加到等待隊列的尾部;否則,添加到頭部。因為喚醒一個WQ_FLAG_EXCLUSEVE標誌的進程後就不再喚醒其他任意類型的進程。添加在尾部可以保證FIFO。
2 當 wake_up 被在一個等待隊列上調用, 它在喚醒第一個有 WQ_FLAG_EXCLUSIVE 標誌的進程後停止喚醒。但核心仍然會喚醒所有的非獨佔等待進程,因為所有的非WQ_FLAG_EXCLUSIVE進程在隊頭。
採用獨佔等待要滿足以下條件:
2 希望對資源進行有效競爭;
2 當資源可用時,喚醒一個進程就足夠來完全消耗資源;
2 所有使用該資源的進程都應採用統一的獨佔等待規則。
使一個進程進入獨佔等待,可調用:
void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue_t *wait, int state);
注意:無法使用通用的wait_event 和它的變體宏函數來進行獨佔等待。
此時需要使用休眠的進階特性,利用等待隊列的prepare_to_wait_exclusive和finish_wait介面函數手動編寫相關代碼,
5.2 進階休眠的基本步驟:
(1)分配和初始化一個 wait_queue_t 結構, 隨後將其添加到正確的等待隊列。
(2)設定進程狀態,標記為休眠。TASK_RUNNING 意思是進程能夠運行。有 2 個狀態指示一個進程是在睡眠: TASK_INTERRUPTIBLE 和 TASK_UNTINTERRUPTIBLE。2.6 核心的驅動代碼通常不需要直接操作進程狀態。但如果需要這樣做使用的代碼是:
void set_current_state(int new_state);
在老的代碼中, 你常常見到如此的東西:current->state = TASK_INTERRUPTIBLE; 但是象這樣直接改變 current 是不推薦的,當資料結構改變時這樣的代碼將會失效。通過改變 current 狀態,只改變了調度器對待進程的方式,但進程還未讓出處理器。
(3)最後一步是放棄處理器。 但必須先檢查進入休眠的條件。如果不做檢查會引入競態: 如果決定休眠後,在做上述準備工作到真正調用schedule時,若等待的條件變為真,不對條件重新進行判斷,則你可能錯過喚醒且長時間休眠。因此典型的代碼下:
if (!condition) schedule();
在調用schedule前,應對條件再次進行檢查。
(4)更改進程狀態並將進程從等待隊列中刪除。
如果代碼是從 schedule 返回,則進程肯定處於TASK_RUNNING 狀態。 如果不需睡眠而跳過對 schedule 的調用,必須將任務狀態重設為 TASK_RUNNING。無論是否調用過schedule,都需要從等待隊列中去除這個進程,否則它可能被多次喚醒。