Android系统固件更新机制设计说明文档

Android系统固件更新机制设计说明文档

V1.1

xxx

2014-9-14

 

修改历史记录

内容

编制\日期

审核\日期

批准\日期

V1.0  建立初稿

Xxx

2014-9-14

 

 

V1.1 增加配图,统一英文单词大小写

Android启动过程错误修正

Xxx

2014-9-19

 

 

 

 

 

 

 

 

 

 

一 编写目的

   本文档主要描述Android板级系统各模块的更新原理和流程,为固件的远程更新提供依据。

 

二 术语和定义

   根据文档补充

   Android: 基于Linux的手持电话设备操作系统,采用Java编写应用程序

   Uboot: Linux嵌入式系统的启动引导系统

   Kernel: Linux内核

   Rootfs: 根文件系统

   System: 这里特指Android系统

   App: Android系统中应用的简称

   Recovery: Android自带的升级系统

   RamDisk: 内存盘,用内存空间模拟磁盘,做文件系统

   XML

   HTTP

   MD5

 

三 参考文档

   关于数据加密标准的参考文档

 

四 概述

   关于Android开发板,其本质上是一个嵌入式系统。不同于桌面PC系统采用磁盘,嵌入式系统一般采用Flash作为非易失性存储器。为了便于系统的运转,整个Flash存储资源是分区使用,类似于桌面PC的分区或者分盘。一个嵌入式Linux系统一般有如下几个模块:Uboot、Kernel及Rootfs。因为Android本质上可以看作是一个嵌入式Linux发行版,自然与常用Linux嵌入式系统在构成上有共同的一面,当然,作为一个独立的系统,Android系统也有其特有的构成部分。

   一般情况下,一个Android开发板应该有如下几个模块:

   Uboot: 启动引导系统

   Kernel: Linux操作系统内核

   RamDisk: 系统根文件系统

   Kernel和RamDisk会打包成一个Boot.img。

   System: Android主文件系统

   Userdata: 用户数据区,保存用户安装的应用程序

   Recovery: 用于保存Recovery系统。

   除了上述基本的一些模块外,可能还有一些辅助模块,比如

   Logo: 开机Logo,比如一幅图片。

   Bootargs: Kernel的启动参数,包含分区信息。

   Misc: 用于Uboot、Recovery及System之间的通信。

   Sdcard: 关联与外部扩展存储设备,扩展外存空间。

   Cache: Android自己的应用下载缓存,可用于保存系统升级包。

   一个完整的Android系统,其Flash分区信息可能如下图所示:

 

 

   以上,就是一个Android开发板Flash中所存储的东西。当我们基于这样一个系统完成一个产品后,使用过程中难免会出现BUG或者有新的需求要添加新的功能。在产品已不在自己手中时,远程更新便成为最直接的解决方案。鉴于不同模块承载的功能不同,在Flash中存储的位置也不同,因而可能存在多个固件的更新,更进一步,固件模块有无文件系统格式,其所采取的更新方式也会有所不同。在下一部分,将针对模块的更新做出整体方案设计。

 

   特别说明:之前版本中,忽略了RamDisk。在Android系统中,一般是由RamDisk充当根文件系统,由RamDisk中的init.rc脚本完成System和其他分区的挂载。实际生成镜像时,由专门的命令将Kernel和RamDisk打包到一起,合成一个称为Boot.img的文件,烧写到Flash中。这一点,在全智的一款电视盒上得到了验证。但是海思的电视盒上没有找到RamDisk,怀疑是直接编译到Kernel中了。后面的讨论中,还是称为Kernel的更新,实际上指Boot.img的更新。

 

