大神教你設定Linux程式的睡眠和喚醒

大雄45發表於2023-10-08
導讀 在 中,僅等待CPU時間的程式稱為就緒程式,它們被放置在一個執行佇列中,一個就緒程式的狀 態標誌位為TASK_RUNNING。一旦一個執行中的程式時間片用完, Linux 核心的排程器會剝奪這個程式對CPU的控制權,並且從執行佇列中選擇一個合適的程式投入執行。

當然,一個程式也可以主動釋放CPU的控制權。函式 schedule()是一個排程函式,它可以被一個程式主動呼叫,從而排程其它程式佔用CPU。一旦這個主動放棄CPU的程式被重新排程佔用 CPU,那麼它將從上次停止執行的位置開始執行,也就是說它將從呼叫schedule()的下一行程式碼處開始執行。
有時候,程式需要等待直到某個特定的事件發生,例如裝置初始化完成、I/O 操作完成或定時器到時等。在這種情況下,程式則必須從執行佇列移出,加入到一個等待佇列中,這個時候程式就進入了睡眠狀態。
大神教你設定Linux程式的睡眠和喚醒大神教你設定Linux程式的睡眠和喚醒

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);
/* 或TASK_INTERRUPTIBLE */
while(!condition) /* ‘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 核心中的例項來看看Linux 核心是如何避免無效睡眠的,這段程式碼出自Linux2.6的核心(linux-2.6.11/kernel/sched.c: 4254):
4253 /* Wait for kthread_stop */
4254 set_current_state(TASK_INTERRUPTIBLE);
4255 while (!kthread_should_stop()) {
4256 schedule();
4257 set_current_state(TASK_INTERRUPTIBLE);
4258 }
4259 __set_current_state(TASK_RUNNING);
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。這樣無論程式等待的條件是否滿足, 程式都不會因為被移出就緒佇列而錯誤地進入睡眠狀態,從而避免了無效喚醒問題。

原文來自:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2980422/,如需轉載,請註明出處,否則將追究法律責任。

相關文章