Try   HackMD

Linux 核心專題: 探討電源管理休眠/回復流程

執行人: wurrrrrrrrrr
專題解說

Reviewed by jserv

  1. 注意課程規範的用語,避免誤用「遍歷」,見「資訊科技詞彙翻譯
  2. 針對 Perfetto 產生的視覺化輸出,說明該如何解讀實驗結果
  3. 重現 LKML 既有的 patchset 實驗時,應解釋其原理、預期效果,並探討何以在你的環境中結果與最初 patchset 不同
  1. 已修正
  2. 待辦
  3. 這個 patch 是讓 suspend 時間縮短的關鍵,因為在原本 suspend 的流程為下列這樣:
    image

    這樣會導致有一些能提早非同步執行的裝置被迫要等同步的裝置。
    image

    做了此改動後那些能提早非同步執行的裝置就可以提早先被 kworker 所執行,不用等待那些同步的裝置。
    照理來說這樣修改完成後應該會使我的 suspend 流程加快但實際上並沒有,因此我還需要觀察是否有些設備的 async 值為 disabled,導致我的 suspend 流程沒有加快。

Reviewed by dingsen-Greenhorn

「prepare(初始化省電模式)」
括號使用不一致,前面是全形括號(,後面是半形)
建議統一為「prepare(初始化省電模式)」。

「回復 (不是『回覆』,後者是 reply 的翻譯)」
這裡用法正確,但要注意用詞,已標明說明,可視需要保留。

「pa裝置先 resume」
這裡應為「parent 裝置先 resume」,可能打錯字。

「drvier」
拼錯字,應為「driver」。

「裝置裝置驅動」
重複「裝置」,應為「裝置驅動」

好的已修正

Reviewed by liangchingyun

在條列第一、二、三個重點時,後面的冒號全半形不統一。

好的已修正

Reviewed by I-Ying-Tsai

你觀察到 async suspend 效果在效能較差的裝置上更為明顯,那有沒有可能根據裝置特性動態切換 suspend 策略?

關於改動後的 async suspend 效果,我的推測是在效能較差的裝置上,可能會有比較好的表現,但這只是個人的推測,並未進行實際測試。至於你提到的第二點,根據裝置特性動態切換 suspend 策略,指的是可能某些裝置在 sync 模式下可能會有較好的效能表現。你想問的是,是否可以將它改為 async 模式來進行改動?
如果是的話可以根據下列這個命令來將原本為 async 的設備變成 sync:

wu@wu-Pro-E500-G6-WS720T:~$ cat /sys/devices/pci0000\:00/0000:00:00.0/power/async
enabled

在原本可以看到這個裝置是以非同步來去做 suspend 和 Resume 的,如果要將它改為同步只需要輸入下列命令:

wu@wu-Pro-E500-G6-WS720T:~$ echo disabled | sudo tee /sys/devices/pci0000\:00/0000:00:00.0/power/async
disabled

那如果是要讓整個系統從非同步的 suspend 和 Resume 變成同步的 suspend 和 Resume 的話就是去修改下列的值:

wu@wu-Pro-E500-G6-WS720T:~$ cat /sys/power/pm_async 
1
wu@wu-Pro-E500-G6-WS720T:~$ echo 0 | sudo tee /sys/power/pm_async 
0

任務簡述

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, completeexit(脫離行程)逆向還原。這些階段涵蓋 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 支援非同步暫停/回復機制,試圖平行化處理,但實際上常因同步鎖或電源域相依性增加反而延長總時間;即使選擇非同步機制,preparecomplete 階段仍需依序執行,缺乏足夠的平行化空間;各裝置的 suspend_late 階段更必須待所有相關硬體共同完成,才能進行 suspend_noirq,即便它們之間並無直接相依。

