SigmaStar Android启动流程
1. 概述¶
本文介绍了SigmaStar Android整体的大致启动流程
2. 总体流程¶
2.1. 总体流程图¶
AOSP的基本启动流程如上图,其中:
-
Boot ROM ~ Bootloader阶段
该阶段的实现是平台强相关的,其主要任务:
-
DDR、Uart等基本设备的初始化。
-
搬移子系统firmware到ddr。
-
引导Bootloder并转跳执行(或者转跳到Firmware由Firmware转跳到Bootloader)。
-
在SigmaStar平台,主要经过ROM Code -> IPL -> IPL_CUST -> TF-A -> U-boot(Bootloader)。
-
-
Bootloader ~ Kernel阶段
该阶段各个厂家可能会选择不同的bootloader,目前主流的UEFI、U-boot、LK(Little Kernel)等,该阶段的主要任务:
-
将下一阶段的Android镜像从Storage中载到内存。
-
校验Android镜像的完整性。
-
解析Android镜像,将Kernel、DTB、Ramdisk拆出来放到内存。
-
跳转到Kernel。
-
-
Kernel阶段
这个阶段就是Kernel的启动阶段,Kernel启动过程中AOSP通用的内核模块将会被加载。Ramdisk将会被挂载成根文件系统(“/”)。内核初始化工作结束后,跟文件系统中的Android First Stage Init(“/init”)将会被执行。
-
First Stage Init阶段
此阶段在是图2-1中的“Init”阶段的一部分流程,该阶段的Init运行在Ramdisk中,它的任务主要是
-
加载/lib/modules中的厂商设备确定(insmod ko)
-
挂载eMMC中的system、vendor、product等系统分区
-
将/system设置为根文件系统(“/”),运行根文件系统中的(“/init”),即Second Stage Init阶段。
-
-
Second Stage Init阶段
此阶段Init的主要任务是解析Android的init.rc文件,入口文件在:/etc/init/hw/init.rc。通过执行init.rc,会带起Android的系统守护进程,例如zygote。
3. BootLoader启动流程¶
本章节会着重介绍U-boot是如何解析Android镜像启动Android Kernel的,对于U-boot本身启动流程不在此赘述。在U-boot启动Android的过程中会涉及到A/B分区的选择、AVB镜像完整性校验、解析boot.img、解析vendor_boot.img等步骤,本章会将这些小流程分开说明。
3.1. A/B分区实现及其相关流程¶
3.1.1. 背景¶
Google为Android设备引入A/B分区的主要目的是实现无缝更新,其实现逻辑大概就是在设备中准备两套系统镜像,暂且称作是slot A和slot B,形如:
如果当前运行的是slot A这组系统镜像时,用户可以在系统运行时通过系统升级APP直接将系统补丁包安装到slot B这组系统镜像,在更新完毕后设定下次启动要从slot B启动即完成了一次所谓的“无缝更新”。
3.1.2. U-boot A/B流程¶
如3.1.1章介绍,U-boot在启动Android时需要确定当前要选择启动过的“slot”
系统会在非易失的存储中保存着一份“slot metadata”,用它来管理当前要从哪个slot启动,“slot metadata”记录了:
-
上一次开机是从哪个slot启动
-
每个slot可以尝试启动的次数
-
每个slot是否成功启动过的状态
-
每个slot是否已经被认为是损坏的状态
根据“slot metadata”,于是在U-boot下就有如下状态机用于决定这次要启动哪个slot,如下图:
图3-1-2: 引导加载程序状态机
可以参考源码:ab_select_slot
3.1.3. 开机过程中遇到“No bootable slot was found”处理¶
由图3-1-2中可知,当“slot metadata”中记录的所有slot都是坏的时候,也就是尝试所有的slot都启动不了之后,将会进入到图3-1-2中橘色的流程中(bootloader recover mode),在SigmaStar平台如下图:
“ANDROID:No bootable slot was found”表示了已经没有可以正常启动的slot,有可能是在启动过程中多次的强制将开发板上下电导致了“retry count”减到了0,可以尝试执行“ab_reset mmc 0#misc”重新初始化ab metadata,如果依然无法正常启动则说明确实有镜像损坏了。这时就需要具体问题具体分析,需要查看前几次重启的开机log,光看吐出“ANDROID:No bootable slot was found”这次的开机log是无法查到原因的。
3.2. AVB2.0(Android Verified Boot 2.0)实现及其流程¶
3.2.1. 背景¶
AVB流程主要用于校验接下来要启动的Android镜像的完整性,防止存储介质中的镜像被篡改之后依然可以正常启动,其实现和常见的镜像安全校验中的“签章-解签”流程基本一致,Google提供了一个通用的镜像加密及校验流程,只需要在AOSP的编译配置文件中简单地配置一些参数,,安全校验中使用到的校验信息的组成大致如下图:
3.2.2. U-boot AVB流程¶
U-boot中的AVB流程有如下的状态机:
3.3. GKI(Generic Kernel Image)启动流程¶
3.3.1. 背景¶
Google在Android 11版本引入了GKI的概念,其目的是为例让所有的Android设备使用的统一由Google Release的Linux Kernel Image,即GKI。于是在AOSP编译出来的镜像中会存在三个相关的镜像:boot.img、vendor_boot.img、vendor.img,其中:
-
boot.img
存放了GKI、GKI ramdisk
-
vendor_boot.img
包含了device-tree(dtb)、vendor ramdisk(ramdisk中包含了vendor fstab和一部分vendor kernel module)
-
vendor.img
包含了一部分vendor kernel module
如此一来,属于厂商的内核设备树、内核驱动模块就都被包含在了vendor_boot和vendor中,属于Google的通用内核以及通用内核模块便包含在了boot中。
那么为何要将厂商的内核模块分别放在vendor_boot和vendor中呢?为什么不全部放在其中的一个镜像中?
首先在AOSP镜像中:
vendor_boot.img是一个简单的镜像格式,类似于uImage。
vendor.img被烧录在外部存储设备中且是一个ext4或f2fs文件系统。
想要通用内核中并不包含厂商的外部存储设备、串口等基本设备驱动,所以在通用内核启动之后需要先加载vendor ramdisk中的基本厂商设备驱动例如eMMC driver才能将vendor以及其他分区挂载上,因此vendor ramdisk必须含有基本的厂商设备驱动。
但是将所有的厂商内核模块都存放在vendor ramdisk会导致vendor ramdisk过大,过大的ramdisk会影响内核启动的速度,于是将一部分可以延迟加载的厂商内核模块放到了vendor中。
3.3.2. 镜像加载启动流程¶
镜像加载流程大致如下图:
经过上图的流程过后,DRAM中的的layout大致如下图:
4. Init启动流程¶
4.1. first stage init¶
4.1.1. first stage init的加载与运行¶
下图介绍了从Kernel一直到“first stage init”的流程:
由3.3章可以了解到,bootloader在跳转到kernel之后,kernel需要先加载所有的内核模块,之后将GKI ramdisk以及vendor ramdisk解压拼成一个完整ramdisk并将该ramdisk挂载到userspace的根目录,最后返回到userpace运行/init,位于ramdisk根目录中的init称之为“first stage init”。
4.1.2. first stage init的流程¶
有3.3.1章及图4-1-1介绍可以了解到,first stage init的主要任务,就是通过insmod命令加载位于/lib/modules目录中的厂商内核驱动模块,有了厂商内核驱动模块的支持就可以正确识别到外部大容量存储设备并将其挂载,所以进接着会解析获取/first_stage_ramdisk/fatab.sstar中带有“first_stage_mount”标记的条目,将这些分区例如system、vendor、product挂载。
到此为止first stage init的任务基本结束,紧接着会将“/system”切换成userpace的根目录,执行/init(/system/bin/init),也就是second stage init。
4.2. second stage init¶
4.2.1. second stage init的主要工作¶
second stage init的主要任务有:
-
Android属性系统的初始化
-
加载Selinux设定
-
加载/etc/init/hw/init.rc
-
触发early-init
-
触发init
-
触发late-init
-
进入死循环,等等待新的trigger触发请求
4.2.2. 基本init.rc语法¶
-
Action(动作)
定义一个action的语法形如:
on <trigger> [&& <trigger>]* <command> <command> <command>
action由一个或多个trigger(触发器)和一个或多个command(命令)组成。trigger的组合表示该action行动的时机,command的组合表示该动作行动时要执行的指令。在init.rc中可以通过命令“trigger”来触发一个action,也可以在init的流程中通过am.QueueEventTrigger("xxx");触发一个action。
例如:
# init.cpp am.QueueEventTrigger("init"); # init.rc on init trigger init_fs on init_fs mkdir /example_dir start /example_service
init.cpp中,入队了名为init的trigger。init.rc中,有一个基于名为init的trigger的action,该action入队了名为init_fs的trigger;有一个基于名为init_fs的trigger的action,该action创建了exapmle_dir目录,并启动了exapmle_service服务。
-
Service(服务)
定义一个service的语法形如:
service <name> <pathname> [ <argument> ]* <option> <option>
service由name(服务名)、pathname(服务的可执行档路径)、argument(可执行档命令行参数)、一个或多个option(选项)组成。serive可以理解成是对守护程序的声明,在init.rc中可以通过命令“start”来启动一个服务。例如:
# init.rc on init start example_service service example_service /vendor/bin/example_service arg1 args class core
在本例中基于“init”这个trigger的action使用命令“start”启动了名为“example_service” 的服务,启动该服务起始就是执行命令:
/vendor/bin/example_service arg1 args
-
Import(导入)
导入的语法很简单,形如:
import <path>
通过导入命令可以将其他的rc文件导入进来。
更多的关于init.rc的语法介绍请参考:https://android.googlesource.com/platform/system/core/+/master/init/README.md
4.2.3. 几个关键的trigger的顺序及其主要任务¶
由4.2.1章已知,second stage init会触发以下几个trigger然后进入到死循环中,执行队列中的action
-
early-init:第一个trigger,它会在cgroups配置完成后且ueventd完成初始化前被触发
-
init:coldboot完成后被触发
-
late-init:当是normal boot时被触发
在这三个trigger中,主要有如下action需要动作:
-
early-fs:启动vold守护进程
-
fs:解析fstab,将没标记上“first_stage”和“latemount”的条目挂载
-
post-fs:完成一些分区挂载的配置
-
late-fs:解析fstab,将标记上“latemount”的条目挂载
-
post-fs-data:挂载data分区,配置data分区加密
-
zygote-start:启动zygote守护进程
-
early-boot:一些zygote启动后需完成的动作
-
boot:一些early-boot完成后需完成的动作