1. 程式人生 > >OTA本質與實現流程分析

OTA本質與實現流程分析

接觸OTA也有段時間了,是時候總結下了。所謂OTA(Over-the-AirTechnology)是指手機終端通過無線網下載遠端伺服器上的升級包,對系統或應用進行升級的技術。有關網路部分不做過多討論,本文重點放在系統升級這一概念上。
一 OTA本質

先以PC機進行類比。假設計算機作業系統裝在C盤,當加電啟動時,載入程式會將C盤的系統程式裝入記憶體並執行,而系統升級或重灌系統,則是將C盤中原來的系統檔案部分或全部重寫。對於手機及其上的ANDROID系統而言,同樣如此,需要一個儲存系統檔案的“硬碟”。

                                                          圖1 某android手機儲存裝置結構圖

圖1 是某款手機的儲存裝置結構圖,其儲存區(紅色框圖部分)分為四部分:SRAM、Nand Flash、SDRAM及外設地址空間。其中Nand Flash中儲存著全部系統資料(通過專門的燒寫工具將編譯後的映象檔案download到Nand Flash中,工具由IC廠商提供),包括boot.img、system.img、recovery.img等,因此Nand Flash即是上文所說的手機上的“硬碟”。圖1最右部分(圖中綠色框圖部分)是Nand Flash儲存區更詳細的劃分,我們將主要關注system分割槽(藍色框圖),因為OTA升級主要是對這部分系統資料的重寫(當然boot分割槽也可升級)。除此之外,藍黑色區域標示的misc分割槽也應值得注意,它在OTA升級中發揮著重要的作用。

OK,一言以蔽之,所謂OTA就是將升級包(zip壓縮包)寫入到系統儲存區,因此我們需要考慮兩個問題,1.升級包是如何生成的?2.升級包是如何寫入到system分割槽的?

二 問題一:升級包的製作

1.整包的製作

升級包有整包與差分包之分。顧名思義,所謂整包即包含整個system分割槽中的資料檔案;而差分包則僅包含兩個版本之間改動的部分。利用整包升級好比對電腦進行重作系統,格式分系統分割槽,並將新系統資料寫入分割槽;而利用差分包升級不會格式化system分割槽,只是對其中部分儲存段的內容進行重寫。除升級包之外,製作過程中還會涉及到另一種zip包,程式碼中稱之為target-files zipfile,我稱之為差分資源包。首先闡述下整包的製作過程。

系統經過整編後,執行make otapackage命令,即可完成整包的製作,而此命令可分為兩個階段進行。首先執行./build/core/Makefile中的程式碼:

# -----------------------------------------------------------------
# OTA update package

name := $(TARGET_PRODUCT)
ifeq ($(TARGET_BUILD_TYPE),debug)
  name := $(name)_debug
endif
name := $(name)-ota-$(FILE_NAME_TAG)

INTERNAL_OTA_PACKAGE_TARGET := $(PRODUCT_OUT)/$(name).zip

$(INTERNAL_OTA_PACKAGE_TARGET): KEY_CERT_PAIR := $(DEFAULT_KEY_CERT_PAIR)

$(INTERNAL_OTA_PACKAGE_TARGET): $(BUILT_TARGET_FILES_PACKAGE) $(OTATOOLS)
        @echo "Package OTA: [email protected]"
        $(hide) ./build/tools/releasetools/ota_from_target_files -v \
           -n \
           -p $(HOST_OUT) \
           -k $(KEY_CERT_PAIR) \
           $(ota_extra_flag) \
           $(BUILT_TARGET_FILES_PACKAGE) [email protected]
.PHONY: otapackage
otapackage: $(INTERNAL_OTA_PACKAGE_TARGET)
# -----------------------------------------------------------------

程式碼段1 make otapackage目的碼

這段程式碼作用在於將系統資源(包括system、recovery、boot等目錄)重新打包,生成差分資源包(即target-files zipfile,下文將統一使用“差分資源包”這一概念)。我們可以看下差分資源包中的檔案結構,如下:

圖2 target-files zipfile目錄結構

其中,OTA目錄值得關注,因為在此目錄下存在著一個至關重要的檔案:OTA/bin/updater(後文會有詳述)。生成的差分資源包被傳遞給./build/tools/releasetools/

ota_from_target_files執行第二階段的操作:製作升級包。

圖3 ./build/tools/releasetools目錄下的檔案

