1. 前言
Linux內核提供了三種Suspend: Freeze、Standby和STR(Suspend to RAM),在用戶空間向”/sys/power/state”文件分別寫入”freeze”、”standby”和”mem”,即可觸發它們。
內核中,Suspend及Resume過程涉及到PM Core、Device PM、各個設備的驅動、Platform dependent PM、CPU control等多個模塊,涉及了console switch、process freeze、CPU hotplug、wakeup處理等過個知識點。就讓我們跟着內核代碼,一一見識它們吧。
2. Suspend功能有關的代碼分佈
內核中Suspend功能有關的代碼包括PM core、Device PM、Platform PM等幾大塊,具體如下:
1)PM Core
kernel/power/main.c----提供用戶空間接口(/sys/power/state)
kernel/power/suspend.c----Suspend功能的主邏輯
kernel/power/suspend_test.c----Suspend功能的測試邏輯
kernel/power/console.c----Suspend過程中對控制檯的處理邏輯
kernel/power/process.c----Suspend過程中對進程的處理邏輯
2)Device PM
drivers/base/power/*----具體可參考“Linux電源管理(4)_Power Management Interface”的描述。
設備驅動----具體設備驅動的位置,不再涉及。
3)Platform dependent PM
include/linux/suspend.h----定義platform dependent PM有關的操作函數集
arch/xxx/mach-xxx/xxx.c或者
arch/xxx/plat-xxx/xxx.c----平臺相關的電源管理操作
3. suspend&resume過程概述
下面圖片對Linux suspend&resume過程做了一個概述,讀者可以順着這個流程閱讀內核源代碼。具體的說明,可以參考後面的代碼分析。
4. 代碼分析
4.1 suspend入口
在用戶空間執行如下操作:
echo "freeze" > /sys/power/state
echo "standby" > /sys/power/state
echo "mem" > /sys/power/state
會通過sysfs觸發suspend的執行,相應的處理代碼如下:
- staticssize_t state_store(struct kobject *kobj,struct kobj_attribute *attr,
- constchar*buf,size_t n)
- {
- suspend_state_t state;
- int error;
- error = pm_autosleep_lock();
- if(error)
- return error;
- if(pm_autosleep_state()> PM_SUSPEND_ON){
- error =-EBUSY;
- goto out;
- }
- state = decode_state(buf, n);
- if(state < PM_SUSPEND_MAX)
- error = pm_suspend(state);
- elseif(state == PM_SUSPEND_MAX)
- error = hibernate();
- else
- error =-EINVAL;
- out:
- pm_autosleep_unlock();
- return error ? error : n;
- }
- power_attr(state);
power_attr定義了一個名稱爲state的attribute文件,該文件的store接口爲state_store,該接口在lock住autosleep功能後,解析用戶傳入的buffer(freeze、standby or mem),轉換成state參數。
state參數的類型爲suspend_state_t,在include\linux\suspend.h中定義,爲電源管理狀態在內核中的表示。具體如下:- typedefint __bitwise suspend_state_t;
- #define PM_SUSPEND_ON ((__force suspend_state_t)0)
- #define PM_SUSPEND_FREEZE ((__force suspend_state_t)1)
- #define PM_SUSPEND_STANDBY ((__force suspend_state_t)2)
- #define PM_SUSPEND_MEM ((__force suspend_state_t)3)
- #define PM_SUSPEND_MIN PM_SUSPEND_FREEZE
- #define PM_SUSPEND_MAX ((__force suspend_state_t)4)
根據state的值,如果不是(PM_SUSPEND_MAX,對應hibernate功能),則調用pm_suspend接口,進行後續的處理。
pm_suspend在kernel/power/suspend.c定義,處理所有的suspend過程。
4.2 pm_suspend & enter_state
pm_suspend的實現非常簡單,簡單的做一下參數合法性判斷,直接調用enter_state接口,如下:
- int pm_suspend(suspend_state_t state)
- {
- int error;
- if(state <= PM_SUSPEND_ON || state >= PM_SUSPEND_MAX)
- return-EINVAL;
- error = enter_state(state);
- if(error){
- suspend_stats.fail++;
- dpm_save_failed_errno(error);
- }else{
- suspend_stats.success++;
- }
- return error;
- }
enter_state代碼爲:
- staticint enter_state(suspend_state_t state)
- {
- int error;
- if(!valid_state(state))
- return-ENODEV;
- if(!mutex_trylock(&pm_mutex))
- return-EBUSY;
- if(state == PM_SUSPEND_FREEZE)
- freeze_begin();
- printk(KERN_INFO "PM: Syncing filesystems ... ");
- sys_sync();
- printk("done.\n");
- pr_debug("PM: Preparing system for %s sleep\n", pm_states[state]);
- error = suspend_prepare(state);
- if(error)
- gotoUnlock;
- if(suspend_test(TEST_FREEZER))
- gotoFinish;
- pr_debug("PM: Entering %s sleep\n", pm_states[state]);
- pm_restrict_gfp_mask();
- error = suspend_devices_and_enter(state);
- pm_restore_gfp_mask();
- Finish:
- pr_debug("PM: Finishing wakeup.\n");
- suspend_finish();
- Unlock:
- mutex_unlock(&pm_mutex);
- return error;
- }
主要工作包括:
a)調用valid_state,判斷該平臺是否支持該電源狀態。
suspend的最終目的,是讓系統進入可恢復的掛起狀態,而該功能必須有平臺相關代碼的參與才能完成,因此內核PM Core就提供了一系列的回調函數(封裝在platform_suspend_ops中),讓平臺代碼(如arch/arm/mach-xxx/pm.c)實現,然後由PM Core在合適的時機調用。這些回調函數包含一個valid函數,就是用來告知PM Core,支持哪些state。
最後看一下valid_state的實現(刪除了無關代碼):- bool valid_state(suspend_state_t state)
- {
- if(state == PM_SUSPEND_FREEZE){
- returntrue;
- }
- /*
- * PM_SUSPEND_STANDBY and PM_SUSPEND_MEMORY states need lowlevel
- * support and need to be valid to the lowlevel
- * implementation, no valid callback implies that none are valid.
- */
- return suspend_ops && suspend_ops->valid && suspend_ops->valid(state);
- }
如果是freeze,無需平臺代碼參與即可支持,直接返回true。對於standby和mem,則需要調用suspend_ops的valid回掉,由底層平臺代碼判斷是否支持。
b)加互斥鎖,只允許一個實例處理suspend。
c)如果state是freeze,調用freeze_begin,進行suspend to freeze相關的特殊動作。我會在後面統一分析freeze的特殊動作,這裏暫不描述。
d)打印提示信息,同步文件系統。
e)調用suspend_prepare,進行suspend前的準備,主要包括switch console和process&thread freezing。如果失敗,則終止suspend過程。
f)然後,調用suspend_devices_and_enter接口,該接口負責suspend和resume的所有實際動作。前半部分,suspend console、suspend device、關中斷、調用平臺相關的suspend_ops使系統進入低功耗狀態。後半部分,在系統被事件喚醒後,處理相關動作,調用平臺相關的suspend_ops恢復系統、開中斷、resume device、resume console。
g)最後,調用suspend_finish,恢復(或等待恢復)process&thread,還原console。
4.3 suspend_prepare
suspend_prepare的代碼如下:- staticint suspend_prepare(suspend_state_t state)
- {
- int error;
- if(need_suspend_ops(state)&&(!suspend_ops ||!suspend_ops->enter))
- return-EPERM;
- pm_prepare_console();
- error = pm_notifier_call_chain(PM_SUSPEND_PREPARE);
- if(error)
- gotoFinish;
- error = suspend_freeze_processes();
- if(!error)
- return0;
- suspend_stats.failed_freeze++;
- dpm_save_failed_step(SUSPEND_FREEZE);
- Finish:
- pm_notifier_call_chain(PM_POST_SUSPEND);
- pm_restore_console();
- return error;
- }
主要工作爲:
a)檢查suspend_ops是否提供了.enter回調,沒有的話,返回錯誤。
b)調用pm_prepare_console,將當前console切換到一個虛擬console並重定向內核的kmsg(需要的話)。該功能稱作VT switch,後面我會在稍微詳細的介紹一下,但Linux控制檯子系統是相當複雜的,更具體的分析,要在控制檯子系統的分析文章中說明。
c)調用pm_notifier_call_chain,發送suspend開始的消息(PM_SUSPEND_PREPARE),後面會詳細描述。
d)調用suspend_freeze_processes,freeze用戶空間進程和一些內核線程。該功能稱作freezing-of-tasks,我會專門用一篇文章去分析它。本文就不再詳細說明了。
e)如果freezing-of-tasks失敗,調用pm_restore_console,將console切換回原來的console,並返回錯誤,以便能終止suspend。
4.4 suspend_devices_and_enter
suspend_devices_and_enter的過程較爲複雜,代碼實現如下:- int suspend_devices_and_enter(suspend_state_t state)
- {
- int error;
- bool wakeup =false;
- if(need_suspend_ops(state)&&!suspend_ops)
- return-ENOSYS;
- trace_machine_suspend(state);
- if(need_suspend_ops(state)&& suspend_ops->begin){
- error = suspend_ops->begin(state);
- if(error)
- gotoClose;
- }
- suspend_console();
- ftrace_stop();
- suspend_test_start();
- error = dpm_suspend_start(PMSG_SUSPEND);
- if(error){
- printk(KERN_ERR "PM: Some devices failed to suspend\n");
- gotoRecover_platform;
- }
- suspend_test_finish("suspend devices");
- if(suspend_test(TEST_DEVICES))
- gotoRecover_platform;
- do{
- error = suspend_enter(state,&wakeup);
- }while(!error &&!wakeup && need_suspend_ops(state)
- && suspend_ops->suspend_again && suspend_ops->suspend_again());
- Resume_devices:
- suspend_test_start();
- dpm_resume_end(PMSG_RESUME);
- suspend_test_finish("resume devices");
- ftrace_start();
- resume_console();
- Close:
- if(need_suspend_ops(state)&& suspend_ops->end)
- suspend_ops->end();
- trace_machine_suspend(PWR_EVENT_EXIT);
- return error;
- Recover_platform:
- if(need_suspend_ops(state)&& suspend_ops->recover)
- suspend_ops->recover();
- gotoResume_devices;
- }
a)再次檢查平臺代碼是否需要提供以及是否提供了suspend_ops。
b)調用suspend_ops的begin回調(有的話),通知平臺代碼,以便讓其作相應的處理(需要的話)。可能失敗,需要跳至Close處執行恢復操作(suspend_ops->end)。
c)調用suspend_console,掛起console。該接口由"kernel\printk.c"實現,主要是hold住一個lock,該lock會阻止其它代碼訪問console。
d)調用ftrace_stop,停止ftrace功能。ftrace是一個很有意思的功能,後面再介紹。
e)調用dpm_suspend_start,調用所有設備的->prepare和->suspend回調函數(具體可參考“Linux電源管理(4)_Power Management Interface”的描述),suspend需要正常suspend的設備。suspend device可能失敗,需要跳至 Recover_platform,執行recover操作(suspend_ops->recover)。
f)以上都是suspend前的準備工作,此時,調用suspend_enter接口,使系統進入指定的電源狀態。該接口的內容如下:- staticint suspend_enter(suspend_state_t state,bool*wakeup)
- {
- int error;
- if(need_suspend_ops(state)&& suspend_ops->prepare){
- error = suspend_ops->prepare();
- if(error)
- gotoPlatform_finish;
- }
- error = dpm_suspend_end(PMSG_SUSPEND);
- if(error){
- printk(KERN_ERR "PM: Some devices failed to power down\n");
- gotoPlatform_finish;
- }
- if(need_suspend_ops(state)&& suspend_ops->prepare_late){
- error = suspend_ops->prepare_late();
- if(error)
- gotoPlatform_wake;
- }
- if(suspend_test(TEST_PLATFORM))
- gotoPlatform_wake;
- /*
- * PM_SUSPEND_FREEZE equals
- * frozen processes + suspended devices + idle processors.
- * Thus we should invoke freeze_enter() soon after
- * all the devices are suspended.
- */
- if(state == PM_SUSPEND_FREEZE){
- freeze_enter();
- gotoPlatform_wake;
- }
- error = disable_nonboot_cpus();
- if(error || suspend_test(TEST_CPUS))
- gotoEnable_cpus;
- arch_suspend_disable_irqs();
- BUG_ON(!irqs_disabled());
- error = syscore_suspend();
- if(!error){
- *wakeup = pm_wakeup_pending();
- if(!(suspend_test(TEST_CORE)||*wakeup)){
- error = suspend_ops->enter(state);
- events_check_enabled =false;
- }
- syscore_resume();
- }
- arch_suspend_enable_irqs();
- BUG_ON(irqs_disabled());
- Enable_cpus:
- enable_nonboot_cpus();
- Platform_wake:
- if(need_suspend_ops(state)&& suspend_ops->wake)
- suspend_ops->wake();
- dpm_resume_start(PMSG_RESUME);
- Platform_finish:
- if(need_suspend_ops(state)&& suspend_ops->finish)
- suspend_ops->finish();
- return error;
- }
f1)該接口處理完後,會通過返回值告知是否enter成功,同時通過wakeup指針,告知調用者,是否有wakeup事件發生,導致電源狀態切換失敗。
f2)調用suspend_ops的prepare回調(有的話),通知平臺代碼,以便讓其在即將進行狀態切換之時,再做一些處理(需要的話)。該回調可能失敗(平臺代碼出現意外),失敗的話,需要跳至Platform_finish處,調用suspend_ops的finish回調,執行恢復操作。
f3)調用dpm_suspend_end,調用所有設備的->suspend_late和->suspend_noirq回調函數(具體可參考“Linux電源管理(4)_Power Management Interface”的描述),suspend late suspend設備和需要在關中斷下suspend的設備。需要說明的是,這裏的noirq,是通過禁止所有的中斷線的形式,而不是通過關全局中斷的方式。同樣,該操作可能會失敗,失敗的話,跳至Platform_finish處,執行恢復動作。
f4)調用suspend_ops的prepare_late回調(有的話),通知平臺代碼,以便讓其在最後關頭,再做一些處理(需要的話)。該回調可能失敗(平臺代碼出現意外),失敗的話,需要跳至Platform_wake處,調用suspend_ops的wake回調,執行device的resume、調用suspend_ops的finish回調,執行恢復操作。
f5)如果是suspend to freeze,執行相應的操作,包括凍結進程、suspended devices(參數爲PM_SUSPEND_FREEZE)、cpu進入idle。如果有任何事件使CPU從idle狀態退出,跳至Platform_wake處,執行wake操作。
f6)調用disable_nonboot_cpus,禁止所有的非boot cpu。也會失敗,執行恢復操作即可。
f7)調用arch_suspend_disable_irqs,關全局中斷。如果無法關閉,則爲bug。
f8)調用syscore_suspend,suspend system core。同樣會失敗,執行恢復操作即可。有關syscore,我會在另一篇文章中詳細描述。
f9)如果很幸運,以上操作都成功了,那麼,切換吧。不過,別高興太早,還得調用pm_wakeup_pending檢查一下,這段時間內,是否有喚醒事件發生,如果有就要終止suspend。
f10)如果一切順利,調用suspend_ops的enter回調,進行狀態切換。這時,系統應該已經suspend了……
f11)suspend過程中,喚醒事件發生,系統喚醒,該函數接着執行resume動作,並最終返回。resume動作基本上是suspend的反動作,就不再繼續分析了。
f12)或者,由於意外,suspend終止,該函數也會返回。
g)suspend_enter返回,如果返回原因不是發生錯誤,且不是wakeup事件。則調用suspend_ops的suspend_again回調,檢查是否需要再次suspend。再什麼情況下要再次suspend呢?需要看具體的平臺了,誰知道呢。
h)繼續resume操作,resume device、start ftrace、resume console、suspend_ops->end等等。
i)該函數返回後,表示系統已經resume。
4.5 suspend_finish
比較簡單:
- staticvoid suspend_finish(void)
- {
- suspend_thaw_processes();
- pm_notifier_call_chain(PM_POST_SUSPEND);
- pm_restore_console();
- }
a)恢復所有的用戶空間進程和內核線程。
b)發送suspend結束的通知。
c)將console切換回原來的。
5. 重要知識點回顧
5.1 VT switch
通常情況下,系統控制檯模塊(drivers\tty\vt\)會在suspend的過程中,重新分配一個console,並將控制檯切換到該console上。然後在resume時,切換回舊的console。這就是VT switch功能。VT switch是很耗時的,因此內核提供了一些機制,控制是否使用這個功能:
1)提供一個接口函數pm_set_vt_switch(drivers\tty\vt\vt_ioctl.c),方便其它內核模塊從整體上關閉或者開啓VT switch功能。
2)VT switch全局開關處於開啓狀態時,滿足如下的一種條件(可參考kernel\power\console.c相關的描述),即會使能VT switch
a)有console driver調用pm_vt_switch_required接口,顯式的要求使能VT switch。PM core的console模塊會把這些信息記錄在一個名稱爲pm_vt_switch_list的鏈表中。
b)系統禁止在suspend的過程中suspend console(由kernel/printk.c中的console_suspend_enabled變量控制)。很有可能需要使用console查看suspend過程,此時爲了使console不混亂,有必要進行VT switch。
c)沒有任何console driver關心是否需要VT switch,換句話說沒有任何driver調用pm_vt_switch_required接口要求使能或禁止VT switch功能。此時會按照舊的習慣,進行VT switch。
因此,suspend過程對console的處理分爲4步:
prepare console:負責在需要VT swich時,將當前console切換到SUSPEND console。- int pm_prepare_console(void)
- {
- if(!pm_vt_switch())
- return0;
- orig_fgconsole = vt_move_to_console(SUSPEND_CONSOLE,1);
- if(orig_fgconsole <0)
- return1;
- orig_kmsg = vt_kmsg_redirect(SUSPEND_CONSOLE);
- return0;
- }
suspend console:掛起console,由kernel/printk.c實現,主要是hold住console用的互斥鎖,使他人無法使用console。
resume console:對console解鎖。
restore console:將console恢復爲初始的console。
- void pm_restore_console(void)
- {
- if(!pm_vt_switch())
- return;
- if(orig_fgconsole >=0){
- vt_move_to_console(orig_fgconsole,0);
- vt_kmsg_redirect(orig_kmsg);
- }
- }
也許,您會問,why VT switch?先留着這個疑問吧,等到分析控制檯時再回答。
5.2 freezing of task
進程的freezing功能,是suspend、hibernate等電源管理功能的組成部分,在新版本內核中,它被獨立出來,作爲一個獨立的電源管理狀態(freeze)。該功能的目的,是在電源管理的狀態切換過程中,確保所有用戶空間進程和部分內核線程處於一個穩定的狀態。有關該功能的具體描述,請參考wowotech後續的文章。
5.3 PM notifier
PM notifier是基於內核blocking notifier功能實現的。blocking notifier提供了一種kernel內部的消息通知機制,消息接受者通過notifier註冊的方式,註冊一個回調函數,關注消息發送者發出的notifier。當消息產生時,消息產生者通過調用回調函數的形式,通知消息接受者。這種調用,是可以被阻塞的,因此稱作blocking notifier。
那suspend功能爲什麼使用notifier呢?原因可能有多種,這裏我舉一個例子,這是我們日常開發中可能會遇到的。
由之前的描述可知,suspend過程中,suspend device發生在進程被freeze之後,resume device發生在進程被恢復之前。那麼:
1)如果有些設備就需要在freeze進程之前suspend怎麼辦?
2)如果有些設備的resume動作需要較多延時,或者要等待什麼事情發生,那麼如果它的resume動作發生在進程恢復之前,豈不是要阻止所有進程的恢復?更甚者,如果該設備要等待某個進程的數據才能resume,怎麼辦?
再來看suspend_prepare和suspend_finish中的處理:- staticint suspend_prepare(suspend_state_t state){
- …
- error = pm_notifier_call_chain(PM_SUSPEND_PREPARE);
- if(error)
- gotoFinish;
- error = suspend_freeze_processes();
- …
- }
- staticvoid suspend_finish(void)
- {
- suspend_thaw_processes();
- pm_notifier_call_chain(PM_POST_SUSPEND);
- pm_restore_console();
- }
原來PM notifier是在設備模型的框架外,開了一個後門,那些比較特殊的driver,可以繞過設備模型,直接接收PM發送的suspend信息,以便執行自身的suspend動作。特別是resume時,可以在其它進程都正好工作的時候,只讓suspend進程等待driver的resume。
感興趣的讀者,可以圍觀一下下面這個活生生的例子(順便提一下,好的設計是不應該有例外的):
drivers\video\omap2\dss\core.c
5.4 device PM ops 和platform PM ops的調用時機
對Linux驅動工程師來說,device PM ops和platform PM ops就是電源管理(suspend)的全部,只要在合適的地方,實現合適的回調函數,即可實現系統的電源管理。但現實太複雜了,以至於kernel提供的這兩個數據結構也很複雜,再回憶一下,如下:- struct dev_pm_ops {
- int(*prepare)(struct device *dev);
- void(*complete)(struct device *dev);
- int(*suspend)(struct device *dev);
- int(*resume)(struct device *dev);
- int(*freeze)(struct device *dev);
- int(*thaw)(struct device *dev);
- int(*poweroff)(struct device *dev);
- int(*restore)(struct device *dev);
- int(*suspend_late)(struct device *dev);
- int(*resume_early)(struct device *dev);
- int(*freeze_late)(struct device *dev);
- int(*thaw_early)(struct device *dev);
- int(*poweroff_late)(struct device *dev);
- int(*restore_early)(struct device *dev);
- int(*suspend_noirq)(struct device *dev);
- int(*resume_noirq)(struct device *dev);
- int(*freeze_noirq)(struct device *dev);
- int(*thaw_noirq)(struct device *dev);
- int(*poweroff_noirq)(struct device *dev);
- int(*restore_noirq)(struct device *dev);
- int(*runtime_suspend)(struct device *dev);
- int(*runtime_resume)(struct device *dev);
- int(*runtime_idle)(struct device *dev);
- };
- struct platform_suspend_ops {
- int(*valid)(suspend_state_t state);
- int(*begin)(suspend_state_t state);
- int(*prepare)(void);
- int(*prepare_late)(void);
- int(*enter)(suspend_state_t state);
- void(*wake)(void);
- void(*finish)(void);
- bool(*suspend_again)(void);
- void(*end)(void);
- void(*recover)(void);
- };
雖然內核的註釋已經相當詳細了,但我們一定會犯暈,到底該實現哪些回調?這些回調的應用場景又是什麼?蝸蝸以爲,要熟練使用這些回調,唯一的方法就是多coding、多理解。除此之外,我們可以總結一下在電源狀態切換時,這些回調的調用時機,從側面幫助理解。如下(只介紹和suspend功能有關的,struct dev_pm_ops簡稱D,struct platform_suspend_ops簡稱P):
5.5 suspend過程的同步和PM wakeup
最重要的事情,如果suspend的過程中,有喚醒事件產生怎麼辦?正常的流程,應該終止suspend,返回並處理事件。但由於suspend過程的特殊性,進程被freeze、關中斷等等,導致事情並沒有那麼簡單,以至於在很久的一段時間內,kernel都不能很好的處理。這也稱作suspend過程的同步問題。
在美好的舊時光裏,suspend大多用於熱關機,因此同步問題的影響並不突出(因爲操作並不頻繁)。但來到新時代之後,事情變了,Android竟然用suspend作日常的待機(操作就相當頻繁了),這時問題就大了。那怎麼解決呢?得靠system wakeup framework,也就是suspend過程中所調用的pm_wakeup_pending接口所在的模塊。我會在下一篇文章中繼續該模塊的分析,這裏就不再繼續了。
原文地址:http://www.wowotech.net/linux_kenrel/suspend_and_resume.html