五 总体方案

   上一部分,我们了解了Flash分区方法,在这一部分,将设计具体的分区方案以及部分分区的更新方案。

   通过对各个分区的介绍,可以看出,基本没有多余的部分。考虑到板子的实际情况,Logo部分可以移除。Kernel部分,鉴于其占用空间不大(相对Flash总体来说),同时又十分重要,为了保证这部分不出现问题,采用A/B区的方式,在Flash中再划分出一块来,命名为Kernel_bak,作为备份Kernel。存在备份后,需要有标记来说明使用哪一个分区的Kernel,所以为此还需要再划分出一小块Flash来存放标记。System部分虽然也很重要,但是其大小都在200M以上,这里就不使用备份方案,而是通过划分更大的Cache分区来解决,并且Cache可以在系统运行过程中,发挥它其他的正常的功能,比如作为应用的下载缓存。Userdata及Sdcard属于用户数据区,不需要更新,这里既不移除也不增加备份区。Recovery为Android升级系统,也不需要更新,保留即可。对于各个分区的大小,设计思想为满足正常大小的前提下,提供一定的宽裕,这样做,一方面可以满足模块本身因为功能增加,可能导致的“体积”增加,另一方面,也可以在一定程度上能够避免Flash本身因为擦写可能出现某些坏块带来的影响。比如Kernel一般在4M左右,这里设计为10M,基本能够满足日后的功能扩展需求,同时有一两个坏块也不会对Kernel的空间产生太大影响。

   基于上述分析,最终的Flash分区方案如下图所示:

 

   下面,我们来看部分分区的更新方案。首先,我们看那些部分可以更新。根据上面的分区方案,基本能够确定,Kernel和System是可以也是需要更新的分区。对于Uboot,按照上述分区方案是无法更新的,至于其他的分区,基本上是不需要进行更新的。又因为整个系统是基于Android的,安装应用自然是这一系统的特色,所以还存在一个单从上述分区中无法看出的应用更新。其次,我们来看具体的更新方式:

   1 Kernel的更新

   Kernel自然不必多说,只能是Flash擦写替换。其更新工作需要由Uboot来完成。为了保护Kernel不因为Flash问题而出错,设计了A\B区方案。也因为如此,Flash中被划分出一小块标记区,用于表明当前是使用A区Kernel还是B区Kernel。Uboot启动后,通过该标记,加载更新的Kernel镜像。而升级时,Uboot根据该标记覆盖更老的Kernel镜像。当替换完成后,Uboot需要修改标记,以便下次启动时,引导升级后的Kernel作为系统运行时的内核。

   2 System的更新

   System的更新实际为Android主系统的更新。System作为系统的根文件系统,可以有两种方式进行更新,一种是直接的Flash擦写,一种是基于文件系统的,也就是文件的覆盖替换。基于Android自带的Recovery方式更新,所采用的是基于文件系统的文件替换方案。Recovery实现为一个小的Linux系统,就是说Recovery=Linux内核+小根文件系统。Recovery的启动是由Uboot决定的,Uboot通过读取misc分区的信息,来判断是引导启动Kernel还是Recovery。当System需要更新时,会修改misc分区的内容,同时在Sdcard分区中指定升级文件,这样,Uboot启动后,读取misc分区内容,分析判断后,引导进入Recovery系统。Recovery会挂载System分区和Sdcard分区,解压Sdcard分区中的升级文件,并用其合并覆盖System分区的内容(实际上就是同名文件)。完成后,清除misc分区和Sdcard分区的标记,这样,再次重启系统后,会引导加载Kernel分区并挂载更新后的System分区,从而完成了主系统的更新。

   3 App的更新

   对于应用,因其只在文件系统层次之上可见,所以只能采用文件覆盖替换方式。只有当Kernel将System分区作为根文件系统挂载之后,我们才可以通过文件系统看到Android系统中的各个应用。Android应用为.apk格式的文件,它打包了应用相关的资源,包括Java字节码文件,图片资源,各种配置文件资源,动态库等。Apk包的升级,一般会弹出图形界面,供用户选择是否升级。是否支持后台强制升级,需要再研究确定。若支持,直接选择Android系统自带升级方案,否则,可考虑单开任务,下载Apk应用到Userdata区,覆盖原有的安装文件,并重启应用来达到更新目的。

 

   在进行具体的流程设计前,还有一些细节需要解决,这放在下一部分介绍。

 

