對 Linux 系統休眠的理解

發表於2016-11-10

今天看了一個關於中斷例程為什麼不能休眠的文章,引發了我的思考。其實這個問題在學習驅動的時候早就應該解決了,但是由於5年前學驅動的時候屬於Linux初學者,能力有限,所以對這個問題就知其然,沒有能力知其所以然。現在回頭看這個問題的時候,感覺應該可以有一個較為清晰的認識了。

首先必須意識到:休眠是一種程式的特殊狀態(即task->state= TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE)]

一、休眠的目的

簡單的說,休眠是為在一個當前程式等待暫時無法獲得的資源或者一個event的到來時(原因),避免當前程式浪費CPU時間(目的),將自己放入程式等待佇列中,同時讓出CPU給別的程式(工作)。休眠就是為了更好地利用CPU

一旦資源可用或event到來,將由核心程式碼(可能是其他程式通過系統呼叫)喚醒某個等待佇列上的部分或全部程式。從這點來說,休眠也是一種程式間的同步機制。

二、休眠的物件

休眠是針對程式,也就是擁有task_struct的獨立個體。

當程式執行某個系統呼叫的時候,暫時無法獲得的某種資源或必須等待某event的到來,在這個系統呼叫的底層實現程式碼就可以通過讓系統排程的手段讓出CPU,讓當前程式處於休眠狀態。

  • 程式什麼時候會被休眠?

    程式進入休眠狀態,必然是他自己的程式碼中呼叫了某個系統呼叫,而這個系統呼叫中存在休眠程式碼。這個休眠程式碼在某種條件下會被啟用,從而讓改變程式狀態,說到底就是以各種方式包含了:

1、條件判斷語句

2、程式狀態改變語句

3、schedule();

三、休眠操作做了什麼

程式被置為休眠,意味著它被標識為處於一個特殊的狀態(TASK_UNINTERRUPTIBLE TASK_INTERRUPTIBLE),並且從排程器的執行佇列中移走這個程式將不在任何 CPU 排程,即不會被執行。 直到發生某些事情改變了那個狀態(to TASK_WAKING)。這時處理器重新開始執行此程式,此時程式會再次檢查是否需要繼續休眠(資源是否真的可用?),如果不需要就做清理工作,並將自己的狀態調整為TASK_RUNNING

四、誰來喚醒休眠程式

程式在休眠後,就不再被排程器執行,就不可能由自己喚醒自己,也就是說程式不可能睡覺睡到自然醒。喚醒工作必然是由其他程式或者核心本身來完成的。喚醒需要改變程式的task_struct中的狀態等,程式碼必然在核心中,所以喚醒必然是在系統呼叫的實現程式碼中(如你驅動中的readwrite方法)以及各種形式的中斷程式碼(包括軟、硬中斷)中。

如果在系統呼叫程式碼中喚醒,則說明是由其他的某個程式來呼叫了這個系統呼叫喚醒了休眠的程式。

如果是中斷中喚醒,那麼喚醒的任務可以說是核心完成了。

  • 如何找到需要喚醒的程式:等待佇列

上面其實已經提到了:休眠程式碼的一個工作就是將當前程式資訊放入一個等待佇列中。它其實是一個包含等待某個特定事件的所有程式相關資訊的連結串列。一個等待佇列由一個wait_queue_head_t 結構體來管理,其定義在中。

wait_queue_head_t 型別的資料結構如下: 

它包含一個自旋鎖和一個連結串列。這是一個等待佇列連結串列頭,連結串列中的元素被宣告做wait_queue_t。自旋鎖用於包含連結串列操作的原子性。 

wait_queue_t包含關於睡眠程式的資訊和喚醒函式

他們在記憶體中的結構大致如下圖所示:

對 Linux 系統休眠的理解

等待佇列頭wait_queue_head_t一般是定義在模組或核心程式碼中的全域性變數,而其中連結的元素 wait_queue_t的定義被包含在了休眠巨集中。

休眠和喚醒的過程如下圖所示:

對 Linux 系統休眠的理解

五、休眠和喚醒的程式碼簡要分析

下面我們簡單分析一下休眠與喚醒的核心原語。

1、休眠:wait_event

2、喚醒:wake_up

