Linux功耗管理(25_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等(可參考“
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的分析文章裡比較好。所以本文就結束好了。