天天看點

Linux 程序的睡眠和喚醒

作者:ElevenLord

Linux程序的睡眠和喚醒

在Linux中,僅等待CPU時間的程序稱為就緒程序,它們被放置在一個運作隊列中,一個就緒程序的狀 态标志位為 TASK_RUNNING。一旦一個運作中的程序時間片用完, Linux 核心的排程器會剝奪這個程序對CPU的控制權,并且從運作隊列中選擇一個合适的程序投入運作。

當然,一個程序也可以主動釋放CPU的控制權。函數 schedule() 是一個排程函數,它可以被一個程序主動調用,進而排程其它程序占用 CPU。一旦這個主動放棄 CPU 的程序被重新排程占用 CPU,那麼它将從上次停止執行的位置開始執行,也就是說它将從調用 schedule() 的下一行代碼處開始執行。

有時候,程序需要等待直到某個特定的事件發生,例如裝置初始化完成、I/O 操作完成或定時器到時等。在這種情況下,程序則必須從運作隊列移出,加入到一個等待隊列中,這個時候程序就進入了睡眠狀态。

Linux 中的程序睡眠狀态有兩種:

  • 一種是可中斷的睡眠狀态,其狀态标志位TASK_INTERRUPTIBLE。
  • 另一種是不可中斷 的睡眠狀态,其狀态标志位為TASK_UNINTERRUPTIBLE。

可中斷的睡眠狀态的程序會睡眠直到某個條件變為真,比如說産生一個硬體中斷、釋放 程序正在等待的系統資源或是傳遞一個信号都可以是喚醒程序的條件。不可中斷睡眠狀态與可中斷睡眠狀态類似,但是它有一個例外,那就是把信号傳遞到這種睡眠 狀态的程序不能改變它的狀态,也就是說它不響應信号的喚醒。不可中斷睡眠狀态一般較少用到,但在一些特定情況下這種狀态還是很有用的,比如說:程序必須等 待,不能被中斷,直到某個特定的事件發生。

在現代的 Linux 作業系統中,程序一般都是用調用 schedule() 的方法進入睡眠狀态的,下面的代碼示範了如何讓正在運作的程序進入睡眠狀态。

sleeping_task = current;
set_current_state(TASK_INTERRUPTIBLE);
schedule();
func1();
/* Rest of the code ... */
           

在第一個語句中,程式存儲了一份程序結構指針 sleeping_task,current 是一個宏,它指向正在執行的程序結構。

set_current_state() 将該程序的狀态從執行狀态 TASK_RUNNING 變成睡眠狀态 TASK_INTERRUPTIBLE。如果 schedule() 是被一個狀态為 TASK_RUNNING 的程序排程,那麼 schedule() 将排程另外一個程序占用CPU。

如果 schedule() 是被一個狀态為 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 的程序排程,那麼還有一個附加的步驟将被執行:目前執行的程序在另外一個程序被排程之前會被從運作隊列中移出,這将導緻正在運作的那個程序進入睡眠,因為它已經不在運作隊列中了。

我們可以使用下面的這個函數将剛才那個進入睡眠的程序喚醒。

wake_up_process(sleeping_task);
           

在調用了 wake_up_process() 以後,這個睡眠程序的狀态會被設定為 TASK_RUNNING,而且排程器會把它加入到運作隊列中去。當然,這個程序隻有在下次被排程器排程到的時候才能真正地投入運作。

無效喚醒

幾乎在所有的情況下,程序都會在檢查了某些條件之後,發現條件不滿足才進入睡眠。可是有的時候程序卻會在判定條件為真後開始睡眠,如果這樣的話程序就會無限期地休眠下去,這就是所謂的無效喚醒問題。

在作業系統中,當多個程序都企圖對共享資料進行某種處理,而 最後的結果又取決于程序運作的順序時,就會發生競争條件,這是作業系統中一個典型的問題,無效喚醒恰恰就是由于競争條件導緻的。

設想有兩個程序A 和B,A 程序正在處理一個連結清單,它需要檢查這個連結清單是否為空,如果不空就對連結清單裡面的資料進行一些操作,同時B程序也在往這個連結清單添加節點。當這個連結清單是空的時候,由于無資料可操作,這時A程序就進入睡眠,當B程序向連結清單裡面添加了節點之後它就喚醒A 程序,其代碼如下:

A程序:

1 spin_lock(&list_lock);
2 if (list_empty(&list_head)) {
3     spin_unlock(&list_lock);
4     set_current_state(TASK_INTERRUPTIBLE);
5     schedule();
6     spin_lock(&list_lock);
7 }
8
9 /* Rest of the code ... */
10 spin_unlock(&list_lock);
           

B程序:

100 spin_lock(&list_lock);
101 list_add_tail(&list_head, new_node);
102 spin_unlock(&list_lock);
103 wake_up_process(processa_task);
           

這裡會出現一個問題,假如當A程序執行到第3行後第4行前的時候,B程序被另外一個處理器排程投入運作。在這個時間片内,B程序執行完了它所有的指令,是以它試圖喚醒A程序,而此時的A程序還沒有進入睡眠,是以喚醒操作無效。

在這之後,A 程序繼續執行,它會錯誤地認為這個時候連結清單仍然是空的,于是将自己的狀态設定為 TASK_INTERRUPTIBLE 然後調用 schedule() 進入睡 眠。由于錯過了B程序喚醒,它将會無限期的睡眠下去,這就是無效喚醒問題,因為即使連結清單中有資料需要處理,A 程序也還是睡眠了。

避免無效喚醒

如何避免無效喚醒問題呢?

我們發現無效喚醒主要發生在檢查條件之後和程序狀态被設定為睡眠狀态之前,本來B程序的 wake_up_process() 提供了一次将A程序狀态置為 TASK_RUNNING 的機會,可惜這個時候A程序的狀态仍然是 TASK_RUNNING,是以 wake_up_process() 将A程序狀态從睡眠狀态轉變為運作狀态的努力 沒有起到預期的作用。

要解決這個問題,必須使用一種保障機制使得判斷連結清單為空和設定程序狀态為睡眠狀态成為一個不可分割的步驟才行,也就是必須消除競争條 件産生的根源,這樣在這之後出現的 wake_up_process() 就可以起到喚醒狀态是睡眠狀态的程序的作用了。

找到了原因後,重新設計一下A程序的代碼結構,就可以避免上面例子中的無效喚醒問題了。

A程序:

1 set_current_state(TASK_INTERRUPTIBLE);
2 spin_lock(&list_lock);
3 if (list_empty(&list_head)) {
4     spin_unlock(&list_lock);
5     schedule();
6     spin_lock(&list_lock);
7 }
8 set_current_state(TASK_RUNNING);
9
10 /* Rest of the code ... */
11 spin_unlock(&list_lock);
           

可以看到,這段代碼在測試條件之前就将目前執行程序狀态轉設定成 TASK_INTERRUPTIBLE 了,并且在連結清單不為空的情況下又将自己置為 TASK_RUNNING 狀态。

這樣一來如果B程序在A程序程序檢查了連結清單為空以後調用 wake_up_process(),那麼A程序的狀态就會自動由原來 TASK_INTERRUPTIBLE 變成 TASK_RUNNING,此後即使程序又調用了 schedule(),由于它現在的狀态是 TASK_RUNNING,是以仍然不會被從運作隊列中移出,因而不會錯誤的進入睡眠,當然也就避免了無效喚醒問題。

Linux核心的例子

在Linux作業系統中,核心的穩定性至關重要,為了避免在Linux作業系統核心中出現無效喚醒問題,Linux核心在需要程序睡眠的時候應該使用類似如下的操作:

/* q 是我們希望睡眠的等待隊列 */
DECLARE_WAITQUEUE(wait, current);
add_wait_queue(q, &wait);
set_current_state(TASK_INTERRUPTIBLE);
/* condition 是等待的條件 */
while (!condition) {
    schedule();
}
set_current_state(TASK_RUNNING);
remove_wait_queue(q, &wait);
           

上面的操作,使得程序通過下面的一系列步驟安全地将自己加入到一個等待隊列中進行睡眠:首先調用 DECLARE_WAITQUEUE() 建立一個等待隊列的項,然後調用 add_wait_queue() 把自己加入到等待隊列中,并且将程序的狀态設定為 TASK_INTERRUPTIBLE 或者 TASK_INTERRUPTIBLE。

然後循環檢查條件是否為真:如果是的話就沒有必要睡眠,如果條件不為真,就調用 schedule()。當程序檢查的條件滿足後,程序又将自己設定為 TASK_RUNNING 并調用 remove_wait_queue() 将自己移出等待隊列。

從上面可以看到,Linux的核心代碼維護者也是在程序檢查條件之前就設定程序的狀态為睡眠狀态,然後才循環檢查條件。如果在程序開始睡眠之前條件就已經達成了,那麼循環會退出并用 set_current_state() 将自己的狀态設定為就緒,這樣同樣保證了程序不會存在錯誤的進入睡眠的傾向,當然也就不會導緻出現無效喚醒問題。

下面讓我們用 Linux 核心中的執行個體來看看其是如何避免無效睡眠的,這段代碼出自 Linux2.6 的核心 (/kernel/sched.c):

/* Wait for kthread_stop */
set_current_state(TASK_INTERRUPTIBLE);
while (!kthread_should_stop()) {
    schedule();
    set_current_state(TASK_INTERRUPTIBLE);
}
__set_current_state(TASK_RUNNING);
return 0;
           

上面的這些代碼屬于遷移服務線程 migration_thread,這個線程不斷地檢查 kthread_should_stop(),直到 kthread_should_stop() 傳回 1 它才可以退出循環,也就是說隻要 kthread_should_stop() 傳回 0 該程序就會一直睡眠。

從代碼中我們可以看出,檢查 kthread_should_stop() 确實是在程序的狀态被置為 TASK_INTERRUPTIBLE 後才開始執行的。是以,如果在條件檢查之後但是在 schedule() 之前有其他程序試圖喚醒它,那麼該程序的喚醒操作不會失效。

小結

通過上面的讨論,可以發現在 Linux 中避免程序的無效喚醒的關鍵是在程序檢查條件之前就将程序的狀态置為 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE,并且如果檢查的條件滿足的話就應該将其狀态重新設定為 TASK_RUNNING。

這樣無論程序等待的條件是否滿足,程序都不會因為被移出就緒隊列而錯誤地進入睡眠狀态,進而避免了無效喚醒問題。