---
# System prepended metadata

title: Linux Suspend/Resume 實驗（三）

---

# Linux Suspend/Resume 實驗（三）
## dpm_list 的結構與順序設計
在 Linux 核心的裝置電源管理（Device Power Management, DPM）中，dpm_list 是一個核心的全域 linked list，用於追蹤系統中所有需要參與 suspend/resume 操作的裝置。這個 list 的結構與插入順序，直接影響到 suspend/resume 過程的正確性與執行效率。

根據設計，dpm_list 中的條目是以**深度優先順序 Depth-First Order** 排列。其原因來自裝置初始化的時序特性：在裝置樹（Device Tree）或裝置模型建立過程中，父裝置會先於子裝置被探測與註冊，而每個裝置註冊進 DPM 系統時（透過 device_pm_add()），都會被插入 dpm_list 的尾端。因此，自然而然形成了「父裝置在前、子裝置在後」的鏈表結構，對應到 suspend 時「子裝置先關、父裝置後關」以及 resume 時「父裝置先開、子裝置後開」的邏輯順序。
```c
void device_pm_add(struct device *dev)
{
       ...
	device_pm_check_callbacks(dev);
	mutex_lock(&dpm_list_mtx);
	if (dev->parent && dev->parent->power.is_prepared)
		dev_warn(dev, "parent %s should not be sleeping\n",
			dev_name(dev->parent));
	list_add_tail(&dev->power.entry, &dpm_list);
	dev->power.in_dpm_list = true;
	mutex_unlock(&dpm_list_mtx);
}

```

正如核心註解所說：

>"The entries in the dpm_list list are in a depth first order, simply because children are guaranteed to be discovered after parents, and are inserted at the back of the list on discovery."

此外，為了保證這個鏈表操作的同步安全，所有對 dpm_list 的修改都需要透過 dpm_list_mtx 鎖保護。然而，由於 device_pm_add() 有可能在持有裝置本身的 mutex 時被呼叫，因此有一個重要原則需遵守：

**在持有 dpm_list_mtx 的情況下，不可以再嘗試取得任何裝置的鎖（如 device_lock()），以避免產生鎖循環與死鎖風險。**

這套設計不僅確保了裝置 suspend/resume 的順序依賴能正確處理，也讓整個裝置電源管理機制在複雜拓撲與並行場景下能維持一致性與可靠性。

在系統建立好 dpm_list 之後，為了執行 suspend 和 resume 流程，核心還會額外建立四個輔助鏈結串列，分別用來管理裝置在不同階段的 power management 狀態轉換。
```c
static LIST_HEAD(dpm_prepared_list);
static LIST_HEAD(dpm_suspended_list);
static LIST_HEAD(dpm_late_early_list);
static LIST_HEAD(dpm_noirq_list);
```
## dpm_prepare
下列為 suspend 階段的原則：
```
Suspend:
→ 裝置 A suspend 失敗 → 馬上停止 → 不能進 sleep
→ 要能準確報告 "誰失敗了"
→ 否則系統會進半睡半醒 → 不安全
```

在一開始執行 dpm_prepare 階段時，系統會依序遍歷 dpm_list 中的每一個裝置，針對每個裝置呼叫對應的 device_prepare() 函式，執行 suspend 前的準備作業，同時設定支援 direct_complete 的裝置。
若當前裝置的準備流程執行成功，則會將該裝置的 list node 移動至下一階段的 dpm_prepared_list 中，以便後續 suspend 流程使用。
此外，當 device_prepare() 函式執行完成後，該裝置將不允許再註冊新的子裝置（child device），以確保裝置階層結構在 suspend 流程中保持穩定。
如果在準備過程中發生錯誤，系統會立即中止 suspend 流程，並開始執行復原動作，撤銷前面已完成的準備步驟，恢復裝置至 suspend 前的狀態。

>No new children of the device may be registered after this function has returned.

