A/B System 概述
Android从7.0开始,引入了新的OTA升级方式 A/B System Updates
A/B系统是指设备上有A和B两套可以工作的系统(用户数据只有一份,为两套系统共用),我们可以理解为一套系统分区,另外一套为备份分区.其系统版本可能一样,也可能不一样;通过升级,可以将旧版本也更新为新版本.当然,设备出厂时这两套系统肯定是一样的.
Android 7.0上传统OTA方式和新的A/B系统方式都存在,编译时只能选择其中的一种OTA方式.由于A/B系统在分区上与传统OTA的分区设计不一样,二者无法兼容,所以7.0以前的系统无法通过OTA方式升级为A/B系统.
7.0以前传统的OTA方式:
设备上有一个Android主系统和一个Recovery系统,Android主系统运行时检测是否需要升级,如果需要升级,
则将升级的数据包下载并存放到cache分区,重启系统后进入Recovery系统,并用cache分区下载好的数据更新Android主系统,
更新完成后重新启动进入Android主系统。如果更新失败,设备重启后就不能正常使用了,唯一的办法就是重新升级,直到成功为止。
而A/B系统主要由运行在Android后台的update_engine和两套分区‘slot A’和‘slot B’组成。
Android系统从其中一套分区启动,在后台运行update_engine监测升级信息并下载升级数据,
然后将数据更新到另外一套分区,写入数据完成后从更新的分区启动
与传统OTA方式相比,A/B系统的变化主要有:
1、系统的分区设置
传统方式只有一套分区
A/B系统有两套分区,称为slot A和slot B
2、跟bootloader沟通的方式
传统方式bootloader通过读取misc分区信息来决定是进入Android主系统还是Recovery系统
A/B系统的bootloader通过特定的分区信息来决定从slot A还是slot B启动
3、系统的编译过程
传统方式在编译时会生成boot.img和recovery.img分别用于Android主系统和Recovery系统的ramdisk
A/B系统只有boot.img,而不再生成单独的recovery.img
4、OTA更新包的生成方式
A/B系统生成OTA包的工具和命令跟传统方式一样,但是生成内容的格式不一样了
A/B 系统更新可带来以下好处:
OTA 更新可以在系统运行期间进行,而不会打断用户。用户可以在 OTA 期间继续使用其设备。在更新期间,唯一的一次宕机发生在设备重新启动到更新后的磁盘分区时。
更新后,重新启动所用的时间不会超过常规重新启动所用的时间。
如果 OTA 无法应用(例如,因为刷机失败),用户将不会受到影响。用户将继续运行旧的操作系统,并且客户端可以重新尝试进行更新。
如果 OTA 更新已应用但无法启动,设备将重新启动回旧分区,并且仍然可以使用。客户端可以重新尝试进行更新。
任何错误(例如 I/O 错误)都只会影响未使用的分区组,并且用户可以进行重试。由于 I/O 负载被特意控制在较低水平,以免影响用户体验,因此发生此类错误的可能性也会降低。
更新包可以流式传输到 A/B 设备,因此在安装之前不需要先下载更新包。流式更新意味着用户没有必要在 /data 或 /cache 上留出足够的可用空间来存储更新包。
缓存分区不再用于存储 OTA 更新包,因此无需确保缓存分区的大小要足以应对日后的更新。
dm-verity 可保证设备将使用未损坏的启动映像。如果设备因 OTA 错误或 dm-verity问题而无法启动,则可以重新启动到旧映像。(Android 验证启动不需要 A/B 更新。)
A/B 更新对 2016 Pixel 分区大小有什么影响?
[图片上传失败...(image-89986-1566889673273)]
OTA 系统分区
在非 A/B 系统Android设备上,闪存空间通常包含以下分区:
boot
boot分区中包含了Linux内核和最小的根文件系统(会被加载到RAM中去).它装载了系统和其它分区,并且boot分区还被用来启动system分区中的运行环境.
system
system分区中包含在 Android 开源项目 (AOSP)上提供源代码的系统应用和库.在正常操作期间,此分区被装载为只读分区;其内容仅在 OTA 更新期间更改.
vendor
vendor分区中包含在 Android 开源项目 (AOSP) 上未提供源代码的系统应用和库.在正常操作期间,此分区被装载为只读分区;其内容仅在 OTA 更新期间更改.
userdata
存储由用户安装的应用所保存的数据等.OTA 更新过程通常不会触及该分区.
cache
几个应用使用的临时保留区域(访问此分区需要使用特殊的应用权限),用于存储下载的 OTA 更新包。其他程序也可使用该空间,但是此类文件可能会随时消失。安装某些 OTA 更新包可能会导致此分区被完全擦除。缓存还包含 OTA 更新的更新日志。
recovery
包含第二个完整的 Linux 系统,其中包括一个内核和特殊的恢复二进制文件(该文件可读取一个软件包并使用其内容来更新其他分区)。
misc
执行恢复操作时使用的微小分区,可在应用 OTA 更新包并重新启动设备时,隐藏某些进程的信息。
A/B系统的分区
bootloader
存放用于引导linux的bootloader
boot_a和boot_b
分别用于存放两套系统各自的linux kernel文件和用于挂载system和其他分区的ramdisk
system_a和system_b
Android主系统分区,分别用于存放两套系统各自的系统应用程序和库文件
vendor_a和vendor_b
Android主系统分区, 分别用于存放两套系统各自的开发厂商定制的一些应用和库文件,很多时候开发厂商也直接将这个分区的内容直接放入system分区
userdata
用户数据分区,存放用户数据,包括用户安装的应用程序和使用时生成的数据
misc或其他名字分区
存放Android主系统和Recovery系统跟bootloader通信的数据,由于存放方式和分区名字没有强制要求,所以部分实现上保留了misc分区(代码中可见Brillo和Intel的平台),另外部分实现采用其他分区存放数据(Broadcom机顶盒平台采用名为eio的分区)。
两者区别为: A/B系统boot,system和vendor分区从传统的一套变为两套,叫做slot A和slot B;并且不再需要cache和recovery分区,同时misc分区也不是必要的
Tips:
什么是Bootloader?
在嵌入式操作系统中,Bootloader在操作系统内核运行之前运行,可以初始化硬件设备、建立内存空间映射图,为调用操作系统内核
准备好正确的环境。Bootloader和硬件是相关的,且厂商一般都会对bootloader加锁,这样就不能随便刷机了。
当然bootloader也是可以解锁的,这里不得不提一下root和bootloader解锁分别是怎么一回事:root是通过内核漏洞获取最高的权
限,也就是所谓的超级用户(su,superuser),属于系统层面,root之后就可以修改system分区的数据;bootloader解锁则属于硬
件层面的解锁boot和recovery分区,解锁bootloader不会root手机
Fastboot和recovery的区别?
Bootloader过程中,先做一些初始化,然后根据组合键做不同的事情,这个过程内核没有加载,机器只是在按顺序执行指令。
Fastboot:在这种模式下,可以修改手机的硬件,并且允许我们发送一些命令给Bootloader。如使用电脑刷机,则需要进入fastboot
模式,通过电脑执行命令将系统镜像刷到通过USB刷到手机中。
Recovery:Recovery是一个小型的操作系统,并且会加载部分文件系统,这样才能从sdcard中读取升级包。
OTA 升级流程
非A/B系统OTA 更新包含以下步骤:
设备会与 OTA 服务器进行定期确认,并被告知是否有更新可用,包括更新软件包的 URL 和向用户显示的描述字符串。
将更新下载到缓存或数据分区,并根据 /system/etc/security/otacerts.zip 中的证书验证加密签名。系统提示用户安装更新。
设备重新启动进入恢复模式,引导恢复分区中的内核和系统启动,而非引导分区中的内核。
恢复分区的二进制文件由 init 启动。它会在 /cache/recovery/command 中寻找将其指向下载软件包的命令行参数。
恢复操作会根据 /res/keys (包含在恢复分区中的 RAM 磁盘的一部分)中的公钥来验证软件包的加密签名。
从软件包中提取数据,并根据需要使用该数据更新引导、系统和/或供应商分区。系统分区上其中一个新文件包含新恢复分区内容。
设备正常重启。
1.加载最新更新的引导分区,在最新更新的系统分区中装载并开始执行二进制文件。
2.作为正常启动的一部分,系统会根据所需内容(预先存储为 /system 中的一个文件)检查恢复分区的内容。二者内容不同,所以恢复分区会被所需内容重新刷写(在后续引导中,恢复分区已经包含新内容,因此无需重新刷写)。
系统更新完成!更新日志可以在 /cache/recovery/last_log.# 中找到。
A/B系统的启动:
[图片上传失败...(image-b87734-1566889673273)]
1.手机启动后,BootLoader会去读取slot metadata,来确定从哪个slot启动
2.检查是否有可启动的分区,如果没有则,直接进入bootloader的recovery mode(即bootloader下的刷机模式),一般是进入fastboot命令行.
3.如果有可启动的分区,则选择可启动分区中优先级最高的slot(例如,直接选择当前设置为active的分区)
4.检查所选择分区的retry count(retry count表示当前分区可以尝试启动的次数),
如果retry count等于0启动成功(启动成功的分区会标记为successful),大于0则继续尝试从当前分区启动,并且累计递减,
而小于0则将所选择分区标记为无效分区(通常设置为unbootable),然后重复第2步,查找下一个可以启动的分区
5.最后从当前slot的boot中启动Linux内核,并且挂载system和其他分区
6.Linux启动后,通过dm-verify机制校验system分区,完成后加载system分区内包含的rootfs,通过/init程序解析/init.rc脚本,完成Android系统的启动
参考内容:
相关链接直达:
ls dev/block/bootdevice/by-name/
应用安装OTA 包需用用到的类:
frameworks/base/core/java/android/os/RecoverySystem.java
校验
应用可调用RecoverySystem 的静态方法verifyPackage
安装
应用可调用RecoverySystem 的静态方法installPackage
手机启动模式检测
机器启动时,首先检测是否有组合键按下,如检测到(音量下+power)组合键,则进入recovery;否则检测系统的/misc分区,根据此分区存储的命令选择不同的模式。
/misc分区下存储着结构体bootloader_message,称之为BCB块,其定义如下:
struct bootloader_message{
char command[32]; //存放不同的启动命令,如果command为空:正常启动机器;如果是boot-recovery,系统会进入Recovery模式;如果是update-radia或update-hboot:刷固件
char status[32]; //存放执行结果
char recovery[1024]; //存放/cache/recovery/command中的命令
};
recovery[1024]中则存放着升级包路径,其存储结构如下:第一行存放字符串“recovery”;第二行存放路径信息“--update_package=/sdcard/update.zip”等。
除了BCB块外,还可以将路径信息--update_package=/sdcard/update.zip写入文件/cache/recovery/command传递给recovery模式。
进入recovery模式后,系统通过get_args函数获取升级包信息。此函数首先获取BCB块信息,如果未检测到相关信息,则继续检测/cache/recovery/command文件;最后,将启动命令boot-recovery及升级包路径--update_package=/sdcard/update.zip重新写入到BCB块中,以便系统下次启动时再次进入到recovery模式,直到升级成功后执行finish_recovery函数清空BCB及/cache/recovery/command文件。
OTA差分包升级失败
升级失败log如下:
I update_engine: [0530/162336:INFO:delta_performer.cc(359)] Applying 21701 operations to partition "system"
E update_engine: [0530/162336:ERROR:delta_performer.cc(1060)] The hash of the source data on disk for this operation doesn't match the expected value. This could mean that the delta update payload was targeted for another version, or that the source partition was modified after it was installed, for example, by mounting a filesystem.
E update_engine: [0530/162336:ERROR:delta_performer.cc(1065)] Expected: sha256|hex = 839ACF5296B9AB820DC822B6C09EBA896905531EB2C581093A357411F1A444A0
E update_engine: [0530/162336:ERROR:delta_performer.cc(1068)] Calculated: sha256|hex = 18AF8D6842A71554893F1DE65B87F2A9639FB390357C71D5383C6ED7A6051AFA
E update_engine: [0530/162336:ERROR:delta_performer.cc(1077)] Operation source (offset:size) in blocks: 0:2,193:1,218:456,23471:8,32769:1,32961:1,37333:4,37351:3,37554:3,37570:2,37951:1,37959:1,38111:1,38125:1,38129:1,38139:1,38147:1,38149:1,38151:2,38155:1,38157:1,38360:5,38372:1,38377:5,38384:1,38437:1,38442:1,38447:1,38452:1,38457:1,38462:1,38467:1
E update_engine: [0530/162336:ERROR:delta_performer.cc(1260)] ValidateSourceHash(source_hasher.raw_hash(), operation, error) failed.
E update_engine: [0530/162336:ERROR:delta_performer.cc(283)] Failed to perform SOURCE_BSDIFF operation 8, which is the operation 0 in partition "system"
E update_engine: [0530/162336:ERROR:download_action.cc(273)] Error 20 in DeltaPerformer's Write method when processing the received payload -- Terminating processing
I update_engine: [0530/162336:INFO:delta_performer.cc(299)] Discarding 113721 unused downloaded bytes
I update_engine: [0530/162336:INFO:multi_range_http_fetcher.cc(171)] Received transfer terminated.
I update_engine: [0530/162336:INFO:multi_range_http_fetcher.cc(123)] TransferEnded w/ code 200
I update_engine: [0530/162336:INFO:multi_range_http_fetcher.cc(125)] Terminating.
I update_engine: [0530/162336:INFO:action_processor.cc(116)] ActionProcessor: finished DownloadAction with code ErrorCode::kDownloadStateInitializationError
I update_engine: [0530/162336:INFO:action_processor.cc(121)] ActionProcessor: Aborting processing due to failure.
I update_engine: [0530/162336:INFO:update_attempter_android.cc(286)] Processing Done.
I update_engine: [0530/162336:INFO:update_attempter_android.cc(306)] Resetting update progress.
The hash of the source data on disk for this operation doesn't match the expected value. This could mean that the delta update payload was targeted for another version, or that the source partition was modified after it was installed, for example, by mounting a filesystem.
这个操作的磁盘上的源数据的散列与预期值不匹配。这可能意味着增量更新有效负载是针对另一个版本的,或者是在安装之后修改了源分区,例如,通过安装文件系统。
原因: make otapackage 会对system.img重新打包 导致重新打包的system.img和out目录下的system.img Hash值不一致.
也就是线刷版本的system.img和OTA包的system.img不一致;整包升级会替换system.img,而差分包升级则需要保证系统内部的system.img和整包中system.img一致才能升级成功(差分包中保存了通过sha256 Hash算法计算出整包system.img的值,通过这个值来确定两个system.img一致).
验证是否如上原因导致:
将整包中system.img通过fastboot烧录到当前系统,再验证差分包升级
adb reboot bootloader
fastboot devices
//切换到system.img所在的目录
fastboot flash system system.img
fastboot reboot
解决:保证差分包或整包的system.img和线刷(也就是out目录下)的system.img保持一致.将make otapackage源包中的system.img替换out目录下system.img
out/target/product/xxx_qn6005_64/obj/PACKAGING/systemimage_intermediates/system.img
out/target/product/xxx_qn6005_64/system.img
脚本实现:
1.在/build/core/Makefile中添加:
$(hide) ./build/tools/releasetools/replace_img_from_target_files.py $@ $(PRODUCT_OUT)
2.在/build/tools/releasetools/中定义replace_img_from_target_files.py脚本
#!/usr/bin/env python
#
# Copyright (C) 2014 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Given a target-files zipfile that does contain images (ie, does
have an IMAGES/ top-level subdirectory), replace the images to
the output dir.
Usage: replace_img_from_target_files target_files output
"""
import sys
if sys.hexversion < 0x02070000:
print >> sys.stderr, "Python 2.7 or newer is required."
sys.exit(1)
import errno
import os
import re
import shutil
import subprocess
import tempfile
import zipfile
image_replace_list = ["boot.img","system.img"]
# missing in Python 2.4 and before
if not hasattr(os, "SEEK_SET"):
os.SEEK_SET = 0
def main(argv):
if len(argv) != 2:
sys.exit(1)
if not os.path.exists(argv[0]):
print "Target file:%s is invalid" % argv[0]
sys.exit(1)
if not os.path.exists(argv[1]):
print "Output dir:%s is invalid" % argv[1]
sys.exit(1)
zf = zipfile.ZipFile(argv[0], 'r')
for img in zf.namelist():
if img.find("IMAGES/") != -1:
if img.find(".img") != -1:
data = zf.read(img)
name = img.replace("IMAGES/", '')
if name in image_replace_list:
print "Replace %s" % name
name = '/'.join((argv[1], name))
file = open(name, "w")
file.write(data)
file.close()
if __name__ == '__main__':
main(sys.argv[1:])