要深入分析這些瓶頸,可運用 Perfetto 等追蹤工具擷取系統行為的追蹤紀錄,再搭配 pm-graph 繪製電源域與行程凍結時序,觀察 I/O 活動、CPU C-state 切換與中斷響應;亦可參考 LKML 上針對非同步暫停流程的程式碼變更討論,評估其對 prepare/complete 階段的影響,並思考是否能在裝置驅動或電源管理子系統中,藉由更細緻的鎖分段或階段重疊機制,縮短整體 Suspend/Resume 時間。

:warning: 務必依循資訊科技詞彙翻譯詞彙對照表規範的用語。了解詞源、語境,謹慎選擇用詞,是必要的態度。

TODO: 彙整 Linux 電源管理相關素材

以第一手材料為主,儘量參照 Linux 基金會舉辦研討會的演講錄影
至少包含以下:

Optimizing suspend/resume

這個演講主要探討了優化 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 實驗(二)Linux Suspend/Resume 實驗(三)可以得知。在演講中,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 這個 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

本場演講一開始主要介紹了 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 時可以還原
  2. 讓裝置進入低功耗狀態

不一定每個驅動都要實作全部階段,但要根據裝置需求選擇。

重要提醒:行為因裝置而異

不同裝置 suspend 的需求差異非常大,核心不會保證所有裝置都會被以同樣方式處理 → 驅動要自行實作正確的 suspend 邏輯,並且沒有明確的裝置狀態輸出資訊可以查詢(除了錯誤碼),也就是 kernel 不會告訴你裝置現在是不是真的進入低功耗。

不同 suspend 實作階段對 GPIO 控制器行為的影響:

Linux 核心支援多階段的裝置 suspend 流程,驅動開發者可根據裝置特性與需求,在不同階段實作對應的 callback 函式,例如 suspend()、suspend_late()、suspend_noirq() 等。這些階段對應系統中斷(IRQ)與裝置依賴狀態的不同條件,因此選擇在哪個階段進行電源管理操作,將直接影響 suspend/resume 的穩定性與功能正確性。

這在處理與 IRQ 和喚醒(wake-up)邏輯密切相關的裝置時,尤為重要,例如 GPIO 控制器。

以下為兩個 GPIO 控制器驅動的實作對比:

static const struct dev_pm_ops rzg2l_pinctrl_pm_ops = {
    NOIRQ_SYSTEM_SLEEP_PM_OPS(rzg2l_pinctrl_suspend_noirq,
                              rzg2l_pinctrl_resume_noirq)
};
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 核心提供的一種「平台無關」淺層掛起模式,只要編譯時啟用了 CONFIG_SUSPEND=y,並且對應架構實作了基本的 idle loop(WFI、mwait 或 PSCI CPU_SUSPEND),就可在 OS 層面自行完成整機掛起/喚醒:

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

在系統 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 機制根據使用情況自動管理。
  2. 裝置彼此之間有繼承關係,整體是一棵樹。
  3. 每個裝置都維護一個 使用計數器(usage counter),用來追蹤該裝置是否正被使用。當裝置使用者(例如驅動程式或子系統)需要啟用裝置時,會呼叫 pm_runtime_get() 將計數器加一;使用完畢後,則呼叫 pm_runtime_put() 將計數器減一,Kernel 根據這個計數器的值來判斷裝置是否閒置。

演講者也將 pm_runtime_get 和 pm_runtime_put 這兩個函式簡化為如下的偽程式碼,幫助理解其基本邏輯與設計概念:

