I/O 裝置的執行時電源管理框架

  1. 2009-2011 Rafael J. Wysocki <rjw@sisk.pl>, Novell Inc.

  1. 2010 Alan Stern <stern@rowland.harvard.edu>

  1. 2014 Intel Corp., Rafael J. Wysocki <rafael.j.wysocki@intel.com>

1. 簡介

I/O 裝置的執行時電源管理 (runtime PM) 支援由電源管理核心 (PM 核心) 層提供,具體方式包括:

  • 電源管理工作佇列 pm_wq,匯流排型別和裝置驅動程式可以將與 PM 相關的工作項放入其中。強烈建議使用 pm_wq 來排隊所有與執行時 PM 相關的工作項,因為這允許它們與系統範圍的電源轉換(記憶體掛起、休眠和從系統睡眠狀態恢復)同步。pm_wqinclude/linux/pm_runtime.h 中宣告,在 kernel/power/main.c 中定義。

  • struct devicepower 成員(型別為 struct dev_pm_info,定義在 include/linux/pm.h 中)中的多個執行時 PM 欄位,可用於同步執行時 PM 操作。

  • struct dev_pm_ops(定義在 include/linux/pm.h 中)中的三個裝置執行時 PM 回撥。

  • 定義在 drivers/base/power/runtime.c 中的一組輔助函式,可用於執行執行時 PM 操作,並且它們之間的同步由 PM 核心負責。鼓勵匯流排型別和裝置驅動程式使用這些函式。

struct dev_pm_ops 中存在的執行時 PM 回撥、struct dev_pm_info 的裝置執行時 PM 欄位以及為執行時 PM 提供的核心輔助函式將在下面描述。

2. 裝置執行時 PM 回撥

struct dev_pm_ops 中定義了三個裝置執行時 PM 回撥:

struct dev_pm_ops {
      ...
      int (*runtime_suspend)(struct device *dev);
      int (*runtime_resume)(struct device *dev);
      int (*runtime_idle)(struct device *dev);
      ...
};

->runtime_suspend()->runtime_resume()->runtime_idle() 回撥由 PM 核心為裝置的子系統執行,該子系統可以是以下任一:

  1. 裝置的 PM 域,如果裝置的 PM 域物件 dev->pm_domain 存在。

  2. 裝置的裝置型別,如果 dev->typedev->type->pm 都存在。

  3. 裝置的裝置類別,如果 dev->classdev->class->pm 都存在。

  4. 裝置的匯流排型別,如果 dev->busdev->bus->pm 都存在。

如果根據上述規則選擇的子系統未提供相關的回撥,PM 核心將直接呼叫儲存在 dev->driver->pm 中的相應驅動程式回撥(如果存在)。

PM 核心始終按照上述順序檢查要使用的回撥,因此回撥的優先順序從高到低依次是:PM 域、裝置型別、類別和匯流排型別。此外,高優先順序的回撥將始終優先於低優先順序的回撥。PM 域、匯流排型別、裝置型別和類別回撥在下文中被稱為子系統級回撥。

預設情況下,回撥總是在啟用了中斷的程序上下文中呼叫。然而,pm_runtime_irq_safe() 輔助函式可以用來告訴 PM 核心,對於給定裝置,在停用中斷的原子上下文中執行 ->runtime_suspend()->runtime_resume()->runtime_idle() 回撥是安全的。這意味著所涉及的回撥例程不能阻塞或睡眠,但這也意味著第 4 節末尾列出的同步輔助函式可以在中斷處理程式中或通常在原子上下文中用於該裝置。

子系統級掛起回撥(如果存在)完全 負責 適當地處理裝置的掛起,這可以(但不必)包括執行裝置驅動程式自己的 ->runtime_suspend() 回撥(從 PM 核心的角度來看,只要子系統級掛起回撥知道如何處理裝置,就沒必要在裝置驅動程式中實現 ->runtime_suspend() 回撥)。

  • 一旦子系統級掛起回撥(如果直接呼叫,則是驅動程式掛起回撥)成功為給定裝置完成,PM 核心就將該裝置視為已掛起,這不一定意味著它已進入低功耗狀態。然而,這應該意味著在為其執行適當的恢復回撥之前,裝置不會處理資料,也不會與 CPU(s) 和 RAM 通訊。裝置在成功執行掛起回撥後的執行時 PM 狀態為“suspended”。

  • 如果掛起回撥返回 -EBUSY-EAGAIN,則裝置的執行時 PM 狀態仍保持“active”,這意味著裝置在此之後必須完全執行。

  • 如果掛起回撥返回的錯誤程式碼與 -EBUSY-EAGAIN 不同,PM 核心將此視為致命錯誤,並將拒絕為該裝置執行第 4 節中描述的輔助函式,直到其狀態直接設定為“active”或“suspended”(PM 核心為此目的提供了特殊的輔助函式)。

特別地,如果驅動程式需要遠端喚醒功能(即允許裝置請求更改其電源狀態的硬體機制,例如 PCI PME)才能正常執行,並且 device_can_wakeup() 為該裝置返回“false”,則 ->runtime_suspend() 應該返回 -EBUSY。另一方面,如果 device_can_wakeup() 為該裝置返回“true”,並且裝置在執行掛起回撥期間進入低功耗狀態,則預計將為該裝置啟用遠端喚醒。通常,所有在執行時進入低功耗狀態的輸入裝置都應啟用遠端喚醒。

