本節主要解決一個問題:當系統進入休眠狀態時,如何暫停設備中斷(IRQ)?在喚醒系統時,如何恢復設備IRQ?
通常情況下,在系統進入休眠狀態後,所有裝置的IRQ(中斷請求線)會被停用。具體時間點在設備的延遲掛起(late suspend)階段之後。以下是相關代碼(省略無關代碼)。
「
#static int suspend_enter(suspend_state_t state, bool *wakeup)
{……
error = dpm_suspend_late(PMSG_SUSPEND);-----late suspend階段
error = platform_suspend_prepare_late(state);
下面的程式碼中會disable各個裝置的irq
error = dpm_suspend_noirq(PMSG_SUSPEND);----進入noirq的階段
error = platform_suspend_prepare_noirq(state);
……
}
」
#在dpm_suspend_noirq函數中,會針對系統中的每一個device,依序呼叫device_suspend_noirq來執行該裝置noirq情況下的suspend callback函數,當然,在此之前會呼叫suspend_device_irqs函數來disable所有裝置的irq。
之所以這麼做,其思路是這樣的:在各個設備驅動完成了late suspend之後,按理說這些已經被suspend的設備不應該再觸發中斷了。如果還有一些裝置沒有被正確的suspend,那麼我們最好的策略就是mask該裝置的irq,從而阻止中斷的遞交。此外,在過去的程式碼中(指interrupt handler),我們對設備共享IRQ的情況處理的不是很好,存在這樣的問題:在共享IRQ的設備們完成suspend之後,如果有中斷觸發,這時候設備驅動的interrupt handler並沒有準備好。在某些場景下,interrupt handler會存取已經suspend裝置的IO位址空間,導致不可預測的issue。這些issue很難debug,因此,我們引入了suspend_device_irqs()以及裝置noirq階段的callback函數。
系統resume過程中,在各個設備的early resume過程之前,各個設備的IRQ會被重新打開,具體代碼如下(刪除了部分無關代碼):
「
#static int suspend_enter(suspend_state_t state, bool *wakeup)
{……
platform_resume_noirq(state);----首先執行noirq階段的resume
dpm_resume_noirq(PMSG_RESUME);------在這裡會恢復irq,然後進入early resume階段
platform_resume_early(state);
dpm_resume_early(PMSG_RESUME);
……}
」
#在dpm_resume_noirq函數中,會呼叫各個裝置驅動的noirq callback,在此之後,呼叫resume_device_irqs函數,完成各個裝置irq的enable。
二、關於IRQF_NO_SUSPEND Flag
當然,有些中斷需要在整個系統的suspend-resume過程中(包括在noirq階段,包括將nonboot CPU推送到offline狀態以及系統resume後,將其重新設定為online的階段)保持能夠觸發的狀態。一個簡單的例子就是timer中斷,此外IPI以及一些特殊目的設備中斷也需要如此。
在中斷申請的時候,IRQF_NO_SUSPEND flag可以用來告知IRQ subsystem,這個中斷就是上一段文字中描述的那種中斷:需要在系統的suspend-resume過程中保持enable狀態。有了這個flag,suspend_device_irqs並不會disable該IRQ,讓該中斷在隨後的suspend和resume過程中,保持中斷開啟。當然,這並不能保證該中斷可以將系統喚醒。如果想要達到喚醒的目的,請呼叫enable_irq_wake。
要注意的是:IRQF_NO_SUSPEND flag影響使用該IRQ的所有周邊(一個IRQ可以被多個週邊裝置共享,不過ARM中不會這麼用)。如果一個IRQ被多個週邊設備共享,並且各個週邊都註冊了對應的interrupt handler,如果其一在申請中斷的時候使用了IRQF_NO_SUSPEND flag,那麼在系統suspend的時候(指suspend_device_irqs之後,按理說各個IRQ已經被disable了),所有該IRQ上的各個設備的interrupt handler都可以被正常的被觸發執行,即便是有些設備在調用request_irq(或者其他中斷註冊函數)的時候沒有設定IRQF_NO_SUSPEND flag。正因為如此,我們應該盡可能的避免同時使用IRQF_NO_SUSPEND 和IRQF_SHARED這兩個flag。
三、系統中斷喚醒介面:enable_irq_wake() 與 disable_irq_wake()
有些中斷可以將系統從睡眠狀態中喚醒,我們稱之為“可以喚醒系統的中斷”,當然,“可以喚醒系統的中斷”需要配置才能啟動喚醒系統這樣的功能。這樣的中斷一般在工作狀態的時候就是作為普通I/O interrupt出現,只要在準備使能喚醒系統功能的時候,才會發起一些特別的配置和設定。
這樣的配置和設定有可能是和硬體系統(例如SOC)上的訊號處理邏輯相關的,我們可以考慮下面的HW block圖:
週邊裝置的中斷訊號被送到「通用的中斷訊號處理模組」和「特定中斷訊號接收模組」。正常運作的時候,我們會turn on「通用的中斷訊號處理模組」的處理邏輯,而turn off「特定中斷訊號接收模組」的處理邏輯。但是,在系統進入睡眠狀態的時候,有可能「通用的中斷訊號處理模組」已經off了,這時候,我們需要啟動「特定中斷訊號接收模組」來接收中斷訊號,從而讓系統suspend-resume模組(它往往是suspend狀態時候唯一能夠工作的HW block了)可以正常的被該中斷訊號喚醒。一旦喚醒,我們最好是turn off“特定中斷訊號接收模組”,讓外設的中斷處理回到正常的工作模式,同時,也避免了系統suspend-resume模組收到不必要的干擾。
IRQ子系統提供了兩個介面函數來完成這個功能:enable_irq_wake()函數用來開啟該週邊中斷線通往系統電源管理模組(也就是上面的suspend-resume模組)之路,另外一個介面是disable_irq_wake(),用來關閉該週邊中斷線通往系統電源管理模組路徑上的各種HW block。
呼叫了enable_irq_wake會影響系統suspend過程中的suspend_device_irqs處理,程式碼如下:
「
#static bool suspend_device_irq(struct irq_desc *desc)
{
……
if (irqd_is_wakeup_set(&desc->irq_data)) {
irqd_set(&desc->irq_data, IRQD_WAKEUP_ARMED);
return true;
}
省略Disable 中斷的程式碼
}
」
#也就是說,一旦呼叫enable_irq_wake設定了該裝置的中斷作為系統suspend的喚醒來源,那麼在該週邊裝置的中斷不會被disable,只是被標記一個IRQD_WAKEUP_ARMED的標記。對於那些不是wakeup source的中斷,在suspend_device_irq 函數中會標記IRQS_SUSPENDED並disable該裝置的irq。在系統喚醒過程中(resume_device_irqs),被diable的中斷會重新enable。
當然,如果在suspend的過程中發生了某些事件(例如wakeup source產生了有效訊號),從而導致本次suspend abort,那麼這個abort事件也會通知到PM core模組。事件並不需要立刻被通知到PM core模組,一般而言,suspend thread會在某些點上去檢查pending的wakeup event。
在系統suspend的過程中,每一個來自wakeup source的中斷都會終止suspend過程或將系統喚醒(如果系統已經進入suspend狀態)。但是,在執行了suspend_device_irqs之後,普通的中斷被屏蔽了,這時候,即使HW觸發了中斷訊號也無法執行其interrupt handler。作為wakeup source的IRQ會怎麼樣呢?雖然它的中斷沒有被mask掉,但其interrupt handler也不會執行(這時候的HW Signal只是用來喚醒系統)。唯一有機會執行的interrupt handler是那些標記IRQF_NO_SUSPEND flag的IRQ,因為它們的中斷總是enable的。當然,這些中斷不應該呼叫enable_irq_wake進行喚醒來源的設定。
四、Interrupts and Suspend-to-Idle
Suspend-to-idle (也稱為”freeze” 狀態)是相對比較新的系統電源管理狀態,相關程式碼如下:
「
#static int suspend_enter(suspend_state_t state, bool *wakeup)
{
……
各個裝置的late suspend階段
各個設備的noirq suspend階段
if (state == PM_SUSPEND_FREEZE) {
freeze_enter();
goto Platform_wake;
}
……
}
」
#Freeze和suspend的前面的操作基本上是一樣的:首先凍結系統中的進程,然後是suspend系統中的形形色色的device,不一樣的地方在noirq suspend完成之後,freeze不會disable那些non-BSP的處理器和syscore suspend階段,而是呼叫freeze_enter函數,把所有的處理器推送到idle狀態。這時候,任何的enable的中斷都可以將系統喚醒。而這也意味著那些標記IRQF_NO_SUSPEND(其IRQ沒有在suspend_device_irqs過程中被mask掉)是有能力將處理器從idle狀態中喚醒(不過,需要注意的是:這種訊號並不會觸發一個系統喚醒訊號),而普通中斷由於其IRQ被disable了,因此無法喚醒idle狀態中的處理器。
那些能夠喚醒系統的wakeup interrupt呢?由於其中斷沒有被mask掉,因此也可以將系統從suspend-to-idle狀態中喚醒。整個過程和將系統從suspend狀態中喚醒一樣,唯一不同的是:將系統從freeze狀態喚醒走的中斷處理路徑,而將系統從suspend狀態喚醒走的喚醒處理路徑,需要電源管理HW BLOCK中特別的中斷處理邏輯的參與。
五、IRQF_NO_SUSPEND 標誌和enable_irq_wake函數不能同時使用
針對一個設備,在申請中斷的時候使用IRQF_NO_SUSPEND flag,又同時呼叫enable_irq_wake設定喚醒源是不合理的,主要原因如下:
1、如果IRQ沒有共享,使用IRQF_NO_SUSPEND flag說明你想要在整個系統的suspend-resume過程中(包括suspend_device_irqs之後的階段)保持中斷打開以便正常的調用其interrupt handler。而呼叫enable_irq_wake函數則表示你想要將該裝置的irq訊號設定為中斷來源,因此並不期望呼叫其interrupt handler。而這兩個需求明顯是互斥的。
2、IRQF_NO_SUSPEND 標誌和enable_irq_wake函數都不是針對一個interrupt handler的,而是針對該IRQ上的所有註冊的handler的。在一個IRQ上共享喚醒來源以及no suspend中斷來源是比較荒謬的。
不過,在非常特殊的場合下,一個IRQ可以設定為wakeup source,同時也設定IRQF_NO_SUSPEND 標誌。為了程式碼邏輯正確,該裝置的驅動程式碼需要滿足一些特別的需求。
以上是Linux系統休眠(System Suspend)與裝置中斷處理的詳細內容。更多資訊請關注PHP中文網其他相關文章!