1. 程式人生 > >Linux電源管理(3)_Generic PM之Reboot過程

Linux電源管理(3)_Generic PM之Reboot過程

1. 前言

在使用計算機的過程中,關機和重啟是最先學會的兩個操作。同樣,這兩個操作在Linux中也存在,稱作shutdown和restart。這就是本文要描述的物件。

在Linux Kernel中,主流的shutdown和restart都是通過“reboot”系統呼叫(具體可參考kernel/sys.c)來實現的,這也是本文使用“Generic PM之Reboot過程”作為標題的原因。另外,除了我們常用的shutdown和restart兩類操作之外,該系統呼叫也提供了其它的reboot方式,也會在這裡一一說明。

2. Kernel支援的reboot方式

也許你會奇怪,reboot是重啟的意思,所以用它實現Restart是合理的,但怎麼用它實現關機操作呢?答案是這樣的:關機之後,早晚也會開機啊!所以關機是一種特殊的Restart過程,只不過持續的時間有點長而已

。所以,核心根據不同的表現方式,將reboot分為如下的幾種方式:

   1: /*      
   2:  * Commands accepted by the _reboot() system call.
   3:  *
   4:  * RESTART     Restart system using default command and mode.
   5:  * HALT        Stop OS and give system control to ROM monitor, if any.
   6:  * CAD_ON      Ctrl-Alt-Del sequence causes RESTART command.
   7:  * CAD_OFF     Ctrl-Alt-Del sequence sends SIGINT to init task.
   8:  * POWER_OFF   Stop OS and remove all power from system, if possible.
   9:  * RESTART2    Restart system using given command string.
  10:  * SW_SUSPEND  Suspend system using software suspend if compiled in.
  11:  * KEXEC       Restart system using a previously loaded Linux kernel
  12:  */
  13:         
  14: #define LINUX_REBOOT_CMD_RESTART        0x01234567
  15: #define LINUX_REBOOT_CMD_HALT           0xCDEF0123
  16: #define LINUX_REBOOT_CMD_CAD_ON         0x89ABCDEF
  17: #define LINUX_REBOOT_CMD_CAD_OFF        0x00000000
  18: #define LINUX_REBOOT_CMD_POWER_OFF      0x4321FEDC
  19: #define LINUX_REBOOT_CMD_RESTART2       0xA1B2C3D4
  20: #define LINUX_REBOOT_CMD_SW_SUSPEND     0xD000FCE2
  21: #define LINUX_REBOOT_CMD_KEXEC          0x4558454

RESTART,正常的重啟,也是我們平時使用的重啟。執行該動作後,系統會重新啟動。

HALT,停止作業系統,然後把控制權交給其它程式碼(如果有的話)。具體的表現形式,依賴於系統的具體實現。

CAD_ON/CAD_OFF,允許/禁止通過Ctrl-Alt-Del組合按鍵觸發重啟(RESTART)動作。 
注1:Ctrl-Alt-Del組合按鍵的響應是由具體的Driver(如Keypad)實現的。

POWER_OFF,正常的關機。執行該動作後,系統會停止作業系統,並去除所有的供電。

RESTART2,重啟的另一種方式。可以在重啟時,攜帶一個字串型別的cmd,該cmd會在重啟前,傳送給任意一個關心重啟事件的程序,同時會傳遞給最終執行重啟動作的machine相關的程式碼。核心並沒有規定該cmd的形式,完全由具體的machine自行定義。

SW_SUSPEND,即前一篇文章中描述的Hibernate操作,會在下一篇文章描述,這裡就暫不涉及。

KEXEC,重啟並執行已經載入好的其它Kernel Image(需要CONFIG_KEXEC的支援),暫不涉及。

3. Reboot相關的操作流程

在Linux作業系統中,可以通過reboot、halt、poweroff等命令,發起reboot,具體的操作流程如下:

