# [Optimizing suspend/resume](https://youtu.be/8Iuyp1fp8QI) 這個演講主要探討了優化 suspend/resume 的研究,並強調了它對手錶和手機等設備的重要影響。在一開始,Saravana Kannan 簡單介紹了 suspend/resume 的過程,並將其劃分為以下幾個階段: ``` Suspend: * suspend enter * sync_filesystems * freeze_processes * dpm_prepare * dpm_suspend * dpm_suspend_late * dpm_suspend_noirq * Power off CPUs Resume: * Power on CPUs * dpm_resume_noirq * dpm_resume_late * dpm_resume * dpm_complete * thaw_processes ``` 這其中的細節能夠在 [Linux Suspend/Resume 實驗(二)](https://hackmd.io/@Guanchun0803/SuspendResume2) 和 [Linux Suspend/Resume 實驗(三)](https://hackmd.io/@Guanchun0803/SuspendResume3)可以得知。在演講中,Saravana Kannan 提到在手機和手錶上很常會執行 suspend/resume 在一天中,這個動作可能會執行上百次甚至上千次,並且在 suspend 過程中,sync_filesystems 大約會佔據 10% 到 35% 的時間。 **第一個重點是:優化 sync_filesystems** 一旦 sync_filesystems 被執行,使用者空間(userspace)將無法中止該操作,而這個同步動作最長可能會耗時超過 25 秒。 這帶來了一個關鍵挑戰:如何在保證資料一致性的同時,避免這類同步操作長時間阻塞整個系統的掛起流程(suspend)? 其中一項潛在的解法是關閉自動的檔案系統同步功能,然而這樣做將帶來資料遺失的風險。 為了在穩定性與資料安全之間取得平衡,Saravana Kannan 提出了一些折衷方案,例如:將系統設定為每隔一小時才嘗試掛起一次,而非頻繁地執行掛起,以避免頻繁執行長時間的同步操作。 但更關鍵的問題是:是否有可能引入一種機制,能夠中止或跳過正在進行的檔案系統同步,或至少讓該操作不會同步時阻塞系統? * 他們的一個嘗試是將 sync_filesystems 的操作移至 userspace 執行,並透過設計一個接收器,由獨立的執行緒負責處理該同步請求;主流程則僅需透過條件變數等待該執行緒完成作業。 **第二個重點:優化 suspend_enter** 目前的 suspend_enter 流程會遍歷所有執行緒,並嘗試將它們移至凍結(freezing)狀態,以準備進入 suspend。 然而,Saravana Kannan 發現,在 suspend_enter 的早期階段,系統會進行一些必要的操作,但隨後該執行序本身就幾乎沒有進一步活動,甚至會陷入睡眠狀態。這樣的行為可能導致整體 suspend 過程效率低下,甚至出現不必要的延遲。 **第三個重點:Async suspend 機制的實際效益不如預期** 雖然 Linux 在 suspend/resume 流程中支援非同步(async)操作,但實務上,這項設計在許多情境下不但沒有提升效能,甚至可能導致整體流程變慢。 原因包括: 許多裝置的 suspend/resume 操作極為簡單甚至是 no-op,只需數微秒即可完成,卻因進入 async 流程而產生不成比例的排程成本。 為了實現 async,需要額外執行下列步驟,導致整體開銷反而大於實際工作量: * work queuing * kworker 喚醒 * context switch * wait_for_completion() 呼叫,用於等待上下游裝置(consumers、children、suppliers、parent)完成作業 這說明 async 並不總是最有效率的選擇,特別是在裝置操作非常快速或高度依賴序列關係的情況下。 為了解決上述問題,該處提出了一項改進方案:將 suspend/resume 的執行流程改為使用 BFS(Breadth-First Search,廣度優先搜尋)方式進行非同步裝置的處理 [PM: sleep: Do breadth first suspend/resume for async suspend/resume](https://lore.kernel.org/linux-pm/20241114220921.2529905-5-saravanak@google.com/) 這個 patch ,該 patch 也啟發了 Rafael J. Wysocki 進一步對其進行整理與強化,最終於 Linux 6.16 被正式合併進主線,成為核心支援的標準機制之一。 **最後一個重點是:解決 S2idle 韌體相依問題的替代架構提案** S2idle 是目前 Linux 支援的淺層 suspend 模式,理論上比 S2RAM(suspend-to-RAM)有更快速的進入與喚醒時間,並且整體使用體驗更順暢。然而,這完全依賴平台韌體的正確支援。 事實上,S2idle 在實做時總是會遇到韌體錯誤,而韌體更新在多數商業設備上幾乎不可能,導致在既有硬體上難以廣泛部署穩定的 S2idle 功能。 此外,隨著系統中 CPU 數量增加,S2idle 相對於 S2RAM 的效能與功耗差距也越來越明顯,這進一步加劇了問題。 本提案討論一種可能的替代方式:以 S2RAM 韌體呼叫為基礎,模擬出一種具備類似 S2idle 行為的架構,以在不完全依賴平台韌體的情況下,仍能獲得合理的 suspend 體驗。 具體構想如下: * 定義一個 虛擬的 C-state(fake C-state),在該 idle 狀態中不做真正的 idle,而是觸發 S2RAM 所用的 hotplug API,將 CPU 等元件離線或降功耗,在從這個 fake C-state 喚醒時,先透過 S2RAM firmware calls 完成 CPU 上電,再由核心透過 IPI(Inter-Processor Interrupt) 叫醒其他 CPU。 # [Linux Power Management Features, Their Relationships and Interactions](https://youtu.be/_jb6U40ZCZk) 本場演講一開始主要介紹了 Linux suspend 的整體流程,以及相關的 kernel 參數設定,這些設定可用來協助檢查 suspend 過程中可能出現的問題或 bug。 下列是啟用電源管理除錯訊息的相關設定: ``` # 啟用 suspend 時的 kernel log 輸出 $ echo 0 > /sys/module/printk/parameters/console_suspend $ echo 8 > /proc/sys/kernel/printk # 開啟 suspend 時間統計與除錯訊息(需有 CONFIG_PM_SLEEP_DEBUG = y ) $ echo 1 > /sys/power/pm_print_times $ echo 1 > /sys/power/pm_debug_messages ``` | Callback | 作用 | | ------------------- | ------------------------------------- | | `->prepare()` | **準備進入 suspend**,此時應避免新增 child device | | `->suspend()` | **停止 I/O 操作** | | `->suspend_late()` | **進一步關閉裝置** | | `->suspend_noirq()` | **IRQ 已停用,不能再用中斷機制** | suspend_noirq() 的額外保證是:「IRQ handler 保證不會再被呼叫」。 ->suspend*() 函式的目的是: 1. 儲存裝置狀態,讓 resume 時可以還原 1. 讓裝置進入低功耗狀態 不一定每個驅動都要實作全部階段,但要根據裝置需求選擇。 重要提醒:行為因裝置而異 不同裝置 suspend 的需求差異非常大,核心不會保證所有裝置都會被以同樣方式處理 → 驅動要自行實作正確的 suspend 邏輯,並且沒有明確的裝置狀態輸出資訊可以查詢(除了錯誤碼),也就是 kernel 不會告訴你裝置現在是不是真的進入低功耗。 **不同 suspend 實作階段對 GPIO 控制器行為的影響**: Linux 核心支援多階段的裝置 suspend 流程,驅動開發者可根據裝置特性與需求,在不同階段實作對應的 callback 函式,例如 suspend()、suspend_late()、suspend_noirq() 等。這些階段對應系統中斷(IRQ)與裝置依賴狀態的不同條件,因此選擇在哪個階段進行電源管理操作,將直接影響 suspend/resume 的穩定性與功能正確性。 這在處理與 IRQ 和喚醒(wake-up)邏輯密切相關的裝置時,尤為重要,例如 GPIO 控制器。 以下為兩個 GPIO 控制器驅動的實作對比: ```c static const struct dev_pm_ops rzg2l_pinctrl_pm_ops = { NOIRQ_SYSTEM_SLEEP_PM_OPS(rzg2l_pinctrl_suspend_noirq, rzg2l_pinctrl_resume_noirq) }; ``` ```c static SIMPLE_DEV_PM_OPS(nmk_pinctrl_pm_ops, nmk_pinctrl_suspend, nmk_pinctrl_resume); ``` * pinctrl-rzg2l 驅動選擇實作於 suspend_noirq() 階段,這是系統中斷(IRQ)已完全停用之後的階段。 * 相較之下,pinctrl-nomadik 驅動則在較早的 suspend() 階段就完成 suspend 設定,這時系統仍允許設定中斷與 wake source。 這樣的差異導致明顯的行為影響:若該 GPIO 預期作為系統的喚醒來源,其相關 pin 的設定必須在中斷仍可用的階段完成,否則在 suspend 過程中,該 GPIO 將無法正確地發揮喚醒功能,進而導致系統 resume 失效。 進一步來說,雖然 suspend_noirq() 提供一個在系統中斷停用後仍可執行的階段,但不應將所有 suspend 行為都延後至此處處理。如簡報中所警示: >Careful! Moving everything to ->suspend_noirq() is not the solution. 若驅動在 suspend 過程中需要中斷(如等待事件完成、執行會喚起 IRQ 的操作),那麼在 noirq 階段執行就會失敗。 常見如 pinctrl_select_state() 等 API,在部分平台可能隱含依賴中斷或其他尚未停用的資源。此時在錯誤階段執行這類操作,將導致行為不穩定甚至無效。 因此,開發者在撰寫驅動中的電源管理邏輯時,應審慎選擇 callback 實作階段,並依據裝置的角色、IRQ 需求與相依順序進行設計。對於與喚醒功能有關的控制器裝置,建議於 ->suspend() 或更早的階段完成所有必要設定,以確保系統 suspend/resume 行為的正確性與穩定性。 **System-wide suspend: suspend to idle**: s2idle 是 Linux 提供的一種與平台無關的 suspend 模式。 它不像 S2RAM 需要 BIOS/firmware 配合,完全由 Linux 自行實作,原理就是「所有 CPU 都 idle 並等待中斷喚醒」。 ```c static void s2idle_enter(void) { s2idle_state = S2IDLE_STATE_ENTER; // 將所有 CPU 推入 idle 狀態 wake_up_all_idle_cpus(); // 自己(主 CPU)也進入 idle,等待中斷喚醒 swait_event_exclusive(s2idle_wait_head, s2idle_state == S2IDLE_STATE_WAKE); // 喚醒後通知所有 CPU 恢復狀態 wake_up_all_idle_cpus(); s2idle_state = S2IDLE_STATE_NONE; } ``` **System-wide suspend: platform-provided modes (standby S2R)**: 這裡在介紹有兩種暫停模式有下列兩種: 1. standby 2. suspend to RAM 這兩種 suspend 模式皆需要底層平台提供支援,因此在某些設備上可能無法啟用、或即使啟用後其實無法正常達到省電目的。 實際行為高度依賴平台,例如 **S2R 理論上應該會**: * 降低 DRAM 頻率 * 讓記憶體控制器進入 self-refresh 模式 但實際上,kernel 並不保證會這麼做,這一切端看平台與韌體是否正確實作 其他不確定因素包括: * standby 可能只是讓 CPU 執行 idle 指令(如 cpu_do_idle() 或 WFI),實際功耗下降有限 * SoC 有可能關閉,也可能不關 * CPU cache 有些平台全部清除、有些只關部分,有些則完全不動 * clocks 也可能全部停用、部分停用,或保持運作 * 有些驅動會自行查 pm_suspend_target_state 來選擇行為 * regulator 子系統可透過裝置樹定義 suspend 目標狀態(如 regulator-state-mem、-standby 等) **Summary:行為是平台特定的** 正因如此,standby 與 S2R 雖為 kernel 支援的 power state,但其實際功耗控制行為是由平台決定的。 在某些機器上 suspend 可能會大幅降低功耗,但在另一台看似「正常睡眠」的裝置中,實際上可能什麼都沒關掉,幾乎沒有省電效果。 **System-wide suspend: hibernation**: Linux 電源管理中,最有效率的省電方式其實是 shutdown: * shutdown 模式下,系統會完全斷電,功耗為零,是最極致的省電手段。 * 然而,shutdown 不會保留任何執行狀態,使用者需從頭重新開機與載入系統。 Hibernate 則提供一種折衷方案: 在 shutdown 前先「儲存所有狀態到磁碟」,待下次開機時能恢復原有作業狀態。 **Hibernate 的優勢與特性** 不是單純關機: * Hibernation 可支援部分硬體作為喚醒來源(如 USB 鍵盤、網卡)。 提升使用體驗: * 對於使用者空間初始化時間較長的系統,hibernate 能顯著縮短「回到工作狀態」的時間。 ![image](https://hackmd.io/_uploads/Byuaoy37gx.png) **在系統 suspend(進入睡眠狀態)期間,clock_gettime(CLOCK_MONOTONIC) 的行為應該是什麼?** clock_gettime(CLOCK_MONOTONIC) 是 POSIX 提供的一個時間查詢 API,用來取得從系統啟動以來經過的時間,不會受到系統時間(wall clock)變動影響,常用於計算時間間隔。 根據 man 2 clock_gettime 文件的說明,CLOCK_MONOTONIC 在系統進入 suspend 時應該會暫停計時,也就是說,這個時鐘不會包含 suspend 期間所經過的時間。 然而,s2idle 模式是一個例外 s2idle 是 Linux 所提供的一種平台無關、極簡實作的 suspend 模式,其本質是讓所有 CPU 進入 idle 狀態,並等待中斷(IRQ)喚醒。為了支援這種喚醒行為,系統在 s2idle 運作期間仍保留中斷功能(例如鍵盤、網卡、RTC timer 等)處於啟用狀態。 這樣的設計,引出了以下一連串的關係: * s2idle 為了能喚醒,IRQ 必須保持開啟 * 中斷處理函式通常假設 timekeeping 子系統仍在運作 * CLOCK_MONOTONIC 的值是由 timekeeping 子系統所維護 * 為了讓 IRQ handler 行為正常,kernel 在 s2idle 中仍維持 CLOCK_MONOTONIC 運作 雖然文件規範明確指出 CLOCK_MONOTONIC 應於 suspend 期間停止計時,但實際上在 s2idle 模式中,它仍然會持續跳動。這代表: * kernel 在 s2idle 模式下,違背了它對 CLOCK_MONOTONIC 行為的一致性承諾。 例外情況:若系統使用 cpuidle 進入 s2idle 有些平台在 s2idle 過程中,會將 CPU 的 idle 控制權交給 cpuidle driver。若該 driver 允許在**關閉中斷**的情況下進入低功耗狀態,那麼 kernel 就能: * 正確暫停 timekeeping 子系統 * 使 CLOCK_MONOTONIC 如預期般凍結 * 完整符合 suspend 規範定義的語意 **Runtime Power Management** 首先提出了三點: 1. 裝置本身不會自己主動進行 suspend/resume,它是由 kernel runtime PM 機制根據使用情況自動管理。 1. 裝置彼此之間有父子關係,整體是一棵樹。 1. 每個裝置都維護一個 使用計數器(usage counter),用來追蹤該裝置是否正被使用。當裝置使用者(例如驅動程式或子系統)需要啟用裝置時,會呼叫 pm_runtime_get() 將計數器加一;使用完畢後,則呼叫 pm_runtime_put() 將計數器減一,Kernel 根據這個計數器的值來判斷裝置是否閒置。 演講者也將 pm_runtime_get 和 pm_runtime_put 這兩個函式簡化為如下的偽程式碼,幫助理解其基本邏輯與設計概念: ```C /* Pseudocode for mental model */ void pm_runtime_get(struct device *dev) { dev->power.usage_count++; if (dev->parent) pm_runtime_get(dev->parent); if (dev->power.usage_count == 1) runtime_resume(dev); } ``` 每當有一個使用者來用這個裝置(例如驅動開啟串口、GPU 等),就會呼叫這個函式,如果裝置之前沒人用(usage_count == 0),就會執行 runtime_resume() 讓裝置先開機,同時也要遞迴讓它的父裝置先 resume(例如 USB 控制器要先開,滑鼠才能用)。 ```C void pm_runtime_put(struct device *dev) { dev->power.usage_count--; if (dev->power.usage_count == 0) runtime_suspend(dev); if (dev->parent) pm_runtime_put(dev->parent); } ``` 每次使用者不再需要這個裝置,就呼叫 pm_runtime_put() ,如果沒有任何使用者(usage_count == 0),就執行 runtime_suspend() 把裝置休眠 同樣也會往上檢查父裝置是否也能關機。 然而,前述的簡化程式碼僅作為概念說明,實際上 Runtime PM 的行為比這更為複雜。針對一些真實場景,作者補充了以下幾種特殊情況與其對應結果: | 實際情境 | 行為結果 | | ----------------------------- | -------------------------------------------------------- | | 在 IRQ context 中呼叫 `pm_runtime_get/pm_runtime_put()` | 系統會將操作改為非同步執行(使用 workqueue),或要求使用具 IRQ-safe 標記的對應函式 | | 呼叫 `pm_runtime_disable()` | 不會立即關閉裝置,而是讓 runtime PM 停止對該裝置進行自動 suspend/resume 管理 | | 即使裝置仍在使用也可被 disable | 裝置本身會維持當前狀態,但後續將無法再動態調整電源狀態,直到再次呼叫 `pm_runtime_enable()` | 更多補充 | 功能類別 | 說明 | | ---------------- | ----------------------------------------------------------- | | 允許/禁止 runtime PM | 控制裝置是否自動參與 suspend/resume 流程(用 `pm_runtime_allow/pm_runtime_forbid`) | | 忽略 callbacks | 某些裝置不需實作 suspend/resume 函式,由上層裝置代管(用 `pm_runtime_no_callbacks`) | | Autosuspend 延遲 | 延後 suspend 動作避免頻繁開關(用 `pm_runtime_auto_suspend()` 和 `pm_runtime_auto_suspend_delay()` 和用 sysfs 調整 delay 時間) | **Linux Suspend × Runtime PM** 在 Linux 的系統掛起過程中,Kernel 為了避免裝置在掛起期間自行恢復,會對 Runtime Power Management(Runtime PM) 採取以下自動行為: * 在 `suspend_late()` 前,自動呼叫 `pm_runtime_disable()`,停用 Runtime PM。 * 在 `resume_early()` 後,自動呼叫 `pm_runtime_enable()`,重新啟用 Runtime PM。 這套機制雖然設計良善,但在某些情境下會產生問題,例如: * 某些裝置(例如 I2C controller)在 suspend_noirq() 或 resume_noirq() 階段仍需要被使用。 * 然而,該裝置此時若處於 autosuspend 狀態,且 Runtime PM 已被停用,就無法被喚醒使用,導致相關操作失敗,進而影響整體 suspend 流程。 為了避免這種狀況,驅動開發者必須: 在 suspend() 階段主動喚醒裝置以確保裝置在後續 suspend_noirq() 階段仍處於「可用狀態」,隨後在 resume 階段補上還原動作。 # [CPU Power Saving Methods for Real-time Workloads](https://youtu.be/9_IOJDOE-Ac) 在多核心即時系統中,CPU C-State 的管理對於平衡功耗與即時反應能力具有關鍵性影響。當處於閒置狀態時,CPU 可進入不同深度的 C-State(例如 C0、C1、C3、C6 等),以達成節能與降溫目的。C-State 越深,關閉的電路越多,帶來的省電效果也越顯著。但同時,恢復這些狀態所需的時間也會增加,形成喚醒延遲的代價。 這些 C-State 的進出行為主要受兩個核心屬性影響: 1. Exit Latency(退出延遲):越深層的 C-State,其喚醒時間越長。 1. Target Residency(目標駐留時間):當 CPU 準備進入較深層的 C-State(例如 C3、C6)以節省更多電力時,必須考量「進入」與「退出」該狀態所需的能量與時間開銷。這些進出動作本身是有成本的,因此只有當 CPU 能夠在該低功耗狀態中停留夠久,這次省電才划算。 Linux Kernel 的電源管理策略(Governor)會根據這些參數與當前負載狀態,動態決定是否允許 CPU 進入特定的 C-State。 然而,這種延遲的不確定性對即時系統尤其致命。即時任務的核心目標並非單純追求「最快速度」,而是要確保每次反應時間都「穩定一致、可預測」,這種特性被稱為確定性(Determinism)。一旦系統反應時間產生抖動(Jitter),即便幅度極小,都可能導致任務錯過時限(Deadline Miss),進而造成整體即時系統功能失效。 尤其當 CPU 進入較深層的 C-State(例如 C3 或 C6)時,硬體會自動關閉或清空部分子系統,如 L1/L2 cache、TLB(Translation Lookaside Buffer) 等。這些元件在喚醒時必須重新建立內容(cache repopulation)、執行同步與校準操作,這過程不僅耗時,而且會因當前系統狀態、背景負載與中斷情況等因素而變動,導致喚醒延遲不再穩定。 因此,傳統即時系統設計往往會完全避開使用深層 C-State,即便這意味著犧牲電力效率,也要換取回應延遲的絕對穩定性。唯有如此,系統才能在高要求的即時應用中(如工業控制、自駕車、網通設備)達成任務時效的可靠保障。 然而,隨著系統日趨複雜與多核心化,不同核心可能執行不同的即時任務,一刀切地禁用深層 C-State 難免導致整體功耗與效能的不平衡。為此,系統應支援核心獨立的 C-State 控制能力,並可依照負載與任務特性動態調整。此外,若能建立可預測的喚醒延遲模型與控制策略,即便使用部分深層 C-State,也可降低對確定性的干擾。 為此,本次演講提出兩項實務上常見的解法: 1. 依據 Exit Latency 設定「延遲容忍門檻(Latency Threshold)」 使用者或應用程式可透過 pm_qos_resume_latency_us 接口設定「喚醒延遲上限」,CPU idle governor 會依此排除喚醒時間過長的 C-State: C1、C2:喚醒快速,可安全使用。 C3:喚醒稍慢,需視任務容忍度選用。 C6:延遲最長,在高敏感度任務中會被排除。 例如,在關鍵任務期間可設定: ```c echo 50 > /sys/devices/system/cpu/cpuN/power/pm_qos_resume_latency_us ``` 或直接禁用所有 C-State(echo n/a),任務結束後再設為 0 放寬限制,恢復省電效益。 ![filtered_c_states_resized](https://hackmd.io/_uploads/B1k9S_2Qlg.png) 這種過濾機制可靈活限制高延遲 C-State 的進入,避免對關鍵任務造成干擾。 2. 根據 Idle Interval 與 Target Residency 動態篩選 C-State 除了考慮延遲門檻之外,系統也會依據預測的 CPU 閒置時長(Idle Interval Time)與各 C-State 的目標駐留時間(Target Residency)來進行選擇: 每個 C-State 都需要 CPU 停留一定時間才能「回本」,這段門檻就是其 Target Residency。 系統會挑選所有 TR 小於等於預估 idle 時間的 C-State 中最深的一個,兼顧節能與效能。 例如,C1 與 C2 進出成本低,即使 idle 時間短也可使用;而像 C6 雖節能效果佳,但若 idle interval 太短,就會被排除避免得不償失。 這種策略可達成所謂的:「選擇在 idle 間隔內,最深且最划算的 C-State(Pick the deepest C-State with TR that fits idle time)」,是實現高效與穩定性的平衡關鍵。 **Additional Strategies** 除了設定 Exit Latency 與 Target Residency 外,實務中還可搭配以下幾項策略,進一步優化即時系統中的 C-State 行為與能效管理: 1. CPU Topology Awareness C-State 的深度受到同群組中其他處理單元狀態的影響。例如在 SMT 或多核心架構中,同一核心內的邏輯處理器、或同封裝內的多核心,如果其中任一處理器仍活躍,整個群組就無法進入較深層的低功耗狀態。 * 將可 idle 的 thread 分組排程在相同拓撲單位內,使整組能一起進入深層 C-State,提升節能效果。 2. Prime Cache Before Critical Phase 從深層 C-State 醒來後,硬體會清空 cache/TLB。若直接開始執行關鍵任務,可能導致 cache miss 帶來延遲與 jitter。為避免這類不可預測性,可在關鍵工作開始前,先進行少量程式與資料的存取來「預熱」,此行為可顯著提升 cache 命中率,減少 jitter,強化 determinism。 3. Kernel-Level Tuning 下列核心參數可微調 CPU 的 idle 行為與中斷響應,有助於打造穩定的即時環境: * isolcpus:隔離特定核心不參與一般排程,保留給 RT 任務。 * irqaffinity:手動配置 IRQ 發送到哪個 CPU,避免干擾。 * nohz_full:指定核心進入 tickless 模式,避免 periodic timer 打斷。 * rcu_nocb:將 RCU callback 延後處理,降低背景負載干擾。 # [The Case for an SoC Power Management Driver](https://youtu.be/FLeXBSjPHt8) 演講一開始介紹了什麼是 SoC(System-on-Chip),演講者以一個具體範例「SoC-9000」作為說明,透過結構圖詳細介紹其內部包含的各項模組及它們彼此之間的連接方式,幫助大家更具體地理解 SoC 的實際架構與運作模式。 ![image](https://hackmd.io/_uploads/ry7z89A7ge.png) * VDD / CLKIN:分別為 SoC 的電源與外部時脈輸入。 * 基礎模組:包含 GPU、USB、MMC、SPI、UART、I2C、USB PHY 等,這些模組依賴底層的電源、時脈與 reset 控制。 * 電源管理模組: * Clk Controller:產生並分發時脈,透過 PLL 鎖頻。 * PLL:提供穩定高頻來源。 * Power Switch:控制模組供電(例如 GPU)。 * Reset Controller:決定模組是否啟用或復原。 * HW Domain:例如 GPU 所屬電源區域,可單獨開關以節能。 再來它介紹了有幾種電源管理的方式 **Device Power Management with Device Tree** 在 Linux 裝置驅動架構中,電源管理是系統穩定與功耗控制的重要環節。這張圖展示了如何透過 Device Tree(DT) 結合各種驅動與框架,有效實現模組化的裝置電源管理流程。 整體架構分為多層: * Hardware / SoC 層:最底層為實體晶片,包含如 clock、reset、interconnect 與 power domain 等資源。 * Firmware 層:在開機初期由 Bootloader 載入 DT,告訴 kernel 系統中有哪些設備與資源拓撲。 * DT(Device Tree)層:描述硬體間的依賴關係,例如裝置需要哪個 clock、reset 或是 power domain。 * Providers / Suppliers 層:由底層驅動提供實作,例如 clock controller、reset controller、interconnect driver 等。 * Framework 層:Linux 核心提供的抽象層,像是 clk、reset、interconnect、power domain 等 framework,統一管理資源使用。 * Consumer APIs 層:提供給上層驅動程式使用的 API,如 clk_get()、dev_pm_get_sync(),用來申請與釋放資源。 * Consumers 層:最上層為實際的設備驅動(Driver A~E),這些驅動程式藉由 API 向下呼叫 framework,以達成開啟時脈、解除 reset、控制電源域等動作。 ![device_power_management_dt](https://hackmd.io/_uploads/ryNnQhRmgg.jpg) **Device Power Management with ACPI** 在 x86、伺服器或筆電平台,固件 (BIOS/UEFI) 會產生完整的 ACPI tables,將所有電源管理細節封裝好──時脈 gating、power gating、reset sequences……kernel 只要呼叫 ACPI 定義的「PM domain」介面,即可達成同樣的結果: 對應流程: * Hardware / SoC 層:與 DT 相同,SoC 提供實體時脈、reset、power-domain 等功能單元。 * Firmware 層 (ACPI):BIOS/UEFI 透過 AML(ACPI Machine Language)table 定義每個裝置的 PM Domain,以及 _ON/_OFF、_PSx、_Sx 等電源狀態切換方法。 * dev_pm APIs 層:Kernel 端只需使用統一的 `pm_runtime_*()` 或 `pm_domain_*()` API;底下自動呼叫 ACPI 通用介面進行電源控制。 * Consumers 層:各外設驅動仍然呼叫 `pm_runtime_get()/put()`,但不需自行解析 DT、drvier;所有細節皆由 ACPI framework 與固件協定完成。 ![image](https://hackmd.io/_uploads/r1wxrh0Xle.png) **Device PM Domains** 1. dev_pm_ops:唯一的 PM Framework * dev_pm_ops 是 Linux Device Model 中統一的電源管理介面,所有裝置電源狀態轉換都透過這組 callback 來協調。 2. 每個 struct device 只有一個 PM Domain * 在 struct device 裡面,有一個指向 struct dev_pm_domain *pm_domain; 的成員。 * 這表示每個邏輯裝置(struct device)都屬於一個唯一的「PM Domain」,負責管理這個裝置的通電、斷電、休眠與喚醒流程。 ```c struct device { … struct dev_pm_domain *pm_domain; … }; ``` **用 ACPI 做電源管理** ACPI 規範裡規定了幾種標準的 device power state(D0~D3): * D0:裝置全通電、可正常工作。 * D3:裝置完全下電、無法運作。 Linux 核心在 ACPI 平台中,統一使用一個全域共用的 PM Domain 實例 acpi_general_pm_domain,所有裝置透過 dev->pm_domain 掛載到這套 callback 機制: ```c /* drivers/acpi/device_pm.c */ static struct dev_pm_domain acpi_general_pm_domain = { .ops = { /* Runtime PM:D0 ↔ D3 */ .runtime_suspend = acpi_subsys_runtime_suspend, .runtime_resume = acpi_subsys_runtime_resume, #ifdef CONFIG_PM_SLEEP /* System PM (S-states) */ .prepare = acpi_subsys_prepare, .complete = acpi_subsys_complete, .suspend = acpi_subsys_suspend, .resume = acpi_subsys_resume, .suspend_late = acpi_subsys_suspend_late, .resume_early = acpi_subsys_resume_early, .suspend_noirq = acpi_subsys_suspend_noirq, .resume_noirq = acpi_subsys_resume_noirq, .freeze = acpi_subsys_freeze, .poweroff = acpi_subsys_poweroff, .poweroff_late = acpi_subsys_poweroff_late, .poweroff_noirq = acpi_subsys_poweroff_noirq, .restore_early = acpi_subsys_restore_early, #endif }, }; ``` 工作流程大致如下: 1. Driver probe 時,裝置預設被設為 D0 狀態(即可用狀態),並由核心自動將 dev->pm_domain 指向 acpi_general_pm_domain。 1. 裝置移除(driver remove)時,Kernel 會將裝置設定為 D3,進行下電處理,避免多餘功耗。 1. 在進行 System Suspend / Resume(如睡眠、休眠) 等系統層級的電源管理時,PM core 會依序呼叫 prepare、suspend、resume、freeze 等 callback,這些都已在 acpi_general_pm_domain 中定義,最終會導向 acpi_device_set_power(),由 ACPI firmware 實際完成電源切換。 1. 整個過程中,Linux kernel 幾乎不需要使用 clk/reset 等 subsystem framework。所有與 SoC 有關的實作細節(如開關時脈、電源 rail、GPIO 等)都被 ACPI firmware 隱藏與包裝好,驅動不需感知硬體細節,只需透過通用介面操作即可。 DT 架構面臨的挑戰 * 驅動程式無法完全獨立,因為需要知道硬體平台(firmware)的特定實作,降低通用性與可重用性 * Probe 時無法保證裝置電源狀態 * SoC 的電源管理細節分散在整個驅動樹中 * PM 資源也可能來自平台裝置(platform devices) * 例如 clock、regulator 等不只來自核心子系統,也可能由平台定義的裝置提供,讓依賴關係更複雜。 * 難掌握整體 SoC 電源狀態。 * 無法協調關閉未使用的 PM 資源。 * 雖然部分裝置已不使用某些電源,但核心沒辦法自動判斷與關閉,導致浪費能源。 **為什麼我們不全面採用 ACPI?** 雖然 ACPI 能夠封裝大部分電源管理邏輯,提供統一的介面來簡化驅動開發,聽起來是個理想的解法,但實務上我們並無法全面採用它。原因在於: 很多產品的韌體(firmware)早已出貨,甚至部署於大量終端設備上,這些韌體使用的是 Device Tree 架構,而非 ACPI。我們不可能也不實際回頭重寫所有這些韌體,只為了導入 ACPI 來解決某些電源管理問題。 而上述 DT 理想的解決方案應該滿足哪些條件? * 與現有的裝置樹相容 * 將 SoC 電源管理細節移除出 platform drivers * 不破壞已經使用 regulator、clock、power domains 等資源的驅動程式 * 所有裝置的電源狀態應一致,避免某些裝置進入睡眠但相關裝置還在運作等問題 * 在 probe()(裝置初始化)與 remove()(卸載)階段,自動開關 PM 資源(如電源、時鐘) * 額外好處:自動關閉沒在用的 PM 資源以節省能源 * 額外好處:提升 runtime PM(執行期間電源管理) 的應用,讓系統在 idle 時更省電 接著,Stephen Boyd 討論了先前曾嘗試過的解決方案,以及目前正在實際運作的解法。 第一個討論的主題是 Probe defer loops caused by PM suppliers (由電源管理(Power Management)供應端造成的探測延遲循環) 首先要提到什麼是「探測」(probe)? 在 Linux 中,每個裝置驅動會對應一個 probe() 函數,當核心發現一個裝置符合這個驅動時,就會呼叫 probe() 來初始化這個裝置。 問題起源:裝置之間有依賴 假設有兩個裝置: * Device A(消費者):需要 clock、電源等來初始化 * Device B(供應者):提供 clock 給 Device A 如果核心在啟動時先 probe 了 Device A,但此時 Device B 還沒初始化好(還沒 probe),那麼 Device A 的 probe 就會失敗並回傳 -EPROBE_DEFER,表示:「我還不能初始化,請稍後再試」,這種機制稱為 probe defer。 而探測延遲「循環」怎麼出現? 假設出現這樣的情況: * Device A 需要 Device B 的 clock → A 延遲等待 B * Device B 也需要某個裝置(例如 power regulator)→ B 延遲等待 C * C 又需要 A 提供某種資源 → C 延遲等待 A * 這就形成了 A → B → C → A 的依賴循環,也就是「探測延遲循環(probe defer loop)」。 下列是先前提出和目前正在實際運作解決辦法: | 方法 | 狀態 | | ------------------------------------------------- | ---------------- | | Resource/Tracking allocation framework |❌ Rejected(已拒絕) | On-demand device probing | ❌ Rejected(已拒絕) | | Device links | ✅ Merged(已合併進主線) | | fw devlink | ✅ Merged(已合併進主線) | 在解決了 Linux 核心中裝置初始化階段的 probe 順序與依賴循環(probe defer loop)問題之後,接下來的挑戰是:如何有效判斷哪些電源管理(Power Management, PM)資源是未被使用的,並在適當時機安全地釋放這些資源。這樣的需求推動了 fw_devlink 與 sync_state() 的設計與整合。fw_devlink 透過解析裝置樹(Device Tree)或 ACPI 提供的資訊,自動建立裝置間的依賴關係,讓核心能夠掌握 consumer 與 supplier 之間的互動。sync_state() 則作為 supplier 驅動中的 callback,在所有依賴該資源的 consumer 裝置成功初始化(probe)後觸發,通知系統可根據實際使用狀況安全釋放未被使用的資源。這種設計有效防止因資源過早關閉導致的初始化失敗,並提升系統整體的電源管理自動化程度。 然而,在實務應用中,sync_state() 機制仍面臨多項限制。例如,Linux 核心中的多個子系統(如 clock、regulator、power domain)在電源操作上缺乏統一的時序協調,即便某個裝置進入 sync_state() 狀態,其他子系統可能尚未準備好,導致誤判而提早釋放資源。其次,它預設所有 PM 資源皆由 kernel 驅動管理,然而實際上許多資源可能來自 firmware、ACPI 或 BIOS,這些無法納入 device_link 的推論範圍。再者,sync_state() 的觸發仰賴所有 consumer 裝置都成功 probe,若有驅動尚未載入或 probe 失敗,將導致整個流程無法進行。最後,fw_devlink 雖然能自動建立裝置依賴,但若裝置樹描述不完整或引用方式非標準,仍可能導致錯誤依賴鏈,影響 sync_state() 的時機判斷。 為了克服既有電源管理架構中的限制,Linux 社群提出了一種新的設計思維——middle-out 架構。這種方法將電源管理區域(Power Management Domains, PM domains)視為一種「中介層(middleware)」,位於供應者(supplier)與消費者(consumer)裝置之間,負責協調不同子系統之間的電源資源使用與釋放決策。相較於過去兩種常見的模型:一是 top-down 架構,由 consumer 驅動主動要求所需資源;另一是 bottom-up 架構,由 supplier 根據 consumer 的 probe 狀態推論資源使用情況並決定釋放時機,middle-out 則是在上下層之間建立明確的界面,由 PM domain 作為中樞協調者,統一管理共享的電源區域,達到更穩定、可控的資源調度。 在這種架構下,PM domains 不僅能感知 consumer 裝置的電源狀態,還能整合來自不同子系統(如 clock、regulator、runtime PM)的資訊,以協調電源開關的時序與順序,避免 race condition 或過早釋放。實際上,共用的 PM 資源仍由 supplier 管理,但其開關與保留邏輯則受到 consumer 狀態所影響,使得整個系統的資源管理更具彈性與延展性。 這樣的架構實作基礎之一就是 Generic PM Domain。Generic PM Domain 是 Linux 核心提供的通用電源管理框架,它基於 dev_pm_domain 擴充設計,並可整合至驅動核心中,行為邏輯與 ACPI 類似。當裝置 probe 時自動上電,移除時則自動斷電。它支援巢狀結構(nestable),能夠形成電源控制層級架構,也支援在裝置樹(Device Tree)中透過 power-domains 和 #power-domain-cells 屬性進行描述與匹配,讓核心可以自動掃描並關聯對應的電源域。透過這種方式,Generic PM Domain 能夠在多裝置共用電源的場景下有效協調各自的使用狀態,確保只有當最後一個使用者 idle 時,該 domain 才會斷電,避免供電提早中斷造成系統錯誤。 ```c // 位置: include/linux/pm_domain.h struct generic_pm_domain { struct dev_pm_domain domain; int (*power_off)(struct generic_pm_domain *domain); int (*power_on)(struct generic_pm_domain *domain); // 其他欄位略... }; ``` 然而,實務上要全面導入 Generic PM Domain 架構,仍面臨一些來自裝置樹整合層的挑戰。首先,power-domains 屬性原本僅為硬體電源開關而設計,若要讓所有 PM domain 進入裝置樹,需要為每個 supplier 裝置額外添加 #power-domain-cells 等欄位,這使得裝置樹配置成本上升、出錯機率增加。其次,of_platform_default_populate() 在系統初始化時會一次性地建立所有 platform device,導致無法在 driver probe 之前即時插入或關聯 PM domain,造成關聯流程與核心節點錯失對應時機。此外,Linux 現行架構每個裝置僅允許對應一個 power domain,當一個裝置已經在裝置樹中聲明 power-domains,便無法再以 Generic PM Domain 作為備援使用;這對某些需要 fallback 或多源控制的應用場景產生限制。最後,部分特殊裝置(例如依動態 workload 調整頻率的裝置)仍仰賴對資源的直接控制,這讓完全改為 PM domain 控管變得困難。 因此 Stephen Boyd 提出了 SoC PM Driver。首先,這個方法的第一步是將一個驅動綁定到 SoC node 本身。這與傳統設計不同,以往 SoC node 在裝置樹中只是一個邏輯容器,不會綁定實際的驅動;而現在,這個 SoC PM Driver 成為控制整個電源域(PM domain)的主體,它實作了 generic_pm_domain 結構,負責控制 power_on 和 power_off 的行為。 接著,所有 SoC node 底下的裝置(如 UART、SPI、I2C 等)會透過裝置樹中的 power-domains 屬性與這個 PM domain 建立關聯,讓核心能夠識別裝置的電源依賴關係。這樣一來,裝置驅動不再需要自己控制時脈(clk)、重置線(reset)或供電(regulator),而是將這些控制權交由 PM domain 統一管理。 第三個步驟是將原本分散在各個 platform driver 中的 PM 程式碼,遷移至 SoC PM Driver 所控制的 PM domain 中。這樣不但可以消除重複程式碼,還能確保電源開關時序的正確性,避免因驅動不一致導致的競爭條件或不穩定行為。PM domain 會根據所有關聯裝置的 power state 動態判斷是否保留或關閉某個資源,這樣的運作也實現了 middle-out 架構的核心精神:以中介層統一協調上下層之間的資源互動。 ![Block_Diagram_Cleaned](https://hackmd.io/_uploads/SJKadpzElx.jpg) 之後 Stephen Boyd 提出一系列關於上述的實做 1. 在 SoC node 上註冊一個驅動,並且讓這個驅動負責建立其底下所有子裝置(child devices) 1. 建立裝置(device)時關聯 PM domain 1. 在 PM domain 中取得電源資源 1. 控制電源開關行為 1. 最終從原本的 platform driver 移除掉 PM 操作邏輯 **在 SoC node 上註冊一個驅動,並且讓這個驅動負責建立其底下所有子裝置(child devices)** 傳統上 SoC node 只是個容器,不會被賦予驅動。但這邊的做法是: 給 SoC node 自己配一個 platform driver,透過這個 driver 把底下的裝置一一掛起來。 1. Introduce driver for SoC node * 寫一個 platform driver,match 裝置樹中的 SoC node(用 "vendor,soc-9000" 這種 compatible) * 驅動本身不控制硬體,只是用來「掃出子裝置」 ```c struct platform_driver soc9000_driver = { .driver.probe = of_platform_default_populate(), .driver.match_table = { "vendor,soc-9000" }, }; ``` 2. Populate child nodes as child platform devices * of_platform_default_populate() 會把這個 SoC node 底下的所有裝置節點轉成 platform device * 等於是由 SoC PM Driver 來主動「創建」子裝置,而不是核心自動完成,這樣一來就可以攔截裝置創建的時機,並在之後的流程中額外插入設定(如掛上 PM domain) **建立裝置(device)時關聯 PM domain** 原本 Linux 會透過 of_platform_default_populate() 自動把裝置樹中的裝置節點轉成 platform device 並掛載到 bus 上。但這個過程是一口氣做完的,導致我們沒有機會在「建立裝置」與「掛載裝置」之間插入邏輯(例如設定 PM domain)。 把 of_platform_default_populate() 邏輯拆解為: 1. 建立 device:先用 of_platform_device_alloc() 建出 platform device。 1. 掛 PM domain:在加入到系統總線(bus)之前,呼叫 dev_pm_domain_set() 把裝置與 PM domain 綁定。 1. 加入裝置:最後用 of_platform_device_add() 把裝置加到 platform bus 上。 ```c pdev = of_platform_device_alloc(); dev_pm_domain_set(&pdev->dev, pm_domain); of_platform_device_add(pdev); ``` **在 PM domain 中取得電源資源** 這些動作原本通常會寫在個別裝置 driver 中, 現在改由 PM domain 透過 .activate() 來統一做,例如: ```c int get_pm_resources(struct device *dev) { clk_get(dev, ...); regulator_get(dev, ...); } pm_domain.activate = get_pm_resources; ``` 這樣裝置還沒綁定驅動時,PM domain 就能先把資源準備好。 ``` Device Tree ↓ Create platform_device ↓ Bind PM domain → call .activate() ↓ 取得電源資源(clk_get, regulator_get) ↓ 裝置 ready ↓ Driver bind → call .probe() ``` **控制電源開關行為** 使用 struct generic_pm_domain::power_{on,off}() 控制資源 例如在 generic_pm_domain 中設定: ```c generic_pm_domain.power_on = power_on_pm_resources; generic_pm_domain.power_off = power_off_pm_resources; ``` 而實作內容是: ```c int power_on_pm_resources(struct device *dev) { clk_prepare_enable(clk); // 啟用 clock regulator_enable(regulator); // 啟用電源穩壓器 ... } ``` PM domain 現在能夠在不依賴個別 driver 的情況下,自動為裝置開啟/關閉電源資源! **最終從原本的 platform driver 移除掉 PM 操作邏輯** 既然 PM 邏輯已集中到 PM domain,我們可以從原來的 platform driver 把那些 clk_get()、regulator_get() 這類東西拔掉。 手法是: * 利用 platform_data 傳遞「這個裝置不需要處理 PM」 * 在 driver 裡檢查 dev_get_platdata(),決定是否跳過原有 PM code。 ```c dev.platform_data = (void *)1UL; if (dev_get_platdata(dev)) skip_pm_stuff(); ``` 而這個辦法也符合上述這幾點 * 與現有的裝置樹相容 * 將 SoC 電源管理細節移除出 platform drivers * 不破壞已經使用 regulator、clock、power domains 等資源的驅動程式 * 所有裝置的電源狀態應一致,避免某些裝置進入睡眠但相關裝置還在運作等問題 * 在 probe()(裝置初始化)與 remove()(卸載)階段,自動開關 PM 資源(如電源、時鐘) * 額外好處:自動關閉沒在用的 PM 資源以節省能源 * 額外好處:提升 runtime PM(執行期間電源管理) 的應用,讓系統在 idle 時更省電 # 測試 ```c # 1. 建一個 500MB 的檔案作為 loop 裝置 dd if=/dev/zero of=~/loop.img bs=1M count = 2048 # 2. 建檔案系統 mkfs.ext4 ~/loop.img # 3. 建挂載目錄並挂載 sudo mkdir -p /mnt/test sudo mount -o loop ~/loop.img /mnt/test ``` 之後可以在 /mnt/test 下執行 dd 產生脏頁: ``` sudo dd if=/dev/zero of=/mnt/test/file bs=1M count=1024 ``` 接著再跑 sync 並用 perf 采樣: ``` sudo perf record -g -- sync sudo perf report ```