子系統級恢復回撥(如果存在)完全負責 適當地處理裝置的恢復,這可以(但不必)包括執行裝置驅動程式自己的 ->runtime_resume() 回撥(從 PM 核心的角度來看,只要子系統級恢復回撥知道如何處理裝置,就沒必要在裝置驅動程式中實現 ->runtime_resume() 回撥)。

  • 一旦子系統級恢復回撥(如果直接呼叫,則是驅動程式恢復回撥)成功完成,PM 核心就將該裝置視為完全執行,這意味著裝置必須能夠根據需要完成 I/O 操作。裝置的執行時 PM 狀態隨後變為“active”。

  • 如果恢復回撥返回錯誤程式碼,PM 核心將此視為致命錯誤,並將拒絕為該裝置執行第 4 節中描述的輔助函式,直到其狀態直接設定為“active”或“suspended”(透過 PM 核心為此目的提供的特殊輔助函式)。

空閒回撥(如果存在,則為子系統級回撥,否則為驅動程式回撥)由 PM 核心在裝置看似空閒時執行,這透過兩個計數器指示給 PM 核心:裝置的使用計數器和裝置的“active”子裝置計數器。

  • 如果使用 PM 核心提供的輔助函式減少了這些計數器中的任何一個,並且結果為零,則檢查另一個計數器。如果該計數器也為零,PM 核心將以裝置作為引數執行空閒回撥。

空閒回撥執行的操作完全取決於相關的子系統(或驅動程式),但預期和推薦的操作是檢查裝置是否可以掛起(即是否滿足掛起裝置所需的所有條件),並在這種情況下為裝置排隊掛起請求。如果沒有空閒回撥,或者回調返回 0,那麼 PM 核心將嘗試對裝置執行執行時掛起,同時也會尊重配置為自動掛起的裝置。本質上,這意味著呼叫 pm_runtime_autosuspend()(請注意,在這種情況下,驅動程式需要更新裝置最後忙碌標記 pm_runtime_mark_last_busy() 來控制延遲)。為了防止這種情況(例如,如果回撥例程已經啟動了延遲掛起),該例程必須返回非零值。負錯誤返回程式碼會被 PM 核心忽略。

第 4 節中描述的 PM 核心提供的輔助函式保證了針對單個裝置的執行時 PM 回撥滿足以下約束:

  1. 回撥是互斥的(例如,禁止為同一裝置並行執行 ->runtime_suspend()->runtime_resume() 或另一個 ->runtime_suspend() 例項),但 ->runtime_suspend()->runtime_resume() 可以與 ->runtime_idle() 並行執行(儘管在為同一裝置執行其他任何回撥時,->runtime_idle() 不會啟動)。

  2. ->runtime_idle()->runtime_suspend() 只能為“active”裝置執行(即,PM 核心只為執行時 PM 狀態為“active”的裝置執行 ->runtime_idle()->runtime_suspend())。

  3. ->runtime_idle()->runtime_suspend() 只能為滿足以下條件的裝置執行:其使用計數器為零 並且 其“active”子裝置計數器為零,或者其 power.ignore_children 標誌已設定。

  4. ->runtime_resume() 只能為“suspended”裝置執行(即,PM 核心只為執行時 PM 狀態為“suspended”的裝置執行 ->runtime_resume())。

此外,PM 核心提供的輔助函式遵循以下規則:

  • 如果 ->runtime_suspend() 即將執行或有待處理的執行請求,則 ->runtime_idle() 不會為同一裝置執行。

  • 執行或排程 ->runtime_suspend() 的請求將取消為同一裝置執行 ->runtime_idle() 的任何待處理請求。

  • 如果 ->runtime_resume() 即將執行或有待處理的執行請求,則其他回撥不會為同一裝置執行。

  • 執行 ->runtime_resume() 的請求將取消為同一裝置執行其他回撥的任何待處理或已排程請求,但已排程的自動掛起除外。

3. 執行時 PM 裝置欄位

struct dev_pm_info(定義在 include/linux/pm.h 中)中存在以下裝置執行時 PM 欄位:

struct timer_list suspend_timer;
  • 用於排程(延遲)掛起和自動掛起請求的定時器

unsigned long timer_expires;
  • 定時器過期時間,以 jiffies 為單位(如果此值不為零,則定時器正在執行並將在該時間過期,否則定時器未執行)

struct work_struct work;
  • 用於排隊請求的工作結構(即 pm_wq 中的工作項)

wait_queue_head_t wait_queue;
  • 當任何輔助函式需要等待另一個函式完成時使用的等待佇列

spinlock_t lock;
  • 用於同步的鎖

atomic_t usage_count;
  • 裝置的使用計數器

atomic_t child_count;
  • 裝置的“active”子裝置計數

unsigned int ignore_children;
  • 如果設定,child_count 的值將被忽略(但仍會更新)

unsigned int disable_depth;
  • 用於停用輔助函式(如果此值為零,它們正常工作);其初始值為 1(即,所有裝置最初都停用了執行時 PM)

int runtime_error;
  • 如果設定,表示發生了致命錯誤(其中一個回撥返回了第 2 節中描述的錯誤程式碼),因此輔助函式將無法工作,直到此標誌被清除;這是失敗回撥返回的錯誤程式碼

unsigned int idle_notification;
  • 如果設定,->runtime_idle() 正在執行

unsigned int request_pending;
  • 如果設定,存在待處理請求(即已排入 pm_wq 的工作項)

enum rpm_request request;
  • 待處理請求的型別(如果 request_pending 已設定,則有效)

unsigned int deferred_resume;
  • 如果 ->runtime_resume() 即將執行,而 ->runtime_suspend() 正在為該裝置執行,並且等待掛起完成不切實際時設定;意味著“一旦掛起完成就立即開始恢復”