Reboot相關的操作流程

  • 一般的Linux作業系統,在使用者空間都提供了一些工具集合(如常在嵌入式系統使用的Busybox),這些工具集合包含了reboot、halt和poweroff三個和Reboot相關的命令。讀者可以參考man幫助文件,瞭解這些命令的解釋和使用說明
  • 使用者空間程式通過reboot系統呼叫,進入核心空間
  • 核心空間根據執行路徑的不同,提供了kernel_restart、kernel_halt和kernel_power_off三個處理函式,響應用空間的reboot請求
  • 這三個處理函式的處理流程大致相同,主要包括:向關心reboot過程的程序傳送Notify事件;呼叫drivers核心模組提供的介面,關閉所有的外部裝置;呼叫drivers syscore模組提供的介面,關閉system core;呼叫Architecture相關的處理函式,進行後續的處理;最後,呼叫machine相關的介面,實現真正意義上的Reboot
  • 另外,藉助TTY模組提供的Sysreq機制,核心提供了其它途徑的關機方法,如某些按鍵組合、向/proc檔案寫入命令等,後面會詳細介紹

4. Reboot過程的內部動作和程式碼分析

4.1 Reboot系統呼叫

Reboot系統呼叫的實現位於“kernel/sys.c”,其函式原型如下:

   1: SYSCALL_DEFINE4(reboot, int, magic1, int, magic2, unsigned int, cmd,
   2:                 void __user *, arg)

該函式的引數解釋如下:

reboot,該系統呼叫的名稱。

magic1、magic2,兩個int型別的“魔力數”,用於防止誤操作。具體在“include/uapi/linux/reboot.h”中定義,感興趣的同學可以去看看(話說這些數字還是蠻有意思的,例如Linus同學及其家人的生日就在裡面,猜出來的可以在文章下面留言)。

cmd,第2章所講述的reboot方式。

arg,其它的額外引數。

reboot系統呼叫的內部動作比較簡單:

1)判斷呼叫者的使用者許可權,如果不是超級使用者(superuser),則直接返回錯誤(這也是我們再使用者空間執行reboot、halt、poweroff等命令時,必須是root使用者的原因);

2)判斷傳入的magic number是否匹配,如果不匹配,直接返回錯誤。這樣就可以儘可能的防止誤動作發生;

3)呼叫reboot_pid_ns介面,檢查是否需要由該介面處理reboot請求。這是一個有關pid namespaces的新特性,也是Linux核心重要的知識點,我們會在其它文章中描述,這裡就不多說了;

4)如果是POWER_OFF命令,且沒有註冊power off的machine處理函式(pm_power_off),把該命令轉換為HALT命令;

5)根據具體的cmd命令,執行具體的處理,包括, 
      如果是RESTART或者RESTART2命令,呼叫kernel_restart。 
      如果是CAD_ON或CAD_OFF命令,更新C_A_D的值,表示是否允許通過Ctrl+Alt+Del組合鍵重啟系統。 
      如果是HALT命令,呼叫kernel_halt。 
      如果是POWER_OFF命令,呼叫kernel_power_off。 
      如果是KEXEC命令,呼叫kernel_kexec介面(暫不在本文涉及)。 
      如果是SW_SUSPEND,呼叫hibernate介面(會在下一章描述);

6)返回上述的處理結果,系統呼叫結束。

4.2 kernel_restart、kernel_halt和kernel_power_off

這三個介面也位於“kernel/sys.c”,實現比較類似,具體動作包括:

1)呼叫kernel_xxx_prepare函式,進行restart/halt/power_off前的準備工作,包括, 
      呼叫blocking_notifier_call_chain介面,向關心reboot事件的程序,傳送SYS_RESTART、SYS_HALT或者SYS_POWER_OFF事件。對RESTART來說,還好將cmd引數一併傳送出去。 
      將系統狀態設定為相應的狀態(SYS_RESTART、SYS_HALT或SYS_POWER_OFF)。 
      呼叫usermodehelper_disable介面,禁止User mode helper(可參考“Linux裝置模型(3)_Uevent”相關的描述)。 
      呼叫device_shutdown,關閉所有的裝置(具體內容會在下一節講述);

2)如果是power_off,且存在PM相關的power off prepare函式(pm_power_off_prepare),則呼叫該回調函式;

3)呼叫migrate_to_reboot_cpu介面,將當前的程序(task)移到一個CPU上; 
注2:對於多CPU的機器,無論哪個CPU觸發了當前的系統呼叫,程式碼都可以執行在任意的CPU上。這個介面將程式碼分派到一個特定的CPU上,並禁止排程器分派程式碼到其它CPU上。也就是說,這個介面被執行後,只有一個CPU在執行,用於完成後續的reboot動作。

4)呼叫syscore_shutdown介面,將系統核心器件關閉(例如中斷等);

5)呼叫printk以及kmsg_dump,向這個世界發出最後的聲音(列印日誌);

