Android系统【分区与升级】
本文的内容基于高通开源Android Q(10.0),部分内容更新至Android S(12.0),只涉及high-level-operating-system(Android)部分。
一、传统分区结构(non-A/B)
1、首先简单梳理一下Android传统的分区结构。
bootloader:设备启动后,会先进入bootloader程序,这里会通过判断开机时的按键组合(也会有一些其他判断条件,暂不赘述)选择启动到哪种模式,这里主要有Android系统、recovery模式、fastboot模式等。 |
boot:包含有Android系统的kernel和ramdisk。如果bootloader选择启动Android系统,则会引导启动此分区的kernel并加载ramdisk,完成内核启动。 |
system:包含有Android系统的可执行程序、库、系统服务和app等。内核启动后,会运行第一个用户态进程init,其会依据init.rc文件中的规则启动Android系统组件,这些系统组件就在system分区中。将Android系统组件启动完成后,最后会启动系统app —— launcher桌面,至此完成Android系统启动。 |
vendor:包含所有不可分发给 Android 开源项目 (AOSP) 的二进制文件,有厂商私有的可执行程序、库、系统服务和app等。可以将此分区看做是system分区的补充,厂商定制ROM的一些功能都可以放在此分区。 |
userdata:用户存储空间。一般新买来的手机此分区几乎是空的,用户安装的app以及用户数据都是存放在此分区中。用户通过系统文件管理器访问到的手机存储(sdcard)即此分区的一部分,是通过fuse或sdcardfs这类用户态文件系统实现的一块特殊存储空间。 |
recovery:包含recovery系统的kernel和ramdisk。如果bootloader选择启动recovery模式,则会引导启动此分区的kernel并加载ramdisk,并启动其中的init继而启动recovery程序,至此可以操作recovery模式功能(主要包括OTA升级、双清等)。 |
cache:主要用于用于存储系统或用户应用产生的临时数据. |
misc:misc 分区供恢复映像使用,存储空间不能小于 4KB,主要用于Android系统和bootloader通信,使Android系统能够重启进入recovery系统并执行相应操作。 |
ramdisk.img:(内存磁盘镜像)是根文件系统:android启动时 首先加载ramdisk.img镜像,并挂载到/目录下,并进行了一系列的初始化动作,包括创建各种需要的目录,初始化console,开启服务等,尽管ramdisk.img需要放在Linux内核镜像(boot.img)中,但却属于Android源代码的一部分。 |
vbmetal: 用于安全验证,bootloader验证vbmeta的签名,再用vbmeta的key以及hash值验证dtbo/boot/system/vendor |
recovery: recovery分区的镜像,一般用作系统恢复(刷机)。 |
2、ramdisk、boot.img、recovery.img之间的关系
-
ramdisk.img会被打包到boot.img和recovery.img中 (不是同一个ramdisk.img).在不同分区中的作用不同。
-
ramdisk.img中比较重要的文件是"init",“init.rc”,其中init是system/core/init/init.c编译而来,boot.img中ramdisk里的init.rc位于system/core/init/init.rc,而recovery.img中ramdisk里的init.rc位于bootable/recovery/etc/init.rc。
-
kernel加载结束以后第一个进程是执行init,init会解析init.rc文件,并起相应的服务。由此可以知道正常开机和进入recovery模式起的进程是不同的。
-
从本质上说,recovery.img和boot.img高达90%是一样的。这就意味着,recovery.img也是Linux内核(zImage)和内存磁盘镜像(ramdisk.img)组成的。这两个镜像中的Linux内核是完全一样的,区别只是ramdisk.img中的少部分文件存在差异。其中最主要的差异是recovery.img和ramdisk.img中的sbin目录中多了一个recovery命令进入Recovery主界面,而不会正常启动Android系统。实现的原理是Recovery.img和boot.img在自己的分区各自有一个Linux内核(zImage),尽管Linux内核都一样,但Linux内核调用的init命令解析的init.rc及其相关文件的内容有一定的差异。而Bootloader根据用户的选择决定使用boot.img中Linux内核,还是使用Recovery.img中的Linux内核启动系统。如果使用前者,Android系统就会正常启动,如果使用后者,就会进入Recovery选择菜单,所以recovery.img和boot.img的第二个差异就是其中的init.rc及其相关配置文件的内容略有不同。
3、OTA升级流程
传统分区结构下,系统的比较简单,主要过程如下:
- Android系统收到服务端下发的OTA推送,将OTA包下载至cache分区。
- OTA包下载完成后,将向misc分区写入指令,表明下次启动时进入recovery模式并使用该OTA包进行升级。
- 重启手机。
- 重启后最先进入bootloader,bootloader会先判断按键组合、电源寄存器等,随后会读取misc分区的内容并解析。由于步骤2中已经向misc分区写入了指令,此处bootloader读取指令后会引导启动recovery系统。
- 进入recovery,读取cache分区中的OTA包,并解析其中的升级脚本,按照其指令对系统各个分区进行升级。如果recovery自身也需要升级,会在此过程中向system中写入recovery-from-boot.p文件,这是一个recovery升级所需要的patch。
- recovery会清除misc分区。
- 重启手机。
- 重启后最先进入bootloader,判断按键组合、电源寄存器、misc分区内容等,默认情况会启动Android系统,此时已经是OTA升级后的新版本系统。
- 新版本Android系统启动后,会检查是否存在recovery-from-boot.p文件,如果存在,则会对recovery进行升级。
4、查看分区的命令
二、A/B分区结构
在Android O之后,Google引入了一种新的分区结构,称为A/B分区,与之对应,传统分区结构被称为non-A/B分区。
A/B分区结构,顾名思义,将系统分区分成了A和B两个槽(slot),手机启动时会选择A槽或者B槽启动,运行过程中仅使用当前槽位的分区。一旦当前运行的槽出现问题,系统仍可以选择另一个槽进行启动,从而保证系统良好的可用性。
采用A/B分区结构,能够实现无缝升级。例如用户正在运行A槽,此时收到OTA推送,则系统会在后台一边下载OTA数据,一边同时对B槽进行升级。当B槽系统升级完成,用户会收到重启提示,此时重启手机将自动切换到B槽的新版本系统。在此过程中,仅重启操作是会被用户感知的,这个重启与普通重启的耗时没有什么区别。
如果OTA失败,也仅仅是待升级的槽出现问题,可以重新尝试OTA,并不会影响用户当前运行的系统。
由于A/B分区结构可以实现一边从服务端获取OTA数据,一边直接写入待升级的槽,不需要临时存储OTA包的空间,因此不再需要在cache或userdata分区预留足够空间。
Google定义了A/B槽的几种标识:
- bootable:标识该槽的系统是否可以启动。有时也用unbootable来标识(例如高通),含义与bootable相反。
- successful:标识该槽的系统是否成功启动过,仅当该槽系统能够启动、运行、进行OTA升级时,才会从用户态标记该槽为successful。
- active:标识该槽是否是当前运行的系统,两个槽中只有一个能标记为active。
系统重启后,在zygote启动前,init会调用update_verifier服务通过dm-verity机制校验本次升级的镜像,通过后则会被标记为successful。如果系统当前active的槽反复多次启动都没能标记为successful,则将该槽标记为unbootable,并将另外一个槽标记为active。
下面看看A/B分区结构发生了哪些变化:
bootloader:功能同non-A/B的bootloader,只是此处会根据A/B槽的bootable、successful、active等标识来选择启动哪个槽。根据不同厂商的实现,可以是唯一的不区分A/B的bootloader,也可以自定义,例如高通的实现bootloader是由唯一的pbl(此分区无法擦写)来选择A/B槽,启动xbl_a/xbl_b,再启动abl_a/abl_b。 |
boot_a/boot_b:包含kernel和recovery的ramdisk。recovery打包在boot分区中,则不再需要recovery分区。并且recovery系统也不再负责OTA升级(由Android系统中update_engine服务负责),仅负责双清等其他操作。 |
system_a/system_b:功能同non-A/B的system分区,只是区分了A和B两个槽。 |
vendor_a/vendor_b:功能同non-A/B的vendor分区,只是区分了A和B两个槽。 |
userdata:功能同non-A/B的userdata,并且用户数据仅存储一份,不区分A/B。 |
misc:功能同non-A/B的misc,不区分A/B。 |
persist:用来存储一些持久化数据,不会随着双清、OTA等操作被清除。不区分A/B。 |
这里需要说明一下,recovery集成在boot中,是由TARGET_NO_RECOVERY和BOARD_USES_RECOVERY_AS_BOOT等变量决定的。如下图所示为Google官方对于A/B分区结构的配置:
三、Android Q的改动
SSI
为了进一步让原生Android和各厂商的软件进行解耦,使设备代码的移植(系统大版本的升级)变得更加友好,Google引入了SSI(Shared System Image)的概念。SSI是指,在Android版本相同的情况下,各个Android设备的ROM镜像中,system.img都是由该版本的原生AOSP(或厂商定制)代码编译出的,是多产品共同使用的、与具体硬件设备无关的系统镜像。而且他与具体硬件设备、产品型号有关的代码会被放在其他分区中。
简单描述一下super物理分区中包含的几个逻辑分区的内容:
逻辑分区名 | 内容描述 |
---|---|
system(SSI) | 通用Android系统组件,原则上不同厂商、不同型号的设备都通用 |
product | 与特定产品有关的模块,包括对于Android系统的定制化 |
vendor | 厂商对系统的定制,例如高通对Android系统附加的功能,包括一些闭源的程序等 |
odm | 可选的分区,包含SOC附加的程序,这些内容可以集成在vendor中,也可以单独分离出odm分区 |
动态分区
自Android Q(10.0)以后,系统支持动态分区(dynamic partition),它将多个系统只读分区(包括system、product、vendor、odm或者其他厂商自定义分区)合并为一个super分区。物理分区只有super分区的概念,而没有system等分区。因此使用fastboot刷机时,是无法直接通过命令
fastboot flash system system.img
进行刷机的,而只能刷写super分区:
fastboot flash super super.img
super分区不区分A/B槽,一个super分区中包含了A、B两套只读分区,并且其中的只读分区也不需要显式指定分区大小,因为他们是在用户态中动态分配挂载的,只要这些只读分区的总大小不超过super分区的物理大小即可。
另一方面,考虑到有些场景下还是需要单独刷写system或其他super中的逻辑分区,Android新增了一个用户态的fastbootd程序,用来在用户态中完成这个工作,有兴趣的读者可以自行查阅相关资料。
有时候,我们在调试阶段也需要修改system等分区中的内容,通常会采用以下方式写入:
# 获取root权限
adb root
# 关闭dm-verity
adb disable-verity
# 重启使生效
adb reboot
# 获取root权限
adb root
# 重新以rw方式挂载
adb remount
由于动态分区中每个分区是连续排列,不需要预留空间的,因此写入时可能会造成空间不足。为了解决这个问题,引入了overlay文件系统,保证对原分区的修改在upper层中(挂载在进行,从而不影响原分区数据。有兴趣的读者可以自行查阅相关资料。
system-as-root
Android Q中强制使用system-as-root(SAR)分区布局方案,但动态分区不能使用system-as-root,这里比较容易迷惑,我阐述一下我的理解。
令人迷惑的官方文档1
令人迷惑的官方文档2
在Android N/O开始,Android引入了system-as-root概念。当时还没有A/B分区结构,没有动态分区,仍然是传统的分区结构。而SAR是将传统的分区结构基础上,在ROM打包阶段,将rootfs不再位于boot.img的ramdisk中,而是与system合并放在的system.img中,这样系统启动时如果启动Android系统(而非recovery),kernel将直接挂载system.img作为rootfs。
是否使用SAR布局的system.img的差别
对于Android Q,必须使用SAR分区布局,默认强制在打包时将rootfs与system合并。而SAR有两种:
- legacy system-as-root(LSAR):上述非动态分区的方案,在Android Q以前使用(主要是Android P)。
- two-stage-init(2SI):Android Q上新开发的动态分区的方案,支持非动态分区与动态分区两种。
如果ROM配置为非动态分区,则kernel启动后,在ramdisk中存在first stage init执行程序,运行后,它将system.img挂载至/system,再通过switch root将/system改为/,从而ramdisk完成了它的使命,结束了生命周期。
如果ROM配置为动态分区,则kernel启动后,挂载first_stage_ramdisk执行其中的first stage init,挂载super分区中的用户态system逻辑分区,再通过switch root将/system改为/,并结束ramdisk生命周期。随后,运行/init -> /system/bin/init程序,即second stage init,启动Android系统。
对于Android Q,BOARD_BUILD_SYSTEM_ROOT_IMAGE配置仅决定:
- true:直接由kernel挂载物理system分区
- false:由first stage init程序挂载用户态system逻辑分区
通过以上的分析也不难理解:如果同时设置BOARD_BUILD_SYSTEM_ROOT_IMAGE=true和PRODUCT_USES_DYNAMIC_PARTITIONS=true,会导致打包错误。
结语
Android分区结构众多,总体可以按照两个维度划分:
- 是否使用A/B分区结构
- 是否使用动态分区结构
随着Android版本的迭代,其分区结构也在快速演进,本文也仅展示了其中冰山一角,与其相关的AVB、dm-verity、selinux等等诸多知识点也都没有提及,有需要的读者可以阅读更专业的文档。
对于Android应用开发人员和Framework开发人员来说,对于系统分区结构的细节可能不必过多深入,我们了解系统分区的总体设计思想,能够更好的帮助理解Android系统的运行机制,解决开发中遇到的问题。