六 分区原理及实现

   在第五部分,讨论了分区划分,但是没有说明具体如何做,才能完成这样的分区划分。在这部分,就解决这个问题。

   在实际动手开工实现前,有一些准备工作需要做,即需要对当前系统有足够的了解。

   1 Android系统的启动引导过程

   【过程分析,有部分不确定信息】

   首先,看看系统的启动过程与分区的关系。之前也了解了,Android实际上就是一个嵌入式Linux系统,所以启动过程同普通嵌入式Linux系统类似。上电时,CPU先从Flash中加载Uboot执行。这一步,Uboot需要放置到Flash的0地址开始处。这样一来,这个启动位置选择就由硬件设计所决定。Uboot在运行过程中,对硬件进行必要的初始化,包括CPU,内存、Flash和网络等。之后,Uboot从Flash指定位置读取Bootargs数据,解析后获知Flash的分区情况。在这之前,Uboot无法直接从Flash中知晓Bootargs的位置,所以Bootargs的位置需要在Uboot编译时就告诉它。如此一来,Bootargs在Flash中的位置需要人为的告诉Uboot,并且需要与Uboot进行紧密的配合。其中一个发生变化,需要考虑另一个是否需要做对应的修改。

   接下来,Uboot需要加载引导Kernel。通过Bootargs,Uboot知道Kernel在Flash的什么地址,然后读取Kernel头的内容,检查Kernel是否压缩,如果压缩,则执行解压缩动作。之后,进行校验码检查,通过以后,就跳转到Kernel入口执行,此时CPU控制权就相应的交到Kernel。因此,在这部分,修改Bootargs中Kernel的位置,同时将Kernel烧到Bootargs中指定的Flash分区位置,就可以保证Kernel的加载引导,因此也就完成了Kernel分区的修改。

   随着Kernel的执行,整个系统开始逐步的构建起来。因为Bootargs中有分区划分信息,所以,通过Bootargs,Kernel基本就知道了整个系统MTD设备分块的信息。在Kernel完成整个系统的构建后,先将RamDisk加载到内存中,并作为根文件系统挂载。在RamDisk中,最重要的就是执行init.rc脚本,该脚本会配置环境变量,挂载proc以及sys这些重要的内存文件系统。除了这些,该脚本会按照特定的文件系统格式(比如yaffs2)和分区信息来挂载System分区、Userdata分区以及Cache分区,这些是Android用到的重要分区。

   System分区挂载后,会执行Android系统下的init进程。该进程会继续完成Android服务和环境的构建,并最终引导启动Launcher(Android桌面应用)。至此,整个Android系统的加载就完成了。

   基本上,上述启动过程是比较通用的过程,与具体板子关系不是很大,整个流程如下图:

【在cpu 内存和Flash的物理连接基础上,也就是静态基础上,运行时的动态变化过程,Uboot肯定要进行内存搬移,图示内存搬移过程】

 

   2 对系统引导细节信息的进一步了解

  【该部分暂不确定,有待调试验证】

   A. 需要了解物理连接,包括CPU、内存以及Flash,确认Flash的0地址是否就是系统的0地址?Uboot放在Flash开始位置,是否就是0地址,还是有别的映射。这步需要了解cpu文档确定。

   B. 需要了解Uboot引导Kernel过程。

   <1>Uboot如何知道Kernel在Flash中的什么位置,是通过Bootargs还是编译时写固定的,这个需要确认;

   <2>Uboot如何知道Kernel的大小,单从Flash中应该是无法获知才对,应该需要通过Kernel头结构了解;

   <3>Uboot如何知道将Kernel加载到内存什么位置,具体做了哪些搬移工作;

   <4>Uboot对Kernel是先读到内存解压到内存另外一个地方还是边读取Flash镜像内容边解压;

   <5>Uboot是否只需要将Bootargs信息传给Kernel即可,是否是必须传递;

   <6>Uboot如何将Bootargs传递给Kernel,通过Flash位置还是内存。

   <7>misc分区如何对Uboot、Recovery以及Android系统三者都可见。Uboot使用该分区来决定启动Kernel还是Recovery,Recovery在完成任务后,需要清理该分区,Android系统使用该分区来标记要进行系统升级。

