Linux 電源管理之 Runtime PM

作者簡介:Loopers,碼齡 11 年,喜歡研究內核基本原理

前言

  1. 什麼是 Runtime PM?

Runtime PM (Runtime Power Management) 翻譯過來就是運行時電源管理。主要的作用是:  每個設備處理好自己的電源管理,在不需要工作時進入低功耗狀態。也就是 "各人自掃門前雪"。

  1. 爲什麼需要 Runtime PM?

system suspend 需要很長時間完成,其中還可能出現失敗。比如 freeze task 的時候。而 suspend 設備速度相對 system suspend 快很對,而且還不需要 freeze task。當設備不忙的時候就進入自己的低功耗模式,這樣一來每個 device(包括 CPU) 都做好自己的事,整個系統就達到最大節省能源。這時候突然想起了一句話 "只要人人都獻出一片愛,世界將變成美好的人間"。

  1. 爲什麼需要 Runtime PM Framework?

以上這些工作都需要 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 的運行機制。

  1. 每個設備都維護一個 usage_count 變量,用於記錄該設備的使用情況。當大於 0 的時候說明該設備在使用,當等於 0 的時候說明該設備沒在使用。

  2. 需要使用該設備的時候,設備驅動調用 pm_runtime_get/pm_runtime_get_sync 接口,增加變量 usage_count 的值;不再使用該設備的時候,調用 pm_runtime_put/pm_runtime_put_sync 接口,減小 usage_count 變量的值。

  3. 每次調用 get 接口的時候,Runtime PM framework 會判斷該設備的狀態。如果該不是 active 狀態,則使用異步 (ASYNC) 或者同步 (SYNC) 方式調用 runtime_resume 回調函數,喚醒設備。

  4. 每次調用 put 接口的時候,Runtime PM framewokr 會判斷設備的引用計數,如果爲零,則使用異步 (ASYNC) 或者同步 (SYNC) 方式調用 runtime_idle 回調函數。

  5. 爲了防止頻繁 suspend,在 suspend 前面引入了 idle 狀態。當設備處於 idle 狀態之後,會在合適的時間調用 suspend 回調函數。通常都會通過 runtime pm helper function 啓動一個 timer, 設置超時時間,在超時之後調用 runtime_suspend 回調函數。

Runtime PM 回調約束

  1. 回調是互斥的 (例如:  對於同一個設備,禁止並行執行 runtime_suspend 和 runtime_resume 或者同一個設備 runtime_suspend 回調)。不過例外情況是: runtime_suspend() 或 runtime_resume()可以和 runtime_idle()並行執行。

  2. runtime_idle() 和 runtime_suspend 回調只能對 "active" 設備執行。

  3. runtime_idle 和 runtime_suspend 回調只能對其引用計數 (usage count) 等於零,且器 active children 個數是零或者 “power.ignore_children” 標誌被設置的設備執行。

  4. runtime_resume 只能對掛起(suspend)的設備執行。

  5. 如果 runtime suspend() 即將被執行,或者有一個掛起的請求執行,runtime idle() 將不會在同一個設備上執行。

  6. 如果 runtime_suspend 回調已經執行或者已經在 pending 狀態,則取消該設備的 runtime idle 請求。

  7. 如果 runtime_resume 回調已經執行,其他 callbacks 則將不被執行對於同一個設備。

  8. 如果該設備下的任何一個子設備都處於 idle,parent 設備纔可以 idle。

  9. 如果 parent 設備下任何一個設備處於 active 狀態,parent 設備必須 active。

  10. 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 供大家參考。

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_resume/__pm_runtime_suspend/__pm_runtime_idle 接口中。

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");

如下是測試結果:

  1. 查看當前設備的 runtime 狀態

cat /sys/devices/platform/runtime_device/power/runtime_status

suspend

  1. 查看設備的 runtime_suspend 時間

cat /sys/devices/platform/runtime_device/power/runtime_suspended_time

341028

  1. 使設備處於 active 狀態

echo on >  /sys/devices/platform/runtime_device/power/control

  1. 使設備進入 suspend 狀態

echo auto > /sys/devices/platform/runtime_device/power/control

  1. 查看轉換狀態的打印
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