enum rpm_status runtime_status;
  • 裝置的執行時 PM 狀態;此欄位的初始值為 RPM_SUSPENDED,這意味著 PM 核心最初將每個裝置視為“suspended”,無論其真實的硬體狀態如何

enum rpm_status last_status;
  • 在停用裝置的執行時 PM 之前捕獲的裝置最後執行時 PM 狀態(最初和當 disable_depth 為 0 時無效)

unsigned int runtime_auto;
  • 如果設定,表示使用者空間已允許裝置驅動程式透過 /sys/devices/.../power/control 介面在執行時對裝置進行電源管理;它只能藉助 pm_runtime_allow()pm_runtime_forbid() 輔助函式進行修改

unsigned int no_callbacks;
  • 表示裝置不使用執行時 PM 回撥(參見第 8 節);它只能透過 pm_runtime_no_callbacks() 輔助函式修改

unsigned int irq_safe;
  • 表示 ->runtime_suspend()->runtime_resume() 回撥將在持有自旋鎖並停用中斷的情況下呼叫

unsigned int use_autosuspend;
  • 表示裝置的驅動程式支援延遲自動掛起(參見第 9 節);它只能透過 pm_runtime{_dont}_use_autosuspend() 輔助函式修改

unsigned int timer_autosuspends;
  • 表示 PM 核心在定時器到期時應嘗試執行自動掛起,而不是正常掛起

int autosuspend_delay;
  • 用於自動掛起的延遲時間(以毫秒為單位)

unsigned long last_busy;
  • 最後一次為該裝置呼叫 pm_runtime_mark_last_busy() 輔助函式的時間(以 jiffies 為單位);用於計算自動掛起的非活動週期

以上所有欄位都是 struct devicepower 成員的組成部分。

4. 執行時 PM 裝置輔助函式

以下執行時 PM 輔助函式定義在 drivers/base/power/runtime.cinclude/linux/pm_runtime.h 中:

void pm_runtime_init(struct device *dev);
  • 初始化 struct dev_pm_info 中的裝置執行時 PM 欄位

void pm_runtime_remove(struct device *dev);
  • 確保在將裝置從裝置層次結構中移除後,該裝置的執行時 PM 將被停用

int pm_runtime_idle(struct device *dev);
  • 執行裝置的子系統級空閒回撥;失敗時返回錯誤程式碼,其中 -EINPROGRESS 表示 ->runtime_idle() 正在執行;如果沒有回撥或回撥返回 0,則執行 pm_runtime_autosuspend(dev) 並返回其結果

int pm_runtime_suspend(struct device *dev);
  • 執行裝置的子系統級掛起回撥;成功時返回 0,如果裝置執行時 PM 狀態已為“suspended”則返回 1,失敗時返回錯誤程式碼,其中 -EAGAIN-EBUSY 表示將來可以安全地再次嘗試掛起裝置,-EACCES 表示 power.disable_depth 不為 0 導致回撥無法執行

int pm_runtime_autosuspend(struct device *dev);
  • pm_runtime_suspend() 相同,但考慮了自動掛起延遲;如果 pm_runtime_autosuspend_expiration() 表明延遲尚未過期,則會在適當時間排程自動掛起並返回 0

int pm_runtime_resume(struct device *dev);
  • 執行裝置的子系統級恢復回撥;成功時返回 0,如果裝置執行時 PM 狀態已為“active”(如果 power.disable_depth 非零,但狀態在從 0 變為 1 時為“active”,也返回 1),失敗時返回錯誤程式碼,其中 -EAGAIN 表示將來可能安全地再次嘗試恢復裝置,但應額外檢查 power.runtime_error-EACCES 表示回撥無法執行,因為 power.disable_depth 不為 0

int pm_runtime_resume_and_get(struct device *dev);
  • 執行 pm_runtime_resume(dev),如果成功,則增加裝置的使用計數器;成功時返回 0(無論裝置的執行時 PM 狀態是否已為“active”),失敗時返回 pm_runtime_resume() 的錯誤程式碼。

int pm_request_idle(struct device *dev);
  • 提交執行裝置子系統級空閒回撥的請求(該請求由 pm_wq 中的工作項表示);成功時返回 0,如果請求未入隊則返回錯誤程式碼

int pm_request_autosuspend(struct device *dev);
  • 在自動掛起延遲過期時排程裝置的子系統級掛起回撥的執行;如果延遲已過期,則立即將工作項入隊

int pm_schedule_suspend(struct device *dev, unsigned int delay);
  • 排程裝置子系統級掛起回撥的未來執行,其中 delay 是在將掛起工作項排入 pm_wq 之前等待的時間(以毫秒為單位)(如果 delay 為零,則立即將工作項入隊);成功時返回 0,如果裝置的 PM 執行時狀態已為“suspended”則返回 1,如果請求尚未排程(如果 delay 為 0,則未入隊)則返回錯誤程式碼;如果 ->runtime_suspend() 的執行已排程且尚未過期,則 delay 的新值將用作等待時間

int pm_request_resume(struct device *dev);
  • 提交執行裝置子系統級恢復回撥的請求(該請求由 pm_wq 中的工作項表示);成功時返回 0,如果裝置執行時 PM 狀態已為“active”則返回 1,如果請求未入隊則返回錯誤程式碼

void pm_runtime_get_noresume(struct device *dev);
  • 增加裝置的使用計數器

int pm_runtime_get(struct device *dev);
  • 增加裝置的使用計數器,執行 pm_request_resume(dev) 並返回其結果

