--- tags: Linux Kernel Internals, 作業系統 --- # Linux 核心設計: Power Management(2): Runtime Power Management model > 以下內容主要摘錄自 [Runtime Power Management Framework for I/O Devices](https://docs.kernel.org/power/runtime_pm.html) ## Overview 與前一章節所介紹的 sleep model 是系統整體的觀點不同,Runtime Power Management model 是基於系統運作時,裝置也有機會獨立進入低功耗狀態的事實,以盡可能在運行同時達到省電。但是這種電源管理的方式難度更高,因為裝置彼此通常不是相互獨立。例如可能存在樹狀結構的關係,則在此狀況下,需要等到所有子裝置都 suspended 之後,才能 suspend parent。此外,根據裝置所在的 bus 類型,裝置睡眠的同時也可能需要執行一些特定於 bus 的操作。再者,對 runtime suspend 和 system suspend/hibernation 之間的同步也是難題之一。 ## Kernel Interface PM core 透過以下幾種方式對 I/O 裝置提供 runtime PM 的支援: * `pm_wq`: PM 子系統建立了一個特殊的 workqueue `pm_wq`,bus 和裝置驅動程式可以將其 PM 相關 work item 放入其中。使用 `pm_wq` 處理 runtime PM 相關的所有工作是普遍的作法,因為這允許 kernel 將其與系統的電源轉換(static PM)做同步 * `struct dev_pm_info`: `struct device` 底下的此結構中可以提供一些訊息以幫助 PM core 將 runtime PM 與其他 PM 做正確同步 * `struct dev_pm_ops`: 裡面包含三個與 runtime PM 相關的 callback `runtime_suspend` 、`runtime_resume`、`runtime_idle` * runtime PM 提供了一系列 helper API 以幫助裝置驅動程式和 PM core 之間正確的處理,詳細會在後續討論 ## Runtime PM Callbacks 如之前所提到,有三個重要的 callback 被用來處理 runtime PM。 ```cpp struct dev_pm_ops { ... int (*runtime_suspend)(struct device *dev); int (*runtime_resume)(struct device *dev); int (*runtime_idle)(struct device *dev); ... }; ``` 從 [`__rpm_get_callback`](https://elixir.bootlin.com/linux/latest/source/drivers/base/power/runtime.c#L21) 的實作,這些 callback 被執行的方式為: * 如果裝置被歸類於特定 PM domain(`dev->pm_domain` 非 NULL),優先執行相關 callback * 若否,則如果裝置隸屬於 device type(`dev->type`) 且該 type 實作了 runtime PM(`dev->type->pm`),執行之 * 若否,如果裝置隸屬於 device class(`dev->class`) 且該 class 實作了 runtime PM(`dev->class->pm`),執行之 * 若否,如果裝置隸屬於 bus type(`dev->bus`) 且該 class 實作了 runtime PM(`dev->bus->pm`),執行之 * 如果上述規則選擇的子系統都不提供 PM callback,PM core 將直接呼叫 `dev->driver->pm` 中的 callback(如果存在) 預設情況下,callback 會在啟用中斷的 process context 中進行。 但是可以透過 `pm_runtime_irq_safe()` 告訴 PM core 在 interrupt 關閉的 atomic context 中為給定裝置運行 callback 是安全。 ### Runtime Suspend Callback 一旦給定裝置的 subsystem 層級的 runtime suspend callback(或直接呼叫驅動程式的 suspend 情況)成功完成,PM core 就會將該裝置視為 "suspended"。這並不意味著它已被置於低功耗狀態,但代表裝置將不會處理任何資料,也不會與 CPU 和 RAM 互動,直到為其執行適當的 resume callback。 如果 suspend callback 回傳值是 `-EBUSY` 或 `-EAGAIN`,則裝置的狀態保持 "active",這表示之後該裝置可以繼續任何正常操作。 如果 suspend callback 回傳值是 `-EBUSY` 和 `-EAGAIN` 之外的的 error code,則 PM core 將此視為致命錯誤,需要先透過特殊的 helper 將其狀態復原為 "active" 或 "suspended",才能再繼續在裝置上運作 runtime PM 管理。 一般來說,需要為 runtime 進入低功耗狀態的所有輸入裝置啟用 remote wakeup。如果驅動程式對應裝置需要 remote wakeup 功能(即允許裝置請求 resume 而非 host 主動喚醒,例如 USB、PCI PME)才能正常運行,但 `device_can_wakeup()` 該裝置得到 `false`,則 ->runtime_suspend () 應回傳 `-EBUSY`。 如果 `device_can_wakeup()` 回傳回 `true`,並且裝置在執行 suspend 後進入低功耗狀態,則子系統會預期已為裝置啟用 remote wakeup。 ### Runtime Resume Callback 一旦給定裝置的 subsystem 層級的 runtime resume callback(或直接呼叫驅動程式的 resume 情況)成功完成,PM core 就會將該裝置視為 "active"。意味著裝置將能夠根據請求完成 I/O 操作。 如果 resume 回傳錯誤,則 PM core 將此視為致命錯誤,需要先透過特殊的 helper 將其狀態復原為 "active" 或 "suspended",才能再繼續在裝置上運作 runtime PM 管理。 ### Runtime Idle Callback 每當裝置可能為 idle 狀態時,PM core 就會執行 idle callback。有兩個關鍵的 counter 計數器會參與本次判斷,分別計算裝置的使用(referenced/usage)數和裝置的 active children 數。 如果使用 PM core 提供的 helper 導致這些 counter 中的任何一個值為零,則 PM core 會檢查另一個 counter。當兩個 counter 都等於 0,PM core 將執行 idle callback。 idle 所作的行為取決於所對應的子系統或驅動程式。但預期的操作是檢查裝置是否可以滿足 suspend 所需的所有條件,並在是的情況下裝裝置的 suspend 請求加入排隊。如果沒有定義 idle callback,或其回傳 0,則 PM core 將嘗試執行裝置的 runtime suspend,同時也會考慮裝置使用 autosuspend 機制的狀況(後續會說明 autosuspend 細節)。如果不想在 idle 之後自動進行 suspend,則 idle callback 需傳回非零值,PM 核心不會對負值的錯誤回傳代碼做額外處理。 ### Constraints PM core 對 Runtime PM callback 的執行方式提供了一些保證和限制: * callbacks 大部分都是 mutual exclusive,除了 `->runtime_suspend()` 或 `->runtime_resume()` 可能平行和 `->runtime_idle()` 一起進行(即便如此,但 idle 不會是在其他 callback 運行的途中開始) * `->runtime_idle()` 和 `->runtime_suspend()` 只在 "active" 裝置執行 * `->runtime_idle()` 和 `->runtime_suspend()` 只在 usage counter 等於 0 的裝置執行,且 active childern counter 等於 0,或是 `power.ignore_children` 被設置 * `->runtime_resume()` 只在 "suspended" 裝置上執行 * 如果 `->runtime_suspend()` 即將被執行或有一個待執行的 request,則 `->runtime_idle()` 將不會對相同裝置執行 * 執行或將執行的 `->runtime_suspend()` 將取消對相同裝置待執行 `->runtime_idle()` * 如果 `->runtime_resume()` 即將被執行或待執行的 request,則不會為相同裝置執行其他 callback * 執行 `->runtime_resume()` 將取消任何待處理其他 callback 請求,autosuspend 除外 ## Runtime PM Infomation `struct dev_pm_info` 的欄位提供的 runtime PM 執行所需的資料,詳細內容可參考 [Runtime PM Device Fields](https://docs.kernel.org/power/runtime_pm.html#runtime-pm-device-fields) ## Helper Functions 驅動程式可透過 [Runtime PM Device Helper Functions](https://docs.kernel.org/power/runtime_pm.html#runtime-pm-device-helper-functions) 來配合 PM core 完成 runtime PM 的實作。 ## Initialization, Probing and Removal 所有裝置最開始的 runtime PM 都是停用的,這代表使用一系列的 [Runtime PM Device Helper Functions](https://docs.kernel.org/power/runtime_pm.html#runtime-pm-device-helper-functions) 都只能得到 `-EAGAIN`。驅動程式需為裝置呼叫 `pm_runtime_enable()` 以啟用之。此外,所有裝置的初始 runtime PM 狀態假設是 "suspended",但這並不一定是裝置的實際狀態。因此在 `pm_runtime_enable()` 之前,假使裝置是 active,驅動程式要藉助 `pm_runtime_set_active()` 正確設置 runtime PM 狀態。 如果裝置有 parent 並且 parent 的 runtime PM 啟用。則為裝置呼叫 `pm_runtime_set_active()` 時,將導致其 parent 也受影響為 active(除 `power.ignore_children` 被設置的特例)。此時直到子裝置啟用 runtime PM (`pm_runtime_enable()`) 前,parent 將無法 runtime suspend。因此,一旦為裝置呼叫了 `pm_runtime_set_active()`,接著應盡快呼叫 `pm_runtime_enable()`,或應藉由 `pm_runtime_set_suspended()` 將其 runtime PM 狀態變更回 "suspended" 以允許 parent 能夠做 suspend。 假使裝置的預設初始 runtime PM 狀態就是 suspended。則其 bus 或其驅動程式的 `probe()` callback 應先透過呼叫 `pm_runtime_enable()` 來啟用裝置的 runtime PM,再使用`pm_runtime_resume()` 來喚醒裝置。 另一方面,如果裝置可能遭遇在 probe 期間存在對裝置執行 runtime PM helper functions 之情境,則成對的 `pm_runtime_put()` 和 `pm_runtime_get_sync()` 將需要被使用以確保裝置在 probe 期間不要重新進入睡眠狀態。 驅動程式 `->remove()` 時通常會進行與 `->probe()` 所為相反的撤銷動作。對 runtime PM 子系統來就是呼叫 `pm_runtime_disable()`、`pm_runtime_dont_use_autosuspend()` 等。 driver core 也會運用 runtime PM helper functions 來正確同步或者恰當的控制裝置的睡眠狀態。更多細節可參考 [Runtime PM Initialization, Device Probing and Removal](https://docs.kernel.org/power/runtime_pm.html#runtime-pm-initialization-device-probing-and-removal) 一節。 ## Runtime PM and System Sleep Runtime PM 雖與在 [Linux 核心設計: Power Management(1): System Sleep model](https://hackmd.io/@RinHizakura/r17lOaDqp) 一節介紹的電源管理方式是兩種機制,但彼此如何正確互動也很重要。如果進行 system sleep 時裝置是 active 的,runtime PM 對此並不影響;但若是裝置此時已然 suspend,則 system sleep 應該對該裝置採取甚麼作為呢? ### System Suspend 先考慮 system suspend 的情況。我們得考慮到在 runtime PM 和 system sleep 可能有不同的喚醒設定。例如,可以在 runtime PM 時啟用 remote wakeup,但在 system sleep 時禁止(`device_may_wakeup(dev)` 傳回 false)。因此,假設在要求 system sleep 時裝置因為 runtime PM 而 suspend,則應該先復原該裝置然後再 suspend 之。同理,如果驅動程式在 runtime PM 和 system sleep 的其他設定有區別(例如 power level 等),也因遵照類似流程。系統框架上,driver 要將這個事先的復原實作在 suspend 的 callback 中,大致的程式碼會像以下這樣: ```cpp if (pm_runtime_suspended(dev)) { /* do something... */ pm_runtime_disable(dev); pm_runtime_set_active(dev); pm_runtime_enable(dev); } ``` 因此除了註解處裝置要根據本身特性執行復原行為外,之後的三個 pm_runtime_* 函式很關鍵。 ### System Resume 那麼在 system resume 時,要怎麼處理在 system suspend 前已經 runtime suspend 的裝置呢? 一般來說,最簡單的做法是無視 system suspend 前的狀況,直接讓其回到 active 狀態。這樣實作可能的理由是: * 裝置可能重新需要切換 power level、設定喚醒方式等 * 原本設置的 remote wakeup 可能在 system suspend 後已經在 firmware 中註銷 * 裝置的 child 可能需要此裝置處於 active 才能正常 resume * 驅動程式對裝置狀態的判斷可能與裝置的物理狀態有差異,例如從 hibernation 中退出可能會發生這種情況 * 裝置 resume 時可能需重新 reset * 即便 system suspend 之前非 active,也可能原本該裝置就即將要被 resume 然而,在某些系統上,system sleep 並不是透過 firmware 或 hardware 行為進入,而是由核心軟體分別把所有硬體組件都置於低功耗狀態,然後系統可以由硬體中斷或其他可完全在核心控制的類似機制下喚醒。這意味著軟體層級可以準確地掌握 resume 期間所有裝置的狀態。在這種情況下,且前述列出的理由也都不符合時,則將裝置恢復到 system suspend 開始之前的狀態可能更有效。 為此,PM core 提供了一種允許裝置層次結構的不同層級之間進行協調的機制。具體作法是,如果 `.prepare()` callback 返回正整數值,PM core 會認為該裝置原本處於 runtime suspended 且之後 system resume 時可以將其保留在相同狀態。但前提是它的所有後代也都處於 runtime suspended。則對此狀況,PM core 將不會對所有這些設備執行任何 system suspend 和 resume,但還是會執行 `.complete()`。這僅適用於與休眠無關的系統掛起轉換。另外還要這注意到 hibernation 等級的 system sleep 並不適用這種機制。 PM core 也會運用 runtime PM helper functions 來正確協調 runtime PM 和 system skeep。更多細節可參考 [Runtime PM and System Sleep](https://docs.kernel.org/power/runtime_pm.html#runtime-pm-and-system-sleep) 一節。 ## Autosuspend 電源管理並不是一旦發現裝置閒置就置其於低功耗模式,等到有需求時再將其復原。這是因為進行電源狀態轉換是需要成本的。轉換過程需要額外的時間和功耗,如果一個裝置在 suspend 之後立刻就需要再 resume,可能不如直接使其處於 active 狀態要來得省電,且裝置不需耗費額外時間在電源狀態的轉換上。 因此理由,一種簡單但有效的規則是: 驅動裝置應該等到裝置處於非活動狀態至少一段時間後,才主動將其至於低功耗的狀態。 這種機制在 Linux 中稱為 autosuspend。留意到這用詞是歷史的慣稱,即前述提到裝置 idle 後需延遲一段時間才進入 suspend 的做法。並不是指裝置硬體會自動 suspend,子系統或驅動程式仍然需要進行相關的軟體行為。 ## PM sysfs 介面 ### `control` 在每個裝置的 `/sys/devices/.../power/control` 檔案下,我們可以設置是否其支援 runtime PM。對應到的 flag 是 `runtime_auto`,由裝置的 bus 類型或者子系統使用 `pm_runtime_allow()` 或 `pm_runtime_forbid()` 來設置。預設 runtime PM 是開啟的。 在 userspace 可以將 "on" 或 "auto" 字串寫入裝置的 `/sys/devices/.../power/control` 檔案來更改 runtime PM 的使用。寫入 "auto" 會呼叫 `pm_runtime_allow()`,允許裝置驅動程式使用註冊的 callback 進行 runtime PM。寫入 "on" 則會呼叫 `pm_runtime_forbid()`,裝置將強制運作在全速狀態,不會進入低功率的模式*。 :::warning 這裡說的不進入低功率模式僅單指軟體控制部分。例如 PCIe 可由 ASPM 藉硬體自動進入省電的 link state,或者 USB3 可由硬體控制的 U1/U2 狀態,不受此檔案影響。 ::: ## Reference * [Runtime Power Management Framework for I/O Devices](https://docs.kernel.org/power/runtime_pm.html) * [Linux电源管理(11)_Runtime PM之功能描述](http://www.wowotech.net/pm_subsystem/rpm_overview.html) * [Adding Runtime Power Management Capabilities to Device Drivers - Shreeya Patel, Collabora](https://www.youtube.com/watch?v=L_pNP9LkTOk) * [BKK19-PM01: PMWG: CPU Idle governor and irq prediction](https://www.youtube.com/watch?v=69HQYihj5zg)