執行人: wurrrrrrrrrr
Linux 在行動裝置上的電源管理以系統 suspend (休眠) / Resume (回復,不是「回覆」,後者是 reply 的翻譯) 為主要操作機制,目的在於在暫停大部分硬體與作業時保持程式的狀態,同時將耗電降至最低。以筆記型電腦為例,播放影片時若需要暫時離開,可關閉顯示器、停用大部分 CPU 運作,並進入 S3(Suspend-to-RAM)或 S4(Suspend-to-Disk)狀態;回復時只需還原記憶體內容與中斷向量,便能迅速回到先前的執行點,縮短回復時間且不丟失程式的狀態。
在上述流程中,核心依序執行 suspend_enter
(同步檔案系統並暫停行程)、prepare(初始化省電模式), suspend
, suspend_late
(最後硬體關閉)與 suspend_noirq
(關閉所有中斷);隨後藉由 resume_noirq
, resume_early
, resume
, complete
與 exit
(脫離行程)逆向還原。這些階段涵蓋 DRAM 與 Flash 的電源域切換、CPU C-state、動態電壓與頻率調整(DVFS)以及裝置 D-state(如 D0–D3)的管理。
2024 年,Google 工程師 Saravana Kannan 在 LPC 演講 Optimizing suspend/resume 中指出,多數時間消耗集中在 suspend_enter
階段的同步檔案系統操作,所有 dirty page 必須寫回儲存裝置,且過程不可中斷,常因 I/O 排隊而延宕數十秒;另有情況是 freeze processes 後所有 CPU 進入 idle,卻無法順利進入下一階段。此外,Linux 支援非同步暫停/回復機制,試圖平行化處理,但實際上常因同步鎖或電源域相依性增加反而延長總時間;即使選擇非同步機制,prepare
與 complete
階段仍需依序執行,缺乏足夠的平行化空間;各裝置的 suspend_late
階段更必須待所有相關硬體共同完成,才能進行 suspend_noirq
,即便它們之間並無直接相依。
要深入分析這些瓶頸,可運用 Perfetto 等追蹤工具擷取系統行為的追蹤紀錄,再搭配 pm-graph 繪製電源域與行程凍結時序,觀察 I/O 活動、CPU C-state 切換與中斷響應;亦可參考 LKML 上針對非同步暫停流程的程式碼變更討論,評估其對 prepare/complete 階段的影響,並思考是否能在裝置驅動或電源管理子系統中,藉由更細緻的鎖分段或階段重疊機制,縮短整體 Suspend/Resume 時間。
以第一手材料為主,儘量參照 Linux 基金會舉辦研討會的演講錄影
至少包含以下:
這個演講主要探討了優化 suspend/resume 的研究,並強調了它對手錶和手機等設備的重要影響。在一開始,Saravana Kannan 簡單介紹了 suspend/resume 的過程,並將其劃分為以下幾個階段:
這其中的細節能夠在 Linux Suspend/Resume 實驗(二) 和 Linux Suspend/Resume 實驗(三)可以得知。在演講中,Saravana Kannan 提到在手機和手錶上很常會執行 suspend/resume 在一天中,這個動作可能會執行上百次甚至上千次,並且在 suspend 過程中,sync_filesystems 大約會佔據 10% 到 35% 的時間。
第一個重點是:優化 sync_filesystems
一旦 sync_filesystems 被執行,使用者空間(userspace)將無法中止該操作,而這個同步動作最長可能會耗時超過 25 秒。
這帶來了一個關鍵挑戰:如何在保證資料一致性的同時,避免這類同步操作長時間阻塞整個系統的掛起流程(suspend)?
其中一項潛在的解法是關閉自動的檔案系統同步功能,然而這樣做將帶來資料遺失的風險。
為了在穩定性與資料安全之間取得平衡,Saravana Kannan 提出了一些折衷方案,例如:將系統設定為每隔一小時才嘗試掛起一次,而非頻繁地執行掛起,以避免頻繁執行長時間的同步操作。
但更關鍵的問題是:是否有可能引入一種機制,能夠中止或跳過正在進行的檔案系統同步,或至少讓該操作不會同步時阻塞系統?
第二個重點:優化 suspend_enter
目前的 suspend_enter 流程會遍歷所有執行緒,並嘗試將它們移至凍結(freezing)狀態,以準備進入 suspend。
然而,Saravana Kannan 發現,在 suspend_enter 的早期階段,系統會進行一些必要的操作,但隨後該執行序本身就幾乎沒有進一步活動,甚至會陷入睡眠狀態。這樣的行為可能導致整體 suspend 過程效率低下,甚至出現不必要的延遲。
第三個重點:Async suspend 機制的實際效益不如預期
雖然 Linux 在 suspend/resume 流程中支援非同步(async)操作,但實務上,這項設計在許多情境下不但沒有提升效能,甚至可能導致整體流程變慢。
原因包括:
許多裝置的 suspend/resume 操作極為簡單甚至是 no-op,只需數微秒即可完成,卻因進入 async 流程而產生不成比例的排程成本。
為了實現 async,需要額外執行下列步驟,導致整體開銷反而大於實際工作量:
這說明 async 並不總是最有效率的選擇,特別是在裝置操作非常快速或高度依賴序列關係的情況下。
為了解決上述問題,該處提出了一項改進方案:將 suspend/resume 的執行流程改為使用 BFS(Breadth-First Search,廣度優先搜尋)方式進行非同步裝置的處理 PM: sleep: Do breadth first suspend/resume for async suspend/resume 這個 patch ,該 patch 也啟發了 Rafael J. Wysocki 進一步對其進行整理與強化,最終於 Linux 6.16 被正式合併進主線,成為核心支援的標準機制之一。
最後一個重點是:解決 S2idle 韌體相依問題的替代架構提案
S2idle 是目前 Linux 支援的淺層 suspend 模式,理論上比 S2RAM(suspend-to-RAM)有更快速的進入與喚醒時間,並且整體使用體驗更順暢。然而,這完全依賴平台韌體的正確支援。
事實上,S2idle 在實做時總是會遇到韌體錯誤,而韌體更新在多數商業設備上幾乎不可能,導致在既有硬體上難以廣泛部署穩定的 S2idle 功能。
此外,隨著系統中 CPU 數量增加,S2idle 相對於 S2RAM 的效能與功耗差距也越來越明顯,這進一步加劇了問題。
本提案討論一種可能的替代方式:以 S2RAM 韌體呼叫為基礎,模擬出一種具備類似 S2idle 行為的架構,以在不完全依賴平台韌體的情況下,仍能獲得合理的 suspend 體驗。
具體構想如下:
本場演講一開始主要介紹了 Linux suspend 的整體流程,以及相關的 kernel 參數設定,這些設定可用來協助檢查 suspend 過程中可能出現的問題或 bug。
下列是啟用電源管理除錯訊息的相關設定:
Callback | 作用 |
---|---|
->prepare() |
準備進入 suspend,此時應避免新增 child device |
->suspend() |
停止 I/O 操作 |
->suspend_late() |
進一步關閉裝置 |
->suspend_noirq() |
IRQ 已停用,不能再用中斷機制 |
suspend_noirq() 的額外保證是:「IRQ handler 保證不會再被呼叫」。
->suspend*() 函式的目的是:
不一定每個驅動都要實作全部階段,但要根據裝置需求選擇。
重要提醒:行為因裝置而異
不同裝置 suspend 的需求差異非常大,核心不會保證所有裝置都會被以同樣方式處理 → 驅動要自行實作正確的 suspend 邏輯,並且沒有明確的裝置狀態輸出資訊可以查詢(除了錯誤碼),也就是 kernel 不會告訴你裝置現在是不是真的進入低功耗。
不同 suspend 實作階段對 GPIO 控制器行為的影響:
Linux 核心支援多階段的裝置 suspend 流程,驅動開發者可根據裝置特性與需求,在不同階段實作對應的 callback 函式,例如 suspend()、suspend_late()、suspend_noirq() 等。這些階段對應系統中斷(IRQ)與裝置依賴狀態的不同條件,因此選擇在哪個階段進行電源管理操作,將直接影響 suspend/resume 的穩定性與功能正確性。
這在處理與 IRQ 和喚醒(wake-up)邏輯密切相關的裝置時,尤為重要,例如 GPIO 控制器。
以下為兩個 GPIO 控制器驅動的實作對比:
這樣的差異導致明顯的行為影響:若該 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 核心提供的一種「平台無關」淺層掛起模式,只要編譯時啟用了 CONFIG_SUSPEND=y,並且對應架構實作了基本的 idle loop(WFI、mwait 或 PSCI CPU_SUSPEND),就可在 OS 層面自行完成整機掛起/喚醒:
System-wide suspend: platform-provided modes (standby S2R):
這裡在介紹有兩種暫停模式有下列兩種:
這兩種 suspend 模式皆需要底層平台提供支援,因此在某些設備上可能無法啟用、或即使啟用後其實無法正常達到省電目的。
實際行為高度依賴平台,例如 S2R 理論上應該會:
但實際上,kernel 並不保證會這麼做,這一切端看平台與韌體是否正確實作
其他不確定因素包括:
Summary:行為是平台特定的
正因如此,standby 與 S2R 雖為 kernel 支援的 power state,但其實際功耗控制行為是由平台決定的。
在某些機器上 suspend 可能會大幅降低功耗,但在另一台看似「正常睡眠」的裝置中,實際上可能什麼都沒關掉,幾乎沒有省電效果。
System-wide suspend: hibernation:
Linux 電源管理中,最有效率的省電方式其實是 shutdown:
Hibernate 則提供一種折衷方案:
在 shutdown 前先「儲存所有狀態到磁碟」,待下次開機時能恢復原有作業狀態。
Hibernate 的優勢與特性
不是單純關機:
提升使用體驗:
在系統 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 等)處於啟用狀態。
這樣的設計,引出了以下一連串的關係:
雖然文件規範明確指出 CLOCK_MONOTONIC 應於 suspend 期間停止計時,但實際上在 s2idle 模式中,它仍然會持續跳動。這代表:
例外情況:若系統使用 cpuidle 進入 s2idle
有些平台在 s2idle 過程中,會將 CPU 的 idle 控制權交給 cpuidle driver。若該 driver 允許在關閉中斷的情況下進入低功耗狀態,那麼 kernel 就能:
Runtime Power Management
首先提出了三點:
演講者也將 pm_runtime_get 和 pm_runtime_put 這兩個函式簡化為如下的偽程式碼,幫助理解其基本邏輯與設計概念:
每當有一個使用者來用這個裝置(例如驅動開啟串口、GPU 等),就會呼叫這個函式,如果裝置之前沒人用(usage_count == 0),就會執行 runtime_resume() 讓裝置先開機,同時也要遞迴讓它的父裝置先 resume(例如 USB 控制器要先開,滑鼠才能用)。
每次使用者不再需要這個裝置,就呼叫 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。這套機制雖然設計良善,但在某些情境下會產生問題,例如:
為了避免這種狀況,驅動開發者必須:
在 suspend() 階段主動喚醒裝置以確保裝置在後續 suspend_noirq() 階段仍處於「可用狀態」,隨後在 resume 階段補上還原動作。
在多核心即時系統中,CPU C-State 的管理對於平衡功耗與即時反應能力具有關鍵性影響。當處於閒置狀態時,CPU 可進入不同深度的 C-State(例如 C0、C1、C3、C6 等),以達成節能與降溫目的。C-State 越深,關閉的電路越多,帶來的省電效果也越顯著。但同時,恢復這些狀態所需的時間也會增加,形成喚醒延遲的代價。
這些 C-State 的進出行為主要受兩個核心屬性影響:
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,也可降低對確定性的干擾。
為此,本次演講提出兩項實務上常見的解法:
使用者或應用程式可透過 pm_qos_resume_latency_us 接口設定「喚醒延遲上限」,CPU idle governor 會依此排除喚醒時間過長的 C-State:
C1、C2:喚醒快速,可安全使用。
C3:喚醒稍慢,需視任務容忍度選用。
C6:延遲最長,在高敏感度任務中會被排除。
例如,在關鍵任務期間可設定:
或直接禁用所有 C-State(echo n/a),任務結束後再設為 0 放寬限制,恢復省電效益。
這種過濾機制可靈活限制高延遲 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 行為與能效管理:
CPU Topology Awareness
C-State 的深度受到同群組中其他處理單元狀態的影響。例如在 SMT 或多核心架構中,同一核心內的邏輯處理器、或同封裝內的多核心,如果其中任一處理器仍活躍,整個群組就無法進入較深層的低功耗狀態。
Prime Cache Before Critical Phase
從深層 C-State 醒來後,硬體會清空 cache/TLB。若直接開始執行關鍵任務,可能導致 cache miss 帶來延遲與 jitter。為避免這類不可預測性,可在關鍵工作開始前,先進行少量程式與資料的存取來「預熱」,此行為可顯著提升 cache 命中率,減少 jitter,強化 determinism。
Kernel-Level Tuning
下列核心參數可微調 CPU 的 idle 行為與中斷響應,有助於打造穩定的即時環境:
演講一開始介紹了什麼是 SoC(System-on-Chip),演講者以一個具體範例「SoC-9000」作為說明,透過結構圖詳細介紹其內部包含的各項模組及它們彼此之間的連接方式,幫助大家更具體地理解 SoC 的實際架構與運作模式。
再來它介紹了有幾種電源管理的方式
Device Power Management with Device Tree
在 Linux 裝置驅動架構中,電源管理是系統穩定與功耗控制的重要環節。這張圖展示了如何透過 Device Tree(DT) 結合各種驅動與框架,有效實現模組化的裝置電源管理流程。
整體架構分為多層:
Device Power Management with ACPI
在 x86、伺服器或筆電平台,固件 (BIOS/UEFI) 會產生完整的 ACPI tables,將所有電源管理細節封裝好──時脈 gating、power gating、reset sequences……kernel 只要呼叫 ACPI 定義的「PM domain」介面,即可達成同樣的結果:
對應流程:
pm_runtime_*()
或 pm_domain_*()
API;底下自動呼叫 ACPI 通用介面進行電源控制。pm_runtime_get()/put()
,但不需自行解析 DT、drvier;所有細節皆由 ACPI framework 與固件協定完成。Device PM Domains
dev_pm_ops:唯一的 PM Framework
每個 struct device 只有一個 PM Domain
用 ACPI 做電源管理
ACPI 規範裡規定了幾種標準的 device power state(D0~D3):
Linux 核心在 ACPI 平台中,統一使用一個全域共用的 PM Domain 實例 acpi_general_pm_domain,所有裝置透過 dev->pm_domain 掛載到這套 callback 機制:
工作流程大致如下:
DT 架構面臨的挑戰
為什麼我們不全面採用 ACPI?
雖然 ACPI 能夠封裝大部分電源管理邏輯,提供統一的介面來簡化驅動開發,聽起來是個理想的解法,但實務上我們並無法全面採用它。原因在於:
很多產品的韌體(firmware)早已出貨,甚至部署於大量終端設備上,這些韌體使用的是 Device Tree 架構,而非 ACPI。我們不可能也不實際回頭重寫所有這些韌體,只為了導入 ACPI 來解決某些電源管理問題。
而上述 DT 理想的解決方案應該滿足哪些條件?
接著,Stephen Boyd 討論了先前曾嘗試過的解決方案,以及目前正在實際運作的解法。
第一個討論的主題是 Probe defer loops caused by PM suppliers (由電源管理(Power Management)供應端造成的探測延遲循環)
首先要提到什麼是「探測」(probe)?
在 Linux 中,每個裝置驅動會對應一個 probe() 函數,當核心發現一個裝置符合這個驅動時,就會呼叫 probe() 來初始化這個裝置。
問題起源:裝置之間有依賴
假設有兩個裝置:
如果核心在啟動時先 probe 了 Device A,但此時 Device B 還沒初始化好(還沒 probe),那麼 Device A 的 probe 就會失敗並回傳 -EPROBE_DEFER,表示:「我還不能初始化,請稍後再試」,這種機制稱為 probe defer。
而探測延遲「循環」怎麼出現?
假設出現這樣的情況:
下列是先前提出和目前正在實際運作解決辦法:
方法 | 狀態 |
---|---|
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 才會斷電,避免供電提早中斷造成系統錯誤。
然而,實務上要全面導入 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 架構的核心精神:以中介層統一協調上下層之間的資源互動。
之後 Stephen Boyd 提出一系列關於上述的實做
在 SoC node 上註冊一個驅動,並且讓這個驅動負責建立其底下所有子裝置(child devices)
傳統上 SoC node 只是個容器,不會被賦予驅動。但這邊的做法是:
給 SoC node 自己配一個 platform driver,透過這個 driver 把底下的裝置一一掛起來。
建立裝置(device)時關聯 PM domain
原本 Linux 會透過 of_platform_default_populate() 自動把裝置樹中的裝置節點轉成 platform device 並掛載到 bus 上。但這個過程是一口氣做完的,導致我們沒有機會在「建立裝置」與「掛載裝置」之間插入邏輯(例如設定 PM domain)。
把 of_platform_default_populate() 邏輯拆解為:
在 PM domain 中取得電源資源
這些動作原本通常會寫在個別裝置 driver 中,
現在改由 PM domain 透過 .activate() 來統一做,例如:
這樣裝置還沒綁定驅動時,PM domain 就能先把資源準備好。
控制電源開關行為
使用 struct generic_pm_domain::power_{on,off}() 控制資源
例如在 generic_pm_domain 中設定:
而實作內容是:
PM domain 現在能夠在不依賴個別 driver 的情況下,自動為裝置開啟/關閉電源資源!
最終從原本的 platform driver 移除掉 PM 操作邏輯
既然 PM 邏輯已集中到 PM domain,我們可以從原來的 platform driver 把那些 clk_get()、regulator_get() 這類東西拔掉。
手法是:
而這個辦法也符合上述這幾點
選用內建 Intel Core i7-10700 處理器的個人電腦和 Raspberry Pi 4B 進行實驗,善用 Perfetto 工具予以視覺化並探討其中是否存在可改進之處。
Linux Suspend/Resume 實驗(一)這是針對 Raspberry Pi 執行 suspend/resume 所做的觀察與測試。
根據本次筆記 Linux Suspend/Resume 實驗(二) 和 Linux Suspend/Resume 實驗(三)可以得知 suspend 和 resume 的流程,並從中找出最耗時的階段。
根據以下兩張圖的分析,可以觀察到耗時最久的在於 suspend_enter
階段的 sync_filesystems
和回復階段的 dpm_resume
階段,因此,若要優化系統的休眠與回復速度,可以優先針對這兩個階段進行改善:
在上述裝置中,評估 PM: sleep: Improvements of async suspend and resume of devices 及相關 patchset (注意版本) 對於電源管理的效益,在 Linux v6.12+ 驗證,並針對稍早整理的演講素材,嘗試提出改進方案。
這個 patch set 改進 Linux 核心中非同步(asynchronous) suspend/resume 的處理機制。
在一般情況下,系統在執行裝置 suspend 或 resume 時,會採用同步方式,也就是:
這個 patch set 整體設計的核心想法是:
有些裝置即使還沒完全符合所有依賴條件,但只要部分條件已經滿足,就可以提早開始執行非同步 suspend/resume。
這樣可以避免一次把太多還不能立刻執行的 async work items 塞進佇列裡,因為那些裝置可能還要等其他裝置處理完才能動作,如果都塞進去等著,只會浪費資源、增加系統負擔,讓整體變慢。
本次提出的 patch set 一共包含 5 個 patch ,為優化 Linux 核心中非同步 suspend/resume 的處理流程。以下是某一台測試系統的初步效能結果,用以觀察這些變更對實際執行時間的影響:
測試結果中各階段的定義如下:
從結果可以明顯看出,在套用 patch [1–3/5] 之後,suspend 的效能有了明顯提升,而這部分的改善很大程度上可歸因於 patch [2/5]。
而這三個 patch 會在 Linux 6.16 的開發週期中合併。
PM: sleep: Resume children after resuming the parent
PM: sleep: Suspend async parents after suspending children
PM: sleep: Make suspend of devices more asynchronous
之前的 async resume 邏輯:
這個 patch 修改:
另外:
本次改動涉及多個與裝置電源恢復(resume)流程相關的核心函式,包括:
下列為其中一個例子:
現在是在該裝置 resume 完成後,主動觸發它所有子裝置的 async resume。
其中的 dpm_root_device
函式的作用是確認是否為 root_device。
這個 patch 根據目前的測試結果,本身並不會對系統整體的 resume 時間造成可測量的影響,但這項修改除了讓 async resume 對於資源較少的系統更加友善。
之前的 async suspend 邏輯:
參照之前針對 resume 流程所做的修改,這次的 patch 讓:
device_suspend()
在裝置本身處理完成後,才開始其父裝置的非同步 suspend;dpm_suspend()
則會優先處理沒有子裝置的裝置,這樣它們就不需要因等待與其無關的「同步裝置」而被延遲 suspend。在這個 patch 中可以發現他先定義了兩個函式:
這個函式為判斷一個裝置是否為 leaf device,也就是沒有子裝置的裝置。
leaf 裝置通常可以在最早的階段進行 async suspend,因為它們沒有下游依賴者,不需要等待子裝置完成。
這個函式為當某個裝置的 suspend 處理完成後,這個函式會啟動其父裝置的 async suspend,前提是父裝置也允許 async suspend。
這個 patch 是讓 suspend 時間縮短的關鍵。
這個 patch 使 device_suspend_late()
和 device_suspend_noirq()
在處理完裝置本身之後,再啟動其父裝置的 async suspend;同時,dpm_suspend_late()
和 dpm_noirq_suspend_devices()
會優先處理沒有子裝置的裝置,讓它們不需要等待與其無關的其他裝置完成 suspend。
和上述第二個 patch 的改動類似。
以下為搭載 Intel Core i7-10700 處理器,核心版本為 6.15 的個人電腦執行 Suspend-to-Idle 測試之結果。
可以觀察到效能上並未有明顯改善,可能是因為該系統在暫停與恢復方面原本就已具備良好表現。事實上,根據 patch 中的測試結果,桌機部分亦顯示其在這方面本就沒有太多優化空間;又或者是系統實際進入的 suspend 狀態不夠深(例如未達到 Suspend-to-RAM 或 Hibernation 等較省電的模式),因此無法充分發揮 patch 所帶來的效能提升。
While both Dell XPS13 systems show a consistent improvement after
applying the first three patches, everything else is essentially a
wash (particularly on the desktop machine that seems to suspend and
resume as fast as it gets already).
根據作者的實驗結果,其 suspend 的速度明顯優於本系統的表現。為了進一步釐清差異的原因,我使用 pm-graph 工具分析各個裝置在 suspend 階段的耗時情形,並發現了一個關鍵因素:目前本系統在 suspend 階段的主要瓶頸來自機械式硬碟的存取延遲。因此,即使進一步優化程式邏輯,整體效能仍可能受到硬體限制的影響。這也可能是我無法觀察到明顯提昇的主因之一。
當我將機械式硬碟拔除後,並觀察執行數據時,發現結果與上述 Coffee Lake Desktop 的表現相符,Suspended time 降至約一百二十多毫秒。然而,這樣一來,桌機的 suspend 時間已經足夠快速,因此無法明顯觀察到進一步的優化效果。
於是我另外找了一台筆電來觀察結果,下列為測試結果:
目前看起來差異不大,我猜這個 patch 可能會在效能比較差的機器上會發揮更明顯的效果。
這個 patch 在之前的基礎上,對裝置的 async suspend 流程做了一些調整:
這個 patch 延續之前的改動,這次調整了裝置 resume 階段的 async 處理流程:
在原本的 async suspend 流程中,第一層迴圈會先針對 leaf 節點進行呼叫,而當 leaf 節點的 suspend 動作完成後,流程會進一步觸發其父節點的 suspend。此次修改的目的是調整該行為,使第一層迴圈中所呼叫的 leaf 節點不再主動觸發其父節點 suspend。
我認為,第一層已呼叫的 leaf 節點數量已經足夠覆蓋 suspend 流程的主要進度,因此不需要額外再針對父節點進行多餘的呼叫,避免因此增加額外的 context switch 負擔。
實測結果顯示,修改後流程的速度約為原本設計的三倍,因此似乎是不可行的。
實測結果顯示修改後與原本效果與效能無明顯差異。
原本 suspend 流程中,針對每個裝置在 suspend 階段都會無條件呼叫 dpm_wait_for_subordinate(dev, async),以確保 subordinate 裝置(通常是 child devices 或透過 device_link 關聯的 consumers)能夠在 parent 裝置 suspend 前正確完成 suspend 動作。
然而 leaf device 以及沒有 consumers 關聯的裝置,本身不存在 subordinate suspend 順序依賴,這類裝置呼叫 dpm_wait_for_subordinate() 並無作用,反而可能會增加 suspend 流程中的執行時間。
因此此次修改調整為僅針對以下條件成立的裝置呼叫 dpm_wait_for_subordinate():
但實際針對這段修改後的執行時間進行測量,並透過 T-test 進行統計檢定後發現,與原本直接對所有裝置執行 dpm_wait_for_subordinate 的結果相比,整體 suspend 效能並無顯著差異,兩者效果相當。
由於這個 patch 將 dpm_root_device 這個函式改為下列:
原本設計上,裝置樹中的根節點會先啟動 async resume。現在修改後,變更為根節點且要沒有供應者關係的裝置才會啟動 async resume。
定義好這個邏輯函式後,我發現它也可以直接整合進 dpm_wait_for_superior() 函式中使用。dpm_wait_for_superior() 的作用是 在 resume 過程中等待父節點和所有供應者完成相關操作,以確保 resume 順序的正確性。
而將根節點或沒有供應者的裝置判斷整合進 dpm_wait_for_superior() 的意圖是 → 讓這類裝置可以提前返回,不需額外執行多餘的函式,進而提早解鎖 dpm_list_mtx,提升整體流程效率。
在確認上述修改方向後,我首先針對 if (dpm_root_device(dev)) 這個條件進行統計,試圖了解該條件在流程中實際成立的次數。初步統計結果顯示,這行條件判斷成立的次數多達 584 次。
基於這個數據,我推測我新增的判斷函式應該也會被觸發大約數百次。然而,進一步實際觀察執行結果時發現,該函式實際僅被執行了 15 次,與預期相差甚大。另一方面,dpm_wait_for_superior 整體被呼叫的次數則達到 777 次。
這結果顯示,儘管在 device_resume_noirq 中 dpm_root_device(dev) 條件成立的次數很多,但實際進入我新增函式的次數明顯更少,說明流程中仍有其他條件或邏輯影響實際執行路徑。
於是我接著觀察 device_resume_noirq 的執行流程,並進行測試後發現了可能的原因。在 device_resume_noirq 中,觀察到以下幾段關鍵程式碼:
我推測當 dpm_root_device(dev) 條件成立時,或許在執行到 dpm_wait_for_superior 函式之前,流程已經提前跳出,因此導致 dpm_wait_for_superior 的實際執行次數遠低於預期。為了驗證這個推測,我針對流程中的兩個判斷式分別進行了統計,觀察各自的判斷成立次數,以進一步確認是否存在提前跳出的情況。
測試結果顯示,第一個判斷式成立的次數高達一千多次。基於這個結果,我進一步推測,當 dpm_root_device(dev) 條件成立時,該裝置的 dev->power.syscore 或 dev->power.direct_complete 也很可能為真,從而導致流程在進入 dpm_wait_for_superior 之前就已經提前跳出。
目前的實作流程裡還是有不少多餘的 context switch。當一個裝置要被 resume 時,必須先等父裝置處理完成,接著還要再等待它的 supplier 完成相關操作。而這個等待的過程是透過 wait_for_completion() 函式來實現的。
wait_for_completion() 是一個阻塞式的同步機制。當呼叫該函式時,當前執行緒會進入睡眠狀態,並且會將自己加入至對應的等待隊列(實作上是一個鏈結串列 swait_queue),直到目標工作透過 complete() 或 complete_all() 函式明確發出完成通知,將 done 欄位設為大於 0 的數值,此時等待隊列中的執行緒才會被喚醒,並繼續執行後續流程。
然而,在實際的裝置 resume 流程中,單一裝置通常並不只有一個 supplier。當裝置進行 resume 時,若某一個 supplier 率先完成並觸發 complete(),當前裝置就會被喚醒;但其他尚未完成的 supplier 仍可能處於處理中,導致該裝置在 resume 流程中反覆進入 wait_for_completion() 等待尚未完成的 supplier。這樣的重複等待與喚醒會產生額外的 context switch,進一步增加 resume 流程中的同步等待開銷與整體延遲。
特別是在裝置層級較深、供應鏈鏈結複雜的系統中,這種情況會更加明顯,導致整體 resume 流程效率下降。因此,如何降低不必要的同步等待與優化裝置間的依賴處理,成為提升 resume 效能的一個重要課題。上述的情形同樣也能套用在 suspend 上。