一個進程的睡眠意味着它的進程狀態標識符被置爲睡眠,而且從調度器的運行隊列中去除,直到某些事件的發生將它們從睡眠態中喚醒,在睡眠態,該進程將不被CPU調度,而且,若是不被喚醒,它將永遠不被運行。html
在驅動中很容易經過調度等方式使當前進程睡眠,可是進程並非在任什麼時候候都是能夠進入睡眠狀態的。 linux
第一條規則是:當運行在原子上下文時不能睡眠:好比持有自旋鎖,順序鎖或者RCU鎖。 數據結構
在關中斷中也不能睡眠。函數
持有信號量時睡眠是合法的,但它所持有的信號量不該該影響喚醒它的進程的執行。另外任何等待該信號量的線程也將睡眠,所以發生在持有信號量時的任何睡眠都應當短暫。ui
進程醒來後應該進行等待事件的檢查,以確保它確實發生了。spa
等待隊列能夠完成進程的睡眠並在事件發生時喚醒它,它由一個進程列表組成。在 Linux 中, 一個等待隊列由一個"等待隊列頭"來管理: .net
linux/wait.h struct __wait_queue_head { spinlock_t lock; struct list_head task_list; }; typedef struct __wait_queue_head wait_queue_head_t;
因爲睡眠的進程頗有可能在等待一箇中斷來改變某些狀態,或通告某些事件的發生,那麼中斷上下文頗有可能修改該等待隊列,因此該結構中的自旋鎖lock必須考慮禁中斷,也即便用spin_lock_irqsave。線程
隊列中的成員是以下數據結構的實例,它們組成了一個雙向鏈表: unix
typedef struct __wait_queue wait_queue_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; };
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