Android 存儲系統解析之架構篇(上)

基於 Android 6.0 的源碼,剖析存儲架構的設計

引言:筆者最近遇到不少存儲相關 bug 導致手機發生 ANR,經過分析後,瓶頸主要在於主線程被 blocked 在 vold 進程. 大抵地一看 MountService/vold/kernel 的整個交互過程涉及多線程通信,IO 相關的操作都是在子線程完成,不應該阻塞 system_server 主線程啊?既然在子線程的工作都阻塞主線程, 那該如何修復這個 bug?爲了回答上面這些疑問,深入研究 Android 的整個存儲系統的架構設計。

一、概述

本文講述 Android 存儲系統的架構與設計,基於 Android 6.0 的源碼,涉及到最爲核心的便是 MountService 和 Vold 這兩個模塊以及之間的交互。爲了縮減篇幅,只展示部分核心代碼。

MountService:Android Binder 服務端,運行在 system_server 進程,用於跟 Vold 進行消息通信,比如MountServiceVold發送掛載 SD 卡的命令, 或者接收到來自Vold的外設熱插拔事件。MountService 作爲 Binder 服務端,那麼相應的 Binder 客戶端便是 StorageManager,通過 binder IPC 與 MountService 交互。

Vold:全稱爲 Volume Daemon,用於管理外部存儲設備的 Native daemon 進程,這是一個非常重要的守護進程,主要由 NetlinkManager,VolumeManager,CommandListener 這 3 部分組成。

1.1 模塊架構

從模塊地角度劃分 Android 整個存儲架構:

圖解:

1.2 進程架構

(1) 先看看 Java framework 層的線程:

MountService 運行在 system_server 進程,這裏查詢的便是 system_server 進程的所有子線程,system_server 進程承載整個 framework 所有核心服務,子線程數有很多,這裏只列舉與 MountService 模塊相關的子線程。

(2) 再看看 Native 層的線程:

Vold 作爲 native 守護進程,進程名爲 "/system/bin/vold",pid=387,通過ps -t可查詢到該進程下所有的子進程 / 線程。

小技巧:有讀者可能會好奇,爲什麼/system/bin/sdcard是子進程,而非子線程呢?要回答這個問題,有兩個方法,其一就是直接看擼源碼,會發現這是通過fork方式創建的,而其他子線程都是通過pthread_create方式創建的。當然其實還有個更快捷的小技巧,就是直接看上圖中的第 4 列,這一列的含義是VSIZE,代表的是進程虛擬地址空間大小,是否共享地址空間,這是進程與線程最大的區別,再來看看 / sdcard 的 VSIZE 大小跟父進程不一樣,基本可以確實 / sdcard 是子進程。

(3) 從進程 / 線程視角來看 Android 存儲架構:

注:圖中紅色字代表的進程 / 線程名,vold 進程通過 pthread_create 的方式創建的 3 個子線程名都爲 vold,圖中只是爲了便於區別才標註爲 vold1, vold2, vold3,其實名稱都爲 vold。

Android 還可劃分爲內核空間 (Kernel Space) 和用戶空間(User space),從上圖可看出,Android 存儲系統在 User space 總共採用 9 個進程 / 線程的架構模型。當然,除了這 9 個進 / 線程, 另外還會在 handler 消息處理過程中使用到 system_server 的兩個子線程:android.fgandroid.io

Tips: 同一個模塊可以運行在各個不同的進程 / 線程, 同一個進程可以運行不同模塊的代碼, 所以從進程角度和模塊角度劃分看到的有所不同的.

爲了闡述清楚存儲系統的通信架構,主要分爲以下 4 個過程:

  1. MountService 發送消息:MountService 是如何從向 vold 守護進程通信;

  2. MountService 接收消息:MountService 接收到 vold 發送過來的消息又是如何處理;

  3. Kernel 上報事件:當存儲設備發生熱插拔等事件,kernel 是如何通知用戶空間的 vold;

  4. 不請自來的廣播:對於事件往往都是 MountService 下發,然後再收到底層的迴應,但對於有些廣播卻非如此,而是由底層直接觸發,對於 MountService 來說卻是 “不請自來” 的消息。