int pm_runtime_get_sync(struct device *dev);
  • 增加裝置的使用計數器,執行 pm_runtime_resume(dev) 並返回其結果;請注意,它在錯誤時不會減少裝置的使用計數器,因此請考慮使用 pm_runtime_resume_and_get() 代替它,特別是當呼叫者檢查其返回值時,因為這可能會導致更清晰的程式碼。

int pm_runtime_get_if_in_use(struct device *dev);
  • 如果 power.disable_depth 非零,則返回 -EINVAL;否則,如果執行時 PM 狀態為 RPM_ACTIVE 且執行時 PM 使用計數器非零,則增加計數器並返回 1;否則返回 0,不改變計數器

int pm_runtime_get_if_active(struct device *dev);
  • 如果 power.disable_depth 非零,則返回 -EINVAL;否則,如果執行時 PM 狀態為 RPM_ACTIVE,則增加計數器並返回 1;否則返回 0,不改變計數器

void pm_runtime_put_noidle(struct device *dev);
  • 減少裝置的使用計數器

int pm_runtime_put(struct device *dev);
  • 減少裝置的使用計數器;如果結果為 0,則執行 pm_request_idle(dev) 並返回其結果

int pm_runtime_put_autosuspend(struct device *dev);
  • 目前與 __pm_runtime_put_autosuspend() 作用相同,但將來也會呼叫 pm_runtime_mark_last_busy(),請勿使用!

int __pm_runtime_put_autosuspend(struct device *dev);
  • 減少裝置的使用計數器;如果結果為 0,則執行 pm_request_autosuspend(dev) 並返回其結果

int pm_runtime_put_sync(struct device *dev);
  • 減少裝置的使用計數器;如果結果為 0,則執行 pm_runtime_idle(dev) 並返回其結果

int pm_runtime_put_sync_suspend(struct device *dev);
  • 減少裝置的使用計數器;如果結果為 0,則執行 pm_runtime_suspend(dev) 並返回其結果

int pm_runtime_put_sync_autosuspend(struct device *dev);
  • 減少裝置的使用計數器;如果結果為 0,則執行 pm_runtime_autosuspend(dev) 並返回其結果

void pm_runtime_enable(struct device *dev);
  • 減少裝置的 power.disable_depth 欄位;如果該欄位等於零,則執行時 PM 輔助函式可以為該裝置執行第 2 節中描述的子系統級回撥

int pm_runtime_disable(struct device *dev);
  • 增加裝置的 power.disable_depth 欄位(如果該欄位之前的值為零,這將阻止為裝置執行子系統級執行時 PM 回撥),確保裝置上所有待處理的執行時 PM 操作都已完成或取消;如果存在待處理的恢復請求並且必須為裝置執行子系統級恢復回撥以滿足該請求,則返回 1,否則返回 0

int pm_runtime_barrier(struct device *dev);
  • 檢查裝置是否有待處理的恢復請求,如果有則(同步)恢復它,取消任何其他關於它的待處理執行時 PM 請求,並等待所有正在進行的執行時 PM 操作完成;如果存在待處理的恢復請求並且必須為裝置執行子系統級恢復回撥以滿足該請求,則返回 1,否則返回 0

void pm_suspend_ignore_children(struct device *dev, bool enable);
  • 設定/取消設定裝置的 power.ignore_children 標誌

int pm_runtime_set_active(struct device *dev);
  • 清除裝置的 power.runtime_error 標誌,將裝置的執行時 PM 狀態設定為“active”,並相應地更新其父裝置的“active”子裝置計數器(僅當 power.runtime_error 已設定或 power.disable_depth 大於零時,使用此函式才有效);如果裝置有一個未處於活躍狀態且 power.ignore_children 標誌未設定的父裝置,則它將失敗並返回錯誤程式碼

void pm_runtime_set_suspended(struct device *dev);
  • 清除裝置的 power.runtime_error 標誌,將裝置的執行時 PM 狀態設定為“suspended”,並相應地更新其父裝置的“active”子裝置計數器(僅當 power.runtime_error 已設定或 power.disable_depth 大於零時,使用此函式才有效)

bool pm_runtime_active(struct device *dev);
  • 如果裝置的執行時 PM 狀態為“active”或其 power.disable_depth 欄位不等於零,則返回 true,否則返回 false

bool pm_runtime_suspended(struct device *dev);
  • 如果裝置的執行時 PM 狀態為“suspended”且其 power.disable_depth 欄位等於零,則返回 true,否則返回 false

bool pm_runtime_status_suspended(struct device *dev);
  • 如果裝置的執行時 PM 狀態為“suspended”,則返回 true

void pm_runtime_allow(struct device *dev);
  • 為裝置設定 power.runtime_auto 標誌並減少其使用計數器(由 /sys/devices/.../power/control 介面使用,以有效地允許裝置在執行時進行電源管理)

void pm_runtime_forbid(struct device *dev);
  • 為裝置取消設定 power.runtime_auto 標誌並增加其使用計數器(由 /sys/devices/.../power/control 介面使用,以有效地阻止裝置在執行時進行電源管理)

void pm_runtime_no_callbacks(struct device *dev);
  • 為裝置設定 power.no_callbacks 標誌並從 /sys/devices/.../power 中移除執行時 PM 屬性(或在設備註冊時阻止它們被新增)

void pm_runtime_irq_safe(struct device *dev);
  • 為裝置設定 power.irq_safe 標誌,使執行時 PM 回撥在中斷關閉的情況下被呼叫