搞清楚上述7个问题,基本上就解决了分区的大部分疑问。

   C. 需要了解Kernel引导Android系统的过程。这块比较单一,只需要确认Kernel如何知道Android根文件系统及RamDisk的位置即可。是通过Bootargs参数还是人为固定写在根文件系统中?在前述基础上明确这块,就解决了整个分区问题。因为,如果Kernel仅通过Bootargs中的分区信息,完成上述三个分区的挂载,那么这几个分区位置的划分,只需要跟Bootargs对应就可以了,无需对Android启动系统做任何修改。不然,除了根文件系统,其他几个分区就需要Kernel执行RamDisk系统中的启动脚本来挂载,如此一来,还需要在编译RamDisk根文件系统时,同时提供分区信息给启动脚本。

   D. 需要了解Recovery对分区信息的解读,是否也是通过Bootargs。如果是,其处理情况就跟前述一致,否则,Recovery也需要进行调整。

   通过上述分析,最简单的情况自然是Bootargs决定整个系统分区的情景,这时,只需要保证Uboot能够获取Bootargs的信息即可。当分区调整后,只要保证在Bootargs中确定的位置跟在Flash中实际烧写的位置一致,就可以保证整个系统的正常启动运行。

   3 分区实现

   通过上面的讨论,确定了一个分区划分,可能需要修改的模块:

   A. Uboot需要修改,以确定Bootargs在Flash中的位置

   B. Bootargs需要修改,以说明各个模块在Flash中具体的位置

   C. 可选的Android启动脚本修改,比如为了单独挂载一些分区,类似自定义的只读分区

   4 分区调试

   获取现有板级分区信息,进行修改调试

   【调试步骤汇总】

   <1>通过串口,分析获得现有可运行版本的分区信息

   <2>通过源码,可以编译Uboot、Kernel以及Android系统

   <3>通过Uboot可以擦写Flash

   <4>通过Uboot修改现有版本的Bootargs,调整Kernel或者Android系统的位置,进行测试

   <5>如果第四步通过,那么基本就可以通过Bootargs完成分区修改,否则就需要从代码着手,获取分区设定信息。
   <6>继续分析分区信息,确认是否有Android自己挂载的分区。若有,是否在启动脚本中,并是否可以通过修改启动脚本,重新编译Android版本实现该分区在新位置的挂载。

   如果能通过这些调试步骤,那么整个系统的分区实现就基本清楚了,之后就可以根据自己的需求,定义自己的分区。

 

