Unix環境高級編程-阻塞訪問原理——等待隊列
一個進程的睡眠意味著它的進程狀態標識符被置為睡眠,並且從調度器的運行隊列中去除,直到某些事件的發生將它們從睡眠態中喚醒,在睡眠態,該進程將不被CPU調度,並且,如果不被喚醒,它將永遠不被運行。
在驅動中很容易通過調度等方式使當前進程睡眠,但是進程並不是在任何時候都是可以進入睡眠狀態的。
第一條規則是:當運行在原子上下文時不能睡眠:比如持有自旋鎖,順序鎖或者RCU鎖。
在關中斷中也不能睡眠。
持有信號量時睡眠是合法的,但它所持有的信號量不應該影響喚醒它的進程的執行。另外任何等待該信號量的線程也將睡眠,因此發生在持有信號量時的任何睡眠都應當短暫。
進程醒來後應該進行等待事件的檢查,以確保它確實發生了。
等待隊列可以完成進程的睡眠並在事件發生時喚醒它,它由一個進程列表組成。在 Linux 中, 一個等待隊列由一個"等待隊列頭"來管理:
linux/wait.h struct __wait_queue_head { spinlock_tlock; struct list_head task_list; }; typedef struct __wait_queue_head wait_queue_head_t;
由於睡眠的進程很有可能在等待一個中斷來改變某些狀態,或通告某些事件的發生,那麽中斷上下文很有可能修改該等待隊列,所以該結構中的自旋鎖lock必須考慮禁中斷,也即使用spin_lock_irqsave。
隊列中的成員是如下數據結構的實例,它們組成了一個雙向鏈表:
typedef struct __wait_queue wait_queue_t; typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, intflags, 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; };
flags的值或者為0,或者為WQ_FLAG_EXCLUSIVE。後者表示等待進程想要被獨占地喚醒。
private指針指向等待進程的task_struct實例。該變量本質上可以指向任何私有數據,單內核只有很少情況下才這麽用。
調用func,喚醒等待進程。
task_list用作一個鏈表元素,將wait_queue_t實例放置到等待隊列中。
為了使用等待隊列,通常需要如下步驟:首先應該建立一個等待隊列頭:
DECLARE_WAIT_QUEUE_HEAD(name);
另外一種方法是靜態聲明,並顯式初始化它:
wait_queue_head_t wait_queue;
init_waitqueue_head(&wait_queue);
接著為使得當前進程進入睡眠,並等待某一事件的發生,需要將它加入到等待隊列中,內核提供了以下函數完成此功能:
wait_event(queue, condition);
wait_event_interruptible(queue, condition);
wait_event_timeout(queue, condition, timeout);
wait_event_interruptible_timeout(queue, condition, timeout);
在所有的形式中,參數queue是要等待的隊列頭,由於這幾個函數都是通過宏實現的,這裏的隊列頭不是指針類型,而是對它的直接使用。條件condition是一個被這些宏在睡眠前後所要求值的任意的布爾表達式。直到條件求值為真,進程持續睡眠。
通過wait_event進入睡眠的進程是不可中斷的,此時進程的state成員置TASK_UNINTERRUPTIBLE位。但是它應該被wait_event_interruptible所替代,它可以被信號中斷,這意味著用戶程序在等待的過程中可以通過信號中斷程序的執行。一個不能被信號中斷的程序很容易激怒使用它的用戶。wait_event函數沒有返回值,而wait_event_interruptible有一個可以識別睡眠被某些信號打斷的返回值-ERESTARTSYS。
wait_event_timeout和wait_event_interruptible_timeout意味著等待一段時間,它以滴答數表示,在這個時間期間超時後,該宏返回一個0值,而不管事件是否發生。
最後,我們需要在其他進程或者線程(也可能是中斷)中通過相對應的函數,喚醒這些隊列上沈睡的進程。內核提供了如下函數:
void wake_up(wait_queue_head_t *queue); void wake_up_interruptible(wait_queue_head_t *queue);
wake_up喚醒所有的在給定隊列上等待的進程。
wake_up_interruptible喚醒所有的在給定隊列上等待的可中斷的睡眠的進程。
盡管wake_up可以替代wake_up_interruptible的功能,但是它們應該使用與wait_event對應的函數。通過等待隊列實現一個管道的讀寫是可行的,內核中fs/pipe.c對管道的實現就是基於等待隊列實現的,盡管它有些復雜。另外對於設備驅動來說,一個溫度采集器在收到讀數據請求後,該進程被放入等待隊列,然後喚醒它的布爾變量在該設備對應的中斷處理程序中被置為真。
註意 wake_up_interruptible的調用可能使多個個睡眠進程醒來,而它們又是獨占訪問某一資源,如何使僅一個進程看到這個真值,這就是WQ_FLAG_EXCLUSIVE的作用,其他進程將繼續睡眠。
等待隊列實現原理
wait_event函數的核心實現如下:
#define __wait_event(wq, condition) do { DEFINE_WAIT(__wait); for (;;) { prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE); if (condition) break; schedule(); } finish_wait(&wq, &__wait); } while (0)
DEFINE_WAIT註冊了一個名為__wait的隊列元素,其中包含一個名為autoremove_wake_function的鉤子函數,它用來喚醒的進程並將該元素從等待隊列中刪除。
prepare_to_wait用來將隊列元素計入等待隊列,並指定進程的state狀態標識為TASK_UNINTERRUPTIBLE,當然對應wait_event_interruptible,則是TASK_INTERRUPTIBLE。
for無限循環決定了當前進程在不滿足condition時總是被調度,其他進程將替換該進程執行。並且這個循環實際上永遠只執行一次,並且只在喚醒時直接
在滿足條件時,finish_wait將進程狀態設置為TASK_RUNNING,並從等待隊列中將其移除。
需要仔細考慮的是for循環的執行,顯然它可能執行一次,也可能是多次,當condition不滿足時,將會產生調度,而在此被調度時,將執行for的下一次循環,那麽prepare_to_wait不是每次都添加一次__wait元素嗎?查看prepare_to_wait代碼可以發現,只有wait->task_list指向的鏈表為空時,也即__wait元素沒有加入任何其他等待隊列時才會把它加入到當前等待隊列中,這也表明一個等待隊列元素只能加入一個等待隊列。
void prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state) { unsigned long flags; wait->flags &= ~WQ_FLAG_EXCLUSIVE; spin_lock_irqsave(&q->lock, flags); if (list_empty(&wait->task_list)) __add_wait_queue(q, wait); set_current_state(state); spin_unlock_irqrestore(&q->lock, flags); }
喚醒一個等待隊列是通過wake_up系列函數完成的,一些列的喚醒函數都有對應的可中斷形式:
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL) #define wake_up_nr(x, nr) __wake_up(x, TASK_NORMAL, nr, NULL) #define wake_up_all(x) __wake_up(x, TASK_NORMAL, 0, NULL) #define wake_up_locked(x) __wake_up_locked((x), TASK_NORMAL)
這裏分析它們的核心實現:
kernel/sched.c void __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); spin_unlock_irqrestore(&q->lock, flags); }
__wake_up首先獲取了自旋鎖,然後調用__wake_up_common。該函數通過list_for_each_entry_safe遍歷等待隊列,如果沒有設置獨占標誌,則根據mode喚醒每個睡眠的進程。nr_exclusiv表示需要喚醒的設置了獨占標誌進程的數目,它在wake_up中設置為1,表明當處理了一個含有WQ_FLAG_EXCLUSIVE標誌進程後,將不再處理,獨占標誌的意義也在於此。另外看到這裏通過func指針執行了真正的喚醒函數。
kernel/sched.c 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; } }
如果含有獨占標誌的進程並不位於隊列尾部,將導致其後的不含有該標誌的進程無法執行,prepare_to_wait_exclusive解決了該問題,它總是將含有獨占標誌的進程插入到隊列尾部,該函數被wait_event_interruptible_exclusive宏調用。
轉自:http://blog.chinaunix.net/uid-20608849-id-3126863.html
Unix環境高級編程-阻塞訪問原理——等待隊列