bool pm_runtime_is_irq_safe(struct device *dev);
  • 如果為裝置設定了 power.irq_safe 標誌,導致執行時 PM 回撥在中斷關閉的情況下被呼叫,則返回 true

void pm_runtime_mark_last_busy(struct device *dev);
  • power.last_busy 欄位設定為當前時間

void pm_runtime_use_autosuspend(struct device *dev);
  • 設定 power.use_autosuspend 標誌,啟用自動掛起延遲;如果該標誌之前被清除且 power.autosuspend_delay 為負,則呼叫 pm_runtime_get_sync

void pm_runtime_dont_use_autosuspend(struct device *dev);
  • 清除 power.use_autosuspend 標誌,停用自動掛起延遲;如果該標誌之前已設定且 power.autosuspend_delay 為負,則減少裝置的使用計數器;呼叫 pm_runtime_idle

void pm_runtime_set_autosuspend_delay(struct device *dev, int delay);
  • power.autosuspend_delay 值設定為 delay(以毫秒為單位);如果 delay 為負,則阻止執行時掛起;如果 power.use_autosuspend 已設定,則根據 power.autosuspend_delay 是否更改為負值或從負值更改,可能會呼叫 pm_runtime_get_sync 或減少裝置的使用計數器並呼叫 pm_runtime_idle;如果 power.use_autosuspend 已清除,則呼叫 pm_runtime_idle

unsigned long pm_runtime_autosuspend_expiration(struct device *dev);
  • 根據 power.last_busypower.autosuspend_delay 計算當前自動掛起延遲期何時到期;如果延遲時間為 1000 毫秒或更長,則到期時間四捨五入到最近的秒;如果延遲期已過期或 power.use_autosuspend 未設定,則返回 0,否則返回以 jiffies 為單位的到期時間

從中斷上下文執行以下輔助函式是安全的:

  • pm_request_idle()

  • pm_request_autosuspend()

  • pm_schedule_suspend()

  • pm_request_resume()

  • pm_runtime_get_noresume()

  • pm_runtime_get()

  • pm_runtime_put_noidle()

  • pm_runtime_put()

  • pm_runtime_put_autosuspend()

  • __pm_runtime_put_autosuspend()

  • pm_runtime_enable()

  • pm_suspend_ignore_children()

  • pm_runtime_set_active()

  • pm_runtime_set_suspended()

  • pm_runtime_suspended()

  • pm_runtime_mark_last_busy()

  • pm_runtime_autosuspend_expiration()

如果已為裝置呼叫 pm_runtime_irq_safe(),則以下輔助函式也可以在中斷上下文中使用:

  • pm_runtime_idle()

  • pm_runtime_suspend()

  • pm_runtime_autosuspend()

  • pm_runtime_resume()

  • pm_runtime_get_sync()

  • pm_runtime_put_sync()

  • pm_runtime_put_sync_suspend()

  • pm_runtime_put_sync_autosuspend()

5. 執行時 PM 初始化、裝置探測和移除

最初,所有裝置的執行時 PM 都被停用,這意味著第 4 節中描述的大多數執行時 PM 輔助函式將返回 -EAGAIN,直到為裝置呼叫 pm_runtime_enable()

此外,所有裝置的初始執行時 PM 狀態都是“suspended”,但這不一定反映裝置的實際物理狀態。因此,如果裝置最初是活躍的(即能夠處理 I/O),則在為裝置呼叫 pm_runtime_enable() 之前,必須藉助 pm_runtime_set_active() 將其執行時 PM 狀態更改為“active”。

然而,如果裝置有父裝置且父裝置的執行時 PM 已啟用,則為該裝置呼叫 pm_runtime_set_active() 將影響父裝置,除非父裝置的 power.ignore_children 標誌已設定。也就是說,在這種情況下,只要子裝置的狀態為“active”,父裝置就無法在執行時使用 PM 核心的輔助函式進行掛起,即使子裝置的執行時 PM 仍然停用(即尚未為子裝置呼叫 pm_runtime_enable() 或已為其呼叫 pm_runtime_disable())。因此,一旦為裝置呼叫了 pm_runtime_set_active(),就應儘快為其呼叫 pm_runtime_enable(),或者藉助 pm_runtime_set_suspended() 將其執行時 PM 狀態更改回“suspended”。

如果裝置的預設初始執行時 PM 狀態(即“suspended”)反映了裝置的實際狀態,則其匯流排型別或驅動程式的 ->probe() 回撥可能需要使用第 4 節中描述的 PM 核心輔助函式之一來喚醒它。在這種情況下,應使用 pm_runtime_resume()。當然,為此目的,必須透過呼叫 pm_runtime_enable() 提前啟用裝置的執行時 PM。

請注意,如果裝置在探測期間可能執行 pm_runtime 呼叫(例如,如果它已註冊到可能回撥的子系統),則 pm_runtime_get_sync() 呼叫與 pm_runtime_put() 呼叫配對將是合適的,以確保裝置在探測期間不會再次進入睡眠狀態。這可能發生在網路裝置層等系統中。

->probe() 完成後,可能需要掛起裝置。因此,驅動程式核心使用非同步的 pm_request_idle() 來提交在該時間點執行裝置子系統級空閒回撥的請求。利用執行時自動掛起功能的驅動程式可能希望在從 ->probe() 返回之前更新最後忙碌標記。

此外,驅動程式核心阻止執行時 PM 回撥與 __device_release_driver() 中的匯流排通知器回調發生競爭,這是必要的,因為某些子系統使用通知器來執行影響執行時 PM 功能的操作。它透過在 driver_sysfs_remove()BUS_NOTIFY_UNBIND_DRIVER 通知之前呼叫 pm_runtime_get_sync() 來實現這一點。這會在裝置處於掛起狀態時恢復裝置,並防止它在這些例程執行期間再次被掛起。