6)最後,由machine-core的程式碼,接管後續的處理。

4.3 device_shutdown

在理解device_shutdown之前,我們需要回憶一下前幾篇有關Linux裝置模型的文章。同時,藉助對電源管理的解析,我們會把在Linux裝置模型系列文章中沒有描述的部分補回來。裝置模型中和device_shutdown有關的邏輯包括:

  • 每個裝置(struct device)都會儲存該裝置的驅動(struct device_driver)指標,以及該裝置所在匯流排(struct bus_type)的指標(具體參考“Linux裝置模型(5)_device和device driver”)
  • 裝置驅動中有一個名稱為“shutdown”的回撥函式,用於在device_shutdown時,關閉該裝置(具體參考“Linux裝置模型(5)_device和device driver”)
  • 匯流排中也有一個名稱為“shutdown”的回撥函式,用於在device_shutdown時,關閉該裝置(具體參考“Linux裝置模型(6)_Bus”)
  • 系統的所有裝置,都存在於“/sys/devices/”目錄下,而該目錄由名稱為“devices_kset”的kset表示。而由“Linux裝置模型(2)_Kobject”的描述可知,kset中會使用一個連結串列儲存其下所有的kobject(也即“/sys/devices/”目錄下的所有裝置)。最終的結果就是,以“devices_kset”為root節點,將核心中所有的裝置(以相應的kobject為代表),組織成一個樹狀結構

介紹完以上的背景知識,我們來看device_shutdown的實現,就非常容易了。該介面位於“drivers/base/core.c”中,執行邏輯如下。

   1: /**
   2:  * device_shutdown - call ->shutdown() on each device to shutdown.
   3:  */
   4: void device_shutdown(void)
   5: {
   6:         struct device *dev, *parent;
   7:         
   8:         spin_lock(&devices_kset->list_lock);
   9:         /*
  10:          * Walk the devices list backward, shutting down each in turn.
  11:          * Beware that device unplug events may also start pulling
  12:          * devices offline, even as the system is shutting down.
  13:          */
  14:         while (!list_empty(&devices_kset->list)) {
  15:                 dev = list_entry(devices_kset->list.prev, struct device,
  16:                                 kobj.entry);
  17:  
  18:                 /*
  19:                  * hold reference count of device's parent to
  20:                  * prevent it from being freed because parent's
  21:                  * lock is to be held
  22:                  */
  23:                 parent = get_device(dev->parent);
  24:                 get_device(dev);
  25:                 /*
  26:                  * Make sure the device is off the kset list, in the
  27:                  * event that dev->*->shutdown() doesn't remove it.
  28:                  */
  29:                 list_del_init(&dev->kobj.entry);
  30:                 spin_unlock(&devices_kset->list_lock);
  31:  
  32:                 /* hold lock to avoid race with probe/release */
  33:                 if (parent)
  34:                         device_lock(parent);
  35:                 device_lock(dev);
  36:  
  37:                 /* Don't allow any more runtime suspends */
  38:                 pm_runtime_get_noresume(dev);
  39:                 pm_runtime_barrier(dev);
  40:  
  41:                 if (dev->bus && dev->bus->shutdown) {
  42:                         if (initcall_debug)
  43:                                 dev_info(dev, "shutdown\n");
  44:                         dev->bus->shutdown(dev);
  45:                 } else if (dev->driver && dev->driver->shutdown) {
  46:                         if (initcall_debug)
  47:                                 dev_info(dev, "shutdown\n");
  48:                         dev->driver->shutdown(dev);
  49:                 }
  50:  
  51:                 device_unlock(dev);
  52:                 if (parent)
  53:                         device_unlock(parent);
  54:  
  55:                 put_device(dev);
  56:                 put_device(parent);
  57:  
  58:                 spin_lock(&devices_kset->list_lock);
  59:         }
  60:         spin_unlock(&devices_kset->list_lock);
  61:         async_synchronize_full();
  62: }

1)遍歷devices_kset的連結串列,取出所有的裝置(struct device);

2)將該裝置從連結串列中刪除;

3)呼叫pm_runtime_get_noresume和pm_runtime_barrier介面,停止所有的Runtime相關的電源管理動作(後續的文章會詳細描述有關Runtime PM的邏輯);

4)如果該裝置的bus提供了shutdown函式,優先呼叫bus的shutdown,關閉裝置;

