本文作者:江蘇潤和軟體股份有限公司 郎建中
1.總體描述
1.1.總體介紹
啟動恢複負責在核心啟動之後到應用啟動之前的系統關鍵程序和服務的啟動過程。涉及以下子產品:
a) init啟動引導
支援使用LiteOS-A核心的平台,目前包括:Hi3516DV300平台和Hi3518EV300平台。
負責處理從核心加載第一個使用者态程序開始,到第一個應用程式啟動之間的系統服務程序啟動過程。啟動恢複子系統除負責加載各系統關鍵程序之外,還需在啟動的同時設定其對應權限,并在子程序啟動後對指定程序實行保活(若程序意外退出要重新啟動),對于特殊程序意外退出時,啟動恢複子系統還要執行系統複位操作。
b) appspawn應用孵化
支援使用LiteOS-A核心的平台,目前包括:Hi3516DV300平台和Hi3518EV300平台。
負責接受應用程式架構的指令孵化應用程序,設定其對應權限,并調用應用程式架構的入口。
c) bootstrap啟動服務子產品
支援使用LiteOS-M核心的平台,目前包括:Hi3861平台。
提供了各服務和功能的啟動入口辨別。在SAMGR啟動時,會調用boostrap辨別的入口函數,并啟動系統服務。
d) 系統屬性
支援使用LiteOS-M核心和LiteOS-A核心的平台,包括:Hi3861平台,Hi3516DV300平台,Hi3518EV300平台。
負責提供擷取與設定作業系統相關的系統屬性。
系統屬性包括:預設系統屬性、OEM廠商系統屬性和自定義系統屬性。
2.代碼目錄結構
base
├──startup 啟動恢複子系統根目錄
├──── frameworks
│ └── syspara_lite
│ ├── LICENSE 開源LICENSE檔案
│ ├── parameter 系統屬性子產品源檔案目錄
│ │ ├── BUILD.gn
│ │ └── src
│ │ ├── BUILD.gn
│ │ ├── param_impl_hal 系統屬性子產品基于LiteOS-M核實作
│ │ └── param_impl_posix 系統屬性子產品基于LiteOS-A核實作
│ └── token
│ ├── BUILD.gn
│ └── src
│ ├── token_impl_hal
│ └── token_impl_posix
├──── hals
│ └── syspara_lite 系統屬性子產品硬體抽象層頭檔案目錄
├──── interfaces
│ └── kits
│ └── syspara_lite 系統屬性子產品對外接口目錄
└──── services
├── appspawn_lite 應用孵化子產品
│ ├── BUILD.gn 應用孵化子產品編譯配置
│ ├── include 應用孵化子產品頭檔案目錄
│ ├── LICENSE 開源LICENSE檔案
│ ├── moduletest 應用孵化子產品自測試代碼目錄
│ └── src 應用孵化子產品源檔案目錄
├── bootstrap_lite 啟動服務子產品
│ ├── BUILD.gn 啟動服務子產品編譯配置
│ ├── LICENSE 開源LICENSE檔案
│ └── source 啟動服務子產品源檔案目錄
└── init_lite 啟動引導子產品
├── BUILD.gn 啟動引導子產品編譯配置
├── include 啟動引導子產品頭檔案目錄
├── LICENSE 開源LICENSE檔案
├── moduletest 啟動引導子產品自測試代碼目錄
└── src 啟動引導子產品源檔案目錄
vendor
└──huawei
└──camera
└──init_configs 啟動引導子產品配置檔案目錄(json格式,部署于/etc/目錄下)
3.代碼分析
本文主要分析init和Spawn子產品中的流程。主要分析如下幾個部分的流程:
1、init子產品的初始化過程–啟動系統服務
2、init子產品的保活流程
3、appspawn的初始化
4、appspawn的孵化流程
3.1.init子產品的初始化過程–啟動系統服務
init程序是在作業系統啟動後第一個啟動的程序。也是所有其他程序的父程序。
init程序的代碼位置:base/startup/services/init_lite,下面是代碼的目錄結構:
整個目錄将被編譯成一個可執行程式init。下面我們從main.c的main函數開始分析。
main()函數的代碼如下:
上面的代碼注釋已經非常好了,這裡簡單翻譯下。代碼分4個步驟:
1、列印系統資訊。這個很簡單,就是列印一下目前的版本資訊。
2、注冊信号處理。這裡主要是注冊系統信号量的處理函數。
3、讀取配置檔案并按照配置啟動服務
4、進入死循環
下面我們先看看系統信号處理部分的SignalInitModule()函數,代碼如下:
上面的代碼可以看到,這裡是注冊了兩個系統信号的處理。并且處理函數都是SigHandler()。
SIGCHLD信号:在一個程序終止或者停止時,将SIGCHLD信号發送給其父程序。這裡應該是指init程序Fork出來的其他服務程序。
SIGTERM信号:程序終止時收到這個信号。這裡應該是隻init程序本身終止。
SigHandler()函數的代碼如下:
上面的信号處理函數根據信号分别處理:
a、SIGCHLD信号:表示由init程序fork出來的服務程序挂了,那麼執行ReapServiceByPID()函數來重新拉起這個服務
b、SIGTERM信号:表示init自己挂了,那麼調用StopAllServices()退出所有的服務,等待關機或者重新開機硬體了。
ReapServiceByPID()在下一節較長的描述。下面我們再來看看InitReadCfg()函數,代碼如下:
我們先來找一個cfg配置檔案的例子分析下。檔案路徑是./vendor/huawei/camera/init_configs/init_liteos_a_3516dv300.cfg,内如如下:
{
"jobs" : [{
"name" : "pre-init",
"cmds" : [
"mkdir /storage/data/log",
"chmod 0755 /storage/data/log",
"chown 4 4 /storage/data/log",
"mkdir /storage/data/softbus",
"chmod 0700 /storage/data/softbus",
"chown 7 7 /storage/data/softbus",
"mkdir /sdcard",
"chmod 0777 /sdcard",
"mount vfat /dev/mmcblk0 /sdcard rw,umask=000",
"mount vfat /dev/mmcblk1 /sdcard rw,umask=000"
]
}, {
"name" : "init",
"cmds" : [
"start shell",
"start apphilogcat",
"start foundation",
"start bundle_daemon",
"start appspawn",
"start media_server",
"start wms_server"
]
}, {
"name" : "post-init",
"cmds" : [
"chown 0 99 /dev/dev_mgr",
"chown 0 99 /dev/hdfwifi",
"chown 0 99 /dev/gpio",
"chown 0 99 /dev/i2c-0",
"chown 0 99 /dev/i2c-1",
"chown 0 99 /dev/i2c-2",
"chown 0 99 /dev/i2c-3",
"chown 0 99 /dev/i2c-4",
"chown 0 99 /dev/i2c-5",
"chown 0 99 /dev/i2c-6",
"chown 0 99 /dev/i2c-7",
"chown 0 99 /dev/uartdev-0",
"chown 0 99 /dev/uartdev-1",
"chown 0 99 /dev/uartdev-2",
"chown 0 99 /dev/uartdev-3",
"chown 0 99 /dev/spidev0.0",
"chown 0 99 /dev/spidev1.0",
"chown 0 99 /dev/spidev2.0",
"chown 0 99 /dev/spidev2.1"
]
}
],
"services" : [{
"name" : "foundation",
"path" : "/bin/foundation",
"uid" : 7,
"gid" : 7,
"once" : 0,
"importance" : 1,
"caps" : [10, 11, 12, 13]
}, {
"name" : "shell",
"path" : "/bin/shell",
"uid" : 2,
"gid" : 2,
"once" : 0,
"importance" : 0,
"caps" : [4294967295]
}, {
"name" : "appspawn",
"path" : "/bin/appspawn",
"uid" : 1,
"gid" : 1,
"once" : 0,
"importance" : 0,
"caps" : [2, 6, 7, 8, 23]
}, {
"name" : "apphilogcat",
"path" : "/bin/apphilogcat",
"uid" : 4,
"gid" : 4,
"once" : 1,
"importance" : 0,
"caps" : []
}, {
"name" : "media_server",
"path" : "/bin/media_server",
"uid" : 5,
"gid" : 5,
"once" : 1,
"importance" : 0,
"caps" : []
}, {
"name" : "wms_server",
"path" : "/bin/wms_server",
"uid" : 0,
"gid" : 0,
"once" : 1,
"importance" : 0,
"caps" : []
}, {
"name" : "bundle_daemon",
"path" : "/bin/bundle_daemon",
"uid" : 8,
"gid" : 8,
"once" : 0,
"importance" : 0,
"caps" : [0, 1]
}
]
}
首先可以看出來這段代碼是JSON格式,分為兩個大段:Jobs和Services。
Jobs是指init要做的事情的集合。并且Jobs被分為三個階段,每一個階段包含一些指令cmd。
1、pre-init階段:調用mkdir,chmod,chown,mount等指令做一些初始化動作。
2、init階段:啟動服務,包括7個服務:shell、apphilogcat(log服務)、foundation(裡面有ability,dms等服務)、bundle_daemon(包管理看護)、appspawn(應用孵化)、media_server(媒體服務)、wms_server(視窗管理,内含IMS服務)。
3、post-init階段:使用chown改變一些裝置節點的權限。
Services是指在init階段用start指令啟動的服務的具體資訊,包括:服務的名字、執行檔案路徑、啟動時用user身份的id、是否once(一次性啟動)、是否important(重要服務挂了後整個系統需要reboot),caps權限等資訊。
結合InitReadCfg()函數我們可以大體知道這個函數的功能大體分兩部分,1、讀取配置檔案并解析JSON的文法格式。從配置檔案中讀取所有的Services資訊和Jobs資訊。2、執行Jobs的三個階段,并且從代碼上可以看到這三個階段的名稱是hard code寫死的。
ReadFileToBuf()這個函數不解析了,就是通過fopen,fread等函數将檔案内容讀入到記憶體中。然後通過cJSON_Parse()函數解析成JSON的格式資料。
我們來看看ParseAllServices()函數,代碼如下:
上面的代碼分兩部分:1、通過JSON格式資料解析配置檔案中的Service,并把結果放在Service數組中。2、調用RegisterServices()函數進行注冊。
我們看到注冊僅僅是将這個數組的位址儲存在g_services中,這個後續會介紹到。
InitReadCfg()中調用的ParseAllJobs()函數與ParseAllServices()類似,這裡不做介紹了。
下面我們來分析下DoJob()函數。
DoJob()函數傳入參數是jobName,也就是寫死的 “pre-init”, “init”, “post-init”三個子串。通過ParseAllJobs()函數,在配置檔案中的Jobs段的所有記憶體被解析到g_jobs數組中,這裡可以看出g_jobCnt應該是3,對應Jobs下的三個Job。g_jobs[0]、g_jobs[1]、g_jobs[2]分别對應了“pre-init”, “init”, “post-init”三個Job。是以代碼中的兩層循環的第一層就是查找與傳入參數jobName比對的那個g_jobs[]元素。然後内部循環執行特定Job下的所有的cmd。
上面在分析cfg配置檔案的時候已經對三個階段的job大緻做了分析,目前整個系統支援的cmd隻有5個,如下表:
我們來看看DoCmd()函數的實作:
從代碼中可以看出,目前支援的cmd都是寫死的,後續也許會有增加。
我們重點分析下start這個cmd。在init階段用這個指令啟動了7個服務。DoStart()函數的代碼如下:
StartServiceByName()中調用的FindServiceByName()就是從g_services[]數組中查詢名稱為servName的服務的資訊,這些資訊包括:服務應用的具體路徑、是否once、是否important等待。
而ServiceStart()函數就是啟動服務的函數了。我們看看代碼:
ServiceStart()函數主要就是通過fork()函數克隆出一個新的子程序。fork()調用後,系統會傳回兩次,一次在父程序中(init程序中),傳回的pid就是克隆出來的子程序的pid。另一次在子程序中,pid為0。是以在if(pid == 0)的判斷中的語句都是在子程序中執行的,主要是調用了execve()函數加載可執行程式,這裡傳入了service-path就是從配置檔案中解析出來的路徑。加載後直接用_exit()退出了。而if語句外面的 service-pid = pid就是在父程序(init程序)中執行,将剛剛fork出來的子程序pid儲存在service數組中,後續在保活流程中要使用的。
3.2.init子產品的保活流程
在前面的初始化流程中有過介紹,在main函數中調用了SignalInitModule()函數注冊了兩個信号的處理,分别是SIGCHLD、SIGTERM。其中SIGCHLD就是子程序挂了後會發給父程序的。這裡的子程序就是init程序通過配置檔案start起來的服務,前面介紹的配置中一共拉起了7個服務,這7個服務構成了整個輕量級裝置的軟體運作支撐環境。如果這7個服務中的一個挂了,那麼需要有保活守護機制來處理問題,類似Android中的watchdog。
我們在上面的分析已經知道SIGCHLD的處理函數SigHandler()函數中會調用ReapServiceByPID()函數來進行保活處理。
我們來看看代碼:
g_services[]數組就是分析配置檔案後形成的Service的資訊,裡面的pid就是在start指令拉起服務後fork出來的子程序pid。for循環就是查找g_services[]數組中比對pid的項,然後看看是否important,如果是那麼說明重要服務挂了,系統需要重新開機。如果不是,那麼調用ServiceReap()函數重新拉起服務。
ServiceReap()代碼如下:
上面的代碼邏輯如下:
1、判斷是否是主動被停止的程序,如果是則直接傳回了。
2、判斷是否是once,并且不需要重新開機的服務,如果是則直接傳回。
3、判斷是否是不需要重新開機的服務,如果是記錄重新開機的次數,如果重新開機次數達到上限則不再重新開機。
4、上述以外的情況屬于一定要重新開機的服務。
5、調用ServiceStart()函數程序重新開機。
3.3.appspawn的初始化
AppSpawn是一個獨立的程序,由init程序初始化階段拉起。AppSpawn是應用孵化器,作用是Fork出AbilityMain程序。整個系統架構圖如下:
前面的分布式排程中有介紹最後通過ams拉起FA,ams則是通過AppSpawn來孵化出(Fork出)一個AbilityMain程序。而AbilityMain就是JS應用的native執行個體,裡面包含了ACE(JS應用開發架構,其中調用的JerryScript引擎跑JS代碼)、graphic_ui部件等。另外AbilityMain還通過IPC通訊到wms_server程序(這個程序也是通過init程序拉起的服務),wms_server程序管理視窗和輸入。
上面的圖中隻有AbilityMain是由AppSpawn拉起的,其他都是由init拉起的系統服務。反過來說AppSpawn隻負責拉起AbilityMain程序,是所有AbilityMain(應用程序)的父程序。
AppSpawn的代碼路徑:base/startup/services/appspawn_lite
目錄結構:
下面我們看下AppSpawn程序的初始化過程,從main.c的main()函數開始,代碼如下:
代碼主要分三個部分:
1、初始化系統服務架構,初始化AppSpawn服務
2、注冊SIGCHLD
3、進入死循環
其中注冊的SIGCHLD的處理函數SignalHandler()函數代碼如下:
上面的代碼可以看到在Ability退出後,AppSpawn作為父程序收到了SIGCHLD信号,但是什麼都沒做。
是以我們重點分析下HOS_SystemInit()函數,代碼如下:
實際上上述代碼是一個标準的形式,在wms_server和bundle_daemon服務中都會有類似的函數,都是調用了SAMGR_Bootstrap(),這個地方就是初始化系統服務子系統的架構。我們在《分布式排程子系統》中有過初步分析,這裡不再詳細介紹。samgr架構初始化過程中會初始化注冊的服務,在我們的AppSpawn中,注冊的服務在appspawn_service.c中,代碼如下:
SYSEX_SERVICE_INIT()宏定義在.\utils\native\lite\include\ohos_init.h中,與以前介紹過的SYS_SERVICE_INIT()宏類似,這裡不再做詳細介紹。由這個宏定義的函數,會在main()函數調用前被執行。
我們來看看AppSpawnInit()函數的實作,主要就是向samgr注冊了服務和Feature。注冊的服務結構體如下:
其中的Initialize()會在初始化階段被調用。Invoke會在遠端IPC中被調用。
我們先看看Initialize()函數,下一節分析下Invoke()函數。
看起來隻是設定了服務的辨別符,其他沒做什麼。
3.4.appspawn的孵化流程
在分布式調用拉起FA的流程中,dms(分布式排程子系統)通過ams(Ability Manager Service)來拉起FA,最終調用的是 amsInterface->StartAbility()函數。這裡的amsInterface就是ams服務的FeatureApi接口。在AMS中,最終通過AppManager::StartAppProcess()函數(foundation\aafwk\services\abilitymgr_lite\src\app_manager.cpp)通過調用AppSpawnClient::SpawnProcess()函數(foundation\aafwk\services\abilitymgr_lite\src\client\app_spawn_client.cpp)來實作IPC遠端調用到AppSpawn服務中。這個過程在其他文章中有涉及,這裡不再介紹。
AppManager::StartAppProcess()函數部分代碼如下:
代碼中可以看出最終調用了Invoke。實際上是通過IClientProxy->Invoke()遠端調用到了IServerProxy->Invoke()。而在Server端的Invoke實作就是appspawn_serivce.c中的Invoke()函數。代碼如下:
代碼分三個部分:
1、參數校驗:參數的funcId必須是ID_CALL_CREATE_SERVICE,這與AppManager::StartAppProcess()函數中的調用是一緻的。
2、IPC的消息接收和解析:這部分大家自己看一下。
3、功能實作:調用CreateProcess()函數
下面我們看下CreateProcess()的代碼:
這個代碼與init程序中ServiceStart()函數實作基本結構一緻,都是fork()出一個子程序,然後在子程序中調用execve()加載可執行程式。差別是這裡加載的可執行程式是寫死的ABILITY_EXE_FILE_FULL_PATH宏,定義為"/bin/abilityMain"(由foundation/aafwk/frameworks/ability_lite目錄編譯出來)。是以,結論就是AppSpawn的唯一任務就是fork出abilityMain程序,而這個abilityMain是所有js應用的native載體,負責Ability的生命周期管理、JerryScript引擎的加載、App中的JS的加載和初始化、JS中定義的component元件的建立和事件的對接等等。