圖3是./build/tools/releasetools目錄下所包含的檔案,這組檔案是Google提供的用來製作升級包的程式碼工具,核心檔案為:ota_from_target_files和img_from_target_files。其中,前者用來製作recovery模式下的升級包;後者則用來製作fastboot下的升級包(fastboot貌似是一種更底層的刷機操作,未過多研究,不再詳述)。其他檔案則是為此二者提供服務的,比如,common.py中包含有製作升級包所需操作的程式碼;edify_generator.py則用於生成recovery模式下升級的指令碼檔案:<升級包>.zip/ META-INF/com/google/android/updater-script。

檔案ota_from_target_files是本文所關注的重點,其中定義了兩個主要的方法:WriteFullOTAPackage和WriteIncrementalOTAPackage。前者用於生成整包,後者用來生成差分包。接著上文,當Makefile呼叫ota_from_target_files,並將差分資源包傳遞進來時,會執行WriteFullOTAPackage。此方法完成的工作包括:(1)將system目錄,boot.img等檔案新增到整包中;(2)生成升級包中的指令碼檔案:<升級包>.zip/META-INF/com/google/android/updater-script;(3)將上文提到的可執行檔案:OTA/bin/

updater新增到升級包中:META-INF/com/google/android/updater-script。摘取部分程式碼片段如下:

script.FormatPartition("/system")  
  script. FormatPartition ("/system")  
  script.UnpackPackageDir("recovery", "/system")  
  script.UnpackPackageDir("system", "/system")  
  
  (symlinks, retouch_files) = CopySystemFiles(input_zip, output_zip)  
  script.MakeSymlinks(symlinks)  
  if OPTIONS.aslr_mode:  
    script.RetouchBinaries(retouch_files)  
  else:  
    script.UndoRetouchBinaries(retouch_files)  
 

程式碼2 WriteFullOTAPackage程式碼片段


其中的script為edify_generator物件,其FormatPartition、UnpackPackageDir等方法分別是向指令碼檔案update-script中寫入格式化分割槽、解壓包等指令。

def AddToZip(self, input_zip, output_zip, input_path=None):  
    """Write the accumulated script to the output_zip file.  input_zip 
    is used as the source for the 'updater' binary needed to run 
    script.  If input_path is not None, it will be used as a local 
    path for the binary instead of input_zip."""  
  
    self.UnmountAll()  
  
    common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",  
                       "\n".join(self.script) + "\n")  
  
    if input_path is None:  
      data = input_zip.read("OTA/bin/updater")  
    else:  
      data = open(os.path.join(input_path, "updater")).read()  
    common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",  
                       data, perms=0755)  
 

程式碼段3  edify_generator中的AddToZip方法

WriteFullOTAPackage執行的最後會呼叫此方法。將資源差分包中OTA/bin/updater檔案copy到升級包中META-INF/com/google/android/update-binary。此檔案是OTA升級的關鍵,其將在recovery模式下被執行,用來將程式碼段2中生成的指令轉換為相應的函式去執行,從而完成對系統資料的重寫。

2.差分包的製作

生成差分包呼叫的是檔案./build/tools/releasetools/ota_from_target_files中的WriteIncrementalOTA方法,呼叫時需要將兩個版本的差分資源包作為引數傳進來,形如:

./build/tools/releasetools/ota_from_target_files –n –i ota_v1.zip ota_v2.zip update.zip

其中,引數n表示忽略時間戳;i表示生成增量包(即差分包);ota_v1.zip與ota_v2.zip分別代表前後兩個版本的差分資源包;而update.zip則表示最終生成的差分包。

WriteIncrementalOTA函式會計算輸入的兩個差分資源包中版本的差異,並將其寫入到差分包中;同時,將updater及生成指令碼檔案udpate-script新增到升級包中。

三 問題二:將升級包寫入系統儲存區

製作完升級包後,之後便是將其寫入到相應儲存區中,這部分工作是在recovery模式下完成的。之前的幾篇筆記亦有描述,recovery模式下通過建立一個新的程序讀取並執行指令碼檔案META-INF/com/google/android/updater-script。見如下程式碼:

const char** args = (const char**)malloc(sizeof(char*) * 5);  
args[0] = binary;  
args[1] = EXPAND(RECOVERY_API_VERSION);   // defined in Android.mk  
char* temp = (char*)malloc(10);  
sprintf(temp, "%d", pipefd[1]);  
args[2] = temp;  
args[3] = (char*)path;  
args[4] = NULL;  
  
pid_t pid = fork();  
if (pid == 0) {  
    close(pipefd[0]);  
    execv(binary, (char* const*)args);  
    _exit(-1);  
}  
close(pipefd[1]);  
 