上面分析的休眠函式是最簡單的休眠喚醒函式,其他類似的函式,如字尾為_timeout_interruptible_interruptible_timeout的函式其實都是在喚醒後的條件判斷上有些不同,多判斷一些喚醒條件而已。這裡就不再贅述了。

六、使用休眠的注意事項

1) 永遠不要在原子上下文中進入休眠,即當驅動在持有一個自旋鎖、seqlock或者 RCU 鎖時不能睡眠;關閉中斷也不能睡眠,終端例程中也不可休眠。

持有一個訊號量時休眠是合法的,如果程式碼在持有一個訊號量時睡眠,任何其他的等待這個訊號量的執行緒也會休眠。發生在持有訊號量時的休眠必須短暫,而且決不能阻塞那個將最終喚醒你的程式。

2)當程式被喚醒,它並不知道休眠了多長時間以及休眠時發生什麼;也不知道是否另有程式也在休眠等待同一事件,且那個程式可能在它之前醒來並獲取了所等待的資源。所以不能對喚醒後的系統狀態做任何的假設,並必須重新檢查等待條件來確保正確的響應。

3)除非確信其他程式會在其他地方喚醒休眠的程式,否則也不能睡眠。使程式可被找到意味著:需要維護一個等待佇列的資料結構。它是一個程式連結串列,其中包含了等待某個特定事件的所有程式的相關資訊。

七、不可在中斷例程中休眠的原因

如果在某個系統呼叫中把當前程式休眠,是有明確目標的,這個目標就是過來call這個系統呼叫的程式(注意這個程式正在running)。

但是中斷和程式是非同步的,在中斷上下文中,當前程式大部分時候和中斷程式碼可能一點關係都沒有。要是在這裡呼叫了休眠程式碼,把當前程式給休眠了,那就極有可能把無關的程式休眠了。再者,如果中斷不斷到來,會殃及許多無辜的程式。

在中斷中休眠某個特定程式是可以實現的,通過核心的task_struct連結串列可以找到的,不論是根據PID還是name。但是隻要這個程式不是當前程式,休眠它也可能沒有必要。可能這個程式本來就在休眠;或者正在執行佇列中但是還沒執行到,如果執行到他了可能又無須休眠了。

還有一個原因是中斷也是所謂的原子上下文,有的中斷例程中會禁止所有中斷,有的中斷例程還會使用自旋鎖等機制,在其中使用休眠也是非常危險的。 下面會介紹。

八、不可在持有自旋鎖、seqlockRCU 鎖或關閉中斷時休眠的原因 

其實自旋鎖、seqlockRCU 鎖或關閉中斷期間的程式碼都稱為原子上下文,比較有代表性的就是自旋鎖spinlock

對於UP系統,如果A程式在擁有spinlock時休眠,這個程式在擁有自旋鎖後主動放棄了處理器。其他的程式就開始使用處理器,只要有一個程式B去獲取同一個自旋鎖,B必然無法獲取,並做所謂的自旋等待。由於自旋鎖禁止所有中斷和搶佔,B的自旋等待是不會被打斷的,並且B也永遠獲得不了鎖。因為BCPU中執行,沒有其他程式可以執行並喚醒A並釋放鎖。系統就此鎖死,只能復位了。

對於SMP系統,如果A程式在擁有spinlock時休眠,這個程式在擁有自旋鎖後主動放棄了處理器。如果所有處理器都為了獲取這個鎖而自旋等待,由於自旋鎖禁止所有中斷和搶佔,,就不會有程式可能去喚醒A了,系統也就鎖死了。

並不是所一旦系統獲得自旋鎖休眠就會死,而是有這個可能。但是注意了計算機的執行速度之快,只要有億分之一的可能,也是很容易發生

所有的原子上下文都有這樣的共性:不可在其中休眠,否則系統極有可能鎖死。

只要對其裝置節點做兩次讀寫操作,系統必死。我在X86 SMP系統,ARMv5ARMv6ARMv7中都做了如下的實驗(單核(UP)系統必須配置CONFIG_DEBUG_SPINLOCK,否則自旋鎖是沒有實際效果(起碼不會有“自旋”), 系統可以多次獲取自旋鎖,沒有實驗效果。之後博文中有詳細描述)。現象都和上面敘述的死法相同,看了原始碼就知道(關鍵在readwrite方法)。以下是實驗記錄:

相關文章