為了允許匯流排型別和驅動程式透過從其 ->remove() 例程呼叫 pm_runtime_suspend() 來將裝置置於掛起狀態,驅動程式核心在 __device_release_driver() 中執行 BUS_NOTIFY_UNBIND_DRIVER 通知後執行 pm_runtime_put_sync()。這要求匯流排型別和驅動程式使其 ->remove() 回撥直接避免與執行時 PM 競爭,但這也允許在移除其驅動程式期間處理裝置時具有更大的靈活性。

驅動程式在 ->remove() 回撥中應撤消在 ->probe() 中所做的執行時 PM 更改。通常這意味著呼叫 pm_runtime_disable()pm_runtime_dont_use_autosuspend() 等。

使用者空間可以透過將其 /sys/devices/.../power/control 屬性的值更改為“on”來有效地禁止裝置驅動程式在執行時對其進行電源管理,這將導致呼叫 pm_runtime_forbid()。原則上,驅動程式也可以使用此機制有效地關閉裝置的執行時電源管理,直到使用者空間將其開啟。也就是說,在初始化期間,驅動程式可以確保裝置的執行時 PM 狀態為“active”並呼叫 pm_runtime_forbid()。然而,應該注意的是,如果使用者空間已經有意地將 /sys/devices/.../power/control 的值更改為“auto”以允許驅動程式在執行時對裝置進行電源管理,則驅動程式以這種方式使用 pm_runtime_forbid() 可能會使其感到困惑。

6. 執行時 PM 和系統睡眠

執行時 PM 和系統睡眠(即系統掛起和休眠,也稱為記憶體掛起和磁碟掛起)以多種方式相互作用。如果裝置在系統睡眠開始時處於活躍狀態,一切都很簡單。但如果裝置已經掛起,應該發生什麼?

裝置可能對執行時 PM 和系統睡眠有不同的喚醒設定。例如,遠端喚醒可能對執行時掛起啟用,但對系統睡眠停用(device_may_wakeup(dev) 返回“false”)。當發生這種情況時,子系統級系統掛起回撥負責更改裝置的喚醒設定(它可能會將此留給裝置驅動程式的系統掛起例程)。為此,可能需要恢復裝置並再次掛起它。如果驅動程式對執行時掛起和系統睡眠使用不同的功耗級別或其他設定,情況也是如此。

在系統恢復期間,最簡單的方法是將所有裝置恢復到全功率狀態,即使它們在系統掛起開始之前已處於掛起狀態。這有幾個原因,包括:

  • 裝置可能需要切換電源級別、喚醒設定等。

  • 韌體可能丟失了遠端喚醒事件。

  • 裝置的子裝置可能需要裝置處於全功率狀態才能自行恢復。

  • 驅動程式對裝置狀態的理解可能與裝置的物理狀態不一致。這可能在從休眠恢復時發生。

  • 裝置可能需要被重置。

  • 即使裝置已掛起,如果其使用計數器 > 0,那麼它很可能在不久的將來無論如何都需要執行時恢復。

如果裝置在系統掛起開始之前已掛起並在恢復期間恢復到全功率狀態,則其執行時 PM 狀態必須更新以反映實際的系統睡眠後狀態。這樣做的方法是:

  • pm_runtime_disable(dev);

  • pm_runtime_set_active(dev);

  • pm_runtime_enable(dev);

PM 核心總是在呼叫 ->suspend() 回撥之前增加執行時使用計數器,並在呼叫 ->resume() 回撥之後減少它。因此,像這樣臨時停用執行時 PM 不會導致任何執行時掛起嘗試永久丟失。如果在使用計數器在 ->resume() 回撥返回後變為零,->runtime_idle() 回撥將照常呼叫。

然而,在某些系統上,系統睡眠並非透過全域性韌體或硬體操作進入。相反,所有硬體元件都由核心以協調方式直接置於低功耗狀態。然後,系統睡眠狀態有效地取決於硬體元件最終所處的狀態,系統透過硬體中斷或完全由核心控制的類似機制從該狀態喚醒。因此,核心從不放棄控制權,並且在恢復期間所有裝置的狀態都精確地為核心所知。如果出現這種情況,並且沒有發生上述任何一種情況(特別是如果系統不是從休眠中喚醒),則將系統掛起開始前已掛起的裝置留在掛起狀態可能更有效。

為此,PM 核心提供了一種機制,允許裝置層次結構的不同級別之間進行協調。具體來說,如果系統掛起 .prepare() 回撥為裝置返回正數,則表示該裝置似乎已在執行時掛起且狀態良好,因此只要其所有後代也保持執行時掛起狀態,就可以將其留在執行時掛起狀態。如果發生這種情況,PM 核心將不會為所有這些裝置執行任何系統掛起和恢復回撥,除了 .complete() 回撥,該回調隨後將完全負責適當地處理裝置。這僅適用於與休眠無關的系統掛起轉換(更多資訊請參見裝置電源管理基礎)。

PM 核心透過執行以下操作,盡力減少執行時 PM 與系統掛起/恢復(和休眠)回撥之間的競爭條件:

  • 在系統掛起期間,在為每個裝置執行子系統級 .prepare() 回撥之前,會為其呼叫 pm_runtime_get_noresume(),並在為每個裝置執行子系統級 .suspend() 回撥之前,會為其呼叫 pm_runtime_barrier()。此外,PM 核心會在為每個裝置執行子系統級 .suspend_late() 回撥之前,以“false”作為第二個引數呼叫 __pm_runtime_disable()

  • 在系統恢復期間,分別在為每個裝置執行子系統級 .resume_early() 回撥之後和執行子系統級 .complete() 回撥之後,會為其呼叫 pm_runtime_enable()pm_runtime_put()

