HarmonyOS启动恢复之init和AppSpawn
作者:互联网
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 ativeliteincludeohos_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()函数(foundationaafwkservicesabilitymgr_litesrcapp_manager.cpp)通过调用AppSpawnClient::SpawnProcess()函数(foundationaafwkservicesabilitymgr_litesrcclientapp_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组件的创建和事件的对接等等。