/* 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() 讓裝置先開機,同時也要遞迴讓它的 parent 裝置先 resume(例如 USB 控制器要先開,滑鼠才能用)。

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() 把裝置休眠
同樣也會往上檢查其 parent 裝置是否也能關機。

然而,前述的簡化程式碼僅作為概念說明,實際上 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

在多核心即時系統中,CPU C-State 的管理對於平衡功耗與即時反應能力具有關鍵性影響。當處於閒置狀態時,CPU 可進入不同深度的 C-State(例如 C0、C1、C3、C6 等),以達成節能與降溫目的。C-State 越深,關閉的電路越多,帶來的省電效果也越顯著。但同時,恢復這些狀態所需的時間也會增加,形成喚醒延遲的代價。

這些 C-State 的進出行為主要受兩個核心屬性影響:

  1. Exit Latency(退出延遲):越深層的 C-State,其喚醒時間越長。
  2. 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:延遲最長,在高敏感度任務中會被排除。

例如,在關鍵任務期間可設定:

echo 50 > /sys/devices/system/cpu/cpuN/power/pm_qos_resume_latency_us

或直接禁用所有 C-State(echo n/a),任務結束後再設為 0 放寬限制,恢復省電效益。

filtered_c_states_resized

這種過濾機制可靈活限制高延遲 C-State 的進入,避免對關鍵任務造成干擾。

  1. 根據 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

演講一開始介紹了什麼是 SoC(System-on-Chip),演講者以一個具體範例「SoC-9000」作為說明,透過結構圖詳細介紹其內部包含的各項模組及它們彼此之間的連接方式,幫助大家更具體地理解 SoC 的實際架構與運作模式。

image

  • 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

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、driver;所有細節皆由 ACPI framework 與固件協定完成。

image

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」,負責管理這個裝置的通電、斷電、休眠與喚醒流程。
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 機制:

/* 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。
  2. 裝置移除(driver remove)時,Kernel 會將裝置設定為 D3,進行下電處理,避免多餘功耗。
  3. 在進行 System Suspend / Resume(如睡眠、休眠) 等系統層級的電源管理時,PM core 會依序呼叫 prepare、suspend、resume、freeze 等 callback,這些都已在 acpi_general_pm_domain 中定義,最終會導向 acpi_device_set_power(),由 ACPI firmware 實際完成電源切換。
  4. 整個過程中,Linux kernel 幾乎不需要使用 clk/reset 等 subsystem framework。所有與 SoC 有關的實作細節(如開關時脈、電源 rail、GPIO 等)都被 ACPI firmware 隱藏與包裝好,驅動不需感知硬體細節,只需透過通用介面操作即可。

DT 架構面臨的挑戰

  • 驅動程式無法完全獨立,因為需要知道硬體平台(firmware)的特定實作,降低通用性與可重用性
  • Probe 時無法保證裝置電源狀態
  • SoC 的電源管理細節分散在整個驅動樹中
  • PM 資源也可能來自平台裝置(platform devices)
  • 難掌握整體 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 才會斷電,避免供電提早中斷造成系統錯誤。

// 位置: 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 欄位,也要為 soc node 去添加 #power-domain-cells 欄位讓所有的 power-domain 都掛在在下面。

其次,of_platform_default_populate() 在系統初始化時會一次性地建立所有 platform device,導致無法在 driver probe 之前即時插入或關聯 PM domain,造成關聯流程與核心節點錯失對應時機。此外,Linux 現行架構每個裝置僅允許對應一個 power domain,當一個裝置已經在裝置樹中聲明 power-domains,便無法再增加 power-domain 來去綁定;這對某些多源控制的應用場景產生限制。最後,部分特殊裝置仍仰賴對資源的直接控制(例如可能需要特定頻率的 clock),這讓完全改為 PM domain 控管變得困難。

因此 Stephen Boyd 提出了 SoC PM Driver

  1. 在 SoC node 上註冊一個驅動,並且讓這個驅動負責建立其底下所有子裝置(child devices)
  2. 建立裝置(device)時關聯 PM domain
  3. 在 PM domain 中取得電源資源
  4. 控制電源開關行為
  5. 最終從原本的 platform driver 移除掉 PM 操作邏輯

Block_Diagram_Cleaned

在 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)
  • 驅動本身不控制硬體,只是用來「掃出子裝置」
struct platform_driver soc9000_driver = {
    .driver.probe = of_platform_default_populate(),
    .driver.match_table = { "vendor,soc-9000" },
};
  1. 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。
  2. 掛 PM domain:在加入到系統總線(bus)之前,呼叫 dev_pm_domain_set() 把裝置與 PM domain 綁定。
  3. 加入裝置:最後用 of_platform_device_add() 把裝置加到 platform bus 上。
pdev = of_platform_device_alloc();
dev_pm_domain_set(&pdev->dev, pm_domain);
of_platform_device_add(pdev);

在 PM domain 中取得電源資源

這些動作原本通常會寫在個別裝置 driver 中,
現在改由 PM domain 透過 .activate() 來統一做,例如:

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 中設定:

generic_pm_domain.power_on = power_on_pm_resources;
generic_pm_domain.power_off = power_off_pm_resources;

而實作內容是:

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。
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 時更省電

TODO: 針對 x86-64 和 Arm64 處理器進行實驗和解讀

選用內建 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 階段,因此,若要優化系統的休眠與回復速度,可以優先針對這兩個階段進行改善:

Screenshot from 2025-05-26 14-28-14
Screenshot from 2025-05-26 14-29-27

TODO: 評估 LKML 相關改進並參與討論

在上述裝置中,評估 PM: sleep: Improvements of async suspend and resume of devices 及相關 patchset (注意版本) 對於電源管理的效益,在 Linux v6.12+ 驗證,並針對稍早整理的演講素材,嘗試提出改進方案。

Improvements of async suspend and resume of devices

這個 patch set 改進 Linux 核心中非同步(asynchronous) suspend/resume 的處理機制。
在一般情況下,系統在執行裝置 suspend 或 resume 時,會採用同步方式,也就是:

  • 同步(sync):每次僅處理一個裝置,必須等前一個裝置 suspend/resume 完成後,才會繼續下一個。
  • 非同步(async):允許多個彼此沒有相依關係的裝置同時進行 suspend/resume,以提升整體處理效率。

這個 patch set 整體設計的核心想法是:

有些裝置即使還沒完全符合所有依賴條件,但只要部分條件已經滿足,就可以提早開始執行非同步 suspend/resume。
這樣可以避免一次把太多還不能立刻執行的 async work items 塞進佇列裡,因為那些裝置可能還要等其他裝置處理完才能動作,如果都塞進去等著,只會浪費資源、增加系統負擔,讓整體變慢。

本次提出的 patch set 一共包含 5 個 patch ,為優化 Linux 核心中非同步 suspend/resume 的處理流程。以下是某一台測試系統的初步效能結果,用以觀察這些變更對實際執行時間的影響:

  • "Baseline":指的是未套用這些 patch 的 linux-pm.git/testing 分支
  • "Parent/child":表示套用了 patch [1–3/5] 的版本
  • "Device links":則是套用了完整的 patch [1–5/5] 的版本

測試結果中各階段的定義如下:

  • s/r 指的是正常的 suspend/resume 流程,也就是裝置的標準睡眠與喚醒操作。
  • noRPM 是在進入更深層 suspend 前的 late suspend 階段,以及剛喚醒時的 early resume。
  • noIRQ 則是進入 suspend/resume 過程中關閉中斷(IRQ)之後所執行的操作階段。
          Baseline       Parent/child    Device links

        Suspend Resume   Suspend Resume   Suspend Resume

s/r     427     449      298     450      294     442
noRPM   13      1        13      1        13      1
noIRQ   31      25       28      24       28      26

s/r     408     442      298     443      301     447
noRPM   13      1        13      1        13      1
noIRQ   32      25       30      25       28      25

s/r     408     444      310     450      298     439
noRPM   13      1        13      1        13      1
noIRQ   31      24       31      26       31      24

從結果可以明顯看出,在套用 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

Resume children after resuming the parent

之前的 async resume 邏輯:

  • 對於每個 resume 階段(除了 complete 階段,這個階段是都是是串行執行的),resume 邏輯首將所有在 dpm_list 裡的異步裝置排入處理隊列。然後它會再次逐一走訪這個 dpm_list,逐個恢復同步裝置。
  • 異步裝置在開始自身的恢復操作之前,需要等待所有它的上游裝置(superior devices)完成恢復。
  • 這樣的過程會導致異步裝置在實際恢復之前經歷多次的休眠和喚醒週期,進而造成 kworker 執行緒的停頓。為了解決這個問題,workqueue 會啟動更多的 kworker 執行緒來處理其他異步裝置。
  • 結果是,會創建過多的執行緒,並且經歷過多的喚醒、休眠和上下文切換,這會使得全異步恢復變得比同步恢復更慢。
static bool is_async(struct device *dev)
{
	return dev->power.async_suspend && pm_async_enabled
		&& !pm_trace_is_enabled();
}
void dpm_resume(pm_message_t state)
{
        ...
	list_for_each_entry(dev, &dpm_suspended_list, power.entry) {
            dpm_clear_async_state(dev);// 清除這個裝置先前的 async 執行狀態
            dpm_async_fn(dev, async_resume);// 試圖對這個裝置排程 async resume
	}
    
        while (!list_empty(&dpm_suspended_list)) {
            dev = to_device(dpm_suspended_list.next);
            list_move_tail(&dev->power.entry, &dpm_prepared_list);
            if (!dev->power.work_in_progress) {
                
                get_device(dev);
                
                mutex_unlock(&dpm_list_mtx);
                
                device_resume(dev, state, false);
                
                put_device(dev);
                
                mutex_lock(&dpm_list_mtx);
            }
        }
        ...
} 
dpm_resume()
  └── dpm_async_fn(dev, async_resume)
         └── async_schedule_dev_nocall(async_resume, dev)

這個 patch 修改:

  • 讓程式碼在裝置的 parent resume 處理完成後,才啟動其子裝置的 async resume
  • 只有對於沒有 parent 裝置的裝置,才會一開始就啟動 async resume。

另外:

  • 在對某裝置執行同步 resume 之前,會先檢查它是否可以安全地改用 async resume,避免對正在以 async 模式 resume 的裝置造成等待或衝突

本次改動涉及多個與裝置電源恢復(resume)流程相關的核心函式,包括:

  • dpm_resume
  • device_resume
  • dpm_resume_early
  • device_resume_early
  • dpm_noirq_resume_devices
  • device_resume_noirq

下列為其中一個例子:

static void device_resume(struct device *dev, pm_message_t state, bool async)
{
        ...
+	dpm_async_resume_children(dev, async_resume);
}

現在是在該裝置 resume 完成後,主動觸發它所有子裝置的 async resume。

void dpm_resume(pm_message_t state)
{
        ...
	list_for_each_entry(dev, &dpm_suspended_list, power.entry) {
            dpm_clear_async_state(dev);
-           dpm_async_fn(dev, async_resume);
+           if (dpm_root_device(dev))
+               dpm_async_with_cleanup(dev, async_resume);
	}
    
        while (!list_empty(&dpm_suspended_list)) {
            dev = to_device(dpm_suspended_list.next);
            list_move_tail(&dev->power.entry, &dpm_prepared_list);
-           if (!dev->power.work_in_progress) {
+           if (!dpm_async_fn(dev, async_resume)) {
                
                get_device(dev);
                
                mutex_unlock(&dpm_list_mtx);
                
                device_resume(dev, state, false);
                
                put_device(dev);
                
                mutex_lock(&dpm_list_mtx);
            }
        }
        ...
}

其中的 dpm_root_device 函式的作用是確認是否為 root_device。
這個 patch 根據目前的測試結果,本身並不會對系統整體的 resume 時間造成可測量的影響,但這項修改除了讓 async resume 對於資源較少的系統更加友善。

Suspend async parents after suspending children

之前的 async suspend 邏輯:

  • async suspend 邏輯逐一走訪 dpm_list,當它遇到異步裝置時,它會 queue 該裝置的工作並繼續處理下一個裝置。然後,若它遇到同步裝置,它會等待該同步裝置及其所有 subordinate devices 完成 suspend 操作後再繼續。
  • 這表示異步裝置可能會被無關的同步裝置阻塞,這樣在 suspend 前它甚至無法排隊。
  • 一旦異步裝置被 queue,它將經歷與恢復邏輯相似的效率問題(例如執行緒創建、喚醒、休眠和上下文切換的開銷)。

參照之前針對 resume 流程所做的修改,這次的 patch 讓:

  • device_suspend() 在裝置本身處理完成後,才開始其 parent 裝置的非同步 suspend;
  • dpm_suspend() 則會優先處理沒有子裝置的裝置,這樣它們就不需要因等待與其無關的「同步裝置」而被延遲 suspend。

在這個 patch 中可以發現他先定義了兩個函式:

static bool dpm_leaf_device(struct device *dev)

這個函式為判斷一個裝置是否為 leaf device,也就是沒有子裝置的裝置。
leaf 裝置通常可以在最早的階段進行 async suspend,因為它們沒有下游依賴者,不需要等待子裝置完成。

static void dpm_async_suspend_parent(struct device *dev, async_func_t func)

這個函式為當某個裝置的 suspend 處理完成後,這個函式會啟動其 parent 裝置的 async suspend,前提是 parent 裝置也允許 async suspend。

static int device_suspend(struct device *dev, pm_message_t state, bool async)
{
        ...

    complete_all(&dev->power.completion);
    TRACE_SUSPEND(error);
-   return error;
+
+   if (error || async_error)
+       return error;
+
+   dpm_async_suspend_parent(dev, async_suspend);
+
+   return 0;
}
int dpm_suspend(pm_message_t state)
{
    ktime_t starttime = ktime_get();
+   struct device *dev;
    int error = 0;

        ...
+   list_for_each_entry_reverse(dev, &dpm_prepared_list, power.entry) {
+       dpm_clear_async_state(dev);
+       if (dpm_leaf_device(dev))
+           dpm_async_with_cleanup(dev, async_suspend);
+   }

    while (!list_empty(&dpm_prepared_list)) {
-       struct device *dev = to_device(dpm_prepared_list.prev);
+       dev = to_device(dpm_prepared_list.prev);

        list_move(&dev->power.entry, &dpm_suspended_list);
        
-       dpm_clear_async_state(dev);

        ...
        mutex_lock(&dpm_list_mtx);

-       if (error || async_error)
+       if (error || async_error) {
+           list_splice(&dpm_prepared_list, &dpm_suspended_list);
            break;
+       }
    }
    ...
}

Make suspend of devices more asynchronous

這個 patch 使 device_suspend_late()device_suspend_noirq() 在處理完裝置本身之後,再啟動其 parent 裝置的 async suspend;同時,dpm_suspend_late()dpm_noirq_suspend_devices() 會優先處理沒有子裝置的裝置,讓它們不需要等待與其無關的其他裝置完成 suspend。

和上述第二個 patch 的改動類似。

修改前測試 Suspend-to-Idle 的結果

以下為搭載 Intel Core i7-10700 處理器,核心版本為 6.15 的個人電腦執行 Suspend-to-Idle 測試之結果。

wu@wu-Pro-E500-G6-WS720T:~$ uname -r
6.15.0-wu-kernel+
          Baseline       Parent/child    Device links

        Suspend Resume   Suspend Resume   Suspend Resume
s/r     536     166    
noRPM   14      1.8       
noIRQ   37      38     

s/r     532     140      
noRPM   15      1.8      
noIRQ   38      38  

s/r     526     141
noRPM   14      1.8        
noIRQ   39      38     

修改後測試 Suspend-to-Idle 的結果

          Baseline       Parent/child    Device links

        Suspend Resume   Suspend Resume   Suspend Resume
s/r     536     166      529     164
noRPM   14      1.8      15      2
noIRQ   37      38       39      37

s/r     532     140      545     141
noRPM   15      1.8      15      2
noIRQ   38      38       39      38

s/r     526     141      532     140
noRPM   14      1.8      14      37
noIRQ   39      38       39      2

可以觀察到效能上並未有明顯改善,可能是因為該系統在暫停與恢復方面原本就已具備良好表現。事實上,根據 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).

	Baseline		Parents/children	Device links

	Suspend	Resume		Suspend	Resume		Suspend	Resume

Dell XPS13 9360

s/r	427	449		298	450		294	442
noRPM	13	1		13	1		13	1
noIRQ	31	25		28	24		28	26

s/r	408	442		298	443		301	447
noRPM	13	1		13	1		13	1
noIRQ	32	25		30	25		28	25

s/r	408	444		310	450		298	439
noRPM	13	1		13	1		13	1
noIRQ	31	24		31	26		31	24

Dell XPS13 9380

s/r	439	283		318	290		319	290
noRPM	15	2		15	1		15	2
noIRQ	198	1766		202	1743		204	1766

s/r	439	281		318	280		320	280
noRPM	15	2		15	1		15	1
noIRQ	199	1781		203	1783		205	1770

s/r	440	279		319	281		320	283
noRPM	14	2		15	1		15	1
noIRQ	197	1777		202	1765		203	1724

Coffee Lake Desktop

s/r	138	347		130	345		132	344
noRPM	15	2		20	2		15	2
noIRQ	15	25		23	25		16	26

s/r	133	345		124	343		131	346
noRPM	14	1		13	1		13	1
noIRQ	15	25		14	25		14	25

s/r	124	343		126	345		128	345
noRPM	13	1		13	1		13	1
noIRQ	14	25		14	25		14	26

根據作者的實驗結果,其 suspend 的速度明顯優於本系統的表現。為了進一步釐清差異的原因,我使用 pm-graph 工具分析各個裝置在 suspend 階段的耗時情形,並發現了一個關鍵因素:目前本系統在 suspend 階段的主要瓶頸來自機械式硬碟的存取延遲。因此,即使進一步優化程式邏輯,整體效能仍可能受到硬體限制的影響。這也可能是我無法觀察到明顯提昇的主因之一。

image
當我將機械式硬碟拔除後,並觀察執行數據時,發現結果與上述 Coffee Lake Desktop 的表現相符,Suspended time 降至約一百二十多毫秒。然而,這樣一來,桌機的 suspend 時間已經足夠快速,因此無法明顯觀察到進一步的優化效果。

於是我另外找了一台筆電來觀察結果,下列為測試結果:

	Baseline		Parents/children

	Suspend	Resume		Suspend	Resume

acer aspire a515-57g

s/r	412	371		402	370
noRPM	28	7		22	6
noIRQ	59	162		56	152

s/r	409	375		404	374
noRPM	29	7		28	5
noIRQ	55	152		56	153

s/r	402	375		411	373
noRPM	30	5		29	5
noIRQ	65	157		52	164

目前看起來差異不大,我猜這個 patch 可能會在效能比較差的機器上會發揮更明顯的效果。

Make async suspend handle suppliers like parents

這個 patch 在之前的基礎上,對裝置的 async suspend 流程做了一些調整:

  • 如果某個裝置還有其他裝置依賴它,就不要太早讓它進入 async suspend;
    相反地,該裝置所依賴的供應裝置,應該在它本身完成掛起後,再開始執行這些供應裝置的 async suspend。

Make async resume handle consumers like children

這個 patch 延續之前的改動,這次調整了裝置 resume 階段的 async 處理流程:

  • 如果一個裝置還依賴其他供應裝置,就不要太早開始喚醒它;
    相反地,那些依賴這個裝置的其他裝置,應該等這個裝置先喚醒完成,再開始執行自己的 async resume。

改動實做

將 async suspend 專注於 leaf 節點

在原本的 async suspend 流程中,第一層迴圈會先針對 leaf 節點進行呼叫,而當 leaf 節點的 suspend 動作完成後,流程會進一步觸發其 parent 節點的 suspend。此次修改的目的是調整該行為,使第一層迴圈中所呼叫的 leaf 節點不再主動觸發其 parent 節點 suspend。

我認為,第一層已呼叫的 leaf 節點數量已經足夠覆蓋 suspend 流程的主要進度,因此不需要額外再針對 parent 節點進行多餘的呼叫,避免因此增加額外的 context switch 負擔。

實測結果顯示,修改後流程的速度約為原本設計的三倍,因此似乎是不可行的。

將 async resume 的對象從 root 節點變更為 root 節點和其小孩

實測結果顯示修改後與原本效果與效能無明顯差異。

優化 suspend 流程:僅針對非 leaf 或有 consumers 裝置呼叫 dpm_wait_for_subordinate()

原本 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():

if (!dpm_leaf_device(dev) || !list_empty(&dev->links.consumers))
    dpm_wait_for_subordinate(dev, async);

但實際針對這段修改後的執行時間進行測量,並透過 T-test 進行統計檢定後發現,與原本直接對所有裝置執行 dpm_wait_for_subordinate 的結果相比,整體 suspend 效能並無顯著差異,兩者效果相當。

將 dpm_root_device 判斷整合進 dpm_wait_for_superior

由於這個 patch 將 dpm_root_device 這個函式改為下列:

static bool dpm_root_device(struct device *dev)
{
-	return !dev->parent;
+	lockdep_assert_held(&dpm_list_mtx);
+
+	/*
+	 * Since this function is required to run under dpm_list_mtx, the
+	 * list_empty() below will only return true if the device's list of
+	 * consumers is actually empty before calling it.
+	 */
+	return !dev->parent && list_empty(&dev->links.suppliers);
}