7. 通用子系統回撥

子系統可能希望透過使用 PM 核心提供的通用電源管理回撥集來節省程式碼空間,這些回撥定義在 driver/base/power/generic_ops.c 中:

int pm_generic_runtime_suspend(struct device *dev);
  • 呼叫此裝置驅動程式提供的 ->runtime_suspend() 回撥並返回其結果,如果未定義則返回 0

int pm_generic_runtime_resume(struct device *dev);
  • 呼叫此裝置驅動程式提供的 ->runtime_resume() 回撥並返回其結果,如果未定義則返回 0

int pm_generic_suspend(struct device *dev);
  • 如果裝置尚未在執行時掛起,則呼叫其驅動程式提供的 ->suspend() 回撥並返回其結果,如果未定義則返回 0

int pm_generic_suspend_noirq(struct device *dev);
  • 如果 pm_runtime_suspended(dev) 返回“false”,則呼叫裝置驅動程式提供的 ->suspend_noirq() 回撥並返回其結果,如果未定義則返回 0

int pm_generic_resume(struct device *dev);
  • 呼叫此裝置驅動程式提供的 ->resume() 回撥,如果成功,則將裝置的執行時 PM 狀態更改為“active”

int pm_generic_resume_noirq(struct device *dev);
  • 呼叫此裝置驅動程式提供的 ->resume_noirq() 回撥

int pm_generic_freeze(struct device *dev);
  • 如果裝置尚未在執行時掛起,則呼叫其驅動程式提供的 ->freeze() 回撥並返回其結果,如果未定義則返回 0

int pm_generic_freeze_noirq(struct device *dev);
  • 如果 pm_runtime_suspended(dev) 返回“false”,則呼叫裝置驅動程式提供的 ->freeze_noirq() 回撥並返回其結果,如果未定義則返回 0

int pm_generic_thaw(struct device *dev);
  • 如果裝置尚未在執行時掛起,則呼叫其驅動程式提供的 ->thaw() 回撥並返回其結果,如果未定義則返回 0

int pm_generic_thaw_noirq(struct device *dev);
  • 如果 pm_runtime_suspended(dev) 返回“false”,則呼叫裝置驅動程式提供的 ->thaw_noirq() 回撥並返回其結果,如果未定義則返回 0

int pm_generic_poweroff(struct device *dev);
  • 如果裝置尚未在執行時掛起,則呼叫其驅動程式提供的 ->poweroff() 回撥並返回其結果,如果未定義則返回 0

int pm_generic_poweroff_noirq(struct device *dev);
  • 如果 pm_runtime_suspended(dev) 返回“false”,則執行裝置驅動程式提供的 ->poweroff_noirq() 回撥並返回其結果,如果未定義則返回 0

int pm_generic_restore(struct device *dev);
  • 呼叫此裝置驅動程式提供的 ->restore() 回撥,如果成功,則將裝置的執行時 PM 狀態更改為“active”

int pm_generic_restore_noirq(struct device *dev);
  • 呼叫此裝置驅動程式提供的 ->restore_noirq() 回撥

如果子系統在子系統級 dev_pm_ops 結構中沒有為 ->runtime_idle()->runtime_suspend()->runtime_resume()->suspend()->suspend_noirq()->resume()->resume_noirq()->freeze()->freeze_noirq()->thaw()->thaw_noirq()->poweroff()->poweroff_noirq()->restore()->restore_noirq() 提供自己的回撥,則這些函式是 PM 核心使用的預設值。

希望將同一函式用作系統掛起、凍結、關機和執行時掛起回撥,以及類似地用於系統恢復、解凍、恢復和執行時恢復的裝置驅動程式,可以在 include/linux/pm_runtime.h 中定義的 DEFINE_RUNTIME_DEV_PM_OPS() 的幫助下實現類似的行為(可能將其最後一個引數設定為 NULL)。

8. “無回撥”裝置

某些“裝置”只是其父裝置的邏輯子裝置,無法獨立進行電源管理。(原型示例是 USB 介面。整個 USB 裝置可以進入低功耗模式或傳送喚醒請求,但單個介面無法做到這一點。)這些裝置的驅動程式不需要執行時 PM 回撥;如果回撥確實存在,->runtime_suspend()->runtime_resume() 將始終返回 0 而不執行任何其他操作,並且 ->runtime_idle() 將始終呼叫 pm_runtime_suspend()

子系統可以透過呼叫 pm_runtime_no_callbacks() 將這些裝置告知 PM 核心。這應該在裝置結構初始化之後和註冊之前完成(儘管在設備註冊之後也可以)。該例程將設定裝置的 power.no_callbacks 標誌,並阻止建立非除錯執行時 PM sysfs 屬性。

power.no_callbacks 設定時,PM 核心將不會呼叫 ->runtime_idle()->runtime_suspend()->runtime_resume() 回撥。相反,它將假定掛起和恢復總是成功,並且空閒裝置應該被掛起。

因此,PM 核心將永遠不會直接通知裝置的子系統或驅動程式有關執行時電源變化的事件。相反,裝置父裝置的驅動程式必須負責在父裝置的電源狀態改變時通知裝置驅動程式。