程式碼段4 建立新程序安裝升級包

分析程式碼之前,首先介紹linux中函式fork與execv的用法。
pid_t forkvoid)

        建立新的程序,fork呼叫的一個奇妙之處就是它僅僅被呼叫一次,卻能夠返回兩次,它可能有三種不同的返回值:
  1)在父程序中,fork返回新建立子程序的程序ID;
  2)在子程序中,fork返回0;
  3)如果出現錯誤,fork返回一個負值;
        在fork函式執行完畢後,如果建立新程序成功,則出現兩個程序,一個是子程序,一個是父程序。在子程序中,fork函式返回0,在父程序中,fork返回新建立子程序的程序ID。我們可以通過fork返回的值來判斷當前程序是子程序還是父程序(http://os.chinaunix.net/a2012/0203/1306/000001306508.shtml)。

int execv(const char *progname, char *const argv[])
        execv會停止執行當前的程序,並且以progname應用程序替換被停止執行的程序,程序ID沒有改變。
        progname: 被執行的應用程式。
        argv: 傳遞給應用程式的引數列表, 注意,這個陣列的第一個引數應該是應用程式名字本身,並且最後一個引數應該為NULL,不參將多個引數合併為一個引數放入陣列。

 程式碼4見於bootable/recovery/install.c的try_update_binary函式中,是OTA升級的核心程式碼之一。通過對fork及execv函式的介紹可知,程式碼4建立了一個新的程序並在新程序中執行升級包中的META-INF/com/google/android/updater-binary檔案(引數binary已在此前賦值),此檔案將按照META-INF/com/google/android/updater-script中的指令將升級包裡的資料寫入到儲存區中。OK,我們來看下META-INF/com/google/android/updater-binary檔案的來歷。

在原始碼的./bootable/recovery/updater目錄下,存在著如下幾個檔案:

圖4 ./bootable/recovery/updater目錄

 

通過檢視Android.mk程式碼可知,檔案install.c、updater.c將會被編譯為可執行檔案updater存放到目錄out/target/product/<product-name>/obj/EXECUTABLES/

updater_intermediates/中;而在生成差分資源包(target-files zipfile)時,會將此檔案新增到壓縮包中。

built_ota_tools := \
	$(call intermediates-dir-for,EXECUTABLES,applypatch)/applypatch \
	$(call intermediates-dir-for,EXECUTABLES,applypatch_static)/applypatch_static \
	$(call intermediates-dir-for,EXECUTABLES,check_prereq)/check_prereq \
	$(call intermediates-dir-for,EXECUTABLES,sqlite3)/sqlite3 \
	$(call intermediates-dir-for,EXECUTABLES,updater)/updater

程式碼段5 Makefile中定義的變數built_ota_tools

$(hide) mkdir -p $(zip_root)/OTA/bin
	$(hide) $(ACP) $(INSTALLED_ANDROID_INFO_TXT_TARGET) $(zip_root)/OTA/
	$(hide) $(ACP) $(PRIVATE_OTA_TOOLS) $(zip_root)/OTA/bin/

程式碼段6 複製built_ota_tools工具到差分資源包

如程式碼段5,Makefile中定義了執行OTA所需要的一組工具(built_ota_tools),其中便包括由圖4中檔案編譯而成的檔案updater;而在生成差分資源包時,會將這組工具拷貝到差分資源包的OTA/bin目錄中(見程式碼段6);在生成升級包時(無論是執行WriteFullOTAPackage還是WriteIncrementalOTAPackage),最後都會呼叫edify_generator的AddToZip方法,將updater新增到升級包中(更名為"META-INF/com/google/android/update-binary");最終在recovery模式下被執行,這便是其來龍去脈。而關於updater的執行,也大致的描述下吧。

由前文可知,updater主要由bootable/recovery/updater目錄下的install.c和updater.c編譯而成,主函式位於updater.c。其中,在install.c中定義了讀寫系統儲存區的操作函式(這才是重寫系統資料的真正程式碼)並將這些函式與updater-script中的指令對映起來。而在updater.c會首先裝載install.c定義的函式,之後便解析升級指令碼updater-script,執行其對應的操作命令。與此同時,執行updater的程序還會與父程序通訊,通知父程序進行UI的相關操作(程式碼見bootable/recovery/install.c中的try_update_binary函式)。

貌似差不多了,就此打住。

PS:唉,發現現在的表達能力真TMD差,寫個總結都TM憋屈得要死。。。

分享到: