Linux電源管理(14)_從設備驅動的角度看電源管理

1. 前言

相信工作稍微久一點的linux驅動工程師都深有體會:

在舊時光裏,實現某一個設備的電源管理功能,是非常簡單的一件事情。大多數設備都被抽象爲platform設備,driver只需要提供suspend/resume/shutdown等回調函數,並註冊到kernel即可。kernel會在系統電源狀態切換的過程中,調用driver提供的回調函數,切換設備的電源狀態。

但是在新時代中,設備電源管理有關的操作,被統一封裝在struct dev_pm_ops結構中了。該結構包含20多個回調函數,再加上覆雜的電源管理機制(常規的suspend/resume、runtime PM等等),使設備驅動的電源管理工作不再那麼單純,工程師(如蝸蝸自己)的思路也不再特別清晰。

因此本文希望能以單一設備的電源管理爲出發點,結合kernel的電源管理機制,介紹怎樣在設備驅動中添加電源管理功能,並分析設備電源狀態切換和系統電源狀態切換的關係。

另外,我們在電源管理系列文章中,介紹了很多的電源管理機制,如generic PM、wakeup event framework、wakelock、autosleep、runtime PM、PM domain、等等,本文也算是對它們的梳理和總結。

2. 功能描述

設備的電源狀態切換,和系統電源狀態切換基本保持一致(runtime PM除外),切換的場景如下:

1)系統reboot的過程,包括halt、power off、restart等(可參考“Linux電源管理(3)_Generic PM之Reboot過程”),要求設備進入shutdown狀態,以避免意外產生。

2)系統suspend/resume的過程(可參考“Linux電源管理(6)_Generic PM之Suspend功能”),要求設備也同步suspend/resume。

3)系統hibernate及恢復的過程,要求設備在suspend/resume的基礎上,增加poweroff的動作。

4)runtime PM過程(可參考“Linux電源管理(11)_Runtime PM之功能描述”),要求設備在引用計數爲0時suspend甚至power off,並在引用計數大於0時power on以及resume。

舊有的電源管理框架中,通過bus、class、device_driver等結構體中的shutdown、suspend、resume三個回調函數,就可以實現上面處runtime PM之外的所有功能。但是在新框架中,特別是引入struct dev_pm_ops結構之後,其中的suspend/resume就不再推薦使用了。

不過,對有些設備來說,例如platform device,如果電源管理需求不是很複雜,driver工程師仍然可以使用舊的方法實現,kernel會自動幫忙轉換爲新的方式。但是,如果有更多需求,就不得不面對struct dev_pm_ops了。下面將會詳細說明。

3. 數據結構回顧

正式開始之前,我們先回顧一下設備電源管理有關的數據結構。它們大多都在之前的文章中介紹過了,本文放在一起,權當一個總結。

3.1 .shutdown回調函數以及使用方法

由於reboot過程是相對獨立和穩定的,且該過程依賴於設備的.shutdown回調函數,這裏把它獨立出來,單獨描述,後面就不再涉及了。

.shutdown回調函數存在於兩個數據結構中:struct device_driver和struct bus_type,在系統reboot的過程中被調用,負責關閉設備。設備驅動可以根據需要,實現其中的一個。我們以一個普通的platform設備爲例,介紹這個過程。

1)定義一個platform_driver,並實現其.shutdown回調,然後調用platform_driver_register將它註冊到kernel中

   1: static void foo_shutdown(struct platform_device *pdev)
   2: {
   3:         ...
   4: } 
   5: static platform_driver foo_pdrv = 
   6: {
   7:         .shutdown = foo_shutdown,
   8:         ...
   9: };

2)platform_driver_register時,會把struct device_driver變量的shutdown函數,替換爲platform設備特有的shutdown函數(platform_drv_shutdown),並調用driver_register將device_driver註冊到kernel

   1: int __platform_driver_register(struct platform_driver *drv,
   2:                                 struct module *owner)
   3: {
   4:
   5:  
   6:         if (drv->shutdown)
   7:                 drv->driver.shutdown = platform_drv_shutdown; 
   8:  
   9:         return driver_register(&drv->driver);
  10: }

3)系統reboot的過程中,會調用每個設備的shutdown函數。對這裏的foo_pdrv而言,會先調用platform_drv_shutdown,它繼續調用foo_shutdown。

3.2 legacy的不再使用的.suspend/.resume

舊的suspend/resume操作,主要依賴struct device_driver、struct class、struct bus_type等結構中的suspend和resume回調函數,其使用方式和上面的.shutdown幾乎完全一樣。以platform設備爲例,只需多定義兩個函數即可,如下:

   1: static int foo_suspend(struct platform_device *pdev, pm_message_t state)
   2: {
   3:         ...
   4: }
   5:  
   6: static int foo_resume(struct platform_device *pdev)
   7: {
   8:         ...
   9: }
  10:  
  11: static void foo_shutdown(struct platform_device *pdev)
  12: {
  13:         ...
  14: }
  15:  
  16: static platform_driver foo_pdrv = {
  17:         .suspend = foo_suspend,
  18:         .resume = foo_resume,
  19:         .shutdown = foo_shutdown,
  20:         ...
  21: };

在較新的kernel中,已經不再建議使用這些回調函數了,但對platform設備來說,如果場景比較簡單,可以照舊使用上面的實現方法,platform.c會自動幫忙轉換爲struct dev_pm_ops回調,具體請參考後面描述。

3.3 struct dev_pm_ops結構

struct dev_pm_ops是設備電源管理的核心數據結構,用於封裝和設備電源管理有關的所有操作

   1: struct dev_pm_ops {
   2:         int (*prepare)(struct device *dev);
   3:         void (*complete)(struct device *dev);
   4:         int (*suspend)(struct device *dev);
   5:         int (*resume)(struct device *dev);
   6:         int (*freeze)(struct device *dev);
   7:         int (*thaw)(struct device *dev);
   8:         int (*poweroff)(struct device *dev);
   9:         int (*restore)(struct device *dev);
  10:         int (*suspend_late)(struct device *dev);
  11:         int (*resume_early)(struct device *dev);
  12:         int (*freeze_late)(struct device *dev);
  13:         int (*thaw_early)(struct device *dev);
  14:         int (*poweroff_late)(struct device *dev);
  15:         int (*restore_early)(struct device *dev);
  16:         int (*suspend_noirq)(struct device *dev);
  17:         int (*resume_noirq)(struct device *dev);
  18:         int (*freeze_noirq)(struct device *dev);
  19:         int (*thaw_noirq)(struct device *dev);
  20:         int (*poweroff_noirq)(struct device *dev);
  21:         int (*restore_noirq)(struct device *dev);
  22:         int (*runtime_suspend)(struct device *dev);
  23:         int (*runtime_resume)(struct device *dev);
  24:         int (*runtime_idle)(struct device *dev);
  25: };

該結構基本上是個大殺器了,該有的東西都有,主要分爲幾類:

傳統suspend的常規路徑,prepare/complete、suspend/resume、freeze/thaw、poweroff、restore;

傳統suspend的特殊路徑,early/late、noirq;

runtime PM,suspend/resume/idle。

各類driver需要做的事情很單純,實現這些回調函數,並保存在合適的位置,我們接着往下看。

3.4 struct dev_pm_ops的位置

   1: struct device {
   2:         ...
   3:         struct dev_pm_domain    *pm_domain;
   4:         const struct device_type *type;
   5:         struct class            *class;
   6:         struct bus_type *bus;
   7:         struct device_driver *driver; 
   8:         ...
   9: };
  10:  
  11:  
  12:  
  13: struct dev_pm_domain {
  14:         struct dev_pm_ops       ops; 
  15:         ...
  16: };
  17:  
  18: struct device_type {
  19:         ...
  20:         const struct dev_pm_ops *pm;
  21: };
  22:  
  23: struct class {
  24:         ...
  25:         const struct dev_pm_ops *pm;
  26:         ...
  27: };
  28:  
  29: struct bus_type {
  30:         ...
  31:         const struct dev_pm_ops *pm;
  32:         ...
  33: };
  34:  
  35: struct device_driver {
  36:         ...
  37:         const struct dev_pm_ops *pm;
  38:         ...
  39: };

可謂是狡兔多窟,struct dev_pm_ops存在於struct device、struct device_type、struct class、struct bus_type、struct device_driver等所有和設備模型有關的實體中

由之前的文章可知,kernel在電源管理的過程中,會按照如下優先級調用dev_pm_ops中的回調函數,以命令設備實現相應的狀態切換

dev->pm_domain->ops、dev->type->pm、dev->class->pm、dev->bus->pm、dev->driver->pm。

因此,設備driver需要做的事情也很單純,實現這些回調函數,並保存在合適的位置。但這麼多位置,到底怎麼實現呢?我們接着分析。

4. struct dev_pm_ops的實現

由之前的描述可知,系統在電源狀態切換時,會按照一定的優先順序,調用設備的pm ops。所謂的優先順序,是指:只要存在優先級高的ops(如dev->pm_domain->ops),則調用該ops,否則繼續查找下一個優先級。因此,設備驅動可以根據該設備的實際情況,在指定層次上,實現dev pm ops,以達到電源管理的目的。

dev pm ops可以存在於pm domain、device type、class、bus、device driver任何一個地方,本章以pm domain、bus和device driver三個典型場景爲例,介紹設備電源管理的實現思路。

注1:爲了方便,我會以struct dev_pm_ops中的.suspend函數爲例,其它類似。

4.1 pm domain

當一個設備屬於某個pm domain時(具體可參考“Linux PM domain framework(1)_概述和使用流程”),系統suspend的過程中,會直接調用pm_domain->ops.suspend。而由pm_genpd_init可知,pm_domain->ops.suspend由pm_genpd_suspend實現:

genpd->domain.ops.suspend = pm_genpd_suspend;

該接口的實現爲:

   1: static int pm_genpd_suspend(struct device *dev)
   2: {
   3:         struct generic_pm_domain *genpd;
   4:  
   5:         dev_dbg(dev, "%s()\n", __func__);
   6:  
   7:         genpd = dev_to_genpd(dev);
   8:         if (IS_ERR(genpd))
   9:                 return -EINVAL;
  10:  
  11:         return genpd->suspend_power_off ? 0 : pm_generic_suspend(dev);
  12: }

最終會調用pm_generic_suspend,由“Linux電源管理(4)_Power Management Interface”的描述可知,該接口最終會調用該設備驅動的suspend接口(如果有的話),即:dev->driver->pm->suspend。

看來是空歡喜一場,本以爲pm domain幫忙做了,設備驅動就可以偷一點懶,誰知道繞來繞去,又把球踢給了設備驅動!讓我們思考一下其中的原因:

1)suspend時,設備的動作到底是什麼,只有設備驅動最清楚,所以,把事情交給driver做,是合理的。

2)那麼,爲什麼要經過pm domain這一層呢?直接調用driver的suspend不就可以了嗎?因爲需要在suspend前,由pm domain做一些處理,例如判斷該設備是否已經掉電(如果掉電了,就不能再suspend了,否則可能有非預期的結果),等等。

4.2 dev->bus->pm

來看另一個例子,如果該設備所在的bus提供了dev_pm_ops呢?開始之前,我們再強調一下這個事實:suspend時,設備的動作到底是什麼,只有設備驅動最清楚,所以,把事情交給driver做,是合理的。所以相信大家猜到了,就算bus有suspend回調,最終還是要繞到設備驅動的suspend接口上。

我們以platform bus爲例,原因是這個bus很簡單,而且我們平時需要面對的大多數設備都是platform設備。

在drivers/base/platform.c中,platform bus是這樣定義的:

   1: struct bus_type platform_bus_type = {
   2:         .name           = "platform",
   3:         .dev_groups     = platform_dev_groups,
   4:         .match          = platform_match,
   5:         .uevent         = platform_uevent,
   6:         .pm             = &platform_dev_pm_ops,
   7: };

接着看一下platform_dev_pm_ops:

   1: static const struct dev_pm_ops platform_dev_pm_ops = {
   2:         .runtime_suspend = pm_generic_runtime_suspend,
   3:         .runtime_resume = pm_generic_runtime_resume,
   4:         USE_PLATFORM_PM_SLEEP_OPS
   5: };

哦,有runtime PM相關的兩個回調,有一個宏定義:USE_PLATFORM_PM_SLEEP_OPS,該宏定義指定了dev_pm_ops的suspend回調爲platform_pm_suspend(其它的類似)。該接口的實現如下:

   1: int platform_pm_suspend(struct device *dev)
   2: {
   3:         struct device_driver *drv = dev->driver;
   4:         int ret = 0;
   5:  
   6:         if (!drv)
   7:                 return 0;
   8:  
   9:         if (drv->pm) {
  10:                 if (drv->pm->suspend)
  11:                         ret = drv->pm->suspend(dev);
  12:         } else {
  13:                 ret = platform_legacy_suspend(dev, PMSG_SUSPEND);
  14:         }
  15:  
  16:         return ret;
  17: }

原來如此,如果該設備的驅動提供了dev_pm_ops指針,調用相應的suspend接口。否則,調用legacy的接口(即pdrv->suspend)。再對比3.1,3.2小節的描述,是不是豁然開朗了?

另外,由於platform bus是一個虛擬的bus,不需要其它的動作。對於一些物理bus,可以在bus的suspend接口中,實現bus有關的suspend操作。這就是設備模型的魅力所在

4.3 dev->driver->pm

無論怎樣,如果一個設備需要在suspend時有一些動作,就一定要在設備驅動中實現suspend,那樣怎麼實現呢?定義一個struct dev_pm_ops變量,並實現設備所需的回調函數,在driver註冊之前,保存在driver->pm指針中即可。

那有什麼變化?大多數的設備是platform設備,我們也可以用舊的方式(3.1,3.2小節),實現platform driver的suspend/resume。但是,在新時代,不建議這樣做了,注意platform_legacy_suspend中的legacy字樣哦,遺產、遺留下來的,只是爲了兼容。如果我們新寫driver,就用新的方式好了。

5. 設備電源狀態的切換過程

本來還想梳理一下系統電源切換的過程中,driver是怎麼處理的。但經過上面的分析,傳統的suspend/resume已經很明確了,無非是按照pm_domain—>device driver或者class—>device driver或者bus—>device driver的順序,調用相應的回調函數。而runtime PM,還是放到runtime PM的分析文章裏比較好。所以本文就結束好了。

發佈了35 篇原創文章 · 獲贊 46 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章