## dpm_suspend
dpm_suspend 遍歷 dpm_prepared_list 並將裝置的 list node 移動至下一階段的 dpm_suspended_list，當它遇到異步設備時，系統會將該裝置的 device_suspend 工作透過 async_schedule_dev_nocall 函式排程到 async workqueue 中執行（queue 該設備的工作。然後，若它遇到同步設備，它會等待該同步設備及其所有 subordinate devices 完成 suspend 操作後再繼續。這表示異步設備可能會被無關的同步設備阻塞，這樣在 suspend 前它甚至無法排隊。因此 [PM: sleep: Suspend async parents after suspending children](https://lore.kernel.org/lkml/10629535.nUPlyArG6x@rjwysocki.net/T/#mdd5a8d91c02383f8508dd456c2eb3b7163dfdafb) 就是在解決這個問題

## dpm_suspend_late
dpm_suspend_late 階段流程與上述類似，不同之處在於 list 由 dpm_suspended_list 改為 dpm_late_early_list，呼叫的函式則由 device_suspend 改為 device_suspend_late。當此階段函式執行時，該裝置的 Runtime PM 會暫時停用。這個 patch 是用來改善此階段的 [PM: sleep: Make suspend of devices more asynchronous](https://lore.kernel.org/lkml/10629535.nUPlyArG6x@rjwysocki.net/T/#mecd027cb96d152ffe8161df9af0e8947a4c3a2f2)。

## dpm_suspend_noirq
dpm_suspend_noirq 階段同樣類似，差別在於 list 改為 dpm_noirq_list，呼叫的函式改為 device_suspend_noirq。此外，在此階段執行期間，該裝置的 interrupts（IRQ）會被停用，以確保 suspend_noirq 操作期間不會有 interrupt 發生。這個 patch 是用來改善此階段的 [PM: sleep: Make suspend of devices more asynchronous](https://lore.kernel.org/lkml/10629535.nUPlyArG6x@rjwysocki.net/T/#mecd027cb96d152ffe8161df9af0e8947a4c3a2f2)。

## dpm_resume_noirq
下列為 resume 階段的原則：
```
Resume:
→ 即使裝置 B resume 失敗 → 系統還是要把其它裝置 resume 完 → 不能讓系統卡住
→ 重點是系統能啟動回來，錯誤只需要 high-level 紀錄
→ 失敗裝置 driver 通常自己會做 error handling
```
在觀察 resume 階段時發現到為什麼 resume 階段不特別記每個裝置的錯誤狀態，而是用一個全域變數（async_error）做簡單記錄？後來得出了下列這張表格
| 階段      | 設計哲學                                                       |
| ------- | ---------------------------------------------------------- |
| suspend | **任何一個裝置 suspend 失敗，流程就中止，系統不能進入低功耗** → 必須精確記錄哪個裝置失敗       |
| resume  | **目標是盡量把所有裝置復原回正常狀態 → 不因單一裝置 resume 失敗而中止整個流程** → 重點是整體能恢復 |

對於每個 resume 階段（除了 complete 階段，這個階段是都是是串行執行的），resume 邏輯首將所有在 list 裡的異步設備排入處理隊列。然後它會再次遍歷這個 list，逐個恢復同步設備。
異步設備在開始自身的恢復操作之前，需要等待所有它的上游設備（superior devices）完成恢復。
這樣的過程會導致異步設備在實際恢復之前經歷多次的休眠和喚醒週期，進而造成 kworker 執行緒的停頓。為了解決這個問題，workqueue 會啟動更多的 kworker 執行緒來處理其他異步設備。
結果是，會創建過多的執行緒，並且經歷過多的喚醒、休眠和上下文切換，這會使得全異步恢復變得比同步恢復更慢。

此階段主要是撤銷 dpm_suspend_noirq 所作的動作。
## dpm_resume_early
此階段主要是撤銷 dpm_suspend_late 所作的動作。
## dpm_resume
此階段主要是撤銷 dpm_suspend 所作的動作，而這個 patch [PM: sleep: Resume children after resuming the parent](https://lore.kernel.org/lkml/10629535.nUPlyArG6x@rjwysocki.net/T/#mece5c40fd3453ffb8e929057388f833ccfcec05c) 是對於上述三個階段的改進。