請注意,在某些情況下,子系統/驅動程式可能不希望為其裝置呼叫 pm_runtime_no_callbacks()。這可能是因為需要實現執行時 PM 回撥的一個子集,或者平臺相關的 PM 域可能附加到裝置,或者裝置透過供應商裝置連結進行電源管理。由於這些原因,併為了避免子系統/驅動程式中的樣板程式碼,PM 核心允許執行時 PM 回撥未分配。更精確地說,如果回撥指標為 NULL,PM 核心將像存在回撥且它返回 0 一樣行事。

9. 自動掛起,或自動延遲掛起

更改裝置的電源狀態並非沒有成本;它需要時間和能源。裝置只有在有理由認為它會長時間保持在低功耗狀態時,才應將其置於該狀態。一個常見的啟發式方法是,一段時間未使用的裝置很可能會保持未使用狀態;遵循此建議,驅動程式不應允許裝置在執行時掛起,直到它們已非活動了一段最短時間。即使啟發式方法最終並非最優,它仍然會防止裝置在低功耗和全功率狀態之間“反彈”過快。

術語“自動掛起”是歷史遺留物。它並不意味著裝置是自動掛起的(子系統或驅動程式仍然必須呼叫適當的 PM 例程);相反,它意味著執行時掛起將自動延遲,直到所需的非活動期過去。

非活動性是根據 power.last_busy 欄位確定的。驅動程式在執行 I/O 後應呼叫 pm_runtime_mark_last_busy() 來更新此欄位,通常在呼叫 __pm_runtime_put_autosuspend() 之前。所需的非活動期長度是一個策略問題。子系統可以透過呼叫 pm_runtime_set_autosuspend_delay() 最初設定此長度,但在設備註冊後,該長度應由使用者空間控制,使用 /sys/devices/.../power/autosuspend_delay_ms 屬性。

為了使用自動掛起,子系統或驅動程式必須呼叫 pm_runtime_use_autosuspend()(最好在註冊裝置之前),此後它們應使用各種 *_autosuspend() 輔助函式,而不是非自動掛起的對應函式。

Instead of: pm_runtime_suspend    use: pm_runtime_autosuspend;
Instead of: pm_schedule_suspend   use: pm_request_autosuspend;
Instead of: pm_runtime_put        use: __pm_runtime_put_autosuspend;
Instead of: pm_runtime_put_sync   use: pm_runtime_put_sync_autosuspend.

驅動程式也可以繼續使用非自動掛起的輔助函式;它們將正常執行,這意味著有時會考慮自動掛起延遲(參見 pm_runtime_idle)。

在某些情況下,即使使用計數器為零且自動掛起延遲時間已過期,驅動程式或子系統也可能希望阻止裝置立即自動掛起。如果 ->runtime_suspend() 回撥返回 -EAGAIN-EBUSY,並且下一個自動掛起延遲過期時間在未來(如果回撥呼叫了 pm_runtime_mark_last_busy(),通常會這樣),PM 核心將自動重新排程自動掛起。->runtime_suspend() 回撥不能自行執行此重新排程,因為在裝置掛起期間(即回撥正在執行期間),不接受任何型別的掛起請求。

該實現非常適合在中斷上下文中進行非同步使用。然而,這種使用不可避免地涉及競爭,因為 PM 核心無法將 ->runtime_suspend() 回撥與 I/O 請求的到來同步。這種同步必須由驅動程式使用其私有鎖來處理。下面是一個示意性的虛擬碼示例:

foo_read_or_write(struct foo_priv *foo, void *data)
{
        lock(&foo->private_lock);
        add_request_to_io_queue(foo, data);
        if (foo->num_pending_requests++ == 0)
                pm_runtime_get(&foo->dev);
        if (!foo->is_suspended)
                foo_process_next_request(foo);
        unlock(&foo->private_lock);
}

foo_io_completion(struct foo_priv *foo, void *req)
{
        lock(&foo->private_lock);
        if (--foo->num_pending_requests == 0) {
                pm_runtime_mark_last_busy(&foo->dev);
                __pm_runtime_put_autosuspend(&foo->dev);
        } else {
                foo_process_next_request(foo);
        }
        unlock(&foo->private_lock);
        /* Send req result back to the user ... */
}

int foo_runtime_suspend(struct device *dev)
{
        struct foo_priv foo = container_of(dev, ...);
        int ret = 0;

        lock(&foo->private_lock);
        if (foo->num_pending_requests > 0) {
                ret = -EBUSY;
        } else {
                /* ... suspend the device ... */
                foo->is_suspended = 1;
        }
        unlock(&foo->private_lock);
        return ret;
}

int foo_runtime_resume(struct device *dev)
{
        struct foo_priv foo = container_of(dev, ...);

        lock(&foo->private_lock);
        /* ... resume the device ... */
        foo->is_suspended = 0;
        pm_runtime_mark_last_busy(&foo->dev);
        if (foo->num_pending_requests > 0)
                foo_process_next_request(foo);
        unlock(&foo->private_lock);
        return 0;
}

重要的是,在 foo_io_completion() 請求自動掛起後,foo_runtime_suspend() 回撥可能與 foo_read_or_write() 發生競爭。因此,foo_runtime_suspend() 在允許掛起繼續之前,必須(在持有私有鎖的情況下)檢查是否有任何待處理的 I/O 請求。

此外,power.autosuspend_delay 欄位可以隨時由使用者空間更改。如果驅動程式關心這一點,它可以在持有私有鎖的情況下,從 ->runtime_suspend() 回撥內部呼叫 pm_runtime_autosuspend_expiration()。如果函式返回非零值,則表示延遲尚未過期,回撥應返回 -EAGAIN