Linux程序的睡眠和喚醒簡析

2021-09-30 23:47:10 字數 4959 閱讀 7338

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

2、無效喚醒

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

設想有兩個程序a 和b,a 程序正在處理乙個鍊錶,它需要檢查這個鍊錶是否為空,如果不空就對鍊錶裡面的資料進行一些操作,同時b程序也在往這個鍊錶新增節點。當這個鍊錶是空的時候,由於無資料可操作,這時a程序就進入睡眠,當b程序向鍊錶裡面新增了節點之後它就喚醒a 程序,其**如下:

a程序:

1 spin_lock(&list_lock);

2 if(list_empty(&list_head))

89 /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 程序也還是睡眠了。

3、避免無效喚醒

如何避免無效喚醒問題呢?我們發現無效喚醒主要發生在檢查條件之後和程序狀態被設定為睡眠狀態之前, 本來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))

8 set_current_state(task_running);

910 /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,所以仍然不會被從執行佇列中移出,因而不會錯誤的進入睡眠,當然也就避免了無效喚醒問題。

4、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())

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

linux0 11程序睡眠喚醒原理分析

程序的睡眠是通過呼叫sleep on函式,該函式修改了程序的狀態並且通過schedule函式切換到其他程序執行,從而實現程序的掛起,task uninterruptible狀態的程序只能被wake up函式喚醒。task interruptible狀態的程序可以被wake up和訊號喚醒。喚醒的時候...

Linux 0 11中程序睡眠和喚醒機制思考

在linux 0.11中,當程序嘗試訪問乙個邊界資料時,有可能由於資源已經被占用而進入睡眠狀態。當資源被釋放後,就需要把睡眠的程序喚醒。我們先來看乙個包括睡眠和喚醒步驟的實際情況 1.程序a在訪問硬碟某區塊bh時發現這一塊並不在告訴快取中,進而發起請求讀取硬碟 此時不一定會立即排程 2.隨後程序排程...

Linux程序休眠和喚醒

當程序以阻塞的方式通訊,在得到結果前程序會掛起休眠。為了將程序以一種安全的方式進入休眠,我們需要牢記兩條規則 一 永遠不要在原子上下文中進入休眠。二 程序休眠後,對環境一無所知。喚醒後,必須再次檢查以確保我們等待的條件真正為真 簡單休眠 完成喚醒任務的 還必須能夠找到我們的程序,這樣才能喚醒休眠的程...