Linux 電源管理之 Runtime PM
作者簡介:Loopers,碼齡 11 年,喜歡研究內核基本原理
前言
- 什麼是 Runtime PM?
Runtime PM (Runtime Power Management) 翻譯過來就是運行時電源管理。主要的作用是: 每個設備處理好自己的電源管理,在不需要工作時進入低功耗狀態。也就是 "各人自掃門前雪"。
- 爲什麼需要 Runtime PM?
system suspend 需要很長時間完成,其中還可能出現失敗。比如 freeze task 的時候。而 suspend 設備速度相對 system suspend 快很對,而且還不需要 freeze task。當設備不忙的時候就進入自己的低功耗模式,這樣一來每個 device(包括 CPU) 都做好自己的事,整個系統就達到最大節省能源。這時候突然想起了一句話 "只要人人都獻出一片愛,世界將變成美好的人間"。
- 爲什麼需要 Runtime PM Framework?
-
改變設備的電源狀態需要整個平臺的支持。
-
當設備處於低功耗模式時,wakeup signal 常常需要 platform 或者 bus 的支持。
-
設備驅動不知道什麼時候去 suspend 設備的,也就是驅動是沒法判斷設備是否處於 idle 狀態,通常需要依賴 subsystem,比如 bus。
-
pm 相關的操作都需要順序執行,這時候就免不了使用 workqueue。
-
Runtime PM 需要和 system-wide suspend 需要保持兼容。比如 system-wide suspend 的時候設備已經處於 Runtime PM 的 SUSPENDED 狀態了,這時候應該怎麼處理此設備?
以上這些工作都需要 Runtime PM framework 的支持,這也都是 Runtime PM framework 應該操心的事情。
Subsystem and Driver Callbacks
爲了實現設備的 runtime pm,需要 subsystem(PM domains, device types, classes and bus types) 和 driver 提供下面三個回調函數:
struct dev_pm_ops {
....
int (*runtime_suspend)(struct device *dev);
int (*runtime_resume)(struct device *dev);
int (*runtime_idle)(struct device *dev);
};
三個回調函數分別用於 suspend device,resume device 和 idle device。通常 Runtime PM framework 會在合適的時機調用三個函數。
Device States
Runtime PM Framework 使用 rpm_status 枚舉類型表示 device 的狀態
enum rpm_status {
RPM_ACTIVE = 0,
RPM_RESUMING,
RPM_SUSPENDED,
RPM_SUSPENDING,
};
RPM_ACTIVE: 設備處於正常工作狀態,處於全速全進狀態。runtime_resume 的回調執行完畢。
RPM_RESUMING: 設備狀態正在從 suspend 到 active 轉換。 runtime_resume 的回調正在執行。
RPM_SUSPENDED: 設備處於低功耗狀態,不能處於 IO 操作。runtime_suspend 的回調執行完畢。
RPM_SUSPENDING: 設備狀態正從 active 到 suspend 狀態轉換。runtime_suspend 回調正在執行。
是的,你沒看錯,的確是沒有調用 runtime_idle 回調。那 runtime_idle 回調什麼時候調用呢? idle 狀態是 suspend 狀態前的一個過渡而已,通常會在 runtime_suspend 調用之前調用一段時間 runtime_idle 回調。
Runtime PM 請求類型
因爲使設備進入 suspend 或者 resume 狀態,有同步和異步的方式。通常在異步的時候會用到 workqueue,這時候就會用到設備的請求類型。
enum rpm_request {
RPM_REQ_NONE = 0,
RPM_REQ_IDLE,
RPM_REQ_SUSPEND,
RPM_REQ_AUTOSUSPEND,
RPM_REQ_RESUME,
};
PRM_REQ_NONE: Do nothing
PRM_REQ_IDLE: 運行設備的 runtime_idle 回調。
PRM_REQ_SUSPEND: 運行設備的 runtime_suspend 回調。
RPM_REQ_AUTOSUSPEN: 在一段時間之後運行設備的 runtime_suspend 回調。
RPM_REQ_RESUME: 運行設備的 runtime_resume 回調。
Runtime PM 數據段
在每個 device 結構中都存在 dev_pm_info 的結構,此結構中通過 CONFIG_PM_RUNTIME 配置字段代碼了 Runtime PM 的信息。
struct dev_pm_info {
....
struct timer_list suspend_timer; //休眠時候用到的定時器。
unsigned long timer_expires; //定時器的超時函數。
struct work_struct work; //用於workqueue中的工作項。
wait_queue_head_t wait_queue; //等待隊列,用於異步pm操作時候使用。
atomic_t usage_count; //設備的引用計數,通過該字段判斷是否有設備使用。
atomic_t child_count; //此設備的"active"子設備的個數。
unsigned int disable_depth:3; //用於禁止Runtime helper function。等於0代表使能,1代表禁止。
unsigned int idle_notification:1; //如果該值被設備,則調用runtime_idle回調函數。
unsigned int request_pending:1; //如果設備,代表工作隊列有work請求。
unsigned int deferred_resume:1; //當設備正在執行-> runtime_suspend()的時候,如果->runtime_resume()將要運行,而等待掛起操作完成並不實際,就會設置該值;這裏的意思是“一旦你掛起完成,我就開始恢復”。
unsigned int run_wake:1; //如果設備能產生runtime wake-up events事件,就設置該值。
unsigned int runtime_auto:1; //如果設置,則表示用戶空間已允許設備驅動程序通過/sys/devices/.../power/control接口在運行時對該設備進行電源管理。
unsigned int no_callbacks:1; //表明該設備不是有Runtime PM callbacks。
unsigned int irq_safe:1; //表示->runtime_suspend()和->runtime_resume()回調函數將在持有自旋鎖並禁止中斷的情況下被調用。
unsigned int use_autosuspend:1; //表示該設備驅動支持延遲自動休眠功能。
unsigned int timer_autosuspends:1; //表明PM核心應該在定時器到期時嘗試進行自動休眠(autosuspend),而不是一個常規的掛起(normal suspend)。
unsigned int memalloc_noio:1;
enum rpm_request request; //runtime pm請求類型。
enum rpm_status runtime_status; //runtime pm設備狀態。
int runtime_error; //如果該值設備,表明有錯誤。
int autosuspend_delay; //延遲時間,用於自動休眠。
unsigned long last_busy;
unsigned long active_jiffies;
unsigned long suspended_jiffies;
unsigned long accounting_timestamp;
};
Runtime PM 運行機制
上面瞭解了 Runtime PM 運行時相關的標誌之後,可能對 runtime 已經有了大概的瞭解,接下來就詳細說下 runtime 的運行機制。
-
每個設備都維護一個 usage_count 變量,用於記錄該設備的使用情況。當大於 0 的時候說明該設備在使用,當等於 0 的時候說明該設備沒在使用。
-
需要使用該設備的時候,設備驅動調用 pm_runtime_get/pm_runtime_get_sync 接口,增加變量 usage_count 的值;不再使用該設備的時候,調用 pm_runtime_put/pm_runtime_put_sync 接口,減小 usage_count 變量的值。
-
每次調用 get 接口的時候,Runtime PM framework 會判斷該設備的狀態。如果該不是 active 狀態,則使用異步 (ASYNC) 或者同步 (SYNC) 方式調用 runtime_resume 回調函數,喚醒設備。
-
每次調用 put 接口的時候,Runtime PM framewokr 會判斷設備的引用計數,如果爲零,則使用異步 (ASYNC) 或者同步 (SYNC) 方式調用 runtime_idle 回調函數。
-
爲了防止頻繁 suspend,在 suspend 前面引入了 idle 狀態。當設備處於 idle 狀態之後,會在合適的時間調用 suspend 回調函數。通常都會通過 runtime pm helper function 啓動一個 timer, 設置超時時間,在超時之後調用 runtime_suspend 回調函數。
Runtime PM 回調約束
-
回調是互斥的 (例如: 對於同一個設備,禁止並行執行 runtime_suspend 和 runtime_resume 或者同一個設備 runtime_suspend 回調)。不過例外情況是: runtime_suspend() 或 runtime_resume()可以和 runtime_idle()並行執行。
-
runtime_idle() 和 runtime_suspend 回調只能對 "active" 設備執行。
-
runtime_idle 和 runtime_suspend 回調只能對其引用計數 (usage count) 等於零,且器 active children 個數是零或者 “power.ignore_children” 標誌被設置的設備執行。
-
runtime_resume 只能對掛起(suspend)的設備執行。
-
如果 runtime suspend() 即將被執行,或者有一個掛起的請求執行,runtime idle() 將不會在同一個設備上執行。
-
如果 runtime_suspend 回調已經執行或者已經在 pending 狀態,則取消該設備的 runtime idle 請求。
-
如果 runtime_resume 回調已經執行,其他 callbacks 則將不被執行對於同一個設備。
-
如果該設備下的任何一個子設備都處於 idle,parent 設備纔可以 idle。
-
如果 parent 設備下任何一個設備處於 active 狀態,parent 設備必須 active。
-
parent 設備下任何一個設備處於 idle, 需要上報給 parent 用於記錄。
Runtime Sys 接口
關於 runtime sys 接口在文件: /kernel/drivers/base/power/sysfs.c 中描述。設備的 runtime 屬性是在 dpm_sysfs_add 函數中增加的。
if (pm_runtime_callbacks_present(dev)) {
rc = sysfs_merge_group(&dev->kobj, &pm_runtime_attr_group);
if (rc)
goto err_out;
}
runtime 的屬性如下:
static struct attribute *runtime_attrs[] = {
#ifdef CONFIG_PM_RUNTIME
#ifndef CONFIG_PM_ADVANCED_DEBUG
&dev_attr_runtime_status.attr,
#endif
&dev_attr_control.attr,
&dev_attr_runtime_suspended_time.attr,
&dev_attr_runtime_active_time.attr,
&dev_attr_autosuspend_delay_ms.attr,
#endif /* CONFIG_PM_RUNTIME */
NULL,
};
其中有五個屬性。分別爲 control, runtime_susupend_time, runtime_active_time, autosuspend_delay_ms,runtime_status 屬性。
/sys/devices/.../power/control
on - 調用 pm_runtime_forbid 接口,增加設備的引用計數,然後 resume 設備。
auto - 調用 pm_runtime_allow 接口,減少設備的引用計數,如果設備的引用計數爲 0,則 idle 設備。
/sys/devices/.../power/runtime_status
active - 設備的狀態是正常工作狀態。
suspend- 設備的狀態是低功耗模式。
suspending - 設備的狀態正在從 active->suspend 轉化。
resuming - 設備的狀態正在從 suspend->active 轉化。
error - 設備 runtime 出現錯誤,此時 runtime_error 的標誌置位。
unsupported - 設備的 runtime 沒有使能,此時 disable_depth 標誌置位。
/sys/devices/.../power/runtime_suspend_time
設備在 suspend 狀態的時間
/sys/devices/.../power/runtime_active_time
設備在 active 狀態的時間
/sys/devices/.../power/autosuspend_delay_ms
設備在 idle 狀態多久之後 suspend,設置延遲 suspend 的延遲時間。
Runtime API
因爲 Runtime API 多達幾十個以上,這裏列舉一些驅動中常用的 API 供大家參考。
- pm_runtime_enable(使能設備的 runtime pm)
void pm_runtime_enable(struct device *dev)
{
unsigned long flags;
spin_lock_irqsave(&dev->power.lock, flags);
if (dev->power.disable_depth > 0)
dev->power.disable_depth--;
else
dev_warn(dev, "Unbalanced %s!\n", __func__);
spin_unlock_irqrestore(&dev->power.lock, flags);
}
disable_depth 在 pm_runtime_init 會被初始化爲 1,enabel 函數就是將此值減去 1 而已。當然了在 disable 函數中會給該值加 1。
-
pm_runtime_get/pm_runtime_put(異步請求增加 / 減少引用計數)
-
pm_runtime_get_sync/pm_runtime_put_sync(同步請求增加 / 減少引用計數)
-
pm_runtime_set_active/pm_runtime_set_suspended(設置設備的 runtime 運行狀態)
-
pm_schedule_suspend(在指定時間之後 suspend)
以上函數接口都比較簡單,最終會調用到__pm_runtime_resume/__pm_runtime_suspend/__pm_runtime_idle 接口中。
- __pm_runtime_resume(resume 設備)
Runtime PM 舉例
寫了一個簡單的測試 runtime 測試例子,如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/platform_device.h>
#include <linux/types.h>
#include <linux/pm_runtime.h>
static int runtime_pm_probe(struct platform_device *pdev)
{
printk(KERN_EMERG "runtime_pm: runtime_pm_probe!\n");
pm_runtime_set_active(&pdev->dev);
pm_runtime_enable(&pdev->dev);
return 0;
}
static int runtime_pm_remove(struct platform_device *pdev)
{
printk(KERN_EMERG "runtime_pm: runtime_pm_remove!\n");
pm_runtime_disable(&pdev->dev);
return 0;
}
static int runtime_pm_suspend(struct device *dev)
{
printk(KERN_EMERG "runtime_pm: runtime_pm_suspend!\n");
return 0;
}
static int runtime_pm_resume(struct device *dev)
{
printk(KERN_EMERG "runtime_pm: runtime_pm_resume!\n");
return 0;
}
static int runtime_pm_idle(struct device *dev)
{
printk(KERN_EMERG "runtime_pm: runtime_pm_idle\n");
return 0;
}
static const struct dev_pm_ops runtime_pm_ops = {
SET_RUNTIME_PM_OPS(runtime_pm_suspend,
runtime_pm_resume,
runtime_pm_idle)
};
static void runtime_pm_release(struct device * dev)
{
}
static struct platform_device runtime_device = {
.name = "runtime_device",
.id = -1,
.dev = {
.release = runtime_pm_release,
},
};
static struct platform_driver runtime_driver = {
.probe = runtime_pm_probe,
.remove = runtime_pm_remove,
.driver = {
.owner = THIS_MODULE,
.name = "runtime_device",
.pm = &runtime_pm_ops,
},
};
static int runtime_pm_init(void)
{
printk(KERN_EMERG "runtime_pm: runtime_pm_init\n");
platform_device_register(&runtime_device);
platform_driver_register(&runtime_driver);
return 0;
}
static void runtime_pm_exit(void)
{
printk(KERN_EMERG "runtime_pm: runtime_pm_exit\n");
platform_driver_unregister(&runtime_driver);
platform_device_unregister(&runtime_device);
}
module_init(runtime_pm_init);
module_exit(runtime_pm_exit);
MODULE_LICENSE("GPL");
如下是測試結果:
- 查看當前設備的 runtime 狀態
cat /sys/devices/platform/runtime_device/power/runtime_status
suspend
- 查看設備的 runtime_suspend 時間
cat /sys/devices/platform/runtime_device/power/runtime_suspended_time
341028
- 使設備處於 active 狀態
echo on > /sys/devices/platform/runtime_device/power/control
- 使設備進入 suspend 狀態
echo auto > /sys/devices/platform/runtime_device/power/control
- 查看轉換狀態的打印
test:/ # dmesg | grep "runtime"
[ 451.432602] c7 runtime_pm: runtime_pm_resume!
[ 509.842328] c5 runtime_pm: runtime_pm_idle
[ 509.846430] c5 runtime_pm: runtime_pm_suspend!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/R41V5qsbAJG-WN3gj5vnLg