--- tags: Linux Kernel Internals, 作業系統 --- # Linux 核心設計: Power Management(1): System Sleep model :::success [BKK19-TR02 - Linux Kernel Power Management - 101](https://youtu.be/lpzniFSLDqs?si=nKW1eJHM2NXnXM2d) 對 Linux Kernel 的 PM 框架做了 overview,推薦在開始本系列文章前預習一番。投影片可以參考[此連結](https://static.linaro.org/connect/yvr18/presentations/yvr18-221.pdf)。 ::: ## 引言 在 Linux 中,大部分程式碼都是屬驅動程式(Device Driver),而多數 Linux 電源管理(Power Management, PM) 的程式碼也是由驅動程式引導。因此,作為認識電源管理的起點,認識驅動程式使用何種模型和介面連接與系統交互,以完成電源管理的目標,是首要了解的課題。 在 Linux 中使用兩種模型來進行電源管理: System Sleep model 和 Runtime Power Management model。本篇將以第一種模型為主進行探討,而 Runtime Power Management model 會在下一章節進行分析。 ## Overview 睡眠(Sleep)是指系統整體性的低功耗狀態。在這種狀態下,userspace 將暫停執行,而核心的活動也將顯著減少。 Device、bus 和 class 這三大構成完整裝置驅動程式的要素,將協作以各自扮演對應角色,做好正確恢復的準備,完成降低耗能的目標。在 Linux 中支援數種的睡眠狀態,並且這些狀態也能對應到 [ACPI](https://en.wikipedia.org/wiki/ACPI) 規範的 [Global System States](https://en.wikipedia.org/wiki/ACPI#Global_states)。 ## Kernel Interface 裝置驅動程式層級在實作上是透過填入 `struct dev_pm_ops` 結構中的 callback 來實現 system sleep。更精準的說是除了 `runtime_*` 名稱以外的 callback 皆與 system sleep 有關。 `struct dev_pm_ops` 在一個裝置的 `struct dev_pm_domain`、 `struct bus_type`、`struct device_type` 或者 `struct class` 中皆存在。Linux 系統的 PM 框架將利用子系統或者內部裝置的這些 `dev_pm_ops`,偕同彼此以進行完整的電源管理流程。留意到並非每種裝置驅動程式的實作介面皆需提供完整的 `dev_pm_ops`,部分裝置的驅動程式框架會基於 `dev_pm_ops` 提供更直接的應用介面。 由於裝置與裝置之間的連接或主從等關係,會讓系統在宏觀上有層次結構的概念,比如 PCI 裝置依賴於 PCI controller 的存在。System sleep 需按層次結構自下而上的順序走訪以 suspend 裝置,並反之自上而下 resume 他們,以確保次序的有效性。PM 框架也是依循此理解運作。 而 domain, bus, class, 和 device 間的上下關係也影響 PM 框架運行 `struct dev_pm_ops` 的次序。對應到 `dev->pm_domain->ops`, `dev->bus->pm`, `dev->type->pm`, `dev->class->pm` 和 `dev->driver->pm`,實際的執行順序是: 1. 如果 `dev->pm_domain` 存在,選擇執行 `dev->pm_domain->ops` 提供的 callback 2. 否則,如果 `dev->type` 和 `dev->type->pm` 都存在,選擇 `dev->type->pm` 3. 否則,如果 `dev->class` 和 `dev->class->pm` 都存在,`dev->class->pm` 4. 否則,如果 `dev->bus` 和 `dev->bus->pm` 都存在,選擇 `dev->bus->pm` 5. 如果子系統在以上的 `pm` 不存在,最後將嘗試裝置驅動程式提供的 `dev->driver->pm`(若存在) 留意到 PM domain、type、class 和 bus 的這些 callback 可能會再間接呼叫 `dev->driver->pm` 中的特定於裝置的 PM 方法。或者也可能使用裝置更高級的介面,例如 USB driver 僅需提供 suspend 和 resume 兩個 callbacks,子系統就可藉此實現 suspend、runtime suspend、hibernation 等電源管理行為。 ## System Sleep states ### S0 嚴格來說此狀態非 sleep 的一種。在 S0 下處一般工作狀態,設備電源全開 ### S0ix: Suspend to Idle(S2I, S2Idle) 此狀態有許多別名,Intel 將其稱為 S0ix,而微軟則稱此為 Modern Stanby。其需要特定的硬體才能支援,是作為替代 S3 的睡眠狀態被設計出。在 S0ix 下可以達與 S3 相近的節能狀態,但減少喚醒所需的時間。 如果進入 S0ix 狀態,系統將會凍結 userspace、暫停計時並將所有 I/O 設備設定為低功耗狀態。這種方法可以帶來相對於 Runtime idle 更多能量耗損的減少,以便處理器可以花費系統暫停時處於最深度空閒狀態的時間。 > 延伸閱讀: [何謂新式待命模式 – Modern Standby?](https://www.allion.com.tw/system-modern-standby/) 在 S0ix 下,系統是透過 in-band 中斷進行喚醒的。所以理論上任何在正常工作狀態下能造成中斷的裝置都可以設定為 S2Idle 下的 wakeup source。 此狀態可以在不支援 standby 或 S2RAM 的平台上使用,也可以與任何更深層睡眠的狀態一起使用,以減少恢復延遲。此外,這機制只在 `CONFIG_SUSPEND` 被配置時啟用。 ### S1: Standby 也稱為 Power on Suspend。這狀態可以提供適度的節能,此時 CPU 的所有暫存器將被重置,並且停止執行指令。但由於 CPU 和記憶體的電源仍被維持,系統不會丟失 context,因此可以迅速的返回工作狀態。 具體實現上,和 S2I 類似,Standby 也會凍結用戶空間、暫停計時並將所有 I/O 裝置置於低功耗狀態。但除此之外還會將非啟動(nonboot) CPU 關閉。並且在進入此狀態期間,所有 low-level system function 也都會暫停進入 suspend(?)。因此,相對於 S0ix,這應該可以節省更多的能量,但恢復延遲也理所當然會比其更長。 相對於 S2I,在 S1 可以將系統從此狀態喚醒的裝置集合通常會較少,並且可能需要依賴平台來適當設定 wakeup 功能。 Standby 的支持需要設定 `CONFIG_SUSPEND` 並且平台向 suspend 子系統註冊之,才表示支援此狀態。 ### S2(?) CPU powered off. Dirty cache is flushed to RAM. ### S3: Suspend to RAM(STR, S2RAM) 在此狀態下,除記憶體外的單元都會進入低功耗狀態。記憶體會被設置成 self refresh mode 模式以保持其內容。也因此此狀態可顯著節省能源。 實現上,進入 standby 時執行的所有步驟也會在 S2RAM 時執行,且根據平台差異可能還會發生其他操作。尤其是在基於 ACPI 的系統上,kernel 會將控制權轉交給 platform firmware(BIOS)。且 S2RAM 的最後一步通常會關閉一些不直接由 kernel 控制的低階元件。 裝置和 CPU 雖然關閉,但其狀態應被保存在未斷電的記憶體中。在許多情況下,所有周邊匯流排在進入 S2RAM 時都會斷電,因此裝置必須能夠處理從 S2RAM 返回到開啟狀態的轉換。 在基於 ACPI 的系統上,S2RAM 需要 platform firmware 中的一些 bootstrap 程式碼才能恢復。在其他平台上也可能發生這種情況。此外,恢復方式相對於 S2I 和 standby,可以將系統從 S2RAM 喚醒的裝置集合通常會更少,甚至可能需要依賴平台支援來適當設定的喚醒功能。 如果設定了 `CONFIG_SUSPEND` 並且平台向子系統註冊了對 S2RAM 才支援。在基於 ACPI 的系統上,它會對應到 ACPI 定義的 S3 系統狀態。 ### S4: Hibernation 這種狀態也別稱為 Suspend-to-Disk(STD),其提供了最大的節能效果,且即使在沒有支援 suspend 功能的低階平台下也可以使用。但是,它需要一些低階的程式碼來恢復。 休眠與其他 sleep 有顯著不同。需要三次系統狀態變更才能進入休眠狀態,並需要兩次系統狀態變更才能從中恢復。 首先,當休眠被觸發時,核心停止所有系統活動,並建立 system image 在記憶體上。接下來,系統會暫時復原 I/O 以將 image 保存到永久性儲存的裝置,之後系統再進入目標的低功耗狀態,其中包括記憶體的幾乎所有硬體組件的電源都將被切斷,除了一組有限的 wakeup source。一旦 system image 被儲存,系統可能會進入特殊的低功耗狀態(如 ACPI S4),或者只是將自己斷電。 在試圖喚醒時,控制權會先在 platform firmwire 的 bootloader 上,其會引導一個稱為 restore kernel 的特殊 kernel 之啟動。後者將在永久性存儲中查找之前保存的 image,如果找到,則將其加載到記憶體中。接下來,系統中的所有活動會如休眠的第一步般停止,此時 restore kernel 使用 image 內容覆蓋自身,並轉移控制權到 image kernel 中的一個特殊的 trampoline。最終,image kernel 會將系統恢復到休眠前的狀態,以允許 userspace 再次運行。 如果設定了 `CONFIG_HIBERNATION` 則支援休眠。注意到只有在特定 CPU 架構支援相關系統復原的低階程式碼下,才能設定之。 ### S5: Shutdown No previous content is retained. Some components may remain powered so the computer can "wake" on input from the keyboard, clock, modem, LAN, or USB device. ## PM sysfs 介面 Linux 中的電源管理機制為 userspace 提供 sysfs 介面,以允許控制系統的睡眠狀態。此介面位於 `/sys/power/` 目錄中,並由數個屬性(檔案)組成,以下我們將逐個探討。 ### `state` 在這個檔案中包含所支援的睡眠狀態相關的字串清單。我們可以對這個檔案寫入清單中的任一字串,以導致 kernel 將系統轉換到對應的睡眠狀態。 舉例來說,"disk"、"freeze" 和 "standby" 分別表示 Hibernation、S2I 和 Standby 睡眠狀態。寫入 "mem" 字串則會根據稍後會提到的 `mem_sleep` 檔案的內容進行睡眠。 ``` $ sudo cat /sys/power/state freeze mem disk ``` :::info 一些細節可見 [`decode_state`](https://elixir.bootlin.com/linux/latest/source/kernel/power/main.c#L660) 之實作方式 ::: 如果系統不支援任何的睡眠狀態,此檔案則不會在系統中存在。 ### `mem_sleep` 這個檔案中包含支援的不同類型 suspend 機制的字串清單。我們可以對此檔案寫入清單中的任一字串,以允許 userspace 選擇對上述的 `state` 檔案寫入 "mem" 時將對應的睡眠方式。 舉例來說,"s2idle"、"shallow" 和 "deep" 分別對應 suspend to idle、standby 和 suspend to RAM。 ``` $ sudo cat /sys/power/mem_sleep s2idle [deep] ``` :::success 用 `[]` 框起來的字串表示當前的選擇 ::: 如果系統不支援任何的 suspend 機制,此檔案則不會在系統中存在。 ### `disk` 這個檔案控制 Hibernation/Suspend-to-Disk 的運作方式。具體來說,它告訴核心在建立完 hibernation image 後要做什麼。也就是對 `state` 寫入 "disk" 字串之後的行為。 ``` $ sudo cat /sys/power/disk [platform] shutdown reboot suspend test_resume ``` * platform: 這選項會將系統置於特殊的低功耗狀態(例如 ACPI S4),以允許特定喚醒方式可用,並可能允許 platform firmware 在喚醒後採用簡化的初始化方式。這只有在對應平台支援的情況下可選 * shutdown: 將系統直接 poweroff * reboot: 重新開機(主要用在診斷系統) * suspend: 結合 suspend,將系統置於上述 `mem_sleep` 檔案所選擇的狀態。如果系統成功從該狀態喚醒,則捨棄 hibernation image 並繼續。否則,使用該 image 來恢復系統狀態 * test_resume: 此方式用以診斷。行為上會好像系統剛從休眠狀態中醒來一樣的載入 image,運行 restore kernel 然後進行完整的 restore。 如果系統不支援任何的 hibernation 機制,此檔案則不會在系統中存在。 ### `image_size` 這個檔案會控制 hibernation image 的大小。可以對其寫入一個非負整數的數字字串,則進行 hibernation 時,系統會盡最大可能確保 image 大小不會超過之(以 bytes 為單位)。 但注意如果無法實現指定大小,仍然會建立 hybernation image,這個檔案只是要求盡可能達到指定數字。換言之,向該檔案寫入 0 會導致 hiberanation image 的大小達到最小。 讀取該檔案會得到目前的設定值,其預設為可用 RAM 大小的 2/5 左右。 ### `pm_trace` 此檔案控制名為「PM trace」的機制。簡單來說是會將最後一次 suspend 或 resume event point 保存在 RTC memory 中,且該資訊可以在 reboot 後取得。這機制有助於更有效地 debug 由於 suspend 或 resume 期間發生的 device driver 故障而導致的系統鎖定或重新啟動。 該檔案內容預設為 0,可以向其中寫入非零整數的字串將其變更為1。則如果檔案內容為 1,每個 suspend/resume 事件點的足跡將覆蓋實際的 RTC 資訊,依次儲存在 RTC memory 中,因此如果儲存後立即發生系統崩潰,資訊可以保留下來並用於除錯。 ### `wakeup` 額外補充: 不同於上述可見的 sysfs 節點是對於整個 PM 子系統的控制方式。每個由 PM 子系統管理的裝置可以有各異的 PM 功能支援,可由 `/sys/devices/.../power/` 路徑下的檔案取得相關資訊或更改配置。 有些裝置可以被用來觸發喚醒系統的事件,使系統退出睡眠狀態。例如透過對鍵盤輸入或者按下開機按鍵,讓系統從睡眠中恢復。與此功能相關的檔案是 `/sys/devices/.../power/wakeup`。 ```cpp struct dev_pm_info { unsigned int can_wakeup:1; ... struct wakeup_source *wakeup; ``` 要在 Linux 核心中使裝置能被用來喚醒系統的睡眠狀態,驅動程式需先使用 `device_set_wakeup_capable()` 來設置裝置對應的 `struct device` 下的 `power.can_wakeup` 欄位。這表示裝置首先在物理上可支援此功能。 `power.wakeup` 則是一個 `struct wakeup_source` 的指標,定義了當將裝置作為喚醒系統的觸發來源時,如何把喚醒事件通知到 PM core 的程式碼實作,使後者能夠將整個系統都恢復到運作狀態。 當裝置在硬體上能夠支援系統喚醒,且在核心中也提供 `power.wakeup` 的實作並由 `device_set_wakeup_capable` 啟用,在 userspace 的`/sys/devices/.../power/` 路徑下就可以看到 `wakeup` 檔案。在 userspace 可以向其中寫入 "enabled" 或者 "disabled" 來決定是否要將該裝置作為喚醒訊號的提供者。 留意到系統的喚醒與 runtime PM 時談論的 remote wakeup 不同。雖然有些情況兩者在物理上是透過一樣的機制,但 Remote wakeup 主要是指在低功耗狀態的裝置可以觸發特定中斷,以通知系統應將其置於全速狀態的功能。但這些中斷在系統進入睡眠時不一定能夠觸發。 ### 小結 * 根據上述,我們知道有兩種方法可以使系統進入 S2I。第一個是將 "freeze" 直接寫入 `/sys/power/state`。第二種是將 "s2idle" 寫入 `/sys/power/mem_sleep`,然後再將 "mem" 寫入 `/sys/power/state` * 同樣的,有兩種方法可以使系統進入 standby。一種是對 `state` 寫入 "standby",另一種是將 "shallow" 寫入 `mem_sleep`,然後再對 `state` 寫 "mem" * 只有一種方法可以讓系統進入 S2RAM: 將 "deep" 寫入 `/sys/power/mem_sleep`,然後將 "mem" 寫入 `/sys/power/state` ## Asynchronous Suspend/Resume * [[RFC] PM: Asynchronous suspend and resume(2009)](https://lore.kernel.org/lkml/200908122218.13975.rjw@sisk.pl/) * [PM updates for async_resume](https://lore.kernel.org/lkml/200912052216.19540.rjw@sisk.pl/) * [Linus's comment on async_resume](https://yarchive.net/comp/linux/async_resume.html) * [PM: Asynchronous suspend and resume(2010)](https://lore.kernel.org/all/201001240033.34253.rjw@sisk.pl/) ## Suspend & Hibernation 流程 關於在 Linux 中如何運用 `dev_pm_ops` 中的 callback 來達到 PM 的管理,以及每個 callback 實作上需注意的細節可參考以下幾個章節。 ### 進入 System Suspend > [Entering System Suspend](https://docs.kernel.org/driver-api/pm/devices.html#entering-system-suspend) #### `prepare` 首先使用的 callback 是 `prepare`。該 callback 的作用是防止 suspend 開始前,有裝置正好要進行註冊造成的競爭問題。與其他 suspend callback 的方向不同,`prepare` 是從裝置樹的頂端開始向底進行。 當 `prepare()` 完成並返回,對應裝置下不能就不能再被註冊新的子設備。`prepare()` 中也可以為即將到來的系統電源轉換做相應準備,但此時還不能將裝置置於低功耗狀態。 此外,如果裝置支援 runtime PM,則 `prepare()` 中不得對相應狀態做調整。但 `prepare` 的返回值可用於向 PM core 提示它可以安全地將裝置留在 runtime suspend,而不需要先將其喚醒然後再重新 suspend 一遍。前提是該裝置的所有子裝置也都處於 runtime suspend。 更準確的說,如果裝置和其子代的 `prepare()` 都返回正值且都在 runtime suspend 的狀態, PM core 就會跳過 `suspend`, `suspend_late` 和 `suspend_noirq` 階段,以及後續對應的 resume 中的步驟。這個過程在術語上被命名為 direct-complete。 我們可以設置 `DPM_FLAG_NO_DIRECT_COMPLETE` 或 `DPM_FLAG_SMART_PREPARE` 來要求不同的 prepare 方式。這些內容正好在 [PCI/PCIe(2): 電源管理 - Flags](https://hackmd.io/UZxyGS6iS9mUfA7_2NQROQ?view#Flags) 章節整理過,這裡就不多花心思說明。 #### `suspend` `suspend()` 中的主要可能的行為是使裝置停止所執行的 I/O、保存必要的裝置暫存器值、然後將其置於適當的低功耗狀態。並且這裡也可以啟用裝置的喚醒事件。 #### `suspend_late` 對於許多裝置來說,suspend 要細分為「靜止裝置」和「保存裝置狀態」兩個階段,然而保存裝置狀態卻又不得不在開啟裝置中斷的狀態下達成。因此核心提供的 callback `suspend` 和 `suspend_late` 可用來區分兩者,同時避免依賴中斷的問題。 此外,在 `suspend_late` 可以確保相關裝置停用 runtime PM,因此有需在此前提下進行的操作,可於此 callback 下實作。 #### `suspend_noirq` `suspend_noirq` 發生在裝置的 IRQ handler 被停用之後。因此在 `suspend_noirq` 應該保存先前未儲存的裝置暫存器,最終將裝置置於適當的低功耗狀態(如果在 `suspend()`/`suspend_late()` 時還未做)。 多數的裝置是不需要實作此 callback 的。其存在的理由主要來自不同裝置共用 interrupt vector 的狀況。例如 PCI,如果沒有區別 `suspend_noirq`,在 PCI 裝置 A 被置於低功耗時可能出現 PCI 裝置 B 的中斷,此時由於共享 interrupt handler,導致競爭而產生錯誤。 ### 退出 System Suspend * [Leaving System Suspend](https://docs.kernel.org/driver-api/pm/devices.html#leaving-system-suspend) ### 進入 Hibernation * [Entering Hibernation](https://docs.kernel.org/driver-api/pm/devices.html#entering-hibernation) ### 退出 Hibernation [Leaving Hibernation](https://docs.kernel.org/driver-api/pm/devices.html#leaving-hibernation) ## Reference * [Device Power Management Basics](https://docs.kernel.org/driver-api/pm/devices.html) * [System Sleep state](https://www.kernel.org/doc/html/next/admin-guide/pm/sleep-states.html) * [Power management of embedded Linux systems](https://www.theseus.fi/bitstream/handle/10024/782615/Marjanen_Lauri.pdf;jsessionid=73463FA366CB2BF89E7E980AF0E5C3A4?sequence=2) * [Introduction to Kernel Power Management - video](https://www.youtube.com/watch?v=juJJZORgVwI) * [Introduction to Kernel Power Management - pdf](http://events17.linuxfoundation.org/sites/events/files/slides/Intro_Kernel_PM.pdf) * [Why We Need More Device PM Callbacks](https://events.static.linuxfound.org/images/stories/pdf/lfcs2012_wysocki.pdf) * [PM / Sleep: Introduce "late suspend" and "early resume" of devices](https://lkml.iu.edu/hypermail/linux/kernel/1201.2/00440.html) https://lwn.net/Articles/505683/