限於篇幅過長,本文先講述前兩個過程,下一篇文章再來說說後兩個過程。

1.3 類關係圖


上圖中 4 個藍色塊便是前面談到的核心模塊。

二、 通信架構

Android 存儲系統中涉及各個進程間通信,這個架構採用的 socket,並沒有採用 Android binder IPC 機制。這樣的架構代碼大量更少,整體架構邏輯也相對簡單,在介紹通信過程前,先來看看 MountService 對象的實例化過程,那麼也就基本明白進程架構中 system_sever 進程爲了 MountService 服務而單獨創建與共享使用到線程情況。

首先,MountService 對象實例化的過程中完成是:

  1. 創建 ICallbacks 回調方法, FgThread 線程名爲 "android.fg",此處用到的 Looper 便是線程 "android.fg" 中的 Looper;

  2. 創建並啓動線程名爲 "MountService" 的 handlerThread;

  3. 創建 OBB 操作的 handler,IoThread 線程名爲 "android.io",此處用到的的 Looper 便是線程 "android.io" 中的 Looper;

  4. 創建 NativeDaemonConnector 對象

  5. 創建並啓動線程名爲 "VoldConnector" 的線程;

  6. 創建並啓動線程名爲 "CryptdConnector" 的線程;

  7. 註冊監聽用戶添加、刪除的廣播;

從這裏便可知道共創建了 3 個線程:MountService,VoldConnector,CryptdConnector,另外還會使用到系統進程中的兩個線程android.fgandroid.io. 這便是在文章開頭進程架構圖中 Java framework 層進程的創建情況.

2.1 MountService 發送消息

system_server 進程與 vold 守護進程間採用 socket 進行通信,這個通信過程是由 MountService 線程向 vold 線程發送消息。這裏以執行 mount 調用爲例:

2.1.1 MountService.mount

public void mount(String volId) {
    //【見小節 2.1.2】
    mConnector.execute("volume", "mount", vol.id, vol.mountFlags, vol.mountUserId);
}

2.1.2 NDC.execute

execute() 經過層層調用到 executeForList()

2.1.3 FL.onDataAvailable

MountService 線程通過 socket 發送 cmd 事件給 vold,對於 vold 守護進程在啓動的過程,初始化 CommandListener 時通過pthread_create創建子線程 vold 來專門監聽 MountService 發送過來的消息,當該線程接收到 socket 消息時,便會調用 onDataAvailable() 方法

2.1.4 FL.dispatchCommand

這是用於分發從 MountService 發送過來的命令,針對不同的命令調用不同的類。在處理過程中遇到下面情況,則會直接發送響應嗎 500 的應答消息給 MountService

2.1.5 CL.runCommand

例如前面發送過來的是volume mount,則會調用到 CommandListener 的內部類 VolumeCmd 的 runCommand 來處理該消息,並進入 mount 分支。

2.1.6 小節

MountService 向 vold 發送消息後,便阻塞在圖中的 MountService 線程的 NDC.execute()方法,那麼何時纔會退出呢?圖的後半段 MonutService 接收消息的過程會有答案,那便是在收到消息,並且消息的響應嗎不屬於區間 [600,700) 則添加事件到 ResponseQueue,從而喚醒阻塞的 MountService 繼續執行。關於上圖的後半段介紹的便是 MountService 接收消息的流程。

2.2 MountService 接收消息

當 Vold 在處理完完 MountService 發送過來的消息後,會通過 sendGenericOkFail 發送應答消息給上層的 MountService。

2.2.1 響應碼

不同的響應碼 (VoldResponseCode),代表着系統不同的處理結果,主要分爲下面幾大類:

Vll7SF

例如當操作執行成功,VoldConnector 線程能收到類似 `RCV <- {200 3 Command succeeded}的響應事件。其中對於 [600,700) 響應碼是由 Vold 進程 "不請自來" 的事件,主要是針對 disk,volume 的一系列操作,比如設備創建,狀態、路徑改變,以及文件類型、uid、標籤改變等事件都是底層直接觸發,後面再會詳細講。介紹完響應碼,接着繼續來說說發送應答消息的過程:

2.2.2 SC.sendMsg

sendMsg 經過層層調用,進入 sendDataLockedv 方法

2.2.3 NDC.listenToSocket

應答消息寫入 socket 管道後,在 MountService 的另個線程 "VoldConnector" 中建立了名爲vold的 socket 的客戶端,通過循環方式不斷監聽 Vold 服務端發送過來的消息。

監聽也是阻塞的過程,當收到不同的消息相應碼,採用不同的行爲:

2.2.4 小節

三、總結

3.1 概括

本文首先從模塊化和進程的視角來整體上描述了 Android 存儲系統的架構,並分別展開對 MountService, vold, kernel 這三者之間的通信流程的剖析。

{1}Java framework 層:採用 1個主線程(system_server) + 3個子線程(VoldConnector, MountService, CryptdConnector);MountService 線程不斷向 vold 下發存儲相關的命令,比如 mount, mkdirs 等操作;而線程 VoldConnector 一直處於等待接收 vold 發送過來的應答事件;CryptdConnector 通信原理和 VoldConnector 大抵相同,有興趣地讀者可自行閱讀。

(2)Native 層:採用 1個主線程(/system/bin/vold) + 3個子線程(vold) + 1子進程(/system/bin/sdcard);vold 進程中會通過pthread_create方式來生成 3 個 vold 子線程,其中兩個 vold 線程分別跟上層 system_server 進程中的線程 VoldConnector 和 CryptdConnector 通信,第 3 個 vold 線程用於與 kernel 進行 netlink 方式通信。

本文更多的是以系統的角度來分析存儲系統,那麼對於 app 來說,那麼地方會直接用到的呢?其實用到的地方很多,例如存儲設備掛載成功會發送廣播讓 app 知曉當前存儲掛載情況;其次當 app 需要創建目錄時,比如getExternalFilesDirs,getExternalCacheDirs等當目錄不存在時都需向存儲系統發出 mkdirs 的命令。另外,MountService 作爲 Binder 服務端,那自然而然會有 Binder 客戶端,那就是StorageManager,這個比較簡單就不再細說了,歡迎大家與 Gityuan。

3.2 架構的思考

以 Google 原生的 Android 存儲系統的架構設計主要採用 Socket 阻塞式通信方式,雖然 vold 的 native 層面有多個子線程幹活,但各司其職,真正處理上層發送過來的命令,仍然是單通道的模式。

目前外置存儲設備比如 sdcard 或者 otg 的硬件質量參差不齊,且隨使用時間碎片化程度也越來越嚴重,對於存儲設備掛載的過程中往往會有磁盤檢測 fsck_msdos 或者整理 fstrim 的動作,那麼勢必會阻塞多線程併發訪問,影響系統穩定性,從而造成系統 ANR。

例如系統剛啓動過程中 reset 操作需要重新掛載外置存儲設備,而緊接着 system_server 主線程需要執行的 volume user_started 操作便會被阻塞,阻塞超過 20s 則系統會拋出 Service Timeout 的 ANR。

最後,本文主要詳細闡述 MountService 與 vold 之間的通信過程,關注《架構師》,繼續瞭解下一篇文章繼續說說 vold 與 kernel 是如何通信,底層是如何主動觸發事件通知 MountService.

作者:袁輝輝

來源:http://gityuan.com/2016/07/23/android-io-arch/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/T2IuW-9oUtAR4GQJXIwrXw