七 远程更新方案设计

   在这一部分,将根据第五部分的整体方案及第六部分的分区实现原理,整合两部分的内容,就具体模块的更新,做出更加具体的流程说明。

   1 数据的远程获取

   要实现远程更新,需要解决的第一个问题就是,如何远程获取更新数据。因为升级包是不能出错的,所以最好采用TCP作为传输层协议。当然UDP也是可以的。为了方便的搭建升级服务器并方便的获取升级包,这里采用HTTP协议作为上层应用协议。如此,可以使用web服务器来作为升级服务器,终端通过HTTP协议获取升级包,整体比较简单高效,而且采用web的方式,也可以有效的避免穿越防火墙的问题。

   又因为可升级的模块包括了Kernel、Android系统以及Android应用,所以,每一次操作是更新那个模块,需要有说明才行。另外,模块的名字,大小,校验码等信息,也是需要的。这样,就需要一个辅助模块,来说明需要更新的模块,名字,具体的位置、校验码等信息,我们将其设计为一个文件,称作配置文件,跟升级文件一样,放在web升级服务器上。每次升级前,终端都需要先下载该配置文件,以获取是否能够升级以及升级文件的大小位置等信息。配置文件具体的定义,见附件A。

   当某个模块需要升级时,管理人员更新升级服务器上的配置文件,比如将对应模块的版本号增加,以便终端能够判断出需要升级的模块。同时,管理人员也需要将新的升级包上传到服务器,并更新配置文件中,有关新升级包的辅助信息。如此一来,整个升级环境才算搭建好。整体环境如下图:

 

   2 服务器的定位

   在上图的流程中,有一个问题需要解决,就是终端如何知道服务器在那里,或者说,终端请求如何路由到服务器?当然可以直接使用服务器的IP地址,但是,一旦固定在代码中,那么在替换为新版本之前,这个地址是无法修改的(除非用户自己去设置),并且Uboot是不能升级的,那么Uboot中的地址就是无法修改的。显然,直接使用IP地址,十分不灵活。所以,升级服务器最好采用域名解决方案。在Uboot、System以及App中固定域名,终端每次启动后,通过域名服务器来获知服务器的IP地址。也可以做另外一种设计,将服务器地址保存在Flash的某个固定位置,这样可以通过其他手段,比如系统应用或者终端管理程序,来动态修改域名地址。而Uboot等具有升级功能的模块,则在执行升级功能时,读取Flash固定位置数据,获取域名地址。

   3 文件的加固

   这里,加固是一个形象的词,虽然我们可以使用HTTP来在一定程度上保证升级文件正确的下载到终端而不发生改变,但是如果升级文件本身被篡改,该如何解决?所以,为了保证升级文件就是开发者提供的,而不是被篡改过的,就需要对文件进行加固处理。主要的解决方案是计算摘要。比如,在配置文件中,同时提供升级文件的MD5校验码,这样,终端在用升级文件覆盖原有文件时,先进行校验,只有校验码一致,才可以进行后续的覆盖动作,否则提示出错返回。

   上面这种方式简单易行,但是有个问题,这种方法本身是矛盾的,因为配置文件本身很可能也被篡改,所以单将MD5放在配置文件中,意义不大。因此,产生一个改进方案,在配置文件中放置加密的摘要,这样可保证摘要的正确性。采用这种方案,需要保护好密钥。

   以上解决了远程升级包的正确获取问题,下面结合远程获取,描述各个模块的更新流程。

   4 Kernel更新流程

   板子上电启动后,首先运行Uboot。Uboot初始化网卡,通过网络自动获取IP,完成网络连接。在正常引导Kernel之前,Uboot进行Kernel升级判断。首先,获取升级服务器域名,进行域名解析,获取升级服务器IP地址。之后,连接服务器,下载配置文件(配置文件名称是固定的)【这些工作都是Uboot默认不支持的,需要额外添加代码实现】,解析配置文件,判断是否需要升级Kernel模块。如果不需要,那么Uboot继续引导较新的Kernel(从A/B区)。否则,Uboot从服务器下载需要升级的Kernel文件,然后计算摘要并与从配置文件中解密的摘要比较,若一致,说明文件没有被篡改。之后将Kernel写到Flash中内核的备份区域。(Uboot通过Flash中特定的标记区可以知道当前用的是哪个区的Kernel)。成功刷写Flash的内核区后,更该标记区,以便Uboot可以引导新的内核。如果下载过程中出现断网或者擦写过程中出现错误,都是失败的升级过程,Uboot设计为最多进行3次尝试,如果还是失败,则启动之前正确的Kernel来保证整个系统的启动。【此处,新标记的写操作如何保证不被断电?】如此,Kernel升级流程完成。整个流程图如下图所示:

 

   后续,新Kernel被继续加载执行,直到挂载根文件系统。在根文件系统的启动脚本中,完成Android主系统相关分区的挂载,包括System、Userdata以及Cache。因为System升级不是由Kernel来完成,所以Kernel中不进行任何升级相关操作。

   5 System更新流程

   Android系统运行起来后,立即启动一个后台例程,用于连接远程服务器。连接流程同之前类似,通过域名解析拿到服务器IP地址,然后下载配置文件,解析配置文件,确定是否需要进行System的升级。如果需要升级System,则开始下载升级文件工作。默认情况下,Recovery是从Sdcard分区获取升级包,但是,Sdcard分区一般挂载到SD卡或者TF卡上,不一定所有盒子任何时候都插着卡。所以,数据不能够下载到Sdcard分区,但又为了能够使用Android自带的Recovery机制,这里,我们将System升级包下载保存到/Cache目录,同时修改Recovery程序,将其默认查找的Sdcard目录换为Cache目录。这样Recovery起来后,就去Cache目录查找升级包,从而完成System的升级。当然,在使用升级包前,必要的校验工作也是不可少的,需要对Cache中保存的System升级包计算摘要,与配置文件中解密的摘要对比,通过后,才说明Cache中保存的新System没有被改动,可以用来升级。最后,还需要写升级标记到misc分区,以便重启系统后能够引导Recovery。

   系统重启后,因为之前写了升级标记,所以Uboot会基于该标记,引导Recovery分区中的小系统。Recovery起来后,分别挂载System分区和Cache分区,并解压Cache分区中的升级文件update.zip。成功后,用update.zip升级包中的文件和目录覆盖System分区的文件目录,达到升级系统的目的。Recovery完成上述工作后,清除misc分区标记,再次重启系统。这次重新启动后,因为标记已被清除,所以Uboot会跳过Recovery部分,直接加载Kernel,Kernel再挂载更新的Android根文件系统,这样就完成了系统的升级。

   当然,如果解压或者覆盖失败,Recovery会保持misc分区标记不擦,然后自动重启系统,重新尝试操作。如此几次仍然不成功,则提示无法进行升级,整个系统挂住。整个流程如下图:

 

   对于System的更新,有两点补充,一是Uboot的设计,需要考虑一个优先级问题,即是先判断Kernel升级还是先检查misc来引导Recovery,以便在两个条件都满足的情况下,决定哪一个动作先执行;二是,默认情况下,Recovery出错,系统最后会挂住,所以出错的处理,还需再验证调试。

   6 App更新流程

   以上,讨论了Kernel的更新流程和System的更新流程。对于APP,用户安装的应用基本都在/data目录下,该目录挂载的是Userdata分区,所以跟系统升级关系不大(系统升级了,应用不一定被升级)。各个Apk具体的安装文件在/data/app下,所以,应用的升级就是替换这里的Apk文件。

   App应用升级是在系统启动后,App运行时进行的。可以在App中设计一个任务,专门进行升级检查。该任务同样解析域名,连接升级服务器,获取配置文件,进行解析,并分析是否需要进行App的升级更新操作。如果需要,则下载新的App安装包,计算摘要。通过后,有两种方案,一种是调用Android自身的App升级接口,一种是绕过Android框架,直接替换安装文件。如总体方案中描述的,第一种方式会弹出是否确认升级的提示框,如果不选择升级,或者不做任何选择,可能就不会进行升级,所以,这种方式虽然接口简单,但是,若是做不到后台强制升级,则就不能采用,那就得用第二种方式,将应用安装包直接拷贝到/data/app目录下,覆盖原有的安装包。这第二种方式需要单独实现,另是否会有应用残留,也需要进一步研究确认。

   采用直接覆盖Apk方式来更新App的流程如下图所示:

 

   到此,各个模块的更新流程介绍完毕。

 