5)如果bus沒有提供shutdown函式,檢測裝置driver是否提供,如果提供,呼叫裝置driver的shutdown,關閉裝置;

6)直至處理完畢所有的裝置。

4.4 system_core_shutdown

system core的shutdown和裝置的shutdown類似,也是從一個連結串列中,遍歷所有的system core,並呼叫它的shutdown介面。後續蝸蝸會專門寫一篇文章介紹syscore,這裡暫不描述。

4.5 machine_restart、machine_halt和machine_power_off

雖然以machine_為字首命名,這三個介面卻是屬於Architecture相關的處理函式,如ARM。以ARM為例,它們在“arch/arm/kernel/process.c”中實現,具體如下。

4.5.1 machine_restart
   1: /*
   2:  * Restart requires that the secondary CPUs stop performing any activity
   3:  * while the primary CPU resets the system. Systems with a single CPU can
   4:  * use soft_restart() as their machine descriptor's .restart hook, since that
   5:  * will cause the only available CPU to reset. Systems with multiple CPUs must
   6:  * provide a HW restart implementation, to ensure that all CPUs reset at once.
   7:  * This is required so that any code running after reset on the primary CPU
   8:  * doesn't have to co-ordinate with other CPUs to ensure they aren't still
   9:  * executing pre-reset code, and using RAM that the primary CPU's code wishes
  10:  * to use. Implementing such co-ordination would be essentially impossible.
  11:  */
  12: void machine_restart(char *cmd)
  13: {
  14:         smp_send_stop();
  15:  
  16:         arm_pm_restart(reboot_mode, cmd);
  17:  
  18:         /* Give a grace period for failure to restart of 1s */
  19:         mdelay(1000);
  20:  
  21:         /* Whoops - the platform was unable to reboot. Tell the user! */
  22:         printk("Reboot failed -- System halted\n");
  23:         local_irq_disable();
  24:         while (1);
  25: }

0)先轉述一下該介面的註釋; 
對於多CPU的機器來說,Restart之前必須保證其它的CPU處於非活動狀態,由其中的一個主CPU負責Restart動作。並且,必須實現一個基於硬體的Restart操作,以保證所有CPU同步Restart,這是設計的重點! 
對於單CPU機器來說,就相對簡單了,可以直接用軟體reset的方式實現Restart。

1)呼叫smp_send_stop介面,確保其它CPU處於非活動狀態;

2)呼叫machine-dependent的restart介面,實現真正的restart。該介面是一個回撥函式,由“arch/arm/kernel/process.c”宣告,由具體的machine程式碼實現。格式如下: 
      void (*arm_pm_restart)(char str, const char *cmd) = null_restart; 
      EXPORT_SYMBOL_GPL(arm_pm_restart);

3)等待1s;

4)如果沒有返回,則restart成功,否則失敗,列印錯誤資訊。

4.5.2 machine_halt

ARM的halt很簡單,就是將其它CPU停下來,並禁止當前CPU的中斷後,死迴圈!確實,中斷被禁止了,又死迴圈了,不halt才怪。程式碼如下:

   1: /*
   2:  * Halting simply requires that the secondary CPUs stop performing any
   3:  * activity (executing tasks, handling interrupts). smp_send_stop()
   4:  * achieves this.
   5:  */
   6: void machine_halt(void)
   7: {
   8:         smp_send_stop();
   9:  
  10:         local_irq_disable();
  11:         while (1);
  12: }
4.5.3 machine_power_off

power off動作和restart類似,即停止其它CPU,呼叫回撥函式。power off的回撥函式和restart類似,就不再說明了。

5. 總結與思考

5.1 Architecture和Machine的概念

本文是我們在分析Linux核心時第一次遇到Architecture和Machine的概念,順便解釋一下。核心程式碼中最常見的目錄結構就是:arch/xxx/mach-xxx/(例如arch/arm/mach-bcm/)。由該目錄結構可知,Architecture(簡稱arch)是指具體的體系結構,如ARM、X86等等。Machine呢,是指具體體系結構下的一個或一系列的SOC,如bcm等。

5.2 電源管理驅動(和reboot有關的部分)需要實現內容

由上面的分析可知,在Reboot的過程中,大部分的邏輯是否核心處理的,具體的driver需要關注2點即可:

1)實現各自的shutdown介面,以正確關閉對應的裝置

2)實現machine-dependent的介面,以確保底層的Machine可以正確restart或者power off

看來還是很簡單的。