原本設計上,裝置樹中的根節點會先啟動 async resume。現在修改後,變更為根節點且要沒有供應者關係的裝置才會啟動 async resume。

定義好這個邏輯函式後,我發現它也可以直接整合進 dpm_wait_for_superior() 函式中使用。dpm_wait_for_superior() 的作用是 在 resume 過程中等待 parent 節點和所有供應者完成相關操作,以確保 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) 條件成立的次數很多,但實際進入我新增函式的次數明顯更少,說明流程中仍有其他條件或邏輯影響實際執行路徑。

void dpm_noirq_resume_devices(pm_message_t state)
{
        ...
	list_for_each_entry(dev, &dpm_suspended_list, power.entry) {
            dpm_clear_async_state(dev);
            if (dpm_root_device(dev))
                dpm_async_with_cleanup(dev, async_resume);
	}
PM: count value: 15 
PM: count value: 777

於是我接著觀察 device_resume_noirq 的執行流程,並進行測試後發現了可能的原因。在 device_resume_noirq 中,觀察到以下幾段關鍵程式碼:

static void device_resume_noirq(struct device *dev, pm_message_t state, bool async)
{
        ...

	if (dev->power.syscore || dev->power.direct_complete)
		goto Out;

	if (!dev->power.is_noirq_suspended)
		goto Out;

	if (!dpm_wait_for_superior(dev, async))
		goto Out;

	skip_resume = dev_pm_skip_resume(dev);

我推測當 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 之前就已經提前跳出。

suspend resume 改進方向

目前的實作流程裡還是有不少多餘的 context switch。當一個裝置要被 resume 時,必須先等 parent 裝置處理完成,接著還要再等待它的 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 上。

相關的改動正在被提出:調整不同驅動程式選項,以使運行時電源管理與系統休眠能更好地協同工作
PM: Reconcile different driver options for runtime PM integration with system sleep