八 总结

   这一部分讨论本文档所提供方案存在的一些问题。

   首先,关于Uboot。一般嵌入式系统,不会进行远程Kernel更新,如果要更新,也是通过另外一个称为Loader的小系统来辅助完成。Loader会实现为裁剪的Linux内核加裁剪的文件系统,整体很小。按本文档提供的通过Uboot来远程升级Kernel,可以节省Loader的额外工作和对Flash空间的额外占用,但是带来了Uboot的复杂度增加。这其中最重要一部分就是对TCP协议的支持。因为要通过HTTP下载升级包,那必须要支持TCP协议。如此一来,可能需要移植一个类似lwip的小协议栈到Uboot中。感觉这样下来,不如做一个小系统代替。除了这一点,对于无线等接入方式,对Uboot也是很大的挑战。

   其次,Uboot主要是进行引导工作,在Uboot中进行太多条件的判断,可能会影响系统的启动时间。

九 附件A 配置文件格式定义

   配置文件用于为终端提供辅助信息。比如,升级包的名称,大小,校验码,版本号等。终端通过先获取配置文件,来了解是否需要升级,并验证升级包的正确性。综合当前软件领域的使用情况,最为常见的配置文件格式还是XML文档格式。许多工具包和插件都支持XML格式文档的解析,同时,XML也能提供最为一致的内容描述。

   使用XML格式文档做为配置文件,在System升级和App升级时,都可以使用Android的控件来解析文档,只不过在Uboot里面,稍显复杂点。不过,Linux下有很多开源的XML解析库,移植起来也很简单。鉴于如此情况,这里就将配置文件设计为XML格式。当然,我们可以采用纯文本的“key=value;”模式来定义配置文件,类似ini格式文档。只不过解析接口可能需要专门实现。

   关于配置文件的具体内容描述如下:

   配置文件由四部分构成,分别描述Kernel、System、App的信息以及平台通用信息。对于Kernel、System和App三部分,每一部分则均包括如下信息:

  1 升级包名称 即包文件名

  2 升级包大小 文件大小

  3 是否强制升级

  4 升级包版本 版本号,一般从低版本向高版本升级

  5 升级包地址 为了灵活性,升级包的URL地址可以通过该字段描述

  6 加密信息 对摘要的加密

  7 可升级设备序列号 可以限定使用该升级包的设备序列号,先小批量验证

猜你喜欢

转载自blog.csdn.net/wwwyue1985/article/details/112750794