歡迎光臨
每天分享高質量文章

關於 Linux 行程的睡眠和喚醒 ,來看這篇就夠了~

1 Linux 行程的睡眠和喚醒

在 Linux 中,僅等待 CPU 時間的行程稱為就緒行程,它們被放置在一個執行佇列中,一個就緒行程的狀 態標誌位為 TASK_RUNNING。一旦一個執行中的行程時間片用完, Linux 內核的排程器會剝奪這個行程對 CPU 的控制權,並且從執行佇列中選擇一個合適的行程投入執行。


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


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


Linux 中的行程睡眠狀態有兩種:一種是可中斷的睡眠狀態,其狀態標誌位 

TASK_INTERRUPTIBLE;


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


在現代的 Linux 作業系統中,行程一般都是用呼叫 schedule() 的方法進入睡眠狀態的,下麵的程式碼演示瞭如何讓正在執行的行程進入睡眠狀態。


  1. sleeping_task = current;

  2. set_current_state(TASK_INTERRUPTIBLE);

  3. schedule();

  4. func1();

  5. /* 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,而且排程器會把它加入到執行佇列中去。當然,這個行程只有在下次被排程器排程到的時候才能真正地投入執行。

2 無效喚醒

幾乎在所有的情況下,行程都會在檢查了某些條件之後,發現條件不滿足才進入睡眠。可是有的時候行程卻會在 判定條件為真後開始睡眠,如果這樣的話行程就會無限期地休眠下去,這就是所謂的無效喚醒問題。在作業系統中,當多個行程都企圖對共享資料進行某種處理,而 最後的結果又取決於行程執行的順序時,就會發生競爭條件,這是作業系統中一個典型的問題,無效喚醒恰恰就是由於競爭條件導致的。


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


A 行程:


  1. 1 spin_lock(&list;_lock);

  2. 2 if(list_empty(&list;_head)) {

  3. 3 spin_unlock(&list;_lock);

  4. 4 set_current_state(TASK_INTERRUPTIBLE);

  5. 5 schedule();

  6. 6 spin_lock(&list;_lock);

  7. 7 }

  8. 8

  9. 9 /* Rest of the code ... */

  10. 10 spin_unlock(&list;_lock);

B 行程:

  1. 100 spin_lock(&list;_lock);

  2. 101 list_add_tail(&list;_head, new_node);

  3. 102 spin_unlock(&list;_lock);

  4. 103 wake_up_process(processa_task);


這裡會出現一個問題,假如當 A 行程執行到第 3 行後第 4 行前的時候,B 行程被另外一個處理器排程投入執行。在這個時間片內,B 行程執行完了它所有的指令,因此它試圖喚醒 A 行程,而此時的 A 行程還沒有進入睡眠,所以喚醒操作無效。在這之後,A 行程繼續執行,它會錯誤地認為這個時候連結串列仍然是空的,於是將自己的狀態設定為 TASK_INTERRUPTIBLE 然後呼叫 schedule() 進入睡 眠。由於錯過了 B 行程喚醒,它將會無限期的睡眠下去,這就是無效喚醒問題,因為即使連結串列中有資料需要處理,A 行程也還是睡眠了。

3 避免無效喚醒


如何避免無效喚醒問題呢?我們發現無效喚醒主要發生在檢查條件之後和行程狀態被設定為睡眠狀態之前, 本來 B 行程的 wake_up_process() 提供了一次將 A 行程狀態置為 TASK_RUNNING 的機會,可惜這個時候 A 行程的狀態仍然是 TASK_RUNNING,所以 wake_up_process() 將 A 行程狀態從睡眠狀態轉變為執行狀態的努力 沒有起到預期的作用。要解決這個問題,必須使用一種保障機制使得判斷連結串列為空和設定行程狀態為睡眠狀態成為一個不可分割的步驟才行,也就是必須消除競爭條 件產生的根源,這樣在這之後出現的 wake_up_process () 就可以起到喚醒狀態是睡眠狀態的行程的作用了。
找到了原因後,重新設計一下 A 行程的程式碼結構,就可以避免上面例子中的無效喚醒問題了。


A 行程:


  1. 1 set_current_state(TASK_INTERRUPTIBLE);

  2. 2 spin_lock(&list;_lock);

  3. 3 if(list_empty(&list;_head)) {

  4. 4 spin_unlock(&list;_lock);

  5. 5 schedule();

  6. 6 spin_lock(&list;_lock);

  7. 7 }

  8. 8 set_current_state(TASK_RUNNING);

  9. 9

  10. 10 /* Rest of the code ... */

  11. 11 spin_unlock(&list;_lock);


可以看到,這段程式碼在測試條件之前就將當前執行行程狀態轉設定成 TASK_INTERRUPTIBLE 了,並且在連結串列不為空的情況下又將自己置為 TASK_RUNNING 狀態。這樣一來如果 B 行程在 A 行程行程檢查了連結串列為空以後呼叫 wake_up_process(),那麼 A 行程的狀態就會自動由原來 TASK_INTERRUPTIBLE變成 TASK_RUNNING,此後即使行程又呼叫了 schedule(),由於它現在的狀態是 TASK_RUNNING,所以仍然不會被從執行佇列中移出,因而不會錯誤的進入睡眠,當然也就避免了無效喚醒問題。

4 Linux 內核的例子


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


  1. /* ‘q’是我們希望睡眠的等待佇列 */

  2. DECLARE_WAITQUEUE(wait,current);

  3. add_wait_queue(q, &wait;);

  4. set_current_state(TASK_INTERRUPTIBLE);

  5. /* 或 TASK_INTERRUPTIBLE */

  6. while(!condition) /* ‘condition’ 是等待的條件 */

  7. schedule();

  8. set_current_state(TASK_RUNNING);

  9. 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 核心中的實體來看看 Linux 內核是如何避免無效睡眠的,這段程式碼出自 Linux2.6 的核心 (linux-2.6.11/kernel/sched.c: 4254):


  1. 4253 /* Wait for kthread_stop */

  2. 4254 set_current_state(TASK_INTERRUPTIBLE);

  3. 4255 while (!kthread_should_stop()) {

  4. 4256 schedule();

  5. 4257 set_current_state(TASK_INTERRUPTIBLE);

  6. 4258 }

  7. 4259 __set_current_state(TASK_RUNNING);

  8. 4260 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。這樣無論行程等待的條件是否滿足, 行程都不會因為被移出就緒佇列而錯誤地進入睡眠狀態,從而避免了無效喚醒問題。

作者:chumojing

原文地址:http://blog.chinaunix.net/uid-12461657-id-3178775.html

《Linux雲端計算及運維架構師高薪實戰班》2018年03月26日即將開課中,120天衝擊Linux運維年薪30萬,改變速約~~~~

    *宣告:推送內容及圖片來源於網路,部分內容會有所改動,版權歸原作者所有,如來源資訊有誤或侵犯權益,請聯絡我們刪除或授權事宜。

    – END –


    更多Linux好文請點選【閱讀原文】

    ↓↓↓

    贊(0)

    分享創造快樂