Linux進程的睡眠和喚醒 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)) { 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 進程也還是睡眠了。 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)) { 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,所以仍然不會被從運行隊列中移出,因而不會錯誤的進入睡眠,當然也就避免了無效喚醒問題。 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()) { 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。這樣無論進程等待的條件是否滿足, 進程都不會因為被移出就緒隊列而錯誤地進入睡眠狀態,從而避免了無效喚醒問題。 |