--- tags: Linux Kernel Internals, 作業系統 --- # Linux 核心設計: Power Management(3): 測試工具 ## 引言 在過去章節中,我們知道 Linux 可以透過各式的機制提供電源管理之功能。然而如果功能實作中有錯誤,加上睡眠狀態可能部分功能將暫停之狀態下,如果發生問題,核心是否還可以產生關鍵除錯訊息?或者可否搭配特定工具來釐清問題? 對於相關功能的核心的開發者,這是至關重要的問題。 除此之外,系統的睡眠和喚醒時間也是調校的重點。對於使用者而言,平台進入低功耗狀態與恢復運作的時間是愈短愈好。則如何分析各個裝置驅動程式的耗時,或者哪個睡眠/恢復階段是時間的瓶頸,同樣是不能忽視的問題。 因此,在本章節中我們將討論如何對 Linux 中的電源管理進行測試、除錯及分析。期待可以彙整不同的工具,以便在問題出現時有效率地找出原因所在! ## PM on QEMU 由於 Power management 並非是系統的必要功能,讀者手上的硬體可能不支援相關功能(比如某些 Raspberry Pi)。所幸 QEMU 對此有一定程度的支援,可以讓我們很容易的測試。因此在正文開始之前,先簡單說明如何透過 QEMU 來觀察系統的睡眠與喚醒。 ### 測試方法 首先,在啟動 QEMU 時,建議額外加上 `-monitor telnet:localhost:7001,server,nowait,nodelay`,這樣我們就可以在另外一個 session 運行 `telnet localhost 7001` 開啟 QEMU monitor,後者可以用來將系統喚醒。 :::info 如果不開啟另一個 session,也可以用 `ctrl-a c` 切換模式 ::: 則假設要測試 S2R,運行以下命令。 ``` # echo mem > /sys/power/state ``` 等待系統進入睡眠後,在 monitor 模式由以下命令可喚醒。 ``` (qemu) system_wakeup ``` 查看 QEMU console 的訊息,如果可以看到 `PM: suspend entry`/`PM: suspend exit` 訊息,就是成功讓系統進入睡眠然後喚醒了! ``` [ 20.617496] PM: suspend entry (deep) [ 20.618524] Filesystems sync: 0.000 seconds [ 20.622658] Freezing user space processes [ 20.625029] Freezing user space processes completed (elapsed 0.002 seconds) [ 20.625204] OOM killer disabled. [ 20.625547] Freezing remaining freezable tasks [ 20.628371] Freezing remaining freezable tasks completed (elapsed 0.002 seconds) [ 20.629149] printk: Suspending console(s) (use no_console_suspend to debug) [ 20.635591] sd 0:0:0:0: [sda] Synchronizing SCSI cache [ 20.636752] ata2.00: Check power mode failed (err_mask=0x1) [ 20.637965] ata1.00: Entering standby power mode [ 20.641079] ACPI: PM: Preparing to enter system sleep state S3 [ 20.641624] ACPI: PM: Saving platform NVS memory [ 20.641724] Disabling non-boot CPUs ... [ 20.643126] ACPI: PM: Low-level resume complete [ 20.643411] ACPI: PM: Restoring platform NVS memory [ 20.647040] ACPI: PM: Waking up from system sleep state S3 [ 20.652441] pci 0000:00:01.0: PIIX3: Enabling Passive Release [ 20.652599] sd 0:0:0:0: [sda] Starting disk [ 20.682151] OOM killer enabled. [ 20.682229] Restarting tasks ... done. [ 20.683722] PM: suspend exit [ 20.807839] ata1: found unknown device (class 0) [ 20.809233] ata2: found unknown device (class 0) [ 270.650816] hrtimer: interrupt took 34062334 ns [ 317.229329] clocksource: timekeeping watchdog on CPU0: hpet retried 1 times befores [ 318.171384] clocksource: timekeeping watchdog on CPU0: hpet wd-wd read-back delay s [ 318.202563] clocksource: wd-tsc-wd read-back delay of 252130ns, clock-skew test sk! [ 332.879666] kworker/u4:0 (11) used greatest stack depth: 12880 bytes left ``` ## Kernel 機制 ### pm_trace > Reference: [DebuggingKernelSuspend](https://wiki.ubuntu.com/DebuggingKernelSuspend) Linux 的一種電源管理機制是可以透過 suspend 和 resume 流程來為裝置提供省電功能。而在此流程可能發生的錯誤中,又以在 resume 部分出現問題為主,且多數原因來自無法從斷電狀態完成 resume 的裝置驅動程式。 針對 resume 問題的除錯並不容易。核心中提供一種稱為 **pm_trace** 的方法,這可以在 resume 期間記錄進行的進度,並在手動重新啟動後取得此資料。 但首先要克服的問題是,考慮到資料要在重新啟動後保留,理當將 trace 資料儲存於永久性儲存裝置中。然而在 resume 階段,電腦此時還沒有啟動可用的永久性儲存裝置,這就產生了矛盾。幸好在 PC 主機板上的 real time clock(RTC) 可在重啟後仍然保留資訊。因此,**pm_trace** 就是藉此硬體來完成 resume 的追蹤。 要啟動 pm_trace 並且強制觸發 suspend,我們可以通過以下命令: ``` $ sudo sh -c "sync && echo 1 > /sys/power/pm_trace && pm-suspend" ``` :::info 如果 pm-suspend 指令不存在的話,可透過以下命令安裝: ``` $ sudo apt install pm-utils ``` 或是通過控制 `/sys/power/state` 來進入 suspend。 ``` $ sudo sh -c "sync && echo 1 > /sys/power/pm_trace && echo mem > /sys/power/state" ``` ::: :::warning [Difference between suspend to "mem" and pm-suspend](https://superuser.com/questions/391053/difference-between-suspend-to-mem-and-pm-suspend) ::: 此時電腦應會進入 suspend 狀態,通常伴隨著電源 LED 緩慢閃爍。發生這種情況後,按電源按鈕則開始 resume。觀察到磁碟指示燈短暫亮起後,這應該就表示 resume 已經開始進行了。如果 resume 無法完成,會有大約 3 分鐘的時間來重啟並得以保留 RTC 中的資訊,可以按住電源按鈕直到電腦關閉再重新開啟。 確保載入出現 resume 問題的相同核心的話,我們就可以開始觀察 pm_trace 所追蹤到的內容了。通過 dmesg 應該可以看到類似下面的信息: ``` $ dmesg ... [ 11.323206] Magic number: 0:798:264 [ 11.323257] hash matches drivers/base/power/resume.c:46 ``` 除此之外,很可能還有另一行 hash match 字樣的內容。如果是這樣,這表示你足夠幸運,因為最後這顯示的很可能就是造成問題的罪魁禍首。例如: ``` hash matches device i2c-9191 ``` 證明這一點的方法是在啟動 suspend 之前手動將該 module 移除,然後重複測試。此外,如果獲得的是一段 device number 而不是特定名稱,那麼 `lspci` 和 `/sys/devices/pci*` 可能可以幫忙找到對應資訊。 ### pm_test #### Hibernation(STD) 在 [Linux 核心設計: Power Management(1): System Sleep model](https://hackmd.io/@RinHizakura/r17lOaDqp) 一節中我們介紹了 sysfs 的介面,可以藉此設定 hiberation 後的行為(`/sys/power/disk`)並觸發之(`/sys/power/state`)。在 Linux 文件 [Debugging hibernation and suspend](https://docs.kernel.org/power/basic-pm-debugging.html) 中也以此建議了幾種常見的 hibernation 情境,建議作為基本測試的項目(詳見文件)。 ``` /* 1 */ # echo reboot > /sys/power/disk # echo disk > /sys/power/state /* 2-1. */ # echo platform > /sys/power/disk # echo disk > /sys/power/state /* 2-2. (if 'platform' is not supported) */ # echo shutdown > /sys/power/disk # echo disk > /sys/power/state /* 3. Check if failures to resume from hibernation are related * to bad platform firmware */ # echo test_resume > /sys/power/disk # echo disk > /sys/power/state ``` 不過,若直接以上述方式測試,若發生問題時有時並不容易直接釐清。要找出 hibernation 失敗的原因,可以搭配使用 kernel 提供的特殊工具 `/sys/power/pm_test`(前提是核心使用 `CONFIG_PM_DEBUG` 進行編譯)。藉此工具可透過測試模式運作 hibernation。目前有 5 種測試模式可選擇: * `freezer`: 測試 process 被 freeze 的情況 * `devices`: 測試 process freezed + device suspending * `platform`: 測試 process freezed + device suspending + platform global control methods(詳見 [ACPI Device Power Management](https://hackmd.io/UZxyGS6iS9mUfA7_2NQROQ#ACPI-Device-Power-Management)) * `processors`: 測試 process freezed + device suspending + platform global control methods + disable nonboot CPUs * `core`: 測試 process freezed + device suspending + platform global control methods + disable nonboot CPUs + platform/system devices suspending :::info 注意到最後 platform global control methods 依賴支援 ACPI 的平台,並只在 `/sys/power/disk` 為 platform 情況下可運作。 ::: 使用 `pm_test` 測試 hibernation 的方法是,先將相應的字串寫入 `/sys/power/pm_test`,然後發出標準的休眠命令。例如,要使用 "devices" 測試模式並選擇 "platfrom" 的休眠模式,則依序執行下列操作: ``` # echo devices > /sys/power/pm_test # echo platform > /sys/power/disk # echo disk > /sys/power/state ``` 以上述案例來說,則核心將嘗試: 1. process freezed 2. device suspending 3. 等待幾秒鐘。預設為 5 秒,但可透過 suspend.pm_test_delay 模組參數進行設定 4. device resuming 5. process thawed 其他測試模式可以此類推。 如果想解除測試模式,則將 `none` 寫入 `/sys/power/pm_test`。讀取 `/sys/power/pm_test` 則可看到所有可用測試選項,其中目前所選由方括號 `[]` 指示。 在測試模式中我們可以看到每個測試級別都比前一個更深入,而core 等級盡可能深入地測試硬體和驅動程序但無需創建 hibernation image。因此,根據經驗,測試上建議應依照 freezer -> devices -> platform -> processors ->core 的順序測試,且最好在每個級別上重複測試幾次,以避免任何隨機因素。 則按照上述順序嘗試,可以有系統的推論分析: * 如果「freezer」測試失敗: 存在無法凍結的任務。這種情況下,通常在 dmesg 輸出可找到有問題的任務 * 如果「devices」測試失敗,很可能存在無法 suspend 或 resume 的裝置驅動程式(留意到後者可能會讓系統測試後 hang 或者不穩定)。要找到這個驅動程序,一種效率不高但可行方式是二分搜尋: * 如果測試失敗,卸載目前載入的一半驅動程式並重複測試 * 如果測試成功,則載入最近卸載的一半驅動程式並重複 * 如卸載所有 modules 測試仍失敗,可能需在 kernel config 中尋找編譯為 builtin module 的驅動程式,並改為使用對應的獨立 module 再次測試 * 如果「platform」測試失敗,則表示 platform firmware(例如 ACPI)的處理有異常 * 如果「processors」測試失敗,則表示停用/啟用 non-boot CPU 有異常 * 可以嘗試使用 /sys/devices/system/cpu/cpu*/online 來 hotplug CPU 比對之間問題 * 如果「core」測試失敗,這表示 platform/system devices 的 suspend 失敗(這些裝置只在一個 CPU 上 suspend,且 CPU 關閉中斷),則問題很可能與硬體相關且嚴重 #### Suspend(STR) 我們可以將 "freezer"、"devices"、"platform"、"processors" 或 "core" 寫入 `/sys/power/pm_test`(如果核心編譯時開啟 `CONFIG_PM_DEBUG` 選項),則會運行與給定字串對應的 suspend 測試程式碼。由於 STR test mode 的定義方式與上一小節的 hibernation 相同,這裡就不再重複說明。 ### `no_console_suspend` 一般情況下,Linux 系統在進入低功耗狀態時,console 也會被暫停,則此時我們可能會錯過重要的除錯資訊。為此,可以通過在開啟參數(bootargs) 加入 `no_console_suspend`,或者由下面的命令讓 console 在電源管理流程中不進行暫停: ``` echo N | sudo tee /sys/module/printk/parameters/console_suspend ``` 留意到此功能可能因平台限制而無法進行完整的電源管理流程,因此不能預設使用,只能在需要釐清錯誤時開啟。 ### `pm_pr_dbg` 在編譯配置加入 `CONFIG_PM_SLEEP_DEBUG` 或 `CONFIG_DYNAMIC_DEBUG ` 下,電源管理流程可以涵蓋更豐富的資訊,具體是藉由 [`pm_pr_dbg`](https://elixir.bootlin.com/linux/v6.10.8/source/include/linux/suspend.h#L553) 所輸出的訊息,完整的訊息控制規則請參閱註解說明: ```c /** * pm_pr_dbg - print pm sleep debug messages * * If pm_debug_messages_on is enabled and the system is entering/leaving * suspend, print message. * If pm_debug_messages_on is disabled and CONFIG_DYNAMIC_DEBUG is enabled, * print message only from instances explicitly enabled on dynamic debug's * control. * If pm_debug_messages_on is disabled and CONFIG_DYNAMIC_DEBUG is disabled, * don't print message. */ ``` 要開啟此功能,可通過下述命令。 ``` echo 1 | sudo tee /sys/power/pm_debug_messages ``` ### `initcall_debug` `initcall_debug` 可以允許顯示裝置驅動程式初始化階段(init/probe)的有關訊息,並計算其耗時。在電源管理流程中,由於也會涉及裝置的初始化,因此可以藉此功能獲得更多除錯資訊。 可以藉由在開機參加入 `initcall_debug` 或是以下方式開啟此功能。 ``` echo 1 | sudo tee /sys/module/kernel/parameters/initcall_debug ``` ### `wake_lock`/`wake_unlock` > [Linux电源管理(9)_wakelocks](http://www.wowotech.net/pm_subsystem/wakelocks.html) 在 sysfs 中存在 `/sys/power/wake_lock` 和 `/sys/power/wake_unlock` 介面。兩者可用來阻止系統進入低功耗(e.g. suspend, hinernate) 模式。一般而言,這個介面搭是配著 Android 上的 autosleep 機制使用。 可參考 [Autosleep and wake locks](https://lwn.net/Articles/479841/) 一文。 ### `wakeup_count` `wakeup_count` 的用途是針對系統睡眠與喚醒事件之間的同步。 預期的系統行為是: * 如果在系統處於睡眠狀態時發生喚醒事件,則系統會被喚醒 * 如果在系統正在轉換到睡眠狀態期間,發生喚醒事件,則應中止該轉換 * 如果在系統處於工作狀態時發生喚醒事件,則在偵測到該事件後的一定時間內,嘗試啟動睡眠狀態的轉換應該會失敗 僅使用之前提過的 `state` 並不能滿足上述要求,因為喚醒事件可能恰好在寫入 `state` 時發生,並且可能在 usersapce 被凍結前就傳遞達到 userspace。因此該事件會保持部分處理狀態,且不會導致向睡眠狀態轉換的中止,直到系統被另一個事件喚醒。 如果在寫入 `state` 之前使用 `wakeup_count` 則可以克服這個問題。具體的作法是,首先讀取 `wakeup_count` 並儲存讀取的值。然後,在完成系統轉換到睡眠狀態的準備工作後,應將儲存的值寫回 `wakeup_count`。如果操作失敗,則表示從讀取 `wakeup_count` 以來至少發生了一次喚醒事件,因此不應寫入 `state`。反之,寫入 `state` 是可以的。但如果在寫入 `wakeup_count` 後偵測到任何喚醒事件,則轉換將被中止。 ## pm-graph 呈開頭所述,電源管理的重要議題之一是分析系統的睡眠與喚醒時間。在此需求下,我們需要方法以剖析低電源模式的進入與退出過程的各階段耗時。[pm-graph](https://github.com/intel/pm-graph) 可以彙整 ftrace 的輸出結果,並將其輸出成易於觀察的時間圖,因此在分析電源管理流程中的時間瓶頸時,能夠給予十分直接的線索。 :::info 可以參考 [Is Linux Suspend ready for the next decade - Len Brown](https://youtu.be/Pv5KvN0on0M?si=TqGCTYdCynpZn_iv) 影片開頭對此工具的介紹。 ::: ## Reference * [Debugging hibernation and suspend](https://docs.kernel.org/power/basic-pm-debugging.html) * [How to get s2ram working](https://www.kernel.org/doc/Documentation/power/s2ram.txt) * [checkbox](https://github.com/canonical/checkbox)