休眠
Linux 系統透過 fork system call 建立新的 process,當 process 被生出來後,他的狀態就是「執行中狀態」,也就是 TASK_RUNNING。不過 process 產生後,並不是馬上就能執行,必須被排程器挑選後做 context switch 才是「正在執行中」的 process。我們以 current 代表「正在執行中」的那一個 process,前面所謂的執行中狀態指的是被放在 task queue / run queue(OS 叫 ready queue)中的 process。當 current 正在執行時,很可能被其他 priority 更高的 process 搶奪執行權,這時 current 就會被放回 ready queue 裡,這是因為 Linux 是一個可搶先(preemptive)排程的作業系統。
今天 current 要存取硬體,這時如果硬體裝置出現問題,無法讀寫資料,此時「Linux 驅動程式必須把 current 放到 wait queue 裡睡覺(等待)」。
Wait queue 就是用來讓 current 睡覺的 kernel API。
Process 被放到 wait queue 時的狀態為 TASK_INTERRUPTIBLE 或是 TASK_UNINTERRUPTIBLE。這個時候因為我們的 process 在睡覺了(被放到 wait queue),所以 scheduler 就會再由 ready queue 裡挑一個 process 來執行。 wait_queue的動態配置:
wait_queue_head_t name;
init_waitqueue_head(&name);
Sleep 與 Wake Up
執行 user process 時,會根據 kernel 內建的排程演算法,由 OS 定期強制切換執行的程式,因此 user process 稱為「preemptive(可被搶佔)」。 切換 user process 的是 OS 本身 (Linux kernel)。為 user process 排程的演算法有許多種,但負責切換的都是 kernel/sched.c 裡面實作的 「schedule()」這個 kernel 函式。
Linux 的 kernel space 並不是 preemptive 的,包含驅動程式在內的 kernel 工作內容都不是 preemptive。 但是有個例外的情形,在發生硬體中斷時,中斷函式會強制取得 CPU 控制權。負責在 user process 之間切換的就是 kernel,而 kernel 內部並沒有其它角色負責排程,但是 kernel 內部可以明確放出 CPU 使用權,也就是直接呼叫 schedule()。 特別是驅動程式,有時需等待硬體完成工作,此時就需要暫時放下自己的工作,讓 CPU 去處理其它事情。 為了讓驅動程式能在進行 DMA 之類工作時,可以明確釋放 CPU 使用權,所以 kernel 就提供了 sleep 與 wake up 的功能。
Sleep 有危險
呼叫 sleep_on() 函式後,就能在 kernel 之內進入 sleep,「進入sleep」具體來說是: 中斷 user context 要求 kernel 做的事情,明確把 CPU 使用權交給另一個 user context
void sleep_on(wait_queue_head_t *q);
void interrupt_sleep_on(wait_queue_head_t *q);
void sleep_on_timeout(wait_queue_head_t *q,long timeout);
引數要傳入 wait_queue_head_t 的指標,這個指標需先以 init_waitqueue_head() 初始化:
void init_waitqueue_head(wait_queue_head_t *q);
由於有可能會造成永遠醒不來的情形,在新的kernel已經都不支援sleep_on()的函式了,取而代之都是使用wait_event()的方式,與sleep_on差異在須透過condition的方式進行休眠。
long wait_event(wait_queue_head_t wq, condition);
long wait_event_interruptible(wait_queue_head_t wq, condition);
long wait_event_timeout(wait_queue_head_t wq, condition, long timeout);
long wait_event_interruptible_timeout(wait_queue_head_t wq, condition, long timeout);
Wake up
要讓睡著的人起來,一定要別人叫它,負責個工作的是 wake_up()。wake_up() 的引數跟傳給 sleep_on() 的是同一個指標,但是如果在呼叫 sleep_on() 之前就呼叫 wake_up() 的話,則什麼都不會做,因此可能會造成永遠叫不醒的狀態。
void wake_up(wait_queue_head_t *q);
void wake_up_interruptible(wait_queue_head_t *q);
wake_up()喚醒在queue裡排排睡的所有行程,wait_up_interruptible()只喚醒queue裡當初表示願意因中斷事件而甦醒的行程。
正確的Sleep方式
以 wait_event_timeout() 入睡 以 wake_up() 喚醒
wait_event_timeout() 會:
在滿足 wake up 條件的時候解除睡眠 未滿足 wake up 條件的時候保持睡眠 經過一段時間後,自動解除睡眠
使用這個函式,可以避免「錯過呼叫時間」而造成的「永久睡眠」問題,也可以在不動用 kernel timer 的情況下,達成 timeout 的效果:
long wait_event_timeout(wait_queue_head_t wq, condition, long timeout);
wait_event_timeout巨集定義在 linux/wait.h,第一引數是 wait_queue_head_t 變數,第二引數則是 process 喚醒的條件,第三引數是時間(以 jiffies 為單位). wait_event_timeout() 會在滿足條件或是被喚醒之前讓 process 保持 sleep 狀態,但如果被喚醒之後,條件還是沒成立的話,就會再次落入 sleep 狀態。 另外,也有可被 signal 解除 sleep 的 wait_event_interruptiable_timeout() 巨集可供使用:
long wait_event_interruptible_timeout(wait_queue_